detached 3c9eb0e

fix(screen): track cursor and autowrap modes (#30)

* fix(screen): track cursor and autowrap modes

* fix(snapshot): preserve ModeSnapshot source compatibility
commit
3c9eb0e0d8f1f43ae01bfadf7c8740f1629e0cf7
author
bnema · · 2026-09-06 11:06 UTC
committer
GitHub · · 2026-09-06 11:06 UTC
parent
8e4df5c
signature
GPG UnverifiedB5690EEEBB952194 The key is unknown
9 files · +294 -73
M doc.go +3 -1
@@ -4,7 +4,9 @@
44 // alternate-screen state, cursor/reporting modes, and common xterm/VT control
55 // sequences. Wide runes that fit are stored as a head cell plus a continuation
66 // cell, and resize or edit operations repair row-boundary splits instead of
7-// leaving orphaned halves.
7+// leaving orphaned halves. Application cursor mode is tracked for hosts that
8+// encode arrow keys; application keypad mode and keypad-key encoding are not
9+// supported.
810 //
911 // Stateful values are single-owner and are not internally synchronized. The
1012 // owner must serialize Write, Resize, Snapshot, and History mutations. Write
@@ -4,7 +4,9 @@
4 // alternate-screen state, cursor/reporting modes, and common xterm/VT control4 // alternate-screen state, cursor/reporting modes, and common xterm/VT control
5 // sequences. Wide runes that fit are stored as a head cell plus a continuation5 // sequences. Wide runes that fit are stored as a head cell plus a continuation
6 // cell, and resize or edit operations repair row-boundary splits instead of6 // cell, and resize or edit operations repair row-boundary splits instead of
7-// leaving orphaned halves.7+// leaving orphaned halves. Application cursor mode is tracked for hosts that
8+// encode arrow keys; application keypad mode and keypad-key encoding are not
9+// supported.
8 //10 //
9 // Stateful values are single-owner and are not internally synchronized. The11 // Stateful values are single-owner and are not internally synchronized. The
10 // owner must serialize Write, Resize, Snapshot, and History mutations. Write12 // owner must serialize Write, Resize, Snapshot, and History mutations. Write
M screen.go +21 -18
@@ -78,24 +78,26 @@ type Screen struct {
7878 csiScratch []int
7979 sgrScratch []int
8080
81- scrollTop int
82- scrollBottom int
83- savedCursor cursorState
84- alternate *screenState
85- graphics *screenGraphicsState
86- syncUpdateActive bool
87- progressState int
88- cursorVisible bool
89- cursorStyle int
90- cursorStyleSet bool
91- mouseMode int
92- mouseSGR bool
93- bracketedPaste bool
94- originMode bool
95- insertMode bool
96- colorSchemeMode bool
97- colorSchemeLight bool
98- colorSchemeSet bool
81+ scrollTop int
82+ scrollBottom int
83+ savedCursor cursorState
84+ alternate *screenState
85+ graphics *screenGraphicsState
86+ syncUpdateActive bool
87+ progressState int
88+ cursorVisible bool
89+ cursorStyle int
90+ cursorStyleSet bool
91+ mouseMode int
92+ mouseSGR bool
93+ bracketedPaste bool
94+ originMode bool
95+ insertMode bool
96+ autoWrapMode bool
97+ applicationCursorMode bool
98+ colorSchemeMode bool
99+ colorSchemeLight bool
100+ colorSchemeSet bool
99101 }
100102
101103 func NewScreen(width, height int) *Screen {
@@ -104,6 +106,7 @@ func NewScreen(width, height int) *Screen {
104106 damage: []renderer.Damage{renderer.FullRedraw()},
105107 damageGeneration: 1,
106108 cursorVisible: true,
109+ autoWrapMode: true,
107110 geometry: Geometry{Cols: width, Rows: height},
108111 }
109112 s.buffer = s.newBuffer(width, height)
@@ -78,24 +78,26 @@ type Screen struct {
78 csiScratch []int78 csiScratch []int
79 sgrScratch []int79 sgrScratch []int
80 80
81- scrollTop int81+ scrollTop int
82- scrollBottom int82+ scrollBottom int
83- savedCursor cursorState83+ savedCursor cursorState
84- alternate *screenState84+ alternate *screenState
85- graphics *screenGraphicsState85+ graphics *screenGraphicsState
86- syncUpdateActive bool86+ syncUpdateActive bool
87- progressState int87+ progressState int
88- cursorVisible bool88+ cursorVisible bool
89- cursorStyle int89+ cursorStyle int
90- cursorStyleSet bool90+ cursorStyleSet bool
91- mouseMode int91+ mouseMode int
92- mouseSGR bool92+ mouseSGR bool
93- bracketedPaste bool93+ bracketedPaste bool
94- originMode bool94+ originMode bool
95- insertMode bool95+ insertMode bool
96- colorSchemeMode bool96+ autoWrapMode bool
97- colorSchemeLight bool97+ applicationCursorMode bool
98- colorSchemeSet bool98+ colorSchemeMode bool
99+ colorSchemeLight bool
100+ colorSchemeSet bool
99 }101 }
100 102
101 func NewScreen(width, height int) *Screen {103 func NewScreen(width, height int) *Screen {
@@ -104,6 +106,7 @@ func NewScreen(width, height int) *Screen {
104 damage: []renderer.Damage{renderer.FullRedraw()},106 damage: []renderer.Damage{renderer.FullRedraw()},
105 damageGeneration: 1,107 damageGeneration: 1,
106 cursorVisible: true,108 cursorVisible: true,
109+ autoWrapMode: true,
107 geometry: Geometry{Cols: width, Rows: height},110 geometry: Geometry{Cols: width, Rows: height},
108 }111 }
109 s.buffer = s.newBuffer(width, height)112 s.buffer = s.newBuffer(width, height)
A screen_autowrap_test.go +157 -0
@@ -0,0 +1,157 @@
1+package vt
2+
3+import (
4+ "bytes"
5+ "testing"
6+
7+ renderer "github.com/bnema/vev-vt/core"
8+)
9+
10+func TestScreenAutoWrapModeRightMargin(t *testing.T) {
11+ tests := []struct {
12+ name string
13+ width int
14+ height int
15+ input string
16+ wantRows []string
17+ wantRow int
18+ wantCol int
19+ wantHistory int
20+ }{
21+ {
22+ name: "successive writes overwrite right margin",
23+ width: 4, height: 2, input: "\x1b[?7labcde",
24+ wantRows: []string{"abce", " "}, wantRow: 0, wantCol: 3,
25+ },
26+ {
27+ name: "bottom row does not scroll",
28+ width: 3, height: 2, input: "top\r\n\x1b[?7labcd",
29+ wantRows: []string{"top", "abd"}, wantRow: 1, wantCol: 2,
30+ },
31+ {
32+ name: "width one overwrites",
33+ width: 1, height: 2, input: "\x1b[?7lab",
34+ wantRows: []string{"b", " "}, wantRow: 0, wantCol: 0,
35+ },
36+ {
37+ name: "vev raw-mode initialization disables wrapping",
38+ width: 4, height: 2, input: "\x1b[?7lstatus",
39+ wantRows: []string{"stas", " "}, wantRow: 0, wantCol: 3,
40+ },
41+ }
42+
43+ for _, test := range tests {
44+ t.Run(test.name, func(t *testing.T) {
45+ s := NewScreenWithHistory(test.width, test.height, HistoryConfig{MaxRows: 10})
46+ s.Write([]byte(test.input))
47+ for row, want := range test.wantRows {
48+ if got := screenRowText(s, row); got != want {
49+ t.Fatalf("row %d = %q, want %q", row, got, want)
50+ }
51+ }
52+ if s.Row != test.wantRow || s.Col != test.wantCol {
53+ t.Fatalf("cursor = (%d,%d), want (%d,%d)", s.Row, s.Col, test.wantRow, test.wantCol)
54+ }
55+ if got := s.History().Len(); got != test.wantHistory {
56+ t.Fatalf("history length = %d, want %d", got, test.wantHistory)
57+ }
58+ if s.buffer.boundaries[0].Soft {
59+ t.Fatal("disabled autowrap created a soft wrap")
60+ }
61+ })
62+ }
63+}
64+
65+func TestScreenAutoWrapWideRuneAtRightMargin(t *testing.T) {
66+ s := NewScreen(4, 2)
67+ s.Write([]byte("abc\x1b[?7l界"))
68+
69+ if got := s.Cell(3, 0); got.Rune != '\uFFFD' || got.Continuation {
70+ t.Fatalf("right-margin cell = %#v, want narrow replacement", got)
71+ }
72+ for x := 0; x < s.Columns(); x++ {
73+ if s.Cell(x, 0).Continuation && (x == 0 || renderer.RuneWidth(s.Cell(x-1, 0).Rune) != 2) {
74+ t.Fatalf("orphaned continuation at column %d", x)
75+ }
76+ }
77+ if s.Row != 0 || s.Col != 3 {
78+ t.Fatalf("cursor = (%d,%d), want (0,3)", s.Row, s.Col)
79+ }
80+}
81+
82+func TestScreenAutoWrapModeTransitionsAndResize(t *testing.T) {
83+ s := NewScreen(4, 2)
84+ s.Write([]byte("abcd")) // Leave a deferred wrap pending.
85+ s.Write([]byte("\x1b[?7le"))
86+ if got := screenRowText(s, 0); got != "abce" {
87+ t.Fatalf("row after disabling pending wrap = %q, want %q", got, "abce")
88+ }
89+
90+ s.Resize(3, 2)
91+ if s.Row != 0 || s.Col != 2 {
92+ t.Fatalf("cursor after resize = (%d,%d), want (0,2)", s.Row, s.Col)
93+ }
94+ s.Write([]byte("f\x1b[?7hgh"))
95+ if s.Row != 1 || s.Col != 1 {
96+ t.Fatalf("cursor after re-enabling wrap = (%d,%d), want (1,1)", s.Row, s.Col)
97+ }
98+ if got := screenRowText(s, 1); got != "h " {
99+ t.Fatalf("wrapped row = %q, want %q", got, "h ")
100+ }
101+}
102+
103+func TestScreenAutoWrapSavedAndReset(t *testing.T) {
104+ s := NewScreen(4, 2)
105+ s.Write([]byte("\x1b[?7l\x1b7\x1b[?7h\x1b8"))
106+ if s.AutoWrapMode() {
107+ t.Fatal("ESC 8 did not restore disabled autowrap mode")
108+ }
109+
110+ s.Write([]byte("\x1bc"))
111+ if !s.AutoWrapMode() {
112+ t.Fatal("RIS did not restore enabled autowrap default")
113+ }
114+}
115+
116+func TestScreenApplicationCursorMode(t *testing.T) {
117+ s := NewScreen(4, 2)
118+ if s.ApplicationCursorMode() {
119+ t.Fatal("application cursor mode enabled by default")
120+ }
121+ s.Write([]byte("\x1b[?"))
122+ s.Write([]byte("1h"))
123+ if !s.ApplicationCursorMode() || !s.Snapshot().ApplicationCursorMode() {
124+ t.Fatal("split DECCKM enable was not tracked in screen and snapshot")
125+ }
126+ s.Write([]byte("\x1b[?1l"))
127+ if s.ApplicationCursorMode() {
128+ t.Fatal("DECCKM disable was not tracked")
129+ }
130+ s.Write([]byte("\x1b[?1h\x1bc"))
131+ if s.ApplicationCursorMode() {
132+ t.Fatal("RIS did not reset DECCKM")
133+ }
134+}
135+
136+func TestScreenCursorAndAutoWrapModeQueries(t *testing.T) {
137+ s := NewScreen(4, 2)
138+ var got bytes.Buffer
139+ s.OnResponse = func(response []byte) { got.Write(response) }
140+ s.Write([]byte("\x1b[?1$p\x1b[?7$p\x1b[?1h\x1b[?7l\x1b[?1$p\x1b[?7$p"))
141+ want := "\x1b[?1;2$y\x1b[?7;1$y\x1b[?1;1$y\x1b[?7;2$y"
142+ if got.String() != want {
143+ t.Fatalf("mode reports = %q, want %q", got.String(), want)
144+ }
145+}
146+
147+func screenRowText(s *Screen, row int) string {
148+ runes := make([]rune, s.Columns())
149+ for col := range s.Columns() {
150+ r := s.Cell(col, row).Rune
151+ if r == 0 {
152+ r = ' '
153+ }
154+ runes[col] = r
155+ }
156+ return string(runes)
157+}
@@ -0,0 +1,157 @@
1+package vt
2+
3+import (
4+ "bytes"
5+ "testing"
6+
7+ renderer "github.com/bnema/vev-vt/core"
8+)
9+
10+func TestScreenAutoWrapModeRightMargin(t *testing.T) {
11+ tests := []struct {
12+ name string
13+ width int
14+ height int
15+ input string
16+ wantRows []string
17+ wantRow int
18+ wantCol int
19+ wantHistory int
20+ }{
21+ {
22+ name: "successive writes overwrite right margin",
23+ width: 4, height: 2, input: "\x1b[?7labcde",
24+ wantRows: []string{"abce", " "}, wantRow: 0, wantCol: 3,
25+ },
26+ {
27+ name: "bottom row does not scroll",
28+ width: 3, height: 2, input: "top\r\n\x1b[?7labcd",
29+ wantRows: []string{"top", "abd"}, wantRow: 1, wantCol: 2,
30+ },
31+ {
32+ name: "width one overwrites",
33+ width: 1, height: 2, input: "\x1b[?7lab",
34+ wantRows: []string{"b", " "}, wantRow: 0, wantCol: 0,
35+ },
36+ {
37+ name: "vev raw-mode initialization disables wrapping",
38+ width: 4, height: 2, input: "\x1b[?7lstatus",
39+ wantRows: []string{"stas", " "}, wantRow: 0, wantCol: 3,
40+ },
41+ }
42+
43+ for _, test := range tests {
44+ t.Run(test.name, func(t *testing.T) {
45+ s := NewScreenWithHistory(test.width, test.height, HistoryConfig{MaxRows: 10})
46+ s.Write([]byte(test.input))
47+ for row, want := range test.wantRows {
48+ if got := screenRowText(s, row); got != want {
49+ t.Fatalf("row %d = %q, want %q", row, got, want)
50+ }
51+ }
52+ if s.Row != test.wantRow || s.Col != test.wantCol {
53+ t.Fatalf("cursor = (%d,%d), want (%d,%d)", s.Row, s.Col, test.wantRow, test.wantCol)
54+ }
55+ if got := s.History().Len(); got != test.wantHistory {
56+ t.Fatalf("history length = %d, want %d", got, test.wantHistory)
57+ }
58+ if s.buffer.boundaries[0].Soft {
59+ t.Fatal("disabled autowrap created a soft wrap")
60+ }
61+ })
62+ }
63+}
64+
65+func TestScreenAutoWrapWideRuneAtRightMargin(t *testing.T) {
66+ s := NewScreen(4, 2)
67+ s.Write([]byte("abc\x1b[?7l界"))
68+
69+ if got := s.Cell(3, 0); got.Rune != '\uFFFD' || got.Continuation {
70+ t.Fatalf("right-margin cell = %#v, want narrow replacement", got)
71+ }
72+ for x := 0; x < s.Columns(); x++ {
73+ if s.Cell(x, 0).Continuation && (x == 0 || renderer.RuneWidth(s.Cell(x-1, 0).Rune) != 2) {
74+ t.Fatalf("orphaned continuation at column %d", x)
75+ }
76+ }
77+ if s.Row != 0 || s.Col != 3 {
78+ t.Fatalf("cursor = (%d,%d), want (0,3)", s.Row, s.Col)
79+ }
80+}
81+
82+func TestScreenAutoWrapModeTransitionsAndResize(t *testing.T) {
83+ s := NewScreen(4, 2)
84+ s.Write([]byte("abcd")) // Leave a deferred wrap pending.
85+ s.Write([]byte("\x1b[?7le"))
86+ if got := screenRowText(s, 0); got != "abce" {
87+ t.Fatalf("row after disabling pending wrap = %q, want %q", got, "abce")
88+ }
89+
90+ s.Resize(3, 2)
91+ if s.Row != 0 || s.Col != 2 {
92+ t.Fatalf("cursor after resize = (%d,%d), want (0,2)", s.Row, s.Col)
93+ }
94+ s.Write([]byte("f\x1b[?7hgh"))
95+ if s.Row != 1 || s.Col != 1 {
96+ t.Fatalf("cursor after re-enabling wrap = (%d,%d), want (1,1)", s.Row, s.Col)
97+ }
98+ if got := screenRowText(s, 1); got != "h " {
99+ t.Fatalf("wrapped row = %q, want %q", got, "h ")
100+ }
101+}
102+
103+func TestScreenAutoWrapSavedAndReset(t *testing.T) {
104+ s := NewScreen(4, 2)
105+ s.Write([]byte("\x1b[?7l\x1b7\x1b[?7h\x1b8"))
106+ if s.AutoWrapMode() {
107+ t.Fatal("ESC 8 did not restore disabled autowrap mode")
108+ }
109+
110+ s.Write([]byte("\x1bc"))
111+ if !s.AutoWrapMode() {
112+ t.Fatal("RIS did not restore enabled autowrap default")
113+ }
114+}
115+
116+func TestScreenApplicationCursorMode(t *testing.T) {
117+ s := NewScreen(4, 2)
118+ if s.ApplicationCursorMode() {
119+ t.Fatal("application cursor mode enabled by default")
120+ }
121+ s.Write([]byte("\x1b[?"))
122+ s.Write([]byte("1h"))
123+ if !s.ApplicationCursorMode() || !s.Snapshot().ApplicationCursorMode() {
124+ t.Fatal("split DECCKM enable was not tracked in screen and snapshot")
125+ }
126+ s.Write([]byte("\x1b[?1l"))
127+ if s.ApplicationCursorMode() {
128+ t.Fatal("DECCKM disable was not tracked")
129+ }
130+ s.Write([]byte("\x1b[?1h\x1bc"))
131+ if s.ApplicationCursorMode() {
132+ t.Fatal("RIS did not reset DECCKM")
133+ }
134+}
135+
136+func TestScreenCursorAndAutoWrapModeQueries(t *testing.T) {
137+ s := NewScreen(4, 2)
138+ var got bytes.Buffer
139+ s.OnResponse = func(response []byte) { got.Write(response) }
140+ s.Write([]byte("\x1b[?1$p\x1b[?7$p\x1b[?1h\x1b[?7l\x1b[?1$p\x1b[?7$p"))
141+ want := "\x1b[?1;2$y\x1b[?7;1$y\x1b[?1;1$y\x1b[?7;2$y"
142+ if got.String() != want {
143+ t.Fatalf("mode reports = %q, want %q", got.String(), want)
144+ }
145+}
146+
147+func screenRowText(s *Screen, row int) string {
148+ runes := make([]rune, s.Columns())
149+ for col := range s.Columns() {
150+ r := s.Cell(col, row).Rune
151+ if r == 0 {
152+ r = ' '
153+ }
154+ runes[col] = r
155+ }
156+ return string(runes)
157+}
M screen_csi.go +4 -0
@@ -174,8 +174,12 @@ func firstPositive(parts []int, fallback int) int {
174174 func (s *Screen) modeReportState(private bool, mode int) int {
175175 if private {
176176 switch mode {
177+ case 1:
178+ return boolModeReportState(s.applicationCursorMode)
177179 case 6:
178180 return boolModeReportState(s.originMode)
181+ case 7:
182+ return boolModeReportState(s.autoWrapMode)
179183 case 2026:
180184 return boolModeReportState(s.syncUpdateActive)
181185 case 2031:
@@ -174,8 +174,12 @@ func firstPositive(parts []int, fallback int) int {
174 func (s *Screen) modeReportState(private bool, mode int) int {174 func (s *Screen) modeReportState(private bool, mode int) int {
175 if private {175 if private {
176 switch mode {176 switch mode {
177+ case 1:
178+ return boolModeReportState(s.applicationCursorMode)
177 case 6:179 case 6:
178 return boolModeReportState(s.originMode)180 return boolModeReportState(s.originMode)
181+ case 7:
182+ return boolModeReportState(s.autoWrapMode)
179 case 2026:183 case 2026:
180 return boolModeReportState(s.syncUpdateActive)184 return boolModeReportState(s.syncUpdateActive)
181 case 2031:185 case 2031:
M screen_cursor.go +13 -7
@@ -8,12 +8,13 @@ import (
88 )
99
1010 type cursorState struct {
11- row int
12- col int
13- style renderer.Style
14- originMode bool
15- insertMode bool
16- saved bool
11+ row int
12+ col int
13+ style renderer.Style
14+ originMode bool
15+ insertMode bool
16+ autoWrapMode bool
17+ saved bool
1718 }
1819
1920 func (s *Screen) cursorReportRow() int {
@@ -74,7 +75,11 @@ func (s *Screen) applyCursorStyle(params string) {
7475 }
7576
7677 func (s *Screen) saveCursor() {
77- s.savedCursor = cursorState{row: s.Row, col: s.Col, style: s.Style, originMode: s.originMode, insertMode: s.insertMode, saved: true}
78+ s.savedCursor = cursorState{
79+ row: s.Row, col: s.Col, style: s.Style,
80+ originMode: s.originMode, insertMode: s.insertMode,
81+ autoWrapMode: s.autoWrapMode, saved: true,
82+ }
7883 }
7984
8085 func (s *Screen) restoreCursor() {
@@ -86,4 +91,5 @@ func (s *Screen) restoreCursor() {
8691 s.Style = s.savedCursor.style
8792 s.originMode = s.savedCursor.originMode
8893 s.insertMode = s.savedCursor.insertMode
94+ s.autoWrapMode = s.savedCursor.autoWrapMode
8995 }
@@ -8,12 +8,13 @@ import (
8 )8 )
9 9
10 type cursorState struct {10 type cursorState struct {
11- row int11+ row int
12- col int12+ col int
13- style renderer.Style13+ style renderer.Style
14- originMode bool14+ originMode bool
15- insertMode bool15+ insertMode bool
16- saved bool16+ autoWrapMode bool
17+ saved bool
17 }18 }
18 19
19 func (s *Screen) cursorReportRow() int {20 func (s *Screen) cursorReportRow() int {
@@ -74,7 +75,11 @@ func (s *Screen) applyCursorStyle(params string) {
74 }75 }
75 76
76 func (s *Screen) saveCursor() {77 func (s *Screen) saveCursor() {
77- s.savedCursor = cursorState{row: s.Row, col: s.Col, style: s.Style, originMode: s.originMode, insertMode: s.insertMode, saved: true}78+ s.savedCursor = cursorState{
79+ row: s.Row, col: s.Col, style: s.Style,
80+ originMode: s.originMode, insertMode: s.insertMode,
81+ autoWrapMode: s.autoWrapMode, saved: true,
82+ }
78 }83 }
79 84
80 func (s *Screen) restoreCursor() {85 func (s *Screen) restoreCursor() {
@@ -86,4 +91,5 @@ func (s *Screen) restoreCursor() {
86 s.Style = s.savedCursor.style91 s.Style = s.savedCursor.style
87 s.originMode = s.savedCursor.originMode92 s.originMode = s.savedCursor.originMode
88 s.insertMode = s.savedCursor.insertMode93 s.insertMode = s.savedCursor.insertMode
94+ s.autoWrapMode = s.savedCursor.autoWrapMode
89 }95 }
M screen_modes.go +16 -1
@@ -27,6 +27,12 @@ func (s *Screen) SyncUpdateActive() bool { return s.syncUpdateActive }
2727 // by the child process.
2828 func (s *Screen) BracketedPasteMode() bool { return s.bracketedPaste }
2929
30+// AutoWrapMode reports whether DEC private mode 7 (DECAWM) is enabled.
31+func (s *Screen) AutoWrapMode() bool { return s.autoWrapMode }
32+
33+// ApplicationCursorMode reports whether DEC private mode 1 (DECCKM) is enabled.
34+func (s *Screen) ApplicationCursorMode() bool { return s.applicationCursorMode }
35+
3036 // ColorSchemeMode reports whether DEC private mode 2031 is currently enabled.
3137 func (s *Screen) ColorSchemeMode() bool { return s.colorSchemeMode }
3238
@@ -104,6 +110,8 @@ func (s *Screen) reset() {
104110 s.graphics = nil
105111 s.originMode = false
106112 s.insertMode = false
113+ s.autoWrapMode = true
114+ s.applicationCursorMode = false
107115 s.cursorVisible = true
108116 s.cursorStyle = 0
109117 s.cursorStyleSet = false
@@ -163,9 +171,16 @@ func (s *Screen) setMode(private bool, parts []int, enabled bool) {
163171 }
164172 for _, mode := range parts {
165173 switch mode {
174+ case 1:
175+ s.applicationCursorMode = enabled
166176 case 6:
167177 s.originMode = enabled
168178 s.homeCursor()
179+ case 7:
180+ s.autoWrapMode = enabled
181+ if !enabled && s.frame.Width > 0 && s.Col >= s.frame.Width {
182+ s.Col = s.frame.Width - 1
183+ }
169184 case 47, 1047, 1049:
170185 if enabled {
171186 s.enterAlternateScreen()
@@ -188,7 +203,7 @@ func (s *Screen) setMode(private bool, parts []int, enabled bool) {
188203 s.mouseSGR = enabled
189204 case 2004:
190205 s.bracketedPaste = enabled
191- case 1, 1004, 1005:
206+ case 1004, 1005:
192207 // Trackable terminal modes that do not directly affect the current
193208 // cell model yet. Consuming them prevents mode bytes from leaking.
194209 continue
@@ -27,6 +27,12 @@ func (s *Screen) SyncUpdateActive() bool { return s.syncUpdateActive }
27 // by the child process.27 // by the child process.
28 func (s *Screen) BracketedPasteMode() bool { return s.bracketedPaste }28 func (s *Screen) BracketedPasteMode() bool { return s.bracketedPaste }
29 29
30+// AutoWrapMode reports whether DEC private mode 7 (DECAWM) is enabled.
31+func (s *Screen) AutoWrapMode() bool { return s.autoWrapMode }
32+
33+// ApplicationCursorMode reports whether DEC private mode 1 (DECCKM) is enabled.
34+func (s *Screen) ApplicationCursorMode() bool { return s.applicationCursorMode }
35+
30 // ColorSchemeMode reports whether DEC private mode 2031 is currently enabled.36 // ColorSchemeMode reports whether DEC private mode 2031 is currently enabled.
31 func (s *Screen) ColorSchemeMode() bool { return s.colorSchemeMode }37 func (s *Screen) ColorSchemeMode() bool { return s.colorSchemeMode }
32 38
@@ -104,6 +110,8 @@ func (s *Screen) reset() {
104 s.graphics = nil110 s.graphics = nil
105 s.originMode = false111 s.originMode = false
106 s.insertMode = false112 s.insertMode = false
113+ s.autoWrapMode = true
114+ s.applicationCursorMode = false
107 s.cursorVisible = true115 s.cursorVisible = true
108 s.cursorStyle = 0116 s.cursorStyle = 0
109 s.cursorStyleSet = false117 s.cursorStyleSet = false
@@ -163,9 +171,16 @@ func (s *Screen) setMode(private bool, parts []int, enabled bool) {
163 }171 }
164 for _, mode := range parts {172 for _, mode := range parts {
165 switch mode {173 switch mode {
174+ case 1:
175+ s.applicationCursorMode = enabled
166 case 6:176 case 6:
167 s.originMode = enabled177 s.originMode = enabled
168 s.homeCursor()178 s.homeCursor()
179+ case 7:
180+ s.autoWrapMode = enabled
181+ if !enabled && s.frame.Width > 0 && s.Col >= s.frame.Width {
182+ s.Col = s.frame.Width - 1
183+ }
169 case 47, 1047, 1049:184 case 47, 1047, 1049:
170 if enabled {185 if enabled {
171 s.enterAlternateScreen()186 s.enterAlternateScreen()
@@ -188,7 +203,7 @@ func (s *Screen) setMode(private bool, parts []int, enabled bool) {
188 s.mouseSGR = enabled203 s.mouseSGR = enabled
189 case 2004:204 case 2004:
190 s.bracketedPaste = enabled205 s.bracketedPaste = enabled
191- case 1, 1004, 1005:206+ case 1004, 1005:
192 // Trackable terminal modes that do not directly affect the current207 // Trackable terminal modes that do not directly affect the current
193 // cell model yet. Consuming them prevents mode bytes from leaking.208 // cell model yet. Consuming them prevents mode bytes from leaking.
194 continue209 continue
M screen_snapshot.go +26 -13
@@ -27,14 +27,16 @@ type ModeSnapshot struct {
2727 // ScreenSnapshot is an owned immutable capture of the active terminal
2828 // viewport. It implements core.CellSource, and Row returns caller-owned storage.
2929 type ScreenSnapshot struct {
30- frame renderer.Frame
31- bounds []LineBound
32- rowIDs []RowID
33- cursor CursorSnapshot
34- modes ModeSnapshot
35- title string
36- graphics *graphics.Snapshot
37- nextRowID RowID
30+ frame renderer.Frame
31+ bounds []LineBound
32+ rowIDs []RowID
33+ cursor CursorSnapshot
34+ modes ModeSnapshot
35+ autoWrap bool
36+ applicationCursor bool
37+ title string
38+ graphics *graphics.Snapshot
39+ nextRowID RowID
3840 }
3941
4042 // Snapshot captures the active visible viewport without mutating Screen,
@@ -51,10 +53,12 @@ func (s *Screen) Snapshot() ScreenSnapshot {
5153 nextRowID++
5254 }
5355 return ScreenSnapshot{
54- frame: s.frame.Clone(),
55- bounds: s.LineBounds(),
56- rowIDs: s.RowIDs(),
57- nextRowID: nextRowID,
56+ frame: s.frame.Clone(),
57+ bounds: s.LineBounds(),
58+ rowIDs: s.RowIDs(),
59+ nextRowID: nextRowID,
60+ autoWrap: s.AutoWrapMode(),
61+ applicationCursor: s.ApplicationCursorMode(),
5862 cursor: CursorSnapshot{
5963 Row: s.CursorRow(),
6064 Col: s.CursorCol(),
@@ -117,7 +121,16 @@ func (s ScreenSnapshot) NextRowID() RowID {
117121
118122 func (s ScreenSnapshot) Cursor() CursorSnapshot { return s.cursor }
119123 func (s ScreenSnapshot) Modes() ModeSnapshot { return s.modes }
120-func (s ScreenSnapshot) Title() string { return s.title }
124+
125+// AutoWrapMode reports whether DEC private mode 7 (DECAWM) was enabled in the
126+// captured screen.
127+func (s ScreenSnapshot) AutoWrapMode() bool { return s.autoWrap }
128+
129+// ApplicationCursorMode reports whether DEC private mode 1 (DECCKM) was enabled
130+// in the captured screen.
131+func (s ScreenSnapshot) ApplicationCursorMode() bool { return s.applicationCursor }
132+
133+func (s ScreenSnapshot) Title() string { return s.title }
121134
122135 // Graphics returns the immutable graphics scene snapshot for the active
123136 // screen buffer, or nil when that buffer has not used Kitty graphics.
@@ -27,14 +27,16 @@ type ModeSnapshot struct {
27 // ScreenSnapshot is an owned immutable capture of the active terminal27 // ScreenSnapshot is an owned immutable capture of the active terminal
28 // viewport. It implements core.CellSource, and Row returns caller-owned storage.28 // viewport. It implements core.CellSource, and Row returns caller-owned storage.
29 type ScreenSnapshot struct {29 type ScreenSnapshot struct {
30- frame renderer.Frame30+ frame renderer.Frame
31- bounds []LineBound31+ bounds []LineBound
32- rowIDs []RowID32+ rowIDs []RowID
33- cursor CursorSnapshot33+ cursor CursorSnapshot
34- modes ModeSnapshot34+ modes ModeSnapshot
35- title string35+ autoWrap bool
36- graphics *graphics.Snapshot36+ applicationCursor bool
37- nextRowID RowID37+ title string
38+ graphics *graphics.Snapshot
39+ nextRowID RowID
38 }40 }
39 41
40 // Snapshot captures the active visible viewport without mutating Screen,42 // Snapshot captures the active visible viewport without mutating Screen,
@@ -51,10 +53,12 @@ func (s *Screen) Snapshot() ScreenSnapshot {
51 nextRowID++53 nextRowID++
52 }54 }
53 return ScreenSnapshot{55 return ScreenSnapshot{
54- frame: s.frame.Clone(),56+ frame: s.frame.Clone(),
55- bounds: s.LineBounds(),57+ bounds: s.LineBounds(),
56- rowIDs: s.RowIDs(),58+ rowIDs: s.RowIDs(),
57- nextRowID: nextRowID,59+ nextRowID: nextRowID,
60+ autoWrap: s.AutoWrapMode(),
61+ applicationCursor: s.ApplicationCursorMode(),
58 cursor: CursorSnapshot{62 cursor: CursorSnapshot{
59 Row: s.CursorRow(),63 Row: s.CursorRow(),
60 Col: s.CursorCol(),64 Col: s.CursorCol(),
@@ -117,7 +121,16 @@ func (s ScreenSnapshot) NextRowID() RowID {
117 121
118 func (s ScreenSnapshot) Cursor() CursorSnapshot { return s.cursor }122 func (s ScreenSnapshot) Cursor() CursorSnapshot { return s.cursor }
119 func (s ScreenSnapshot) Modes() ModeSnapshot { return s.modes }123 func (s ScreenSnapshot) Modes() ModeSnapshot { return s.modes }
120-func (s ScreenSnapshot) Title() string { return s.title }124+
125+// AutoWrapMode reports whether DEC private mode 7 (DECAWM) was enabled in the
126+// captured screen.
127+func (s ScreenSnapshot) AutoWrapMode() bool { return s.autoWrap }
128+
129+// ApplicationCursorMode reports whether DEC private mode 1 (DECCKM) was enabled
130+// in the captured screen.
131+func (s ScreenSnapshot) ApplicationCursorMode() bool { return s.applicationCursor }
132+
133+func (s ScreenSnapshot) Title() string { return s.title }
121 134
122 // Graphics returns the immutable graphics scene snapshot for the active135 // Graphics returns the immutable graphics scene snapshot for the active
123 // screen buffer, or nil when that buffer has not used Kitty graphics.136 // screen buffer, or nil when that buffer has not used Kitty graphics.
M screen_snapshot_test.go +39 -26
@@ -9,14 +9,16 @@ import (
99
1010 func TestScreenSnapshotGeometryAndMetadata(t *testing.T) {
1111 tests := []struct {
12- name string
13- screen func() *Screen
14- wantColumns int
15- wantRows int
16- wantTitle string
17- wantCursor CursorSnapshot
18- wantModes ModeSnapshot
19- checkRows func(t *testing.T, snapshot ScreenSnapshot)
12+ name string
13+ screen func() *Screen
14+ wantColumns int
15+ wantRows int
16+ wantTitle string
17+ wantCursor CursorSnapshot
18+ wantModes ModeSnapshot
19+ wantAutoWrap bool
20+ wantApplicationCursor bool
21+ checkRows func(t *testing.T, snapshot ScreenSnapshot)
2022 }{
2123 {
2224 name: "nil receiver is the zero snapshot",
@@ -32,13 +34,15 @@ func TestScreenSnapshotGeometryAndMetadata(t *testing.T) {
3234 name: "normal viewport captures cursor modes and title",
3335 screen: func() *Screen {
3436 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"))
37+ screen.Write([]byte("\x1b]2;editor\x07\x1b[2;3H\x1b[?25l\x1b[2 q\x1b[?1h\x1b[?2004h\x1b[?2026h\x1b[?2031h\x1b[?1002h\x1b[?1006h"))
3638 return screen
3739 },
38- wantColumns: 6,
39- wantRows: 3,
40- wantTitle: "editor",
41- wantCursor: CursorSnapshot{Row: 1, Col: 2, Visible: false, Style: 2, StyleSet: true},
40+ wantColumns: 6,
41+ wantRows: 3,
42+ wantTitle: "editor",
43+ wantCursor: CursorSnapshot{Row: 1, Col: 2, Visible: false, Style: 2, StyleSet: true},
44+ wantAutoWrap: true,
45+ wantApplicationCursor: true,
4246 wantModes: ModeSnapshot{
4347 BracketedPaste: true,
4448 SynchronizedUpdate: true,
@@ -59,9 +63,10 @@ func TestScreenSnapshotGeometryAndMetadata(t *testing.T) {
5963 screen.Resize(0, 0)
6064 return screen
6165 },
62- wantTitle: "collapsed",
63- wantCursor: CursorSnapshot{Visible: true},
64- wantModes: ModeSnapshot{BracketedPaste: true},
66+ wantTitle: "collapsed",
67+ wantCursor: CursorSnapshot{Visible: true},
68+ wantAutoWrap: true,
69+ wantModes: ModeSnapshot{BracketedPaste: true},
6570 checkRows: func(t *testing.T, snapshot ScreenSnapshot) {
6671 require.Nil(t, snapshot.Row(0))
6772 require.Equal(t, LineBound{}, snapshot.Bound(0))
@@ -75,9 +80,10 @@ func TestScreenSnapshotGeometryAndMetadata(t *testing.T) {
7580 screen.Resize(0, 2)
7681 return screen
7782 },
78- wantRows: 2,
79- wantTitle: "zero-columns",
80- wantCursor: CursorSnapshot{Visible: true},
83+ wantRows: 2,
84+ wantTitle: "zero-columns",
85+ wantCursor: CursorSnapshot{Visible: true},
86+ wantAutoWrap: true,
8187 checkRows: func(t *testing.T, snapshot ScreenSnapshot) {
8288 require.NotNil(t, snapshot.Row(0))
8389 require.Empty(t, snapshot.Row(0))
@@ -94,9 +100,10 @@ func TestScreenSnapshotGeometryAndMetadata(t *testing.T) {
94100 screen.Resize(2, 0)
95101 return screen
96102 },
97- wantColumns: 2,
98- wantTitle: "zero-rows",
99- wantCursor: CursorSnapshot{Visible: true},
103+ wantColumns: 2,
104+ wantTitle: "zero-rows",
105+ wantCursor: CursorSnapshot{Visible: true},
106+ wantAutoWrap: true,
100107 checkRows: func(t *testing.T, snapshot ScreenSnapshot) {
101108 require.Nil(t, snapshot.Row(0))
102109 require.Equal(t, LineBound{}, snapshot.Bound(0))
@@ -112,6 +119,8 @@ func TestScreenSnapshotGeometryAndMetadata(t *testing.T) {
112119 require.Equal(t, test.wantTitle, snapshot.Title())
113120 require.Equal(t, test.wantCursor, snapshot.Cursor())
114121 require.Equal(t, test.wantModes, snapshot.Modes())
122+ require.Equal(t, test.wantAutoWrap, snapshot.AutoWrapMode())
123+ require.Equal(t, test.wantApplicationCursor, snapshot.ApplicationCursorMode())
115124 test.checkRows(t, snapshot)
116125 })
117126 }
@@ -169,9 +178,10 @@ func TestScreenSnapshotTitle(t *testing.T) {
169178
170179 func TestScreenSnapshotFidelity(t *testing.T) {
171180 tests := []struct {
172- name string
173- make func() (*Screen, []renderer.Cell, []LineBound)
174- wantModes ModeSnapshot
181+ name string
182+ make func() (*Screen, []renderer.Cell, []LineBound)
183+ wantModes ModeSnapshot
184+ wantAutoWrap bool
175185 }{
176186 {
177187 name: "styles wide continuation and bounds are exact",
@@ -195,6 +205,7 @@ func TestScreenSnapshotFidelity(t *testing.T) {
195205 screen.buffer.boundaries[0] = LineBound{End: 3, Soft: true}
196206 return screen, cells, []LineBound{{End: 3, Soft: true}, {}}
197207 },
208+ wantAutoWrap: true,
198209 },
199210 {
200211 name: "active alternate screen is captured",
@@ -204,7 +215,8 @@ func TestScreenSnapshotFidelity(t *testing.T) {
204215 cells := append([]renderer.Cell(nil), screen.frame.Row(0)...)
205216 return screen, cells, screen.LineBounds()
206217 },
207- wantModes: ModeSnapshot{AlternateScreen: true},
218+ wantAutoWrap: true,
219+ wantModes: ModeSnapshot{AlternateScreen: true},
208220 },
209221 }
210222
@@ -216,6 +228,7 @@ func TestScreenSnapshotFidelity(t *testing.T) {
216228 require.Equal(t, screen.frame.Height, snapshot.Rows())
217229 require.Equal(t, wantCells, snapshot.Row(0))
218230 require.Equal(t, test.wantModes, snapshot.Modes())
231+ require.Equal(t, test.wantAutoWrap, snapshot.AutoWrapMode())
219232 for row, want := range wantBounds {
220233 require.Equal(t, want, snapshot.Bound(row))
221234 }
@@ -9,14 +9,16 @@ import (
9 9
10 func TestScreenSnapshotGeometryAndMetadata(t *testing.T) {10 func TestScreenSnapshotGeometryAndMetadata(t *testing.T) {
11 tests := []struct {11 tests := []struct {
12- name string12+ name string
13- screen func() *Screen13+ screen func() *Screen
14- wantColumns int14+ wantColumns int
15- wantRows int15+ wantRows int
16- wantTitle string16+ wantTitle string
17- wantCursor CursorSnapshot17+ wantCursor CursorSnapshot
18- wantModes ModeSnapshot18+ wantModes ModeSnapshot
19- checkRows func(t *testing.T, snapshot ScreenSnapshot)19+ wantAutoWrap bool
20+ wantApplicationCursor bool
21+ checkRows func(t *testing.T, snapshot ScreenSnapshot)
20 }{22 }{
21 {23 {
22 name: "nil receiver is the zero snapshot",24 name: "nil receiver is the zero snapshot",
@@ -32,13 +34,15 @@ func TestScreenSnapshotGeometryAndMetadata(t *testing.T) {
32 name: "normal viewport captures cursor modes and title",34 name: "normal viewport captures cursor modes and title",
33 screen: func() *Screen {35 screen: func() *Screen {
34 screen := NewScreen(6, 3)36 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"))37+ screen.Write([]byte("\x1b]2;editor\x07\x1b[2;3H\x1b[?25l\x1b[2 q\x1b[?1h\x1b[?2004h\x1b[?2026h\x1b[?2031h\x1b[?1002h\x1b[?1006h"))
36 return screen38 return screen
37 },39 },
38- wantColumns: 6,40+ wantColumns: 6,
39- wantRows: 3,41+ wantRows: 3,
40- wantTitle: "editor",42+ wantTitle: "editor",
41- wantCursor: CursorSnapshot{Row: 1, Col: 2, Visible: false, Style: 2, StyleSet: true},43+ wantCursor: CursorSnapshot{Row: 1, Col: 2, Visible: false, Style: 2, StyleSet: true},
44+ wantAutoWrap: true,
45+ wantApplicationCursor: true,
42 wantModes: ModeSnapshot{46 wantModes: ModeSnapshot{
43 BracketedPaste: true,47 BracketedPaste: true,
44 SynchronizedUpdate: true,48 SynchronizedUpdate: true,
@@ -59,9 +63,10 @@ func TestScreenSnapshotGeometryAndMetadata(t *testing.T) {
59 screen.Resize(0, 0)63 screen.Resize(0, 0)
60 return screen64 return screen
61 },65 },
62- wantTitle: "collapsed",66+ wantTitle: "collapsed",
63- wantCursor: CursorSnapshot{Visible: true},67+ wantCursor: CursorSnapshot{Visible: true},
64- wantModes: ModeSnapshot{BracketedPaste: true},68+ wantAutoWrap: true,
69+ wantModes: ModeSnapshot{BracketedPaste: true},
65 checkRows: func(t *testing.T, snapshot ScreenSnapshot) {70 checkRows: func(t *testing.T, snapshot ScreenSnapshot) {
66 require.Nil(t, snapshot.Row(0))71 require.Nil(t, snapshot.Row(0))
67 require.Equal(t, LineBound{}, snapshot.Bound(0))72 require.Equal(t, LineBound{}, snapshot.Bound(0))
@@ -75,9 +80,10 @@ func TestScreenSnapshotGeometryAndMetadata(t *testing.T) {
75 screen.Resize(0, 2)80 screen.Resize(0, 2)
76 return screen81 return screen
77 },82 },
78- wantRows: 2,83+ wantRows: 2,
79- wantTitle: "zero-columns",84+ wantTitle: "zero-columns",
80- wantCursor: CursorSnapshot{Visible: true},85+ wantCursor: CursorSnapshot{Visible: true},
86+ wantAutoWrap: true,
81 checkRows: func(t *testing.T, snapshot ScreenSnapshot) {87 checkRows: func(t *testing.T, snapshot ScreenSnapshot) {
82 require.NotNil(t, snapshot.Row(0))88 require.NotNil(t, snapshot.Row(0))
83 require.Empty(t, snapshot.Row(0))89 require.Empty(t, snapshot.Row(0))
@@ -94,9 +100,10 @@ func TestScreenSnapshotGeometryAndMetadata(t *testing.T) {
94 screen.Resize(2, 0)100 screen.Resize(2, 0)
95 return screen101 return screen
96 },102 },
97- wantColumns: 2,103+ wantColumns: 2,
98- wantTitle: "zero-rows",104+ wantTitle: "zero-rows",
99- wantCursor: CursorSnapshot{Visible: true},105+ wantCursor: CursorSnapshot{Visible: true},
106+ wantAutoWrap: true,
100 checkRows: func(t *testing.T, snapshot ScreenSnapshot) {107 checkRows: func(t *testing.T, snapshot ScreenSnapshot) {
101 require.Nil(t, snapshot.Row(0))108 require.Nil(t, snapshot.Row(0))
102 require.Equal(t, LineBound{}, snapshot.Bound(0))109 require.Equal(t, LineBound{}, snapshot.Bound(0))
@@ -112,6 +119,8 @@ func TestScreenSnapshotGeometryAndMetadata(t *testing.T) {
112 require.Equal(t, test.wantTitle, snapshot.Title())119 require.Equal(t, test.wantTitle, snapshot.Title())
113 require.Equal(t, test.wantCursor, snapshot.Cursor())120 require.Equal(t, test.wantCursor, snapshot.Cursor())
114 require.Equal(t, test.wantModes, snapshot.Modes())121 require.Equal(t, test.wantModes, snapshot.Modes())
122+ require.Equal(t, test.wantAutoWrap, snapshot.AutoWrapMode())
123+ require.Equal(t, test.wantApplicationCursor, snapshot.ApplicationCursorMode())
115 test.checkRows(t, snapshot)124 test.checkRows(t, snapshot)
116 })125 })
117 }126 }
@@ -169,9 +178,10 @@ func TestScreenSnapshotTitle(t *testing.T) {
169 178
170 func TestScreenSnapshotFidelity(t *testing.T) {179 func TestScreenSnapshotFidelity(t *testing.T) {
171 tests := []struct {180 tests := []struct {
172- name string181+ name string
173- make func() (*Screen, []renderer.Cell, []LineBound)182+ make func() (*Screen, []renderer.Cell, []LineBound)
174- wantModes ModeSnapshot183+ wantModes ModeSnapshot
184+ wantAutoWrap bool
175 }{185 }{
176 {186 {
177 name: "styles wide continuation and bounds are exact",187 name: "styles wide continuation and bounds are exact",
@@ -195,6 +205,7 @@ func TestScreenSnapshotFidelity(t *testing.T) {
195 screen.buffer.boundaries[0] = LineBound{End: 3, Soft: true}205 screen.buffer.boundaries[0] = LineBound{End: 3, Soft: true}
196 return screen, cells, []LineBound{{End: 3, Soft: true}, {}}206 return screen, cells, []LineBound{{End: 3, Soft: true}, {}}
197 },207 },
208+ wantAutoWrap: true,
198 },209 },
199 {210 {
200 name: "active alternate screen is captured",211 name: "active alternate screen is captured",
@@ -204,7 +215,8 @@ func TestScreenSnapshotFidelity(t *testing.T) {
204 cells := append([]renderer.Cell(nil), screen.frame.Row(0)...)215 cells := append([]renderer.Cell(nil), screen.frame.Row(0)...)
205 return screen, cells, screen.LineBounds()216 return screen, cells, screen.LineBounds()
206 },217 },
207- wantModes: ModeSnapshot{AlternateScreen: true},218+ wantAutoWrap: true,
219+ wantModes: ModeSnapshot{AlternateScreen: true},
208 },220 },
209 }221 }
210 222
@@ -216,6 +228,7 @@ func TestScreenSnapshotFidelity(t *testing.T) {
216 require.Equal(t, screen.frame.Height, snapshot.Rows())228 require.Equal(t, screen.frame.Height, snapshot.Rows())
217 require.Equal(t, wantCells, snapshot.Row(0))229 require.Equal(t, wantCells, snapshot.Row(0))
218 require.Equal(t, test.wantModes, snapshot.Modes())230 require.Equal(t, test.wantModes, snapshot.Modes())
231+ require.Equal(t, test.wantAutoWrap, snapshot.AutoWrapMode())
219 for row, want := range wantBounds {232 for row, want := range wantBounds {
220 require.Equal(t, want, snapshot.Bound(row))233 require.Equal(t, want, snapshot.Bound(row))
221 }234 }
M screen_write.go +15 -7
@@ -61,11 +61,16 @@ func (s *Screen) putPrintable(r rune) {
6161 if s.frame.Width == 0 || s.frame.Height == 0 {
6262 return
6363 }
64- // Deferred wrap: cursor sits past the last column.
64+ // Deferred wrap: cursor sits past the last column. Disabling DECAWM
65+ // cancels a pending wrap and leaves subsequent output at the right margin.
6566 if s.Col >= s.frame.Width {
66- s.buffer.soft(s.Row)
67- s.Col = 0
68- s.relinkWrap(s.index())
67+ if s.autoWrapMode {
68+ s.buffer.soft(s.Row)
69+ s.Col = 0
70+ s.relinkWrap(s.index())
71+ } else {
72+ s.Col = s.frame.Width - 1
73+ }
6974 }
7075 if s.Row >= s.frame.Height {
7176 s.Row = s.frame.Height - 1
@@ -74,9 +79,9 @@ func (s *Screen) putPrintable(r rune) {
7479 // A wide rune must never straddle the right edge. If it does not fit on the
7580 // current line, clear the abandoned last cell and wrap to the next line.
7681 if w == 2 && s.Col+1 >= s.frame.Width {
77- if s.frame.Width < 2 {
78- // The screen is too narrow to ever hold a wide rune; store a narrow
79- // replacement so the cell's renderer width matches its layout.
82+ if s.frame.Width < 2 || !s.autoWrapMode {
83+ // A wide rune cannot be represented at the right margin without an
84+ // orphaned continuation cell. Use the narrow replacement glyph.
8085 r = '\uFFFD'
8186 w = renderer.RuneWidth(r)
8287 } else {
@@ -136,6 +141,9 @@ func (s *Screen) putPrintable(r rune) {
136141 }
137142 s.record(renderer.Damage{Kind: renderer.DamageText, X: lo, Y: s.Row, Width: hi - lo + 1, Height: 1, Count: 1})
138143 s.Col += w
144+ if !s.autoWrapMode && s.Col >= s.frame.Width {
145+ s.Col = s.frame.Width - 1
146+ }
139147 }
140148
141149 // clearWidePairAt blanks both halves of a wide-character pair when the cell at
@@ -61,11 +61,16 @@ func (s *Screen) putPrintable(r rune) {
61 if s.frame.Width == 0 || s.frame.Height == 0 {61 if s.frame.Width == 0 || s.frame.Height == 0 {
62 return62 return
63 }63 }
64- // Deferred wrap: cursor sits past the last column.64+ // Deferred wrap: cursor sits past the last column. Disabling DECAWM
65+ // cancels a pending wrap and leaves subsequent output at the right margin.
65 if s.Col >= s.frame.Width {66 if s.Col >= s.frame.Width {
66- s.buffer.soft(s.Row)67+ if s.autoWrapMode {
67- s.Col = 068+ s.buffer.soft(s.Row)
68- s.relinkWrap(s.index())69+ s.Col = 0
70+ s.relinkWrap(s.index())
71+ } else {
72+ s.Col = s.frame.Width - 1
73+ }
69 }74 }
70 if s.Row >= s.frame.Height {75 if s.Row >= s.frame.Height {
71 s.Row = s.frame.Height - 176 s.Row = s.frame.Height - 1
@@ -74,9 +79,9 @@ func (s *Screen) putPrintable(r rune) {
74 // A wide rune must never straddle the right edge. If it does not fit on the79 // A wide rune must never straddle the right edge. If it does not fit on the
75 // current line, clear the abandoned last cell and wrap to the next line.80 // current line, clear the abandoned last cell and wrap to the next line.
76 if w == 2 && s.Col+1 >= s.frame.Width {81 if w == 2 && s.Col+1 >= s.frame.Width {
77- if s.frame.Width < 2 {82+ if s.frame.Width < 2 || !s.autoWrapMode {
78- // The screen is too narrow to ever hold a wide rune; store a narrow83+ // A wide rune cannot be represented at the right margin without an
79- // replacement so the cell's renderer width matches its layout.84+ // orphaned continuation cell. Use the narrow replacement glyph.
80 r = '\uFFFD'85 r = '\uFFFD'
81 w = renderer.RuneWidth(r)86 w = renderer.RuneWidth(r)
82 } else {87 } else {
@@ -136,6 +141,9 @@ func (s *Screen) putPrintable(r rune) {
136 }141 }
137 s.record(renderer.Damage{Kind: renderer.DamageText, X: lo, Y: s.Row, Width: hi - lo + 1, Height: 1, Count: 1})142 s.record(renderer.Damage{Kind: renderer.DamageText, X: lo, Y: s.Row, Width: hi - lo + 1, Height: 1, Count: 1})
138 s.Col += w143 s.Col += w
144+ if !s.autoWrapMode && s.Col >= s.frame.Width {
145+ s.Col = s.frame.Width - 1
146+ }
139 }147 }
140 148
141 // clearWidePairAt blanks both halves of a wide-character pair when the cell at149 // clearWidePairAt blanks both halves of a wide-character pair when the cell at

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 2

Tags 13

↑↓ moveenter openesc close