156 lines
4.2 KiB
Go
156 lines
4.2 KiB
Go
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)
|
|
}
|
|
}
|