mirror of https://github.com/liamg/aminal.git
text selection and copy/paste
This commit is contained in:
parent
88f0731e76
commit
6df9ad1cf3
11
README.md
11
README.md
|
@ -58,18 +58,19 @@ Ensure you have your latest graphics card drivers installed before use.
|
|||
| Resizing/content reordering | ✔ |
|
||||
| ANSI escape codes | ✔ |
|
||||
| UTF-8 input | ✔ |
|
||||
| UTF-8 output | ✔ | Works as long as the font in use supports the relevant characters.
|
||||
| Copy/paste | | Paste working, no mouse interaction for copy
|
||||
| Customisable colour schemes | ✔ | Complete, but the config file has no entry for this yet
|
||||
| Config file | ✔ | Minimal options atm
|
||||
| UTF-8 output | ✔ |
|
||||
| Copy/paste | ✔ |
|
||||
| Customisable colour schemes | ✔ |
|
||||
| Config file | ✔ |
|
||||
| Scrolling | ✔ |
|
||||
| Mouse interaction | |
|
||||
| Mouse interaction | ✔ |
|
||||
| Sweet render effects | |
|
||||
|
||||
## Keyboard Shortcuts
|
||||
|
||||
| Operation | Key(s) |
|
||||
| ------------------ | ---------------- |
|
||||
| Copy | ctrl + shift + c |
|
||||
| Paste | ctrl + shift + v |
|
||||
| Toggle slomo | ctrl + shift + ; |
|
||||
| Interrupt (SIGINT) | ctrl + c |
|
||||
|
|
188
buffer/buffer.go
188
buffer/buffer.go
|
@ -2,6 +2,7 @@ package buffer
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Buffer struct {
|
||||
|
@ -20,6 +21,16 @@ type Buffer struct {
|
|||
replaceMode bool // overwrite character at cursor or insert new
|
||||
autoWrap bool
|
||||
dirty bool
|
||||
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
|
||||
}
|
||||
|
||||
type Position struct {
|
||||
Line int
|
||||
Col int
|
||||
}
|
||||
|
||||
// NewBuffer creates a new terminal buffer
|
||||
|
@ -36,6 +47,183 @@ func NewBuffer(viewCols uint16, viewLines uint16, attr CellAttributes) *Buffer {
|
|||
return b
|
||||
}
|
||||
|
||||
func (buffer *Buffer) SelectWordAtPosition(col uint16, row uint16) {
|
||||
|
||||
cell := buffer.GetCell(col, row)
|
||||
if cell == nil || cell.Rune() == 0x00 {
|
||||
return
|
||||
}
|
||||
|
||||
start := col
|
||||
end := col
|
||||
|
||||
for i := col; i >= 0; i-- {
|
||||
cell := buffer.GetCell(i, row)
|
||||
if cell == nil {
|
||||
break
|
||||
}
|
||||
if isRuneSelectionMarker(cell.Rune()) {
|
||||
break
|
||||
}
|
||||
start = i
|
||||
}
|
||||
|
||||
for i := col; i < buffer.viewWidth; i++ {
|
||||
cell := buffer.GetCell(i, row)
|
||||
if cell == nil {
|
||||
break
|
||||
}
|
||||
if isRuneSelectionMarker(cell.Rune()) {
|
||||
break
|
||||
}
|
||||
end = i
|
||||
}
|
||||
|
||||
buffer.selectionStart = &Position{
|
||||
Col: int(start),
|
||||
Line: int(buffer.convertViewLineToRawLine(row)),
|
||||
}
|
||||
buffer.selectionEnd = &Position{
|
||||
Col: int(end),
|
||||
Line: int(buffer.convertViewLineToRawLine(row)),
|
||||
}
|
||||
buffer.emitDisplayChange()
|
||||
|
||||
}
|
||||
|
||||
// bounds for word selection
|
||||
func isRuneSelectionMarker(r rune) bool {
|
||||
switch r {
|
||||
case ',', ' ', ':', ';', 0, '\'', '"', '[', ']', '(', ')', '{', '}':
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (buffer *Buffer) GetSelectedText() string {
|
||||
if buffer.selectionStart == nil || buffer.selectionEnd == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
text := ""
|
||||
|
||||
for row := buffer.selectionStart.Line; row <= buffer.selectionEnd.Line; row++ {
|
||||
|
||||
if row >= len(buffer.lines) {
|
||||
break
|
||||
}
|
||||
|
||||
line := buffer.lines[row]
|
||||
|
||||
minX := 0
|
||||
maxX := int(buffer.viewWidth) - 1
|
||||
if row == buffer.selectionStart.Line {
|
||||
minX = buffer.selectionStart.Col
|
||||
} else if !line.wrapped {
|
||||
text += "\n"
|
||||
}
|
||||
if row == buffer.selectionEnd.Line {
|
||||
maxX = buffer.selectionEnd.Col
|
||||
}
|
||||
|
||||
for col := minX; col <= maxX; col++ {
|
||||
if col >= len(line.cells) {
|
||||
break
|
||||
}
|
||||
cell := line.cells[col]
|
||||
text += string(cell.Rune())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return text
|
||||
}
|
||||
|
||||
func (buffer *Buffer) StartSelection(col uint16, row uint16) {
|
||||
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(buffer.convertViewLineToRawLine(row)),
|
||||
}
|
||||
buffer.selectionEnd = &Position{
|
||||
Col: int(buffer.ViewWidth() - 1),
|
||||
Line: int(buffer.convertViewLineToRawLine(row)),
|
||||
}
|
||||
buffer.emitDisplayChange()
|
||||
} else {
|
||||
buffer.SelectWordAtPosition(col, row)
|
||||
buffer.selectionExpanded = true
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
buffer.selectionExpanded = false
|
||||
}
|
||||
|
||||
buffer.selectionComplete = false
|
||||
buffer.selectionStart = &Position{
|
||||
Col: int(col),
|
||||
Line: int(buffer.convertViewLineToRawLine(row)),
|
||||
}
|
||||
buffer.selectionClickTime = time.Now()
|
||||
}
|
||||
|
||||
func (buffer *Buffer) EndSelection(col uint16, row uint16, complete bool) {
|
||||
|
||||
if buffer.selectionComplete {
|
||||
return
|
||||
}
|
||||
|
||||
buffer.selectionComplete = complete
|
||||
|
||||
defer buffer.emitDisplayChange()
|
||||
|
||||
if buffer.selectionStart == nil {
|
||||
buffer.selectionEnd = nil
|
||||
return
|
||||
}
|
||||
|
||||
if int(col) == buffer.selectionStart.Col && int(buffer.convertViewLineToRawLine(row)) == int(buffer.selectionStart.Line) && complete {
|
||||
return
|
||||
}
|
||||
|
||||
buffer.selectionEnd = &Position{
|
||||
Col: int(col),
|
||||
Line: int(buffer.convertViewLineToRawLine(row)),
|
||||
}
|
||||
}
|
||||
|
||||
func (buffer *Buffer) InSelection(col uint16, row uint16) bool {
|
||||
|
||||
if buffer.selectionStart == nil || buffer.selectionEnd == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
var x1, x2, y1, y2 int
|
||||
|
||||
// first, let's put the selection points in the correct order, earliest first
|
||||
if buffer.selectionStart.Line > buffer.selectionEnd.Line || (buffer.selectionStart.Line == buffer.selectionEnd.Line && buffer.selectionStart.Col > buffer.selectionEnd.Col) {
|
||||
y2 = buffer.selectionStart.Line
|
||||
y1 = buffer.selectionEnd.Line
|
||||
x2 = buffer.selectionStart.Col
|
||||
x1 = buffer.selectionEnd.Col
|
||||
} else {
|
||||
y1 = buffer.selectionStart.Line
|
||||
y2 = buffer.selectionEnd.Line
|
||||
x1 = buffer.selectionStart.Col
|
||||
x2 = buffer.selectionEnd.Col
|
||||
}
|
||||
|
||||
rawY := int(buffer.convertViewLineToRawLine(row))
|
||||
return (rawY > y1 || (rawY == y1 && int(col) >= x1)) && (rawY < y2 || (rawY == y2 && int(col) <= x2))
|
||||
}
|
||||
|
||||
func (buffer *Buffer) IsDirty() bool {
|
||||
if !buffer.dirty {
|
||||
return false
|
||||
|
|
|
@ -73,4 +73,5 @@ type ColourScheme struct {
|
|||
LightMagenta Colour `toml:"light_magenta"`
|
||||
LightCyan Colour `toml:"light_cyan"`
|
||||
White Colour `toml:"white"`
|
||||
Selection Colour `toml:"selection"`
|
||||
}
|
||||
|
|
|
@ -22,5 +22,6 @@ var DefaultConfig = Config{
|
|||
LightMagenta: strToColourNoErr("#ff99a1"),
|
||||
LightCyan: strToColourNoErr("#9ed9d8"),
|
||||
White: strToColourNoErr("#f6f6c9"),
|
||||
Selection: strToColourNoErr("#333366"),
|
||||
},
|
||||
}
|
||||
|
|
13
gui/gui.go
13
gui/gui.go
|
@ -29,6 +29,7 @@ type GUI struct {
|
|||
fontScale float32
|
||||
renderer *OpenGLRenderer
|
||||
colourAttr uint32
|
||||
mouseDown bool
|
||||
}
|
||||
|
||||
func New(config *config.Config, terminal *terminal.Terminal, logger *zap.SugaredLogger) *GUI {
|
||||
|
@ -134,6 +135,7 @@ func (gui *GUI) Render() error {
|
|||
gui.window.SetCharCallback(gui.char)
|
||||
gui.window.SetScrollCallback(gui.glfwScrollCallback)
|
||||
gui.window.SetMouseButtonCallback(gui.mouseButtonCallback)
|
||||
gui.window.SetCursorPosCallback(gui.mouseMoveCallback)
|
||||
gui.window.SetRefreshCallback(func(w *glfw.Window) {
|
||||
gui.terminal.SetDirty()
|
||||
})
|
||||
|
@ -224,9 +226,16 @@ func (gui *GUI) Render() error {
|
|||
cy = cy + uint(gui.terminal.GetScrollOffset())
|
||||
cursor = cx == uint(x) && cy == uint(y)
|
||||
}
|
||||
gui.renderer.DrawCellBg(cell, uint(x), uint(y), cursor)
|
||||
|
||||
var colour *config.Colour
|
||||
|
||||
if gui.terminal.ActiveBuffer().InSelection(uint16(x), uint16(y)) {
|
||||
colour = &gui.config.ColourScheme.Selection
|
||||
}
|
||||
|
||||
gui.renderer.DrawCellBg(cell, uint(x), uint(y), cursor, colour)
|
||||
if hasText {
|
||||
gui.renderer.DrawCellText(cell, uint(x), uint(y))
|
||||
gui.renderer.DrawCellText(cell, uint(x), uint(y), nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,6 +36,9 @@ func (gui *GUI) key(w *glfw.Window, key glfw.Key, scancode int, action glfw.Acti
|
|||
case modsPressed(mods, glfw.ModControl, glfw.ModShift):
|
||||
modStr = "6"
|
||||
switch key {
|
||||
case glfw.KeyC:
|
||||
gui.window.SetClipboardString(gui.terminal.ActiveBuffer().GetSelectedText())
|
||||
return
|
||||
case glfw.KeyV:
|
||||
if s, err := gui.window.GetClipboardString(); err == nil {
|
||||
_ = gui.terminal.Paste([]byte(s))
|
||||
|
|
35
gui/mouse.go
35
gui/mouse.go
|
@ -8,8 +8,32 @@ import (
|
|||
"github.com/liamg/aminal/terminal"
|
||||
)
|
||||
|
||||
func (gui *GUI) mouseMoveCallback(w *glfw.Window, xpos float64, ypos float64) {
|
||||
|
||||
if gui.mouseDown {
|
||||
px, py := w.GetCursorPos()
|
||||
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())))
|
||||
gui.terminal.ActiveBuffer().EndSelection(x, y, false)
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *GUI) mouseButtonCallback(w *glfw.Window, button glfw.MouseButton, action glfw.Action, mod glfw.ModifierKey) {
|
||||
|
||||
// before we forward clicks on (below), we need to handle them locally for url clicking, text highlighting etc.
|
||||
px, py := w.GetCursorPos()
|
||||
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
|
||||
ty := int(y) + 1
|
||||
|
||||
if action == glfw.Press {
|
||||
gui.mouseDown = true
|
||||
gui.terminal.ActiveBuffer().StartSelection(x, y)
|
||||
} else if action == glfw.Release {
|
||||
gui.mouseDown = false
|
||||
gui.terminal.ActiveBuffer().EndSelection(x, y, true)
|
||||
}
|
||||
// https://www.xfree86.org/4.8.0/ctlseqs.html
|
||||
|
||||
/*
|
||||
|
@ -40,10 +64,7 @@ func (gui *GUI) mouseButtonCallback(w *glfw.Window, button glfw.MouseButton, act
|
|||
|
||||
if action == glfw.Press {
|
||||
b := rune(button)
|
||||
px, py := w.GetCursorPos()
|
||||
x := int(math.Floor(px/float64(gui.renderer.CellWidth()))) + 1
|
||||
y := int(math.Floor(py/float64(gui.renderer.CellHeight()))) + 1
|
||||
packet := fmt.Sprintf("\x1b[M%c%c%c", (rune(b + 32)), (rune(x + 32)), (rune(y + 32)))
|
||||
packet := fmt.Sprintf("\x1b[M%c%c%c", (rune(b + 32)), (rune(tx + 32)), (rune(ty + 32)))
|
||||
|
||||
gui.terminal.Write([]byte(packet))
|
||||
}
|
||||
|
@ -96,10 +117,8 @@ func (gui *GUI) mouseButtonCallback(w *glfw.Window, button glfw.MouseButton, act
|
|||
if mod&glfw.ModControl > 0 {
|
||||
b |= 16
|
||||
}
|
||||
px, py := w.GetCursorPos()
|
||||
x := int(math.Floor(px/float64(gui.renderer.CellWidth()))) + 1
|
||||
y := int(math.Floor(py/float64(gui.renderer.CellHeight()))) + 1
|
||||
packet := fmt.Sprintf("\x1b[M%c%c%c", (rune(b + 32)), (rune(x + 32)), (rune(y + 32)))
|
||||
|
||||
packet := fmt.Sprintf("\x1b[M%c%c%c", (rune(b + 32)), (rune(tx + 32)), (rune(ty + 32)))
|
||||
gui.logger.Infof("Sending mouse packet: '%v'", packet)
|
||||
gui.terminal.Write([]byte(packet))
|
||||
|
||||
|
|
|
@ -192,16 +192,21 @@ func (r *OpenGLRenderer) DrawCursor(col uint, row uint, colour config.Colour) {
|
|||
rect.Draw()
|
||||
}
|
||||
|
||||
func (r *OpenGLRenderer) DrawCellBg(cell buffer.Cell, col uint, row uint, cursor bool) {
|
||||
func (r *OpenGLRenderer) DrawCellBg(cell buffer.Cell, col uint, row uint, cursor bool, colour *config.Colour) {
|
||||
|
||||
var bg [3]float32
|
||||
|
||||
if cursor {
|
||||
bg = r.config.ColourScheme.Cursor
|
||||
} else if cell.Attr().Reverse {
|
||||
bg = cell.Fg()
|
||||
if colour != nil {
|
||||
bg = *colour
|
||||
} else {
|
||||
bg = cell.Bg()
|
||||
|
||||
if cursor {
|
||||
bg = r.config.ColourScheme.Cursor
|
||||
} else if cell.Attr().Reverse {
|
||||
bg = cell.Fg()
|
||||
} else {
|
||||
bg = cell.Bg()
|
||||
}
|
||||
}
|
||||
|
||||
if bg != r.config.ColourScheme.Background {
|
||||
|
@ -211,14 +216,18 @@ func (r *OpenGLRenderer) DrawCellBg(cell buffer.Cell, col uint, row uint, cursor
|
|||
}
|
||||
}
|
||||
|
||||
func (r *OpenGLRenderer) DrawCellText(cell buffer.Cell, col uint, row uint) {
|
||||
func (r *OpenGLRenderer) DrawCellText(cell buffer.Cell, col uint, row uint, colour *config.Colour) {
|
||||
|
||||
var fg [3]float32
|
||||
|
||||
if cell.Attr().Reverse {
|
||||
fg = cell.Bg()
|
||||
if colour != nil {
|
||||
fg = *colour
|
||||
} else {
|
||||
fg = cell.Fg()
|
||||
if cell.Attr().Reverse {
|
||||
fg = cell.Bg()
|
||||
} else {
|
||||
fg = cell.Fg()
|
||||
}
|
||||
}
|
||||
|
||||
var alpha float32 = 1
|
||||
|
@ -227,12 +236,12 @@ func (r *OpenGLRenderer) DrawCellText(cell buffer.Cell, col uint, row uint) {
|
|||
}
|
||||
r.font.SetColor(fg[0], fg[1], fg[2], alpha)
|
||||
|
||||
x := float32(col) * r.cellWidth
|
||||
y := (float32(row+1) * r.cellHeight) - (r.font.LinePadding())
|
||||
x := float32(r.areaX) + float32(col)*r.cellWidth
|
||||
y := float32(r.areaY) + (float32(row+1) * r.cellHeight) - (r.font.LinePadding())
|
||||
|
||||
if cell.Attr().Bold { // bold means draw text again one pixel to right, so it's fatter
|
||||
if r.boldFont != nil {
|
||||
y := (float32(row+1) * r.cellHeight) - (r.boldFont.LinePadding())
|
||||
y := float32(r.areaY) + (float32(row+1) * r.cellHeight) - (r.boldFont.LinePadding())
|
||||
r.boldFont.SetColor(fg[0], fg[1], fg[2], alpha)
|
||||
r.boldFont.Print(x, y, string(cell.Rune()))
|
||||
return
|
||||
|
|
|
@ -62,11 +62,6 @@ type Winsize struct {
|
|||
y uint16 //ignored, but necessary for ioctl calls
|
||||
}
|
||||
|
||||
type Position struct {
|
||||
Line int
|
||||
Col int
|
||||
}
|
||||
|
||||
func New(pty *os.File, logger *zap.SugaredLogger, config *config.Config) *Terminal {
|
||||
|
||||
return &Terminal{
|
||||
|
@ -92,7 +87,6 @@ func New(pty *os.File, logger *zap.SugaredLogger, config *config.Config) *Termin
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
func (terminal *Terminal) SetBracketedPasteMode(enabled bool) {
|
||||
terminal.bracketedPasteMode = enabled
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue