From 86627135dd4c506bf37a53b85e07657f6452fad6 Mon Sep 17 00:00:00 2001 From: rrrooommmaaa Date: Tue, 26 Feb 2019 16:14:51 +0300 Subject: [PATCH] #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 --- gui/gui.go | 3 + gui/mouse.go | 212 +++++++++++++++++++++++++++++-------------- terminal/csi.go | 8 +- terminal/modes.go | 95 +++++++++++++++++-- terminal/terminal.go | 13 +++ 5 files changed, 249 insertions(+), 82 deletions(-) diff --git a/gui/gui.go b/gui/gui.go index ea4c2a7..2d968c6 100644 --- a/gui/gui.go +++ b/gui/gui.go @@ -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 diff --git a/gui/mouse.go b/gui/mouse.go index 5113b51..3c8962d 100644 --- a/gui/mouse.go +++ b/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)) +} diff --git a/terminal/csi.go b/terminal/csi.go index 8756ff2..303dddd 100644 --- a/terminal/csi.go +++ b/terminal/csi.go @@ -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 { diff --git a/terminal/modes.go b/terminal/modes.go index a851ddf..3630e44 100644 --- a/terminal/modes.go +++ b/terminal/modes.go @@ -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 diff --git a/terminal/terminal.go b/terminal/terminal.go index 3469636..1e117f0 100644 --- a/terminal/terminal.go +++ b/terminal/terminal.go @@ -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