From 9ac50344d4964f466b7cf4505cde74e517c63340 Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Mon, 9 Dec 2024 18:49:28 -0500 Subject: [PATCH] feat(ansi): define invalid mouse button behavior and add tests --- ansi/mouse.go | 9 +- ansi/mouse_test.go | 237 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 244 insertions(+), 2 deletions(-) create mode 100644 ansi/mouse_test.go diff --git a/ansi/mouse.go b/ansi/mouse.go index 0dc380c3..09dfe62b 100644 --- a/ansi/mouse.go +++ b/ansi/mouse.go @@ -84,7 +84,7 @@ func (b MouseButton) String() string { // - The eighth bit indicates additional buttons. // // If button is [MouseRelease], and motion is false, this returns a release -// event. +// event. If button is undefined, this function returns 0xff. func (b MouseButton) Button(motion, shift, alt, ctrl bool) (m byte) { // mouse bit shifts const ( @@ -108,6 +108,8 @@ func (b MouseButton) Button(motion, shift, alt, ctrl bool) (m byte) { } else if b >= MouseBackward && b <= MouseButton11 { m = byte(b - MouseBackward) m |= bitAdd + } else { + m = 0xff // invalid button } if shift { @@ -126,6 +128,10 @@ func (b MouseButton) Button(motion, shift, alt, ctrl bool) (m byte) { return } +// x10Offset is the offset for X10 mouse events. +// See https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking +const x10Offset = 32 + // MouseX10 returns an escape sequence representing a mouse event in X10 mode. // Note that this requires the terminal support X10 mouse modes. // @@ -133,7 +139,6 @@ func (b MouseButton) Button(motion, shift, alt, ctrl bool) (m byte) { // // See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking func MouseX10(b byte, x, y int) string { - const x10Offset = 32 return "\x1b[M" + string(b+x10Offset) + string(byte(x)+x10Offset+1) + string(byte(y)+x10Offset+1) } diff --git a/ansi/mouse_test.go b/ansi/mouse_test.go new file mode 100644 index 00000000..35abfc08 --- /dev/null +++ b/ansi/mouse_test.go @@ -0,0 +1,237 @@ +package ansi + +import ( + "fmt" + "testing" +) + +func TestMouseButton(t *testing.T) { + type test struct { + name string + btn MouseButton + motion, shift, alt, ctrl bool + want byte + } + + cases := []test{ + { + name: "mouse release", + btn: MouseRelease, + want: 0b0000_0011, + }, + { + name: "mouse left", + btn: MouseLeft, + want: 0b0000_0000, + }, + { + name: "mouse right", + btn: MouseRight, + want: 0b0000_0010, + }, + { + name: "mouse wheel up", + btn: MouseWheelUp, + want: 0b0100_0000, + }, + { + name: "mouse wheel right", + btn: MouseWheelRight, + want: 0b0100_0011, + }, + { + name: "mouse backward", + btn: MouseBackward, + want: 0b1000_0000, + }, + { + name: "mouse forward", + btn: MouseForward, + want: 0b1000_0001, + }, + { + name: "mouse button 10", + btn: MouseButton10, + want: 0b1000_0010, + }, + { + name: "mouse button 11", + btn: MouseButton11, + want: 0b1000_0011, + }, + { + name: "mouse middle with motion", + btn: MouseMiddle, + motion: true, + want: 0b0010_0001, + }, + { + name: "mouse middle with shift", + btn: MouseMiddle, + shift: true, + want: 0b0000_0101, + }, + { + name: "mouse middle with motion and alt", + btn: MouseMiddle, + motion: true, + alt: true, + want: 0b0010_1001, + }, + { + name: "mouse right with shift, alt, and ctrl", + btn: MouseRight, + shift: true, + alt: true, + ctrl: true, + want: 0b0001_1110, + }, + { + name: "mouse button 10 with motion, shift, alt, and ctrl", + btn: MouseButton10, + motion: true, + shift: true, + alt: true, + ctrl: true, + want: 0b1011_1110, + }, + { + name: "mouse left with motion, shift, and ctrl", + btn: MouseLeft, + motion: true, + shift: true, + ctrl: true, + want: 0b0011_0100, + }, + { + name: "invalid mouse button", + btn: MouseButton(0xff), + want: 0b1111_1111, + }, + { + name: "mouse wheel down with motion", + btn: MouseWheelDown, + motion: true, + want: 0b0110_0001, + }, + { + name: "mouse wheel down with shift and ctrl", + btn: MouseWheelDown, + shift: true, + ctrl: true, + want: 0b0101_0101, + }, + { + name: "mouse wheel left with alt", + btn: MouseWheelLeft, + alt: true, + want: 0b0100_1010, + }, + { + name: "mouse middle with all modifiers", + btn: MouseMiddle, + motion: true, + shift: true, + alt: true, + ctrl: true, + want: 0b0011_1101, + }, + } + + for i, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + got := tc.btn.Button(tc.motion, tc.shift, tc.alt, tc.ctrl) + if got != tc.want { + t.Errorf("test %d: got %08b; want %08b", i+1, got, tc.want) + } + }) + } +} + +func TestMouseSgr(t *testing.T) { + type test struct { + name string + btn byte + x, y int + release bool + } + + cases := []test{ + { + name: "mouse left", + btn: MouseLeft.Button(false, false, false, false), + x: 0, + y: 0, + }, + { + name: "wheel down", + btn: MouseWheelDown.Button(false, false, false, false), + x: 1, + y: 10, + }, + { + name: "mouse right with shift, alt, and ctrl", + btn: MouseRight.Button(false, true, true, true), + x: 10, + y: 1, + }, + { + name: "mouse release", + btn: MouseRelease.Button(false, false, false, false), + x: 5, + y: 5, + release: true, + }, + { + name: "mouse button 10 with motion, shift, alt, and ctrl", + btn: MouseButton10.Button(true, true, true, true), + x: 10, + y: 10, + }, + { + name: "mouse wheel up with motion", + btn: MouseWheelUp.Button(true, false, false, false), + x: 15, + y: 15, + }, + { + name: "mouse middle with all modifiers", + btn: MouseMiddle.Button(true, true, true, true), + x: 20, + y: 20, + }, + { + name: "mouse wheel left at max coordinates", + btn: MouseWheelLeft.Button(false, false, false, false), + x: 223, + y: 223, + }, + { + name: "mouse forward release", + btn: MouseForward.Button(false, false, false, false), + x: 100, + y: 100, + release: true, + }, + { + name: "mouse backward with shift and ctrl", + btn: MouseBackward.Button(false, true, false, true), + x: 50, + y: 50, + }, + } + + for i, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + m := MouseSgr(tc.btn, tc.x, tc.y, tc.release) + action := 'M' + if tc.release { + action = 'm' + } + want := fmt.Sprintf("\x1b[<%d;%d;%d%c", tc.btn, tc.x+1, tc.y+1, action) + if m != want { + t.Errorf("test %d: got %q; want %q", i+1, m, want) + } + }) + } +}