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.
|
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.
|
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.
|
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]
|
[colours]
|
||||||
cursor = "#e8dfd6"
|
cursor = "#e8dfd6"
|
||||||
|
|
|
@ -6,7 +6,6 @@ import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Buffer struct {
|
type Buffer struct {
|
||||||
|
@ -18,8 +17,6 @@ type Buffer struct {
|
||||||
selectionStart *Position
|
selectionStart *Position
|
||||||
selectionEnd *Position
|
selectionEnd *Position
|
||||||
selectionComplete bool // whether the selected text can update or whether it is final
|
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
|
terminalState *TerminalState
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -48,7 +45,7 @@ func (buffer *Buffer) GetURLAtPosition(col uint16, viewRow uint16) string {
|
||||||
|
|
||||||
candidate := ""
|
candidate := ""
|
||||||
|
|
||||||
for i := col; i >= 0; i-- {
|
for i := col; i >= uint16(0); i-- {
|
||||||
cell := buffer.GetRawCell(i, row)
|
cell := buffer.GetRawCell(i, row)
|
||||||
if cell == nil {
|
if cell == nil {
|
||||||
break
|
break
|
||||||
|
@ -82,6 +79,26 @@ func (buffer *Buffer) GetURLAtPosition(col uint16, viewRow uint16) string {
|
||||||
return candidate
|
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) {
|
func (buffer *Buffer) SelectWordAtPosition(col uint16, viewRow uint16) {
|
||||||
|
|
||||||
row := buffer.convertViewLineToRawLine(viewRow) - uint64(buffer.terminalState.scrollLinesFromBottom)
|
row := buffer.convertViewLineToRawLine(viewRow) - uint64(buffer.terminalState.scrollLinesFromBottom)
|
||||||
|
@ -94,7 +111,7 @@ func (buffer *Buffer) SelectWordAtPosition(col uint16, viewRow uint16) {
|
||||||
start := col
|
start := col
|
||||||
end := col
|
end := col
|
||||||
|
|
||||||
for i := col; i >= 0; i-- {
|
for i := col; i >= uint16(0); i-- {
|
||||||
cell := buffer.GetRawCell(i, row)
|
cell := buffer.GetRawCell(i, row)
|
||||||
if cell == nil {
|
if cell == nil {
|
||||||
break
|
break
|
||||||
|
@ -124,8 +141,9 @@ func (buffer *Buffer) SelectWordAtPosition(col uint16, viewRow uint16) {
|
||||||
Col: int(end),
|
Col: int(end),
|
||||||
Line: int(row),
|
Line: int(row),
|
||||||
}
|
}
|
||||||
buffer.emitDisplayChange()
|
|
||||||
|
|
||||||
|
buffer.selectionComplete = true
|
||||||
|
buffer.emitDisplayChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
// bounds for word selection
|
// bounds for word selection
|
||||||
|
@ -202,37 +220,14 @@ func (buffer *Buffer) GetSelectedText() string {
|
||||||
|
|
||||||
func (buffer *Buffer) StartSelection(col uint16, viewRow uint16) {
|
func (buffer *Buffer) StartSelection(col uint16, viewRow uint16) {
|
||||||
row := buffer.convertViewLineToRawLine(viewRow) - uint64(buffer.terminalState.scrollLinesFromBottom)
|
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.selectionComplete = false
|
||||||
buffer.selectionStart = &Position{
|
|
||||||
|
buffer.selectionStart = &Position {
|
||||||
Col: int(col),
|
Col: int(col),
|
||||||
Line: int(row),
|
Line: int(row),
|
||||||
}
|
}
|
||||||
buffer.selectionClickTime = time.Now()
|
|
||||||
|
buffer.selectionEnd = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (buffer *Buffer) EndSelection(col uint16, viewRow uint16, complete bool) {
|
func (buffer *Buffer) EndSelection(col uint16, viewRow uint16, complete bool) {
|
||||||
|
|
|
@ -14,6 +14,7 @@ type Config struct {
|
||||||
KeyMapping KeyMappingConfig `toml:"keys"`
|
KeyMapping KeyMappingConfig `toml:"keys"`
|
||||||
SearchURL string `toml:"search_url"`
|
SearchURL string `toml:"search_url"`
|
||||||
MaxLines uint64 `toml:"max_lines"`
|
MaxLines uint64 `toml:"max_lines"`
|
||||||
|
CopyAndPasteWithMouse bool `toml:"copy_and_paste_with_mouse"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type KeyMappingConfig map[string]string
|
type KeyMappingConfig map[string]string
|
||||||
|
|
|
@ -29,6 +29,7 @@ var DefaultConfig = Config{
|
||||||
KeyMapping: KeyMappingConfig(map[string]string{}),
|
KeyMapping: KeyMappingConfig(map[string]string{}),
|
||||||
SearchURL: "https://www.google.com/search?q=$QUERY",
|
SearchURL: "https://www.google.com/search?q=$QUERY",
|
||||||
MaxLines: 1000,
|
MaxLines: 1000,
|
||||||
|
CopyAndPasteWithMouse: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|
|
@ -48,6 +48,11 @@ type GUI struct {
|
||||||
resizeLock *sync.Mutex
|
resizeLock *sync.Mutex
|
||||||
handCursor *glfw.Cursor
|
handCursor *glfw.Cursor
|
||||||
arrowCursor *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 {
|
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/go-gl/glfw/v3.2/glfw"
|
||||||
"github.com/liamg/aminal/terminal"
|
"github.com/liamg/aminal/terminal"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (gui *GUI) glfwScrollCallback(w *glfw.Window, xoff float64, yoff float64) {
|
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) {
|
func (gui *GUI) mouseMoveCallback(w *glfw.Window, px float64, py float64) {
|
||||||
|
|
||||||
scale := gui.scale()
|
x, y := gui.convertMouseCoordinates(px, py)
|
||||||
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())))
|
|
||||||
|
|
||||||
if gui.mouseDown {
|
if gui.mouseDown {
|
||||||
gui.terminal.ActiveBuffer().EndSelection(x, y, false)
|
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) {
|
func (gui *GUI) mouseButtonCallback(w *glfw.Window, button glfw.MouseButton, action glfw.Action, mod glfw.ModifierKey) {
|
||||||
|
|
||||||
if gui.overlay != nil {
|
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.
|
// before we forward clicks on (below), we need to handle them locally for url clicking, text highlighting etc.
|
||||||
px, py := w.GetCursorPos()
|
x, y := gui.convertMouseCoordinates(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())))
|
|
||||||
tx := int(x) + 1 // vt100 is 1 indexed
|
tx := int(x) + 1 // vt100 is 1 indexed
|
||||||
ty := int(y) + 1
|
ty := int(y) + 1
|
||||||
|
|
||||||
if button == glfw.MouseButtonLeft {
|
activeBuffer := gui.terminal.ActiveBuffer()
|
||||||
|
|
||||||
|
switch button {
|
||||||
|
case glfw.MouseButtonLeft:
|
||||||
if action == glfw.Press {
|
if action == glfw.Press {
|
||||||
gui.mouseDown = true
|
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 {
|
} else if action == glfw.Release {
|
||||||
gui.mouseDown = false
|
gui.mouseDown = false
|
||||||
gui.terminal.ActiveBuffer().EndSelection(x, y, true)
|
activeBuffer.EndSelection(x, y, true)
|
||||||
if url := gui.terminal.ActiveBuffer().GetURLAtPosition(x, y); url != "" {
|
|
||||||
go gui.launchTarget(url)
|
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
|
// https://www.xfree86.org/4.8.0/ctlseqs.html
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
Loading…
Reference in New Issue