diff --git a/README.md b/README.md index ebb224f..8851945 100644 --- a/README.md +++ b/README.md @@ -113,6 +113,7 @@ slomo = false # Enable slow motion output mode, useful for debuggi shell = "/bin/bash" # The shell to run for the terminal session. Defaults to the users shell. search_url = "https://www.google.com/search?q=$QUERY" # The search engine to use for the "search selected text" action. Defaults to google. Set this to your own search url using $QUERY as the keywords to replace when searching. max_lines = 1000 # Maximum number of lines in the terminal buffer. +copy_and_paste_with_mouse = true # Text selected with the mouse is copied to the clipboard on end selection, and is pasted on right mouse button click. [colours] cursor = "#e8dfd6" diff --git a/buffer/buffer.go b/buffer/buffer.go index 2555743..fe0f127 100644 --- a/buffer/buffer.go +++ b/buffer/buffer.go @@ -6,7 +6,6 @@ import ( "io/ioutil" "net/url" "os" - "time" ) type Buffer struct { @@ -18,8 +17,6 @@ type Buffer struct { selectionStart *Position selectionEnd *Position selectionComplete bool // whether the selected text can update or whether it is final - selectionExpanded bool // whether the selection to word expansion has already run on this point - selectionClickTime time.Time terminalState *TerminalState } @@ -48,7 +45,7 @@ func (buffer *Buffer) GetURLAtPosition(col uint16, viewRow uint16) string { candidate := "" - for i := col; i >= 0; i-- { + for i := col; i >= uint16(0); i-- { cell := buffer.GetRawCell(i, row) if cell == nil { break @@ -82,6 +79,26 @@ func (buffer *Buffer) GetURLAtPosition(col uint16, viewRow uint16) string { return candidate } +func (buffer *Buffer) IsSelectionComplete() bool { + return buffer.selectionComplete +} + +func (buffer *Buffer) SelectLineAtPosition(col uint16, viewRow uint16) { + row := buffer.convertViewLineToRawLine(viewRow) - uint64(buffer.terminalState.scrollLinesFromBottom) + + buffer.selectionStart = &Position { + Col: 0, + Line: int(row), + } + buffer.selectionEnd = &Position { + Col: int(buffer.ViewWidth() - 1), + Line: int(row), + } + + buffer.selectionComplete = true + buffer.emitDisplayChange() +} + func (buffer *Buffer) SelectWordAtPosition(col uint16, viewRow uint16) { row := buffer.convertViewLineToRawLine(viewRow) - uint64(buffer.terminalState.scrollLinesFromBottom) @@ -94,7 +111,7 @@ func (buffer *Buffer) SelectWordAtPosition(col uint16, viewRow uint16) { start := col end := col - for i := col; i >= 0; i-- { + for i := col; i >= uint16(0); i-- { cell := buffer.GetRawCell(i, row) if cell == nil { break @@ -124,8 +141,9 @@ func (buffer *Buffer) SelectWordAtPosition(col uint16, viewRow uint16) { Col: int(end), Line: int(row), } - buffer.emitDisplayChange() + buffer.selectionComplete = true + buffer.emitDisplayChange() } // bounds for word selection @@ -202,37 +220,14 @@ func (buffer *Buffer) GetSelectedText() string { func (buffer *Buffer) StartSelection(col uint16, viewRow uint16) { row := buffer.convertViewLineToRawLine(viewRow) - uint64(buffer.terminalState.scrollLinesFromBottom) - if buffer.selectionComplete { - buffer.selectionEnd = nil - - if buffer.selectionStart != nil && time.Since(buffer.selectionClickTime) < time.Millisecond*500 { - if buffer.selectionExpanded { - //select whole line! - buffer.selectionStart = &Position{ - Col: 0, - Line: int(row), - } - buffer.selectionEnd = &Position{ - Col: int(buffer.ViewWidth() - 1), - Line: int(row), - } - buffer.emitDisplayChange() - } else { - buffer.SelectWordAtPosition(col, viewRow) - buffer.selectionExpanded = true - } - return - } - - buffer.selectionExpanded = false - } - buffer.selectionComplete = false - buffer.selectionStart = &Position{ + + buffer.selectionStart = &Position { Col: int(col), Line: int(row), } - buffer.selectionClickTime = time.Now() + + buffer.selectionEnd = nil } func (buffer *Buffer) EndSelection(col uint16, viewRow uint16, complete bool) { diff --git a/config/config.go b/config/config.go index bae52f5..9bfea1d 100644 --- a/config/config.go +++ b/config/config.go @@ -14,6 +14,7 @@ type Config struct { KeyMapping KeyMappingConfig `toml:"keys"` SearchURL string `toml:"search_url"` MaxLines uint64 `toml:"max_lines"` + CopyAndPasteWithMouse bool `toml:"copy_and_paste_with_mouse"` } type KeyMappingConfig map[string]string diff --git a/config/defaults.go b/config/defaults.go index 42724f6..9d3f5fd 100644 --- a/config/defaults.go +++ b/config/defaults.go @@ -29,6 +29,7 @@ var DefaultConfig = Config{ KeyMapping: KeyMappingConfig(map[string]string{}), SearchURL: "https://www.google.com/search?q=$QUERY", MaxLines: 1000, + CopyAndPasteWithMouse: true, } func init() { diff --git a/gui/gui.go b/gui/gui.go index e9e1004..686db93 100644 --- a/gui/gui.go +++ b/gui/gui.go @@ -48,6 +48,11 @@ type GUI struct { resizeLock *sync.Mutex handCursor *glfw.Cursor arrowCursor *glfw.Cursor + + prevLeftClickX uint16 + prevLeftClickY uint16 + leftClickTime time.Time + leftClickCount int // number of clicks in a serie - single click, double click, or triple click } func Min(x, y int) int { diff --git a/gui/mouse.go b/gui/mouse.go index dcd4e1b..341713b 100644 --- a/gui/mouse.go +++ b/gui/mouse.go @@ -6,6 +6,7 @@ import ( "github.com/go-gl/glfw/v3.2/glfw" "github.com/liamg/aminal/terminal" + "time" ) func (gui *GUI) glfwScrollCallback(w *glfw.Window, xoff float64, yoff float64) { @@ -35,12 +36,7 @@ func (gui *GUI) getArrowCursor() *glfw.Cursor { func (gui *GUI) mouseMoveCallback(w *glfw.Window, px float64, py float64) { - scale := gui.scale() - px = px / float64(scale) - py = py / float64(scale) - - x := uint16(math.Floor((px - float64(gui.renderer.areaX)) / float64(gui.renderer.CellWidth()))) - y := uint16(math.Floor((py - float64(gui.renderer.areaY)) / float64(gui.renderer.CellHeight()))) + x, y := gui.convertMouseCoordinates(px, py) if gui.mouseDown { gui.terminal.ActiveBuffer().EndSelection(x, y, false) @@ -62,6 +58,35 @@ func (gui *GUI) mouseMoveCallback(w *glfw.Window, px float64, py float64) { } } +func (gui *GUI) convertMouseCoordinates(px float64, py float64) (uint16, uint16) { + scale := gui.scale() + px = px / float64(scale) + py = py / float64(scale) + x := uint16(math.Floor((px - float64(gui.renderer.areaX)) / float64(gui.renderer.CellWidth()))) + y := uint16(math.Floor((py - float64(gui.renderer.areaY)) / float64(gui.renderer.CellHeight()))) + + return x, y +} + +func (gui *GUI) updateLeftClickCount(x uint16, y uint16) int { + defer func() { + gui.leftClickTime = time.Now() + gui.prevLeftClickX = x + gui.prevLeftClickY = y + }() + + if gui.prevLeftClickX == x && gui.prevLeftClickY == y && time.Since(gui.leftClickTime) < time.Millisecond * 500 { + gui.leftClickCount++ + if gui.leftClickCount > 3 { + gui.leftClickCount = 3 + } + } else { + gui.leftClickCount = 1 + } + + return gui.leftClickCount +} + func (gui *GUI) mouseButtonCallback(w *glfw.Window, button glfw.MouseButton, action glfw.Action, mod glfw.ModifierKey) { if gui.overlay != nil { @@ -72,28 +97,58 @@ func (gui *GUI) mouseButtonCallback(w *glfw.Window, button glfw.MouseButton, act } // before we forward clicks on (below), we need to handle them locally for url clicking, text highlighting etc. - px, py := w.GetCursorPos() - scale := gui.scale() - px = px / float64(scale) - py = py / float64(scale) - x := uint16(math.Floor((px - float64(gui.renderer.areaX)) / float64(gui.renderer.CellWidth()))) - y := uint16(math.Floor((py - float64(gui.renderer.areaY)) / float64(gui.renderer.CellHeight()))) + x, y := gui.convertMouseCoordinates(w.GetCursorPos()) tx := int(x) + 1 // vt100 is 1 indexed ty := int(y) + 1 - if button == glfw.MouseButtonLeft { + activeBuffer := gui.terminal.ActiveBuffer() + switch button { + case glfw.MouseButtonLeft: if action == glfw.Press { gui.mouseDown = true - gui.terminal.ActiveBuffer().StartSelection(x, y) + + clickCount := gui.updateLeftClickCount(x, y) + if clickCount == 1 || !activeBuffer.IsSelectionComplete() { + activeBuffer.StartSelection(x, y) + } else { + switch clickCount { + case 2: + activeBuffer.SelectWordAtPosition(x, y) + case 3: + activeBuffer.SelectLineAtPosition(x, y) + } + } } else if action == glfw.Release { gui.mouseDown = false - gui.terminal.ActiveBuffer().EndSelection(x, y, true) - if url := gui.terminal.ActiveBuffer().GetURLAtPosition(x, y); url != "" { - go gui.launchTarget(url) + activeBuffer.EndSelection(x, y, true) + + handled := false + if gui.config.CopyAndPasteWithMouse { + selectedText := activeBuffer.GetSelectedText() + if selectedText != "" { + gui.window.SetClipboardString(selectedText) + handled = true + } + } + + if !handled { + if url := activeBuffer.GetURLAtPosition(x, y); url != "" { + go gui.launchTarget(url) + } + } + } + + case glfw.MouseButtonRight: + if gui.config.CopyAndPasteWithMouse && action == glfw.Press { + str, err := gui.window.GetClipboardString() + if err == nil { + activeBuffer.ClearSelection() + _ = gui.terminal.Paste([]byte(str)) } } } + // https://www.xfree86.org/4.8.0/ctlseqs.html /*