Skip to content

Commit

Permalink
feat(ansi): define invalid mouse button behavior and add tests
Browse files Browse the repository at this point in the history
  • Loading branch information
aymanbagabas committed Dec 9, 2024
1 parent 67128e2 commit 9ac5034
Show file tree
Hide file tree
Showing 2 changed files with 244 additions and 2 deletions.
9 changes: 7 additions & 2 deletions ansi/mouse.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -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 {
Expand All @@ -126,14 +128,17 @@ 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.
//
// CSI M Cb Cx Cy
//
// 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)
}

Expand Down
237 changes: 237 additions & 0 deletions ansi/mouse_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}

0 comments on commit 9ac5034

Please sign in to comment.