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)