package session // These tests pin the behavior of the unversioned charmbracelet/x/vt // dependency. If an upstream bump changes input encoding or mode-callback // semantics, they fail loudly instead of termd silently emitting wrong bytes. import ( "io" "sync" "testing" "time" uv "github.com/charmbracelet/ultraviolet" "github.com/charmbracelet/x/ansi" "github.com/charmbracelet/x/vt" ) // collector continuously drains the emulator's encoded-input pipe. It must be // running before any Send* call: the pipe is synchronous, so an unread write // blocks forever (the daemon's pump goroutine plays this role in production). type collector struct { mu sync.Mutex buf []byte } func collect(em *vt.Emulator) *collector { c := &collector{} go func() { b := make([]byte, 4096) for { n, err := em.Read(b) if n > 0 { c.mu.Lock() c.buf = append(c.buf, b[:n]...) c.mu.Unlock() } if err != nil { return } } }() return c } // expect polls until the collected bytes equal want, then resets the buffer. func (c *collector) expect(t *testing.T, label, want string) { t.Helper() deadline := time.Now().Add(2 * time.Second) for { c.mu.Lock() got := string(c.buf) c.mu.Unlock() if got == want { c.mu.Lock() c.buf = nil c.mu.Unlock() return } if time.Now().After(deadline) { t.Fatalf("%s: got %q, want %q", label, got, want) } time.Sleep(5 * time.Millisecond) } } // expectNothing asserts no bytes arrive within a settle window. func (c *collector) expectNothing(t *testing.T, label string) { t.Helper() time.Sleep(50 * time.Millisecond) c.mu.Lock() got := string(c.buf) c.mu.Unlock() if got != "" { t.Fatalf("%s: got %q, want nothing", label, got) } } func TestVTKeyEncodingRespectsDECCKM(t *testing.T) { em := vt.NewEmulator(80, 24) c := collect(em) em.SendKey(uv.KeyPressEvent{Code: uv.KeyUp}) em.SendKey(uv.KeyPressEvent{Code: 'c', Mod: uv.ModCtrl}) em.SendKey(uv.KeyPressEvent{Code: uv.KeyEnter}) c.expect(t, "normal mode", "\x1b[A\x03\r") // Application cursor keys (DECSET ?1) switches arrows to SS3. if _, err := io.WriteString(em, "\x1b[?1h"); err != nil { t.Fatal(err) } em.SendKey(uv.KeyPressEvent{Code: uv.KeyUp}) c.expect(t, "DECCKM mode", "\x1bOA") } func TestVTMouseEncoding(t *testing.T) { em := vt.NewEmulator(80, 24) c := collect(em) // No tracking mode: SendMouse must emit nothing (termd's 409 pre-check // assumes this, but must not depend on it). em.SendMouse(uv.MouseClickEvent{X: 1, Y: 1, Button: uv.MouseLeft}) c.expectNothing(t, "mouse without tracking mode") // SGR encoding (?1000 + ?1006): CSI < b;x+1;y+1 M/m, 1-based cells. if _, err := io.WriteString(em, "\x1b[?1000h\x1b[?1006h"); err != nil { t.Fatal(err) } em.SendMouse(uv.MouseClickEvent{X: 10, Y: 5, Button: uv.MouseLeft}) em.SendMouse(uv.MouseReleaseEvent{X: 10, Y: 5, Button: uv.MouseLeft}) c.expect(t, "SGR click", "\x1b[<0;11;6M\x1b[<0;11;6m") em.SendMouse(uv.MouseWheelEvent{X: 0, Y: 0, Button: uv.MouseWheelUp}) c.expect(t, "SGR wheel", "\x1b[<64;1;1M") // Legacy X10 encoding when ?1006 is off. if _, err := io.WriteString(em, "\x1b[?1006l"); err != nil { t.Fatal(err) } em.SendMouse(uv.MouseClickEvent{X: 2, Y: 3, Button: uv.MouseLeft}) c.expect(t, "X10 click", "\x1b[M\x20\x23\x24") } func TestVTModeCallbacks(t *testing.T) { em := vt.NewEmulator(80, 24) var ms modeState ms.cursorVisible = true altScreen := false em.SetCallbacks(vt.Callbacks{ EnableMode: func(m ansi.Mode) { ms.set(m, true) }, DisableMode: func(m ansi.Mode) { ms.set(m, false) }, AltScreen: func(on bool) { altScreen = on }, }) if _, err := io.WriteString(em, "\x1b[?1049h\x1b[?1h\x1b[?1002h\x1b[?1006h\x1b[?2004h"); err != nil { t.Fatal(err) } if !altScreen { t.Error("alt screen callback did not fire on ?1049h") } if got := ms.mouseMode(); got != "button_event" { t.Errorf("mouse mode = %q, want button_event", got) } if !ms.sgrExt || !ms.appCursorKeys || !ms.bracketedPaste { t.Errorf("modes not all tracked: %+v", ms) } if _, err := io.WriteString(em, "\x1b[?1002l\x1b[?1049l"); err != nil { t.Fatal(err) } if altScreen { t.Error("alt screen still on after ?1049l") } if got := ms.mouseMode(); got != "off" { t.Errorf("mouse mode after disable = %q, want off", got) } }