mirror of https://github.com/liamg/aminal.git
Add "select with mouse to copy" / "right click to paste" functionality (#181)
This commit is contained in:
parent
23cf1e8b7d
commit
97fe7362ce
|
@ -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"
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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 {
|
||||
|
|
89
gui/mouse.go
89
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
|
||||
|
||||
/*
|
||||
|
|
Loading…
Reference in New Issue