feat(vt): add owned screen snapshots
- commit
- 0461ab84977fc8ed286bbd4707326d21e63803f4
- author
- brice · · 2026-07-28 14:06 UTC
- committer
- brice · · 2026-07-29 05:00 UTC
- parent
- 43eb2f4
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +258 -0
A pkg/vt/screen_snapshot.go +89 -0
| @@ -0,0 +1,89 @@ | |||
| 1 | + | package vt | |
| 2 | + | ||
| 3 | + | import "github.com/bnema/vev/pkg/renderer" | |
| 4 | + | ||
| 5 | + | // CursorSnapshot is the cursor state captured with a visible screen snapshot. | |
| 6 | + | type CursorSnapshot struct { | |
| 7 | + | Row int | |
| 8 | + | Col int | |
| 9 | + | Visible bool | |
| 10 | + | Style int | |
| 11 | + | StyleSet bool | |
| 12 | + | } | |
| 13 | + | ||
| 14 | + | // ModeSnapshot is the renderer-relevant VT mode state captured with a screen. | |
| 15 | + | type ModeSnapshot struct { | |
| 16 | + | AlternateScreen bool | |
| 17 | + | BracketedPaste bool | |
| 18 | + | SynchronizedUpdate bool | |
| 19 | + | ColorSchemeMode bool | |
| 20 | + | MouseTracking int | |
| 21 | + | MouseSGR bool | |
| 22 | + | } | |
| 23 | + | ||
| 24 | + | // ScreenSnapshot is an owned immutable-by-convention capture of the active | |
| 25 | + | // terminal viewport. Row returns caller-owned storage; BorrowedRow must not be | |
| 26 | + | // mutated and remains valid while the snapshot is retained. | |
| 27 | + | type ScreenSnapshot struct { | |
| 28 | + | frame renderer.Frame | |
| 29 | + | bounds []LineBound | |
| 30 | + | cursor CursorSnapshot | |
| 31 | + | modes ModeSnapshot | |
| 32 | + | title string | |
| 33 | + | } | |
| 34 | + | ||
| 35 | + | // Snapshot captures the active visible viewport without mutating Screen, | |
| 36 | + | // history, or pending damage. Screen remains single-owner; callers must | |
| 37 | + | // serialize Snapshot with Write, Resize, and host mutations. | |
| 38 | + | func (s *Screen) Snapshot() ScreenSnapshot { | |
| 39 | + | if s == nil { | |
| 40 | + | return ScreenSnapshot{} | |
| 41 | + | } | |
| 42 | + | cursorStyle, cursorStyleSet := s.CursorStyle() | |
| 43 | + | mouseTracking, mouseSGR := s.MouseMode() | |
| 44 | + | return ScreenSnapshot{ | |
| 45 | + | frame: s.Frame.Clone(), | |
| 46 | + | bounds: s.LineBounds(), | |
| 47 | + | cursor: CursorSnapshot{ | |
| 48 | + | Row: s.CursorRow(), | |
| 49 | + | Col: s.CursorCol(), | |
| 50 | + | Visible: s.CursorVisible(), | |
| 51 | + | Style: cursorStyle, | |
| 52 | + | StyleSet: cursorStyleSet, | |
| 53 | + | }, | |
| 54 | + | modes: ModeSnapshot{ | |
| 55 | + | AlternateScreen: s.AltScreenActive(), | |
| 56 | + | BracketedPaste: s.BracketedPasteMode(), | |
| 57 | + | SynchronizedUpdate: s.SyncUpdateActive(), | |
| 58 | + | ColorSchemeMode: s.ColorSchemeMode(), | |
| 59 | + | MouseTracking: mouseTracking, | |
| 60 | + | MouseSGR: mouseSGR, | |
| 61 | + | }, | |
| 62 | + | title: s.TerminalTitle(), | |
| 63 | + | } | |
| 64 | + | } | |
| 65 | + | ||
| 66 | + | func (s ScreenSnapshot) Columns() int { return s.frame.Width } | |
| 67 | + | func (s ScreenSnapshot) Rows() int { return s.frame.Height } | |
| 68 | + | ||
| 69 | + | func (s ScreenSnapshot) Row(y int) []renderer.Cell { | |
| 70 | + | return append([]renderer.Cell(nil), s.BorrowedRow(y)...) | |
| 71 | + | } | |
| 72 | + | ||
| 73 | + | func (s ScreenSnapshot) BorrowedRow(y int) []renderer.Cell { | |
| 74 | + | if y < 0 || y >= s.frame.Height { | |
| 75 | + | return nil | |
| 76 | + | } | |
| 77 | + | return s.frame.Row(y) | |
| 78 | + | } | |
| 79 | + | ||
| 80 | + | func (s ScreenSnapshot) Bound(y int) LineBound { | |
| 81 | + | if y < 0 || y >= len(s.bounds) { | |
| 82 | + | return LineBound{} | |
| 83 | + | } | |
| 84 | + | return s.bounds[y] | |
| 85 | + | } | |
| 86 | + | ||
| 87 | + | func (s ScreenSnapshot) Cursor() CursorSnapshot { return s.cursor } | |
| 88 | + | func (s ScreenSnapshot) Modes() ModeSnapshot { return s.modes } | |
| 89 | + | func (s ScreenSnapshot) Title() string { return s.title } | |
| @@ -0,0 +1,89 @@ | |||||
| 1 | + | package vt | |||
| 2 | + | ||||
| 3 | + | import "github.com/bnema/vev/pkg/renderer" | |||
| 4 | + | ||||
| 5 | + | // CursorSnapshot is the cursor state captured with a visible screen snapshot. | |||
| 6 | + | type CursorSnapshot struct { | |||
| 7 | + | Row int | |||
| 8 | + | Col int | |||
| 9 | + | Visible bool | |||
| 10 | + | Style int | |||
| 11 | + | StyleSet bool | |||
| 12 | + | } | |||
| 13 | + | ||||
| 14 | + | // ModeSnapshot is the renderer-relevant VT mode state captured with a screen. | |||
| 15 | + | type ModeSnapshot struct { | |||
| 16 | + | AlternateScreen bool | |||
| 17 | + | BracketedPaste bool | |||
| 18 | + | SynchronizedUpdate bool | |||
| 19 | + | ColorSchemeMode bool | |||
| 20 | + | MouseTracking int | |||
| 21 | + | MouseSGR bool | |||
| 22 | + | } | |||
| 23 | + | ||||
| 24 | + | // ScreenSnapshot is an owned immutable-by-convention capture of the active | |||
| 25 | + | // terminal viewport. Row returns caller-owned storage; BorrowedRow must not be | |||
| 26 | + | // mutated and remains valid while the snapshot is retained. | |||
| 27 | + | type ScreenSnapshot struct { | |||
| 28 | + | frame renderer.Frame | |||
| 29 | + | bounds []LineBound | |||
| 30 | + | cursor CursorSnapshot | |||
| 31 | + | modes ModeSnapshot | |||
| 32 | + | title string | |||
| 33 | + | } | |||
| 34 | + | ||||
| 35 | + | // Snapshot captures the active visible viewport without mutating Screen, | |||
| 36 | + | // history, or pending damage. Screen remains single-owner; callers must | |||
| 37 | + | // serialize Snapshot with Write, Resize, and host mutations. | |||
| 38 | + | func (s *Screen) Snapshot() ScreenSnapshot { | |||
| 39 | + | if s == nil { | |||
| 40 | + | return ScreenSnapshot{} | |||
| 41 | + | } | |||
| 42 | + | cursorStyle, cursorStyleSet := s.CursorStyle() | |||
| 43 | + | mouseTracking, mouseSGR := s.MouseMode() | |||
| 44 | + | return ScreenSnapshot{ | |||
| 45 | + | frame: s.Frame.Clone(), | |||
| 46 | + | bounds: s.LineBounds(), | |||
| 47 | + | cursor: CursorSnapshot{ | |||
| 48 | + | Row: s.CursorRow(), | |||
| 49 | + | Col: s.CursorCol(), | |||
| 50 | + | Visible: s.CursorVisible(), | |||
| 51 | + | Style: cursorStyle, | |||
| 52 | + | StyleSet: cursorStyleSet, | |||
| 53 | + | }, | |||
| 54 | + | modes: ModeSnapshot{ | |||
| 55 | + | AlternateScreen: s.AltScreenActive(), | |||
| 56 | + | BracketedPaste: s.BracketedPasteMode(), | |||
| 57 | + | SynchronizedUpdate: s.SyncUpdateActive(), | |||
| 58 | + | ColorSchemeMode: s.ColorSchemeMode(), | |||
| 59 | + | MouseTracking: mouseTracking, | |||
| 60 | + | MouseSGR: mouseSGR, | |||
| 61 | + | }, | |||
| 62 | + | title: s.TerminalTitle(), | |||
| 63 | + | } | |||
| 64 | + | } | |||
| 65 | + | ||||
| 66 | + | func (s ScreenSnapshot) Columns() int { return s.frame.Width } | |||
| 67 | + | func (s ScreenSnapshot) Rows() int { return s.frame.Height } | |||
| 68 | + | ||||
| 69 | + | func (s ScreenSnapshot) Row(y int) []renderer.Cell { | |||
| 70 | + | return append([]renderer.Cell(nil), s.BorrowedRow(y)...) | |||
| 71 | + | } | |||
| 72 | + | ||||
| 73 | + | func (s ScreenSnapshot) BorrowedRow(y int) []renderer.Cell { | |||
| 74 | + | if y < 0 || y >= s.frame.Height { | |||
| 75 | + | return nil | |||
| 76 | + | } | |||
| 77 | + | return s.frame.Row(y) | |||
| 78 | + | } | |||
| 79 | + | ||||
| 80 | + | func (s ScreenSnapshot) Bound(y int) LineBound { | |||
| 81 | + | if y < 0 || y >= len(s.bounds) { | |||
| 82 | + | return LineBound{} | |||
| 83 | + | } | |||
| 84 | + | return s.bounds[y] | |||
| 85 | + | } | |||
| 86 | + | ||||
| 87 | + | func (s ScreenSnapshot) Cursor() CursorSnapshot { return s.cursor } | |||
| 88 | + | func (s ScreenSnapshot) Modes() ModeSnapshot { return s.modes } | |||
| 89 | + | func (s ScreenSnapshot) Title() string { return s.title } | |||
A pkg/vt/screen_snapshot_test.go +169 -0
| @@ -0,0 +1,169 @@ | |||
| 1 | + | package vt | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "testing" | |
| 5 | + | ||
| 6 | + | "github.com/stretchr/testify/require" | |
| 7 | + | ) | |
| 8 | + | ||
| 9 | + | func TestScreenSnapshotGeometryAndMetadata(t *testing.T) { | |
| 10 | + | tests := []struct { | |
| 11 | + | name string | |
| 12 | + | screen func() *Screen | |
| 13 | + | wantColumns int | |
| 14 | + | wantRows int | |
| 15 | + | wantTitle string | |
| 16 | + | wantCursor CursorSnapshot | |
| 17 | + | wantModes ModeSnapshot | |
| 18 | + | checkRows func(t *testing.T, snapshot ScreenSnapshot) | |
| 19 | + | }{ | |
| 20 | + | { | |
| 21 | + | name: "nil receiver is the zero snapshot", | |
| 22 | + | screen: func() *Screen { return nil }, | |
| 23 | + | wantCursor: CursorSnapshot{}, | |
| 24 | + | wantModes: ModeSnapshot{}, | |
| 25 | + | checkRows: func(t *testing.T, snapshot ScreenSnapshot) { | |
| 26 | + | require.Nil(t, snapshot.Row(0)) | |
| 27 | + | require.Nil(t, snapshot.BorrowedRow(0)) | |
| 28 | + | require.Equal(t, LineBound{}, snapshot.Bound(0)) | |
| 29 | + | }, | |
| 30 | + | }, | |
| 31 | + | { | |
| 32 | + | name: "normal viewport captures cursor modes and title", | |
| 33 | + | screen: func() *Screen { | |
| 34 | + | screen := NewScreen(6, 3) | |
| 35 | + | screen.Write([]byte("\x1b]2;editor\x07\x1b[2;3H\x1b[?25l\x1b[2 q\x1b[?2004h\x1b[?2026h\x1b[?2031h\x1b[?1002h\x1b[?1006h")) | |
| 36 | + | return screen | |
| 37 | + | }, | |
| 38 | + | wantColumns: 6, | |
| 39 | + | wantRows: 3, | |
| 40 | + | wantTitle: "editor", | |
| 41 | + | wantCursor: CursorSnapshot{Row: 1, Col: 2, Visible: false, Style: 2, StyleSet: true}, | |
| 42 | + | wantModes: ModeSnapshot{ | |
| 43 | + | BracketedPaste: true, | |
| 44 | + | SynchronizedUpdate: true, | |
| 45 | + | ColorSchemeMode: true, | |
| 46 | + | MouseTracking: 1002, | |
| 47 | + | MouseSGR: true, | |
| 48 | + | }, | |
| 49 | + | checkRows: func(t *testing.T, snapshot ScreenSnapshot) { | |
| 50 | + | require.Len(t, snapshot.BorrowedRow(0), 6) | |
| 51 | + | require.Equal(t, LineBound{}, snapshot.Bound(0)) | |
| 52 | + | }, | |
| 53 | + | }, | |
| 54 | + | { | |
| 55 | + | name: "zero by zero preserves screen metadata", | |
| 56 | + | screen: func() *Screen { | |
| 57 | + | screen := NewScreen(2, 2) | |
| 58 | + | screen.Write([]byte("\x1b]2;collapsed\x07\x1b[?2004h")) | |
| 59 | + | screen.Resize(0, 0) | |
| 60 | + | return screen | |
| 61 | + | }, | |
| 62 | + | wantTitle: "collapsed", | |
| 63 | + | wantCursor: CursorSnapshot{Visible: true}, | |
| 64 | + | wantModes: ModeSnapshot{BracketedPaste: true}, | |
| 65 | + | checkRows: func(t *testing.T, snapshot ScreenSnapshot) { | |
| 66 | + | require.Nil(t, snapshot.Row(0)) | |
| 67 | + | require.Equal(t, LineBound{}, snapshot.Bound(0)) | |
| 68 | + | }, | |
| 69 | + | }, | |
| 70 | + | { | |
| 71 | + | name: "zero columns retains empty physical rows and bounds", | |
| 72 | + | screen: func() *Screen { | |
| 73 | + | screen := NewScreen(2, 2) | |
| 74 | + | screen.Write([]byte("\x1b]2;zero-columns\x07")) | |
| 75 | + | screen.Resize(0, 2) | |
| 76 | + | return screen | |
| 77 | + | }, | |
| 78 | + | wantRows: 2, | |
| 79 | + | wantTitle: "zero-columns", | |
| 80 | + | wantCursor: CursorSnapshot{Visible: true}, | |
| 81 | + | checkRows: func(t *testing.T, snapshot ScreenSnapshot) { | |
| 82 | + | require.NotNil(t, snapshot.BorrowedRow(0)) | |
| 83 | + | require.Empty(t, snapshot.BorrowedRow(0)) | |
| 84 | + | require.Nil(t, snapshot.Row(0)) | |
| 85 | + | require.Empty(t, snapshot.Row(1)) | |
| 86 | + | require.Equal(t, LineBound{}, snapshot.Bound(1)) | |
| 87 | + | require.Nil(t, snapshot.Row(2)) | |
| 88 | + | }, | |
| 89 | + | }, | |
| 90 | + | { | |
| 91 | + | name: "zero rows retains columns without rows", | |
| 92 | + | screen: func() *Screen { | |
| 93 | + | screen := NewScreen(2, 2) | |
| 94 | + | screen.Write([]byte("\x1b]2;zero-rows\x07")) | |
| 95 | + | screen.Resize(2, 0) | |
| 96 | + | return screen | |
| 97 | + | }, | |
| 98 | + | wantColumns: 2, | |
| 99 | + | wantTitle: "zero-rows", | |
| 100 | + | wantCursor: CursorSnapshot{Visible: true}, | |
| 101 | + | checkRows: func(t *testing.T, snapshot ScreenSnapshot) { | |
| 102 | + | require.Nil(t, snapshot.BorrowedRow(0)) | |
| 103 | + | require.Equal(t, LineBound{}, snapshot.Bound(0)) | |
| 104 | + | }, | |
| 105 | + | }, | |
| 106 | + | } | |
| 107 | + | ||
| 108 | + | for _, test := range tests { | |
| 109 | + | t.Run(test.name, func(t *testing.T) { | |
| 110 | + | snapshot := test.screen().Snapshot() | |
| 111 | + | require.Equal(t, test.wantColumns, snapshot.Columns()) | |
| 112 | + | require.Equal(t, test.wantRows, snapshot.Rows()) | |
| 113 | + | require.Equal(t, test.wantTitle, snapshot.Title()) | |
| 114 | + | require.Equal(t, test.wantCursor, snapshot.Cursor()) | |
| 115 | + | require.Equal(t, test.wantModes, snapshot.Modes()) | |
| 116 | + | test.checkRows(t, snapshot) | |
| 117 | + | }) | |
| 118 | + | } | |
| 119 | + | } | |
| 120 | + | ||
| 121 | + | func TestScreenSnapshotColorSchemeModeExcludesHostPreference(t *testing.T) { | |
| 122 | + | tests := []struct { | |
| 123 | + | name string | |
| 124 | + | run func(*Screen) | |
| 125 | + | want bool | |
| 126 | + | }{ | |
| 127 | + | { | |
| 128 | + | name: "host preference alone is not DEC 2031 mode", | |
| 129 | + | run: func(screen *Screen) { screen.SetColorScheme(true) }, | |
| 130 | + | want: false, | |
| 131 | + | }, | |
| 132 | + | { | |
| 133 | + | name: "DEC 2031 mode is captured independently", | |
| 134 | + | run: func(screen *Screen) { | |
| 135 | + | screen.SetColorScheme(false) | |
| 136 | + | screen.Write([]byte("\x1b[?2031h")) | |
| 137 | + | }, | |
| 138 | + | want: true, | |
| 139 | + | }, | |
| 140 | + | } | |
| 141 | + | ||
| 142 | + | for _, test := range tests { | |
| 143 | + | t.Run(test.name, func(t *testing.T) { | |
| 144 | + | screen := NewScreen(4, 2) | |
| 145 | + | test.run(screen) | |
| 146 | + | require.Equal(t, test.want, screen.Snapshot().Modes().ColorSchemeMode) | |
| 147 | + | }) | |
| 148 | + | } | |
| 149 | + | } | |
| 150 | + | ||
| 151 | + | func TestScreenSnapshotTitle(t *testing.T) { | |
| 152 | + | tests := []struct { | |
| 153 | + | name string | |
| 154 | + | feed string | |
| 155 | + | want string | |
| 156 | + | }{ | |
| 157 | + | {name: "empty title", want: ""}, | |
| 158 | + | {name: "latest title replaces the previous value", feed: "\x1b]2;first\x07\x1b]0;second\x07", want: "second"}, | |
| 159 | + | {name: "UTF-8 title is preserved", feed: "\x1b]2;éditeur 界\x07", want: "éditeur 界"}, | |
| 160 | + | } | |
| 161 | + | ||
| 162 | + | for _, test := range tests { | |
| 163 | + | t.Run(test.name, func(t *testing.T) { | |
| 164 | + | screen := NewScreen(8, 2) | |
| 165 | + | screen.Write([]byte(test.feed)) | |
| 166 | + | require.Equal(t, test.want, screen.Snapshot().Title()) | |
| 167 | + | }) | |
| 168 | + | } | |
| 169 | + | } | |
| @@ -0,0 +1,169 @@ | |||||
| 1 | + | package vt | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "testing" | |||
| 5 | + | ||||
| 6 | + | "github.com/stretchr/testify/require" | |||
| 7 | + | ) | |||
| 8 | + | ||||
| 9 | + | func TestScreenSnapshotGeometryAndMetadata(t *testing.T) { | |||
| 10 | + | tests := []struct { | |||
| 11 | + | name string | |||
| 12 | + | screen func() *Screen | |||
| 13 | + | wantColumns int | |||
| 14 | + | wantRows int | |||
| 15 | + | wantTitle string | |||
| 16 | + | wantCursor CursorSnapshot | |||
| 17 | + | wantModes ModeSnapshot | |||
| 18 | + | checkRows func(t *testing.T, snapshot ScreenSnapshot) | |||
| 19 | + | }{ | |||
| 20 | + | { | |||
| 21 | + | name: "nil receiver is the zero snapshot", | |||
| 22 | + | screen: func() *Screen { return nil }, | |||
| 23 | + | wantCursor: CursorSnapshot{}, | |||
| 24 | + | wantModes: ModeSnapshot{}, | |||
| 25 | + | checkRows: func(t *testing.T, snapshot ScreenSnapshot) { | |||
| 26 | + | require.Nil(t, snapshot.Row(0)) | |||
| 27 | + | require.Nil(t, snapshot.BorrowedRow(0)) | |||
| 28 | + | require.Equal(t, LineBound{}, snapshot.Bound(0)) | |||
| 29 | + | }, | |||
| 30 | + | }, | |||
| 31 | + | { | |||
| 32 | + | name: "normal viewport captures cursor modes and title", | |||
| 33 | + | screen: func() *Screen { | |||
| 34 | + | screen := NewScreen(6, 3) | |||
| 35 | + | screen.Write([]byte("\x1b]2;editor\x07\x1b[2;3H\x1b[?25l\x1b[2 q\x1b[?2004h\x1b[?2026h\x1b[?2031h\x1b[?1002h\x1b[?1006h")) | |||
| 36 | + | return screen | |||
| 37 | + | }, | |||
| 38 | + | wantColumns: 6, | |||
| 39 | + | wantRows: 3, | |||
| 40 | + | wantTitle: "editor", | |||
| 41 | + | wantCursor: CursorSnapshot{Row: 1, Col: 2, Visible: false, Style: 2, StyleSet: true}, | |||
| 42 | + | wantModes: ModeSnapshot{ | |||
| 43 | + | BracketedPaste: true, | |||
| 44 | + | SynchronizedUpdate: true, | |||
| 45 | + | ColorSchemeMode: true, | |||
| 46 | + | MouseTracking: 1002, | |||
| 47 | + | MouseSGR: true, | |||
| 48 | + | }, | |||
| 49 | + | checkRows: func(t *testing.T, snapshot ScreenSnapshot) { | |||
| 50 | + | require.Len(t, snapshot.BorrowedRow(0), 6) | |||
| 51 | + | require.Equal(t, LineBound{}, snapshot.Bound(0)) | |||
| 52 | + | }, | |||
| 53 | + | }, | |||
| 54 | + | { | |||
| 55 | + | name: "zero by zero preserves screen metadata", | |||
| 56 | + | screen: func() *Screen { | |||
| 57 | + | screen := NewScreen(2, 2) | |||
| 58 | + | screen.Write([]byte("\x1b]2;collapsed\x07\x1b[?2004h")) | |||
| 59 | + | screen.Resize(0, 0) | |||
| 60 | + | return screen | |||
| 61 | + | }, | |||
| 62 | + | wantTitle: "collapsed", | |||
| 63 | + | wantCursor: CursorSnapshot{Visible: true}, | |||
| 64 | + | wantModes: ModeSnapshot{BracketedPaste: true}, | |||
| 65 | + | checkRows: func(t *testing.T, snapshot ScreenSnapshot) { | |||
| 66 | + | require.Nil(t, snapshot.Row(0)) | |||
| 67 | + | require.Equal(t, LineBound{}, snapshot.Bound(0)) | |||
| 68 | + | }, | |||
| 69 | + | }, | |||
| 70 | + | { | |||
| 71 | + | name: "zero columns retains empty physical rows and bounds", | |||
| 72 | + | screen: func() *Screen { | |||
| 73 | + | screen := NewScreen(2, 2) | |||
| 74 | + | screen.Write([]byte("\x1b]2;zero-columns\x07")) | |||
| 75 | + | screen.Resize(0, 2) | |||
| 76 | + | return screen | |||
| 77 | + | }, | |||
| 78 | + | wantRows: 2, | |||
| 79 | + | wantTitle: "zero-columns", | |||
| 80 | + | wantCursor: CursorSnapshot{Visible: true}, | |||
| 81 | + | checkRows: func(t *testing.T, snapshot ScreenSnapshot) { | |||
| 82 | + | require.NotNil(t, snapshot.BorrowedRow(0)) | |||
| 83 | + | require.Empty(t, snapshot.BorrowedRow(0)) | |||
| 84 | + | require.Nil(t, snapshot.Row(0)) | |||
| 85 | + | require.Empty(t, snapshot.Row(1)) | |||
| 86 | + | require.Equal(t, LineBound{}, snapshot.Bound(1)) | |||
| 87 | + | require.Nil(t, snapshot.Row(2)) | |||
| 88 | + | }, | |||
| 89 | + | }, | |||
| 90 | + | { | |||
| 91 | + | name: "zero rows retains columns without rows", | |||
| 92 | + | screen: func() *Screen { | |||
| 93 | + | screen := NewScreen(2, 2) | |||
| 94 | + | screen.Write([]byte("\x1b]2;zero-rows\x07")) | |||
| 95 | + | screen.Resize(2, 0) | |||
| 96 | + | return screen | |||
| 97 | + | }, | |||
| 98 | + | wantColumns: 2, | |||
| 99 | + | wantTitle: "zero-rows", | |||
| 100 | + | wantCursor: CursorSnapshot{Visible: true}, | |||
| 101 | + | checkRows: func(t *testing.T, snapshot ScreenSnapshot) { | |||
| 102 | + | require.Nil(t, snapshot.BorrowedRow(0)) | |||
| 103 | + | require.Equal(t, LineBound{}, snapshot.Bound(0)) | |||
| 104 | + | }, | |||
| 105 | + | }, | |||
| 106 | + | } | |||
| 107 | + | ||||
| 108 | + | for _, test := range tests { | |||
| 109 | + | t.Run(test.name, func(t *testing.T) { | |||
| 110 | + | snapshot := test.screen().Snapshot() | |||
| 111 | + | require.Equal(t, test.wantColumns, snapshot.Columns()) | |||
| 112 | + | require.Equal(t, test.wantRows, snapshot.Rows()) | |||
| 113 | + | require.Equal(t, test.wantTitle, snapshot.Title()) | |||
| 114 | + | require.Equal(t, test.wantCursor, snapshot.Cursor()) | |||
| 115 | + | require.Equal(t, test.wantModes, snapshot.Modes()) | |||
| 116 | + | test.checkRows(t, snapshot) | |||
| 117 | + | }) | |||
| 118 | + | } | |||
| 119 | + | } | |||
| 120 | + | ||||
| 121 | + | func TestScreenSnapshotColorSchemeModeExcludesHostPreference(t *testing.T) { | |||
| 122 | + | tests := []struct { | |||
| 123 | + | name string | |||
| 124 | + | run func(*Screen) | |||
| 125 | + | want bool | |||
| 126 | + | }{ | |||
| 127 | + | { | |||
| 128 | + | name: "host preference alone is not DEC 2031 mode", | |||
| 129 | + | run: func(screen *Screen) { screen.SetColorScheme(true) }, | |||
| 130 | + | want: false, | |||
| 131 | + | }, | |||
| 132 | + | { | |||
| 133 | + | name: "DEC 2031 mode is captured independently", | |||
| 134 | + | run: func(screen *Screen) { | |||
| 135 | + | screen.SetColorScheme(false) | |||
| 136 | + | screen.Write([]byte("\x1b[?2031h")) | |||
| 137 | + | }, | |||
| 138 | + | want: true, | |||
| 139 | + | }, | |||
| 140 | + | } | |||
| 141 | + | ||||
| 142 | + | for _, test := range tests { | |||
| 143 | + | t.Run(test.name, func(t *testing.T) { | |||
| 144 | + | screen := NewScreen(4, 2) | |||
| 145 | + | test.run(screen) | |||
| 146 | + | require.Equal(t, test.want, screen.Snapshot().Modes().ColorSchemeMode) | |||
| 147 | + | }) | |||
| 148 | + | } | |||
| 149 | + | } | |||
| 150 | + | ||||
| 151 | + | func TestScreenSnapshotTitle(t *testing.T) { | |||
| 152 | + | tests := []struct { | |||
| 153 | + | name string | |||
| 154 | + | feed string | |||
| 155 | + | want string | |||
| 156 | + | }{ | |||
| 157 | + | {name: "empty title", want: ""}, | |||
| 158 | + | {name: "latest title replaces the previous value", feed: "\x1b]2;first\x07\x1b]0;second\x07", want: "second"}, | |||
| 159 | + | {name: "UTF-8 title is preserved", feed: "\x1b]2;éditeur 界\x07", want: "éditeur 界"}, | |||
| 160 | + | } | |||
| 161 | + | ||||
| 162 | + | for _, test := range tests { | |||
| 163 | + | t.Run(test.name, func(t *testing.T) { | |||
| 164 | + | screen := NewScreen(8, 2) | |||
| 165 | + | screen.Write([]byte(test.feed)) | |||
| 166 | + | require.Equal(t, test.want, screen.Snapshot().Title()) | |||
| 167 | + | }) | |||
| 168 | + | } | |||
| 169 | + | } | |||