#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:
rrrooommmaaa 2019-02-26 16:14:51 +03:00 committed by Liam Galvin
parent 8c4c842ff1
commit 86627135dd
5 changed files with 249 additions and 82 deletions

View File

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

View File

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

View File

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

View File

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

View File

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