text selection and copy/paste

This commit is contained in:
Liam Galvin 2018-10-24 12:15:43 +01:00
parent 88f0731e76
commit 6df9ad1cf3
9 changed files with 259 additions and 34 deletions

View File

@ -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 |

View File

@ -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

View File

@ -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"`
}

View File

@ -22,5 +22,6 @@ var DefaultConfig = Config{
LightMagenta: strToColourNoErr("#ff99a1"),
LightCyan: strToColourNoErr("#9ed9d8"),
White: strToColourNoErr("#f6f6c9"),
Selection: strToColourNoErr("#333366"),
},
}

View File

@ -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)
}
}
}

View File

@ -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))

View File

@ -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))

View File

@ -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

View File

@ -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
}