TUI Library written in Go
Files | Log | Commits | Refs | README
Author: SM
Date: 2025-09-03
Subject: SGR mouse format sends separate events for press and release
commit d8147fcfdb69e80eaf0839aa75a1e467fd58990b Author: SM <seb.michalk@gmail.com> Date: Wed Sep 3 20:52:39 2025 +0200 SGR mouse format sends separate events for press and release diff --git a/example.go b/example.go index 10a7b0a..69fe985 100644 --- a/example.go +++ b/example.go @@ -9,7 +9,7 @@ import ( "runtime" "strings" "syscall" - tb "tb-example/tinybox" + tb "tinybox-example/tinybox" "time" ) diff --git a/go.mod b/go.mod index dc4ab72..de0a27d 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ -module tb-example +module tinybox-example go 1.24.4 diff --git a/tinybox/tb.go b/tinybox/tb.go index fc15e55..0ec37cc 100644 --- a/tinybox/tb.go +++ b/tinybox/tb.go @@ -20,7 +20,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -/* tinybox - minimal tui library */ +/* tinybox */ package tb @@ -28,6 +28,8 @@ import ( "fmt" "os" "os/signal" + "strconv" + "strings" "syscall" "time" "unsafe" @@ -134,6 +136,7 @@ type Event struct { Y int Button MouseButton Mod KeyMod + Press bool } type EventType int @@ -616,6 +619,72 @@ func PollEventTimeout(timeout time.Duration) (Event, error) { return PollEvent() } +func parseSGRMouse(buf []byte) (Event, error) { + // SGR format: \033[<button;x;y[Mm] + if len(buf) < 9 || buf[0] != 27 || buf[1] != '[' || buf[2] != '<' { + return Event{}, fmt.Errorf("not SGR mouse format") + } + + endIdx := -1 + press := false + for i := 3; i < len(buf); i++ { + if buf[i] == 'M' { + endIdx = i + press = true + break + } else if buf[i] == 'm' { + endIdx = i + press = false + break + } + } + + if endIdx == -1 { + return Event{}, fmt.Errorf("no SGR terminator found") + } + + params := string(buf[3:endIdx]) + parts := strings.Split(params, ";") + if len(parts) != 3 { + return Event{}, fmt.Errorf("invalid SGR parameter count") + } + + button, err := strconv.Atoi(parts[0]) + if err != nil { + return Event{}, fmt.Errorf("invalid button: %v", err) + } + + x, err := strconv.Atoi(parts[1]) + if err != nil { + return Event{}, fmt.Errorf("invalid x: %v", err) + } + + y, err := strconv.Atoi(parts[2]) + if err != nil { + return Event{}, fmt.Errorf("invalid y: %v", err) + } + + var mouseButton MouseButton + switch button & 3 { + case 0: + mouseButton = MouseLeft + case 1: + mouseButton = MouseMiddle + case 2: + mouseButton = MouseRight + } + + if button >= 64 { + if button&1 != 0 { + mouseButton = MouseWheelDown + } else { + mouseButton = MouseWheelUp + } + } + + return Event{Type: EventMouse, Button: mouseButton, X: x - 1, Y: y - 1, Press: press}, nil +} + func parseInput(buf []byte) (Event, error) { if len(buf) == 0 { return Event{}, fmt.Errorf("no input") @@ -627,6 +696,11 @@ func parseInput(buf []byte) (Event, error) { if len(buf) == 1 { return Event{Type: EventKey, Key: KeyEscape}, nil } + if len(buf) >= 6 && buf[1] == '[' && buf[2] == '<' { + if evt, err := parseSGRMouse(buf); err == nil { + return evt, nil + } + } if len(buf) >= 3 && buf[1] == '[' { switch buf[2] { case 'A': @@ -727,7 +801,7 @@ func parseMouseEvent(buf []byte) (Event, error) { } } - return Event{Type: EventMouse, Button: button, X: x, Y: y}, nil + return Event{Type: EventMouse, Button: button, X: x, Y: y, Press: true}, nil } func EnableMouse() { @@ -1032,11 +1106,11 @@ func FlushInput() { } func SaveBuffer() { - needRealloc := term.savedBuffer == nil || + needRealloc := term.savedBuffer == nil || len(term.savedBuffer) != term.height || (len(term.savedBuffer) > 0 && len(term.savedBuffer[0]) != term.width) - - if needRealloc { + + if needRealloc { term.savedBuffer = make([][]Cell, term.height) for i := range term.savedBuffer { term.savedBuffer[i] = make([]Cell, term.width)