mirror of https://github.com/liamg/aminal.git
#180/#234: CSI SM/RM multiple modes, Mouse button-tracking (#233)
* #180: added support for several modes specification in CSI SM/RM * added Button Mouse Mode and SGR encoding
This commit is contained in:
parent
8c4c842ff1
commit
86627135dd
|
@ -40,6 +40,7 @@ type GUI struct {
|
|||
renderer *OpenGLRenderer
|
||||
colourAttr uint32
|
||||
mouseDown bool
|
||||
mouseDownModifier glfw.ModifierKey
|
||||
overlay overlay
|
||||
terminalAlpha float32
|
||||
showDebugInfo bool
|
||||
|
@ -51,6 +52,8 @@ type GUI struct {
|
|||
|
||||
prevLeftClickX uint16
|
||||
prevLeftClickY uint16
|
||||
prevMotionTX int
|
||||
prevMotionTY int
|
||||
leftClickTime time.Time
|
||||
leftClickCount int // number of clicks in a serie - single click, double click, or triple click
|
||||
mouseMovedAfterSelectionStarted bool
|
||||
|
|
212
gui/mouse.go
212
gui/mouse.go
|
@ -4,10 +4,11 @@ import (
|
|||
"fmt"
|
||||
"math"
|
||||
|
||||
"time"
|
||||
|
||||
"github.com/go-gl/glfw/v3.2/glfw"
|
||||
"github.com/liamg/aminal/buffer"
|
||||
"github.com/liamg/aminal/terminal"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (gui *GUI) glfwScrollCallback(w *glfw.Window, xoff float64, yoff float64) {
|
||||
|
@ -40,7 +41,13 @@ func (gui *GUI) mouseMoveCallback(w *glfw.Window, px float64, py float64) {
|
|||
x, y := gui.convertMouseCoordinates(px, py)
|
||||
|
||||
if gui.mouseDown {
|
||||
gui.terminal.ActiveBuffer().ExtendSelection(x, y, false)
|
||||
if gui.terminal.GetMouseMode() == terminal.MouseModeButtonEvent {
|
||||
tx := int(x) + 1 // vt100 is 1 indexed
|
||||
ty := int(y) + 1
|
||||
gui.emitButtonEventToTerminal(tx, ty, glfw.MouseButtonLeft, nil, gui.mouseDownModifier)
|
||||
} else {
|
||||
gui.terminal.ActiveBuffer().ExtendSelection(x, y, false)
|
||||
}
|
||||
} else {
|
||||
|
||||
hint := gui.terminal.ActiveBuffer().GetHintAtPosition(x, y)
|
||||
|
@ -87,6 +94,34 @@ func (gui *GUI) updateLeftClickCount(x uint16, y uint16) int {
|
|||
return gui.leftClickCount
|
||||
}
|
||||
|
||||
func btnCode(button glfw.MouseButton, release bool, mod glfw.ModifierKey) (b byte, ok bool) {
|
||||
if release {
|
||||
b = 3
|
||||
} else {
|
||||
switch button {
|
||||
case glfw.MouseButton1:
|
||||
b = 0
|
||||
case glfw.MouseButton2:
|
||||
b = 1
|
||||
case glfw.MouseButton3:
|
||||
b = 2
|
||||
default:
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
|
||||
if mod&glfw.ModShift > 0 {
|
||||
b |= 4
|
||||
}
|
||||
if mod&glfw.ModSuper > 0 {
|
||||
b |= 8
|
||||
}
|
||||
if mod&glfw.ModControl > 0 {
|
||||
b |= 16
|
||||
}
|
||||
return b, true
|
||||
}
|
||||
|
||||
func (gui *GUI) mouseButtonCallback(w *glfw.Window, button glfw.MouseButton, action glfw.Action, mod glfw.ModifierKey) {
|
||||
|
||||
if gui.overlay != nil {
|
||||
|
@ -101,56 +136,28 @@ func (gui *GUI) mouseButtonCallback(w *glfw.Window, button glfw.MouseButton, act
|
|||
tx := int(x) + 1 // vt100 is 1 indexed
|
||||
ty := int(y) + 1
|
||||
|
||||
activeBuffer := gui.terminal.ActiveBuffer()
|
||||
|
||||
switch button {
|
||||
case glfw.MouseButtonLeft:
|
||||
if action == glfw.Press {
|
||||
gui.mouseDownModifier = mod
|
||||
gui.mouseDown = true
|
||||
|
||||
clickCount := gui.updateLeftClickCount(x, y)
|
||||
switch clickCount {
|
||||
case 1:
|
||||
activeBuffer.StartSelection(x, y, buffer.SelectionChar)
|
||||
case 2:
|
||||
activeBuffer.StartSelection(x, y, buffer.SelectionWord)
|
||||
case 3:
|
||||
activeBuffer.StartSelection(x, y, buffer.SelectionLine)
|
||||
if gui.terminal.GetMouseMode() != terminal.MouseModeButtonEvent {
|
||||
gui.handleSelectionButtonPress(x, y)
|
||||
}
|
||||
gui.mouseMovedAfterSelectionStarted = false
|
||||
|
||||
} else if action == glfw.Release {
|
||||
gui.mouseDown = false
|
||||
|
||||
if x != gui.prevLeftClickX || y != gui.prevLeftClickY {
|
||||
gui.mouseMovedAfterSelectionStarted = true
|
||||
}
|
||||
|
||||
if gui.leftClickCount != 1 || gui.mouseMovedAfterSelectionStarted {
|
||||
activeBuffer.ExtendSelection(x, y, true)
|
||||
}
|
||||
|
||||
// Do copy to clipboard *or* open URL, but not both.
|
||||
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)
|
||||
}
|
||||
if gui.terminal.GetMouseMode() != terminal.MouseModeButtonEvent {
|
||||
gui.handleSelectionButtonRelease(x, y)
|
||||
}
|
||||
}
|
||||
|
||||
case glfw.MouseButtonRight:
|
||||
if gui.config.CopyAndPasteWithMouse && action == glfw.Press {
|
||||
if gui.config.CopyAndPasteWithMouse && action == glfw.Press && gui.terminal.GetMouseMode() == terminal.MouseModeNone {
|
||||
str, err := gui.window.GetClipboardString()
|
||||
if err == nil {
|
||||
activeBuffer := gui.terminal.ActiveBuffer()
|
||||
activeBuffer.ClearSelection()
|
||||
_ = gui.terminal.Paste([]byte(str))
|
||||
}
|
||||
|
@ -214,36 +221,7 @@ func (gui *GUI) mouseButtonCallback(w *glfw.Window, button glfw.MouseButton, act
|
|||
|
||||
Wheel mice may return buttons 4 and 5. Those buttons are represented by the same event codes as buttons 1 and 2 respectively, except that 64 is added to the event code. Release events for the wheel buttons are not reported.
|
||||
*/
|
||||
var b byte
|
||||
if action == glfw.Press {
|
||||
switch button {
|
||||
case glfw.MouseButton1:
|
||||
b = 0
|
||||
case glfw.MouseButton2:
|
||||
b = 1
|
||||
case glfw.MouseButton3:
|
||||
b = 2
|
||||
default:
|
||||
return
|
||||
}
|
||||
} else if action == glfw.Release {
|
||||
b = 3
|
||||
} else {
|
||||
return
|
||||
}
|
||||
if mod&glfw.ModShift > 0 {
|
||||
b |= 4
|
||||
}
|
||||
if mod&glfw.ModSuper > 0 {
|
||||
b |= 8
|
||||
}
|
||||
if mod&glfw.ModControl > 0 {
|
||||
b |= 16
|
||||
}
|
||||
|
||||
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))
|
||||
gui.emitButtonEventToTerminal(tx, ty, button, &action, mod)
|
||||
|
||||
case terminal.MouseModeVT200Highlight:
|
||||
/*
|
||||
|
@ -253,9 +231,15 @@ func (gui *GUI) mouseButtonCallback(w *glfw.Window, button glfw.MouseButton, act
|
|||
|
||||
case terminal.MouseModeButtonEvent:
|
||||
/*
|
||||
Button-event tracking is essentially the same as normal tracking, but xterm also reports button-motion events. Motion events are reported only if the mouse pointer has moved to a different character cell. It is enabled by specifying parameter 1002 to DECSET. On button press or release, xterm sends the same codes used by normal tracking mode. On button-motion events, xterm adds 32 to the event code (the third character, C b ). The other bits of the event code specify button and modifier keys as in normal mode. For example, motion into cell x,y with button 1 down is reported as CSI M @ C x C y . ( @ = 32 + 0 (button 1) + 32 (motion indicator) ). Similarly, motion with button 3 down is reported as CSI M B C x C y . ( B = 32 + 2 (button 3) + 32 (motion indicator) ).
|
||||
Button-event tracking is essentially the same as normal tracking, but xterm also reports button-motion events.
|
||||
Motion events are reported only if the mouse pointer has moved to a different character cell. It is enabled by specifying parameter 1002 to DECSET.
|
||||
On button press or release, xterm sends the same codes used by normal tracking mode.
|
||||
On button-motion events, xterm adds 32 to the event code (the third character, C b ).
|
||||
The other bits of the event code specify button and modifier keys as in normal mode.
|
||||
For example, motion into cell x,y with button 1 down is reported as CSI M @ C x C y . ( @ = 32 + 0 (button 1) + 32 (motion indicator) ).
|
||||
Similarly, motion with button 3 down is reported as CSI M B C x C y . ( B = 32 + 2 (button 3) + 32 (motion indicator) ).
|
||||
*/
|
||||
panic("Mouse button event mode not supported")
|
||||
gui.emitButtonEventToTerminal(tx, ty, button, &action, mod)
|
||||
|
||||
case terminal.MouseModeAnyEvent:
|
||||
/*
|
||||
|
@ -270,3 +254,93 @@ func (gui *GUI) mouseButtonCallback(w *glfw.Window, button glfw.MouseButton, act
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
func (gui *GUI) handleSelectionButtonPress(x uint16, y uint16) {
|
||||
activeBuffer := gui.terminal.ActiveBuffer()
|
||||
clickCount := gui.updateLeftClickCount(x, y)
|
||||
switch clickCount {
|
||||
case 1:
|
||||
activeBuffer.StartSelection(x, y, buffer.SelectionChar)
|
||||
case 2:
|
||||
activeBuffer.StartSelection(x, y, buffer.SelectionWord)
|
||||
case 3:
|
||||
activeBuffer.StartSelection(x, y, buffer.SelectionLine)
|
||||
}
|
||||
gui.mouseMovedAfterSelectionStarted = false
|
||||
}
|
||||
|
||||
func (gui *GUI) handleSelectionButtonRelease(x uint16, y uint16) {
|
||||
activeBuffer := gui.terminal.ActiveBuffer()
|
||||
if x != gui.prevLeftClickX || y != gui.prevLeftClickY {
|
||||
gui.mouseMovedAfterSelectionStarted = true
|
||||
}
|
||||
|
||||
if gui.leftClickCount != 1 || gui.mouseMovedAfterSelectionStarted {
|
||||
activeBuffer.ExtendSelection(x, y, true)
|
||||
}
|
||||
|
||||
// Do copy to clipboard *or* open URL, but not both.
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *GUI) emitButtonEventToTerminal(tx int, ty int, button glfw.MouseButton, action *glfw.Action, mod glfw.ModifierKey) {
|
||||
motion := action == nil
|
||||
|
||||
release := false
|
||||
if !motion {
|
||||
if *action == glfw.Release {
|
||||
release = true
|
||||
} else if *action != glfw.Press {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ext := gui.terminal.GetMouseExtMode()
|
||||
|
||||
// For SGR, normal button encoding (as for Press event)
|
||||
b, ok := btnCode(button, release && ext != terminal.MouseExtSGR, mod)
|
||||
|
||||
if !ok {
|
||||
return // unknown button
|
||||
}
|
||||
|
||||
// @todo check limits for non-SGR encoding
|
||||
|
||||
if motion {
|
||||
b |= 32
|
||||
|
||||
// after applying limits we can check the final values
|
||||
if tx == gui.prevMotionTX && ty == gui.prevMotionTY {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
gui.prevMotionTX = tx
|
||||
gui.prevMotionTY = ty
|
||||
|
||||
var packet string
|
||||
if ext == terminal.MouseExtSGR {
|
||||
final := 'M'
|
||||
if release {
|
||||
final = 'm'
|
||||
}
|
||||
packet = fmt.Sprintf("\x1b[<%d;%d;%d%c", b, tx, ty, final)
|
||||
} else {
|
||||
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))
|
||||
}
|
||||
|
|
|
@ -25,8 +25,8 @@ var csiSequences = []csiMapping{
|
|||
{id: 'd', handler: csiLinePositionAbsolute, expectedParams: &expectedParams{min: 0, max: 1}, description: "Line Position Absolute [row] (default = [1,column]) (VPA)"},
|
||||
{id: 'f', handler: csiCursorPositionHandler, description: "Horizontal and Vertical Position [row;column] (default = [1,1]) (HVP)"},
|
||||
{id: 'g', handler: csiTabClearHandler, description: "Tab Clear (TBC)"},
|
||||
{id: 'h', handler: csiSetModeHandler, expectedParams: &expectedParams{min: 1, max: 1}, description: "Set Mode (SM)"},
|
||||
{id: 'l', handler: csiResetModeHandler, expectedParams: &expectedParams{min: 1, max: 1}, description: "Reset Mode (RM)"},
|
||||
{id: 'h', handler: csiSetModeHandler, expectedParams: &expectedParams{min: 1, max: ^uint8(0)}, description: "Set Mode (SM)"},
|
||||
{id: 'l', handler: csiResetModeHandler, expectedParams: &expectedParams{min: 1, max: ^uint8(0)}, description: "Reset Mode (RM)"},
|
||||
{id: 'm', handler: sgrSequenceHandler, description: "Character Attributes (SGR)"},
|
||||
{id: 'n', handler: csiDeviceStatusReportHandler, description: "Device Status Report (DSR)"},
|
||||
{id: 'r', handler: csiSetMarginsHandler, expectedParams: &expectedParams{min: 0, max: 2}, description: "Set Scrolling Region [top;bottom] (default = full size of window) (DECSTBM), VT100"},
|
||||
|
@ -415,11 +415,11 @@ func csiEraseCharactersHandler(params []string, terminal *Terminal) error {
|
|||
|
||||
func csiResetModeHandler(params []string, terminal *Terminal) error {
|
||||
terminal.ActiveBuffer().ClearSelection()
|
||||
return csiSetMode(strings.Join(params, ""), false, terminal)
|
||||
return csiSetModes(params, false, terminal)
|
||||
}
|
||||
|
||||
func csiSetModeHandler(params []string, terminal *Terminal) error {
|
||||
return csiSetMode(strings.Join(params, ""), true, terminal)
|
||||
return csiSetModes(params, true, terminal)
|
||||
}
|
||||
|
||||
func csiWindowManipulation(params []string, terminal *Terminal) error {
|
||||
|
|
|
@ -1,6 +1,51 @@
|
|||
package terminal
|
||||
|
||||
import "fmt"
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func recoverCodeFromEnabled(enabled bool) string {
|
||||
code := ""
|
||||
if enabled {
|
||||
code = "h"
|
||||
} else {
|
||||
code = "l"
|
||||
}
|
||||
return code
|
||||
}
|
||||
|
||||
func csiSetModes(modes []string, enabled bool, terminal *Terminal) error {
|
||||
if len(modes) == 0 {
|
||||
return fmt.Errorf("CSI %s without parameters is not allowed", recoverCodeFromEnabled(enabled))
|
||||
}
|
||||
if len(modes) == 1 {
|
||||
return csiSetMode(modes[0], enabled, terminal)
|
||||
}
|
||||
// should we propagate DEC prefix?
|
||||
const decPrefix = '?'
|
||||
isDec := len(modes[0]) > 0 && modes[0][0] == decPrefix
|
||||
|
||||
// iterate through params, propagating DEC prefix to subsequent elements
|
||||
errorStrings := make([]string, 0)
|
||||
for i, v := range modes {
|
||||
updatedMode := v
|
||||
if i > 0 && isDec {
|
||||
updatedMode = string(decPrefix) + v
|
||||
}
|
||||
err := csiSetMode(updatedMode, enabled, terminal)
|
||||
if err != nil {
|
||||
errorStrings = append(errorStrings, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
if len(errorStrings) > 0 {
|
||||
return fmt.Errorf(strings.Join(errorStrings, "\n"))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func csiSetMode(modeStr string, enabled bool, terminal *Terminal) error {
|
||||
|
||||
|
@ -79,7 +124,7 @@ func csiSetMode(modeStr string, enabled bool, terminal *Terminal) error {
|
|||
} else {
|
||||
terminal.UseMainBuffer()
|
||||
}
|
||||
case "?1000", "?1006;1000", "?10061000": // ?10061000 seen from htop
|
||||
case "?1000", "?10061000": // ?10061000 seen from htop
|
||||
// enable mouse tracking
|
||||
// 1000 refers to ext mode for extended mouse click area - otherwise only x <= 255-31
|
||||
if enabled {
|
||||
|
@ -89,6 +134,44 @@ func csiSetMode(modeStr string, enabled bool, terminal *Terminal) error {
|
|||
terminal.logger.Infof("Turning off VT200 mouse mode")
|
||||
terminal.SetMouseMode(MouseModeNone)
|
||||
}
|
||||
case "?1002":
|
||||
if enabled {
|
||||
terminal.logger.Infof("Turning on Button Event mouse mode")
|
||||
terminal.SetMouseMode(MouseModeButtonEvent)
|
||||
} else {
|
||||
terminal.logger.Infof("Turning off Button Event mouse mode")
|
||||
terminal.SetMouseMode(MouseModeNone)
|
||||
}
|
||||
case "?1003":
|
||||
return errors.New("Any Event mouse mode is not supported")
|
||||
/*
|
||||
if enabled {
|
||||
terminal.logger.Infof("Turning on Any Event mouse mode")
|
||||
terminal.SetMouseMode(MouseModeAnyEvent)
|
||||
} else {
|
||||
terminal.logger.Infof("Turning off Any Event mouse mode")
|
||||
terminal.SetMouseMode(MouseModeNone)
|
||||
}
|
||||
*/
|
||||
case "?1005":
|
||||
return errors.New("UTF-8 ext mouse mode is not supported")
|
||||
/*
|
||||
if enabled {
|
||||
terminal.logger.Infof("Turning on UTF-8 ext mouse mode")
|
||||
terminal.SetMouseExtMode(MouseExtUTF)
|
||||
} else {
|
||||
terminal.logger.Infof("Turning off UTF-8 ext mouse mode")
|
||||
terminal.SetMouseExtMode(MouseExtNone)
|
||||
}
|
||||
*/
|
||||
case "?1006":
|
||||
if enabled {
|
||||
terminal.logger.Infof("Turning on SGR ext mouse mode")
|
||||
terminal.SetMouseExtMode(MouseExtSGR)
|
||||
} else {
|
||||
terminal.logger.Infof("Turning off SGR ext mouse mode")
|
||||
terminal.SetMouseExtMode(MouseExtNone)
|
||||
}
|
||||
case "?1048":
|
||||
if enabled {
|
||||
terminal.ActiveBuffer().SaveCursor()
|
||||
|
@ -104,13 +187,7 @@ func csiSetMode(modeStr string, enabled bool, terminal *Terminal) error {
|
|||
case "?2004":
|
||||
terminal.SetBracketedPasteMode(enabled)
|
||||
default:
|
||||
code := ""
|
||||
if enabled {
|
||||
code = "h"
|
||||
} else {
|
||||
code = "l"
|
||||
}
|
||||
return fmt.Errorf("Unsupported CSI %s%s code", modeStr, code)
|
||||
return fmt.Errorf("Unsupported CSI %s%s code", modeStr, recoverCodeFromEnabled(enabled))
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
@ -19,6 +19,7 @@ const (
|
|||
)
|
||||
|
||||
type MouseMode uint
|
||||
type MouseExtMode uint
|
||||
|
||||
const (
|
||||
MouseModeNone MouseMode = iota
|
||||
|
@ -27,6 +28,9 @@ const (
|
|||
MouseModeVT200Highlight
|
||||
MouseModeButtonEvent
|
||||
MouseModeAnyEvent
|
||||
MouseExtNone MouseExtMode = iota
|
||||
MouseExtUTF
|
||||
MouseExtSGR
|
||||
)
|
||||
|
||||
type Terminal struct {
|
||||
|
@ -44,6 +48,7 @@ type Terminal struct {
|
|||
reverseHandlers []chan bool
|
||||
modes Modes
|
||||
mouseMode MouseMode
|
||||
mouseExtMode MouseExtMode
|
||||
bracketedPasteMode bool
|
||||
isDirty bool
|
||||
charWidth float32
|
||||
|
@ -121,6 +126,14 @@ func (terminal *Terminal) GetMouseMode() MouseMode {
|
|||
return terminal.mouseMode
|
||||
}
|
||||
|
||||
func (terminal *Terminal) SetMouseExtMode(mode MouseExtMode) {
|
||||
terminal.mouseExtMode = mode
|
||||
}
|
||||
|
||||
func (terminal *Terminal) GetMouseExtMode() MouseExtMode {
|
||||
return terminal.mouseExtMode
|
||||
}
|
||||
|
||||
func (terminal *Terminal) IsOSCTerminator(char rune) bool {
|
||||
_, ok := terminal.platformDependentSettings.OSCTerminators[char]
|
||||
return ok
|
||||
|
|
Loading…
Reference in New Issue