fix(screen): support DEC special graphics charsets, REP and DECSTR
- commit
- a3aae4548101cb114ec74e65ff456b10ed2558a5
- author
- brice · · 2026-10-15 02:29 UTC
- parent
- 36c429d
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
7 files · +137 -3
M screen.go +12 -0
| @@ -106,6 +106,12 @@ type Screen struct { | |||
| 106 | 106 | colorSchemeMode bool | |
| 107 | 107 | colorSchemeLight bool | |
| 108 | 108 | colorSchemeSet bool | |
| 109 | + | // charsets holds the final byte designated to G0 and G1 (ESC ( F and | |
| 110 | + | // ESC ) F); shiftOut selects G1 into GL (SO) instead of G0 (SI). | |
| 111 | + | charsets [2]byte | |
| 112 | + | shiftOut bool | |
| 113 | + | // lastPrinted is the last graphic rune written, repeated by REP. | |
| 114 | + | lastPrinted rune | |
| 109 | 115 | } | |
| 110 | 116 | ||
| 111 | 117 | func NewScreen(width, height int) *Screen { | |
| @@ -424,6 +430,12 @@ func (s *Screen) consumeEscape(data []byte) (consumed int, partial bool) { | |||
| 424 | 430 | if len(data) < 3 { | |
| 425 | 431 | return 0, true | |
| 426 | 432 | } | |
| 433 | + | switch data[1] { | |
| 434 | + | case '(': | |
| 435 | + | s.charsets[0] = data[2] | |
| 436 | + | case ')': | |
| 437 | + | s.charsets[1] = data[2] | |
| 438 | + | } | |
| 427 | 439 | return 3, false | |
| 428 | 440 | default: | |
| 429 | 441 | if data[1] >= 0x30 && data[1] <= 0x7e { | |
| @@ -106,6 +106,12 @@ type Screen struct { | |||||
| 106 | colorSchemeMode bool | 106 | colorSchemeMode bool | ||
| 107 | colorSchemeLight bool | 107 | colorSchemeLight bool | ||
| 108 | colorSchemeSet bool | 108 | colorSchemeSet bool | ||
| 109 | + | // charsets holds the final byte designated to G0 and G1 (ESC ( F and | |||
| 110 | + | // ESC ) F); shiftOut selects G1 into GL (SO) instead of G0 (SI). | |||
| 111 | + | charsets [2]byte | |||
| 112 | + | shiftOut bool | |||
| 113 | + | // lastPrinted is the last graphic rune written, repeated by REP. | |||
| 114 | + | lastPrinted rune | |||
| 109 | } | 115 | } | ||
| 110 | 116 | ||||
| 111 | func NewScreen(width, height int) *Screen { | 117 | func NewScreen(width, height int) *Screen { | ||
| @@ -424,6 +430,12 @@ func (s *Screen) consumeEscape(data []byte) (consumed int, partial bool) { | |||||
| 424 | if len(data) < 3 { | 430 | if len(data) < 3 { | ||
| 425 | return 0, true | 431 | return 0, true | ||
| 426 | } | 432 | } | ||
| 433 | + | switch data[1] { | |||
| 434 | + | case '(': | |||
| 435 | + | s.charsets[0] = data[2] | |||
| 436 | + | case ')': | |||
| 437 | + | s.charsets[1] = data[2] | |||
| 438 | + | } | |||
| 427 | return 3, false | 439 | return 3, false | ||
| 428 | default: | 440 | default: | ||
| 429 | if data[1] >= 0x30 && data[1] <= 0x7e { | 441 | if data[1] >= 0x30 && data[1] <= 0x7e { | ||
A screen_charset.go +26 -0
| @@ -0,0 +1,26 @@ | |||
| 1 | + | package vt | |
| 2 | + | ||
| 3 | + | // decSpecialGraphics maps 0x5f..0x7e to the DEC Special Graphics set | |
| 4 | + | // (designated by ESC ( 0 or ESC ) 0), used by ncurses for line drawing. | |
| 5 | + | var decSpecialGraphics = [...]rune{ | |
| 6 | + | ' ', '◆', '▒', '␉', '␌', '␍', '␊', '°', '±', '', '␋', '┘', '┐', '┌', '└', '┼', | |
| 7 | + | '⎺', '⎻', '─', '⎼', '⎽', '├', '┤', '┴', '┬', '│', '≤', '≥', 'π', '≠', '£', '·', | |
| 8 | + | } | |
| 9 | + | ||
| 10 | + | // maxRepeat bounds REP to one full screen: repeating further only rewrites | |
| 11 | + | // cells and scrolls, so a hostile count cannot stall the parser. | |
| 12 | + | func maxRepeat(width, height int) int { | |
| 13 | + | return max(width*height, 1) | |
| 14 | + | } | |
| 15 | + | ||
| 16 | + | // translateCharset maps r through the character set currently invoked into GL. | |
| 17 | + | func (s *Screen) translateCharset(r rune) rune { | |
| 18 | + | g := 0 | |
| 19 | + | if s.shiftOut { | |
| 20 | + | g = 1 | |
| 21 | + | } | |
| 22 | + | if s.charsets[g] == '0' && r >= 0x5f && r <= 0x7e { | |
| 23 | + | return decSpecialGraphics[r-0x5f] | |
| 24 | + | } | |
| 25 | + | return r | |
| 26 | + | } | |
| @@ -0,0 +1,26 @@ | |||||
| 1 | + | package vt | |||
| 2 | + | ||||
| 3 | + | // decSpecialGraphics maps 0x5f..0x7e to the DEC Special Graphics set | |||
| 4 | + | // (designated by ESC ( 0 or ESC ) 0), used by ncurses for line drawing. | |||
| 5 | + | var decSpecialGraphics = [...]rune{ | |||
| 6 | + | ' ', '◆', '▒', '␉', '␌', '␍', '␊', '°', '±', '', '␋', '┘', '┐', '┌', '└', '┼', | |||
| 7 | + | '⎺', '⎻', '─', '⎼', '⎽', '├', '┤', '┴', '┬', '│', '≤', '≥', 'π', '≠', '£', '·', | |||
| 8 | + | } | |||
| 9 | + | ||||
| 10 | + | // maxRepeat bounds REP to one full screen: repeating further only rewrites | |||
| 11 | + | // cells and scrolls, so a hostile count cannot stall the parser. | |||
| 12 | + | func maxRepeat(width, height int) int { | |||
| 13 | + | return max(width*height, 1) | |||
| 14 | + | } | |||
| 15 | + | ||||
| 16 | + | // translateCharset maps r through the character set currently invoked into GL. | |||
| 17 | + | func (s *Screen) translateCharset(r rune) rune { | |||
| 18 | + | g := 0 | |||
| 19 | + | if s.shiftOut { | |||
| 20 | + | g = 1 | |||
| 21 | + | } | |||
| 22 | + | if s.charsets[g] == '0' && r >= 0x5f && r <= 0x7e { | |||
| 23 | + | return decSpecialGraphics[r-0x5f] | |||
| 24 | + | } | |||
| 25 | + | return r | |||
| 26 | + | } | |||
A screen_charset_test.go +53 -0
| @@ -0,0 +1,53 @@ | |||
| 1 | + | package vt | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "strings" | |
| 5 | + | "testing" | |
| 6 | + | ) | |
| 7 | + | ||
| 8 | + | func TestCharsetsAndRepeat(t *testing.T) { | |
| 9 | + | tests := []struct { | |
| 10 | + | name string | |
| 11 | + | input string | |
| 12 | + | want string | |
| 13 | + | }{ | |
| 14 | + | {name: "G0 DEC special graphics draws lines", input: "\x1b(0lqqk\x1b(Bq", want: "┌──┐q"}, | |
| 15 | + | {name: "G1 via SO and SI", input: "\x1b)0q\x0eqx\x0fq", want: "q─│q"}, | |
| 16 | + | {name: "SO without G1 designation stays ASCII", input: "\x0eq\x0f", want: "q"}, | |
| 17 | + | {name: "chars outside the graphics range pass through", input: "\x1b(0Aa~", want: "A▒·"}, | |
| 18 | + | {name: "DECSC and DECRC restore charsets", input: "\x1b(0\x1b7\x1b(B\x1b8q", want: "─"}, | |
| 19 | + | {name: "DECSTR resets charsets", input: "\x1b(0q\x1b[!pq", want: "─q"}, | |
| 20 | + | {name: "RIS resets charsets", input: "\x1b(0\x1bcq", want: "q"}, | |
| 21 | + | {name: "REP repeats the last graphic char", input: "\x1b(0q\x1b[4b", want: "─────"}, | |
| 22 | + | {name: "REP defaults to one", input: "a\x1b[b", want: "aa"}, | |
| 23 | + | {name: "REP without a prior char is ignored", input: "\x1b[3bx", want: "x"}, | |
| 24 | + | {name: "REP after control repeats last graphic", input: "ab\r\x1b[2b", want: "bb"}, | |
| 25 | + | {name: "partial charset escape across writes", input: "\x1b(", want: ""}, | |
| 26 | + | } | |
| 27 | + | for _, tt := range tests { | |
| 28 | + | t.Run(tt.name, func(t *testing.T) { | |
| 29 | + | s := NewScreen(10, 3) | |
| 30 | + | s.Write([]byte(tt.input)) | |
| 31 | + | if got := strings.TrimRight(lineText(s, 0), " "); got != tt.want { | |
| 32 | + | t.Fatalf("line = %q, want %q", got, tt.want) | |
| 33 | + | } | |
| 34 | + | }) | |
| 35 | + | } | |
| 36 | + | } | |
| 37 | + | ||
| 38 | + | func TestCharsetDesignationSplitAcrossWrites(t *testing.T) { | |
| 39 | + | s := NewScreen(10, 1) | |
| 40 | + | s.Write([]byte("\x1b(")) | |
| 41 | + | s.Write([]byte("0q")) | |
| 42 | + | if got := strings.TrimRight(lineText(s, 0), " "); got != "─" { | |
| 43 | + | t.Fatalf("line = %q, want %q", got, "─") | |
| 44 | + | } | |
| 45 | + | } | |
| 46 | + | ||
| 47 | + | func TestRepeatIsBounded(t *testing.T) { | |
| 48 | + | s := NewScreen(4, 2) | |
| 49 | + | s.Write([]byte("x\x1b[999999999b")) | |
| 50 | + | if got := lineText(s, 0); got != "xxxx" { | |
| 51 | + | t.Fatalf("line = %q, want %q", got, "xxxx") | |
| 52 | + | } | |
| 53 | + | } | |
| @@ -0,0 +1,53 @@ | |||||
| 1 | + | package vt | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "strings" | |||
| 5 | + | "testing" | |||
| 6 | + | ) | |||
| 7 | + | ||||
| 8 | + | func TestCharsetsAndRepeat(t *testing.T) { | |||
| 9 | + | tests := []struct { | |||
| 10 | + | name string | |||
| 11 | + | input string | |||
| 12 | + | want string | |||
| 13 | + | }{ | |||
| 14 | + | {name: "G0 DEC special graphics draws lines", input: "\x1b(0lqqk\x1b(Bq", want: "┌──┐q"}, | |||
| 15 | + | {name: "G1 via SO and SI", input: "\x1b)0q\x0eqx\x0fq", want: "q─│q"}, | |||
| 16 | + | {name: "SO without G1 designation stays ASCII", input: "\x0eq\x0f", want: "q"}, | |||
| 17 | + | {name: "chars outside the graphics range pass through", input: "\x1b(0Aa~", want: "A▒·"}, | |||
| 18 | + | {name: "DECSC and DECRC restore charsets", input: "\x1b(0\x1b7\x1b(B\x1b8q", want: "─"}, | |||
| 19 | + | {name: "DECSTR resets charsets", input: "\x1b(0q\x1b[!pq", want: "─q"}, | |||
| 20 | + | {name: "RIS resets charsets", input: "\x1b(0\x1bcq", want: "q"}, | |||
| 21 | + | {name: "REP repeats the last graphic char", input: "\x1b(0q\x1b[4b", want: "─────"}, | |||
| 22 | + | {name: "REP defaults to one", input: "a\x1b[b", want: "aa"}, | |||
| 23 | + | {name: "REP without a prior char is ignored", input: "\x1b[3bx", want: "x"}, | |||
| 24 | + | {name: "REP after control repeats last graphic", input: "ab\r\x1b[2b", want: "bb"}, | |||
| 25 | + | {name: "partial charset escape across writes", input: "\x1b(", want: ""}, | |||
| 26 | + | } | |||
| 27 | + | for _, tt := range tests { | |||
| 28 | + | t.Run(tt.name, func(t *testing.T) { | |||
| 29 | + | s := NewScreen(10, 3) | |||
| 30 | + | s.Write([]byte(tt.input)) | |||
| 31 | + | if got := strings.TrimRight(lineText(s, 0), " "); got != tt.want { | |||
| 32 | + | t.Fatalf("line = %q, want %q", got, tt.want) | |||
| 33 | + | } | |||
| 34 | + | }) | |||
| 35 | + | } | |||
| 36 | + | } | |||
| 37 | + | ||||
| 38 | + | func TestCharsetDesignationSplitAcrossWrites(t *testing.T) { | |||
| 39 | + | s := NewScreen(10, 1) | |||
| 40 | + | s.Write([]byte("\x1b(")) | |||
| 41 | + | s.Write([]byte("0q")) | |||
| 42 | + | if got := strings.TrimRight(lineText(s, 0), " "); got != "─" { | |||
| 43 | + | t.Fatalf("line = %q, want %q", got, "─") | |||
| 44 | + | } | |||
| 45 | + | } | |||
| 46 | + | ||||
| 47 | + | func TestRepeatIsBounded(t *testing.T) { | |||
| 48 | + | s := NewScreen(4, 2) | |||
| 49 | + | s.Write([]byte("x\x1b[999999999b")) | |||
| 50 | + | if got := lineText(s, 0); got != "xxxx" { | |||
| 51 | + | t.Fatalf("line = %q, want %q", got, "xxxx") | |||
| 52 | + | } | |||
| 53 | + | } | |||
M screen_csi.go +13 -0
| @@ -63,6 +63,10 @@ func (s *Screen) applyCSI(params string, cmd byte) { | |||
| 63 | 63 | s.respond(resp) | |
| 64 | 64 | } | |
| 65 | 65 | case 'p': | |
| 66 | + | if params == "!" { | |
| 67 | + | s.softReset() | |
| 68 | + | return | |
| 69 | + | } | |
| 66 | 70 | if modeText, ok := strings.CutSuffix(params, "$"); ok { | |
| 67 | 71 | if private { | |
| 68 | 72 | modeText = strings.TrimPrefix(modeText, "?") | |
| @@ -83,6 +87,15 @@ func (s *Screen) applyCSI(params string, cmd byte) { | |||
| 83 | 87 | resp = append(resp, "$y"...) | |
| 84 | 88 | s.respond(resp) | |
| 85 | 89 | } | |
| 90 | + | case 'b': | |
| 91 | + | // REP repeats the last printed graphic character; ncurses relies on it | |
| 92 | + | // for long runs when terminfo advertises rep (xterm-256color does). | |
| 93 | + | if private || s.lastPrinted == 0 { | |
| 94 | + | return | |
| 95 | + | } | |
| 96 | + | for n := min(firstPositive(parts, 1), maxRepeat(s.frame.Width, s.frame.Height)); n > 0; n-- { | |
| 97 | + | s.putPrintable(s.lastPrinted) | |
| 98 | + | } | |
| 86 | 99 | case 'm': | |
| 87 | 100 | if strings.HasPrefix(params, ">") { | |
| 88 | 101 | return | |
| @@ -63,6 +63,10 @@ func (s *Screen) applyCSI(params string, cmd byte) { | |||||
| 63 | s.respond(resp) | 63 | s.respond(resp) | ||
| 64 | } | 64 | } | ||
| 65 | case 'p': | 65 | case 'p': | ||
| 66 | + | if params == "!" { | |||
| 67 | + | s.softReset() | |||
| 68 | + | return | |||
| 69 | + | } | |||
| 66 | if modeText, ok := strings.CutSuffix(params, "$"); ok { | 70 | if modeText, ok := strings.CutSuffix(params, "$"); ok { | ||
| 67 | if private { | 71 | if private { | ||
| 68 | modeText = strings.TrimPrefix(modeText, "?") | 72 | modeText = strings.TrimPrefix(modeText, "?") | ||
| @@ -83,6 +87,15 @@ func (s *Screen) applyCSI(params string, cmd byte) { | |||||
| 83 | resp = append(resp, "$y"...) | 87 | resp = append(resp, "$y"...) | ||
| 84 | s.respond(resp) | 88 | s.respond(resp) | ||
| 85 | } | 89 | } | ||
| 90 | + | case 'b': | |||
| 91 | + | // REP repeats the last printed graphic character; ncurses relies on it | |||
| 92 | + | // for long runs when terminfo advertises rep (xterm-256color does). | |||
| 93 | + | if private || s.lastPrinted == 0 { | |||
| 94 | + | return | |||
| 95 | + | } | |||
| 96 | + | for n := min(firstPositive(parts, 1), maxRepeat(s.frame.Width, s.frame.Height)); n > 0; n-- { | |||
| 97 | + | s.putPrintable(s.lastPrinted) | |||
| 98 | + | } | |||
| 86 | case 'm': | 99 | case 'm': | ||
| 87 | if strings.HasPrefix(params, ">") { | 100 | if strings.HasPrefix(params, ">") { | ||
| 88 | return | 101 | return | ||
M screen_cursor.go +6 -1
| @@ -14,6 +14,8 @@ type cursorState struct { | |||
| 14 | 14 | originMode bool | |
| 15 | 15 | insertMode bool | |
| 16 | 16 | autoWrapMode bool | |
| 17 | + | charsets [2]byte | |
| 18 | + | shiftOut bool | |
| 17 | 19 | saved bool | |
| 18 | 20 | } | |
| 19 | 21 | ||
| @@ -78,7 +80,8 @@ func (s *Screen) saveCursor() { | |||
| 78 | 80 | s.savedCursor = cursorState{ | |
| 79 | 81 | row: s.Row, col: s.Col, style: s.Style, | |
| 80 | 82 | originMode: s.originMode, insertMode: s.insertMode, | |
| 81 | - | autoWrapMode: s.autoWrapMode, saved: true, | |
| 83 | + | autoWrapMode: s.autoWrapMode, charsets: s.charsets, | |
| 84 | + | shiftOut: s.shiftOut, saved: true, | |
| 82 | 85 | } | |
| 83 | 86 | } | |
| 84 | 87 | ||
| @@ -92,4 +95,6 @@ func (s *Screen) restoreCursor() { | |||
| 92 | 95 | s.originMode = s.savedCursor.originMode | |
| 93 | 96 | s.insertMode = s.savedCursor.insertMode | |
| 94 | 97 | s.autoWrapMode = s.savedCursor.autoWrapMode | |
| 98 | + | s.charsets = s.savedCursor.charsets | |
| 99 | + | s.shiftOut = s.savedCursor.shiftOut | |
| 95 | 100 | } | |
| @@ -14,6 +14,8 @@ type cursorState struct { | |||||
| 14 | originMode bool | 14 | originMode bool | ||
| 15 | insertMode bool | 15 | insertMode bool | ||
| 16 | autoWrapMode bool | 16 | autoWrapMode bool | ||
| 17 | + | charsets [2]byte | |||
| 18 | + | shiftOut bool | |||
| 17 | saved bool | 19 | saved bool | ||
| 18 | } | 20 | } | ||
| 19 | 21 | ||||
| @@ -78,7 +80,8 @@ func (s *Screen) saveCursor() { | |||||
| 78 | s.savedCursor = cursorState{ | 80 | s.savedCursor = cursorState{ | ||
| 79 | row: s.Row, col: s.Col, style: s.Style, | 81 | row: s.Row, col: s.Col, style: s.Style, | ||
| 80 | originMode: s.originMode, insertMode: s.insertMode, | 82 | originMode: s.originMode, insertMode: s.insertMode, | ||
| 81 | - | autoWrapMode: s.autoWrapMode, saved: true, | 83 | + | autoWrapMode: s.autoWrapMode, charsets: s.charsets, |
| 84 | + | shiftOut: s.shiftOut, saved: true, | |||
| 82 | } | 85 | } | ||
| 83 | } | 86 | } | ||
| 84 | 87 | ||||
| @@ -92,4 +95,6 @@ func (s *Screen) restoreCursor() { | |||||
| 92 | s.originMode = s.savedCursor.originMode | 95 | s.originMode = s.savedCursor.originMode | ||
| 93 | s.insertMode = s.savedCursor.insertMode | 96 | s.insertMode = s.savedCursor.insertMode | ||
| 94 | s.autoWrapMode = s.savedCursor.autoWrapMode | 97 | s.autoWrapMode = s.savedCursor.autoWrapMode | ||
| 98 | + | s.charsets = s.savedCursor.charsets | |||
| 99 | + | s.shiftOut = s.savedCursor.shiftOut | |||
| 95 | } | 100 | } | ||
M screen_modes.go +18 -0
| @@ -121,10 +121,28 @@ func (s *Screen) reset() { | |||
| 121 | 121 | s.bracketedPaste = false | |
| 122 | 122 | s.kittyKeyboard = nil | |
| 123 | 123 | s.colorSchemeMode = false | |
| 124 | + | s.charsets = [2]byte{} | |
| 125 | + | s.shiftOut = false | |
| 126 | + | s.lastPrinted = 0 | |
| 124 | 127 | s.resetScrollRegion() | |
| 125 | 128 | s.fullRedraw() | |
| 126 | 129 | } | |
| 127 | 130 | ||
| 131 | + | // softReset implements DECSTR (CSI ! p): it resets modes, rendition, margins, | |
| 132 | + | // and character sets without touching screen contents or the cursor position. | |
| 133 | + | func (s *Screen) softReset() { | |
| 134 | + | s.Style = renderer.DefaultStyle() | |
| 135 | + | s.savedCursor = cursorState{} | |
| 136 | + | s.originMode = false | |
| 137 | + | s.insertMode = false | |
| 138 | + | s.autoWrapMode = true | |
| 139 | + | s.applicationCursorMode = false | |
| 140 | + | s.cursorVisible = true | |
| 141 | + | s.charsets = [2]byte{} | |
| 142 | + | s.shiftOut = false | |
| 143 | + | s.resetScrollRegion() | |
| 144 | + | } | |
| 145 | + | ||
| 128 | 146 | func (s *Screen) setScrollRegion(parts []int) { | |
| 129 | 147 | if s.frame.Height == 0 { | |
| 130 | 148 | return | |
| @@ -121,10 +121,28 @@ func (s *Screen) reset() { | |||||
| 121 | s.bracketedPaste = false | 121 | s.bracketedPaste = false | ||
| 122 | s.kittyKeyboard = nil | 122 | s.kittyKeyboard = nil | ||
| 123 | s.colorSchemeMode = false | 123 | s.colorSchemeMode = false | ||
| 124 | + | s.charsets = [2]byte{} | |||
| 125 | + | s.shiftOut = false | |||
| 126 | + | s.lastPrinted = 0 | |||
| 124 | s.resetScrollRegion() | 127 | s.resetScrollRegion() | ||
| 125 | s.fullRedraw() | 128 | s.fullRedraw() | ||
| 126 | } | 129 | } | ||
| 127 | 130 | ||||
| 131 | + | // softReset implements DECSTR (CSI ! p): it resets modes, rendition, margins, | |||
| 132 | + | // and character sets without touching screen contents or the cursor position. | |||
| 133 | + | func (s *Screen) softReset() { | |||
| 134 | + | s.Style = renderer.DefaultStyle() | |||
| 135 | + | s.savedCursor = cursorState{} | |||
| 136 | + | s.originMode = false | |||
| 137 | + | s.insertMode = false | |||
| 138 | + | s.autoWrapMode = true | |||
| 139 | + | s.applicationCursorMode = false | |||
| 140 | + | s.cursorVisible = true | |||
| 141 | + | s.charsets = [2]byte{} | |||
| 142 | + | s.shiftOut = false | |||
| 143 | + | s.resetScrollRegion() | |||
| 144 | + | } | |||
| 145 | + | ||||
| 128 | func (s *Screen) setScrollRegion(parts []int) { | 146 | func (s *Screen) setScrollRegion(parts []int) { | ||
| 129 | if s.frame.Height == 0 { | 147 | if s.frame.Height == 0 { | ||
| 130 | return | 148 | return | ||
M screen_write.go +9 -2
| @@ -9,7 +9,13 @@ func (s *Screen) putRune(r rune) { | |||
| 9 | 9 | s.OnBell() | |
| 10 | 10 | } | |
| 11 | 11 | return | |
| 12 | - | case 0x00, 0x0e, 0x0f, 0x7f: | |
| 12 | + | case 0x0e: | |
| 13 | + | s.shiftOut = true | |
| 14 | + | return | |
| 15 | + | case 0x0f: | |
| 16 | + | s.shiftOut = false | |
| 17 | + | return | |
| 18 | + | case 0x00, 0x7f: | |
| 13 | 19 | return | |
| 14 | 20 | case '\r': | |
| 15 | 21 | s.Col = 0 | |
| @@ -35,7 +41,7 @@ func (s *Screen) putRune(r rune) { | |||
| 35 | 41 | return | |
| 36 | 42 | } | |
| 37 | 43 | if r >= 0x20 { | |
| 38 | - | s.putPrintable(r) | |
| 44 | + | s.putPrintable(s.translateCharset(r)) | |
| 39 | 45 | } | |
| 40 | 46 | } | |
| 41 | 47 | } | |
| @@ -61,6 +67,7 @@ func (s *Screen) putPrintable(r rune) { | |||
| 61 | 67 | if s.frame.Width == 0 || s.frame.Height == 0 { | |
| 62 | 68 | return | |
| 63 | 69 | } | |
| 70 | + | s.lastPrinted = r | |
| 64 | 71 | // Deferred wrap: cursor sits past the last column. Disabling DECAWM | |
| 65 | 72 | // cancels a pending wrap and leaves subsequent output at the right margin. | |
| 66 | 73 | if s.Col >= s.frame.Width { | |
| @@ -9,7 +9,13 @@ func (s *Screen) putRune(r rune) { | |||||
| 9 | s.OnBell() | 9 | s.OnBell() | ||
| 10 | } | 10 | } | ||
| 11 | return | 11 | return | ||
| 12 | - | case 0x00, 0x0e, 0x0f, 0x7f: | 12 | + | case 0x0e: |
| 13 | + | s.shiftOut = true | |||
| 14 | + | return | |||
| 15 | + | case 0x0f: | |||
| 16 | + | s.shiftOut = false | |||
| 17 | + | return | |||
| 18 | + | case 0x00, 0x7f: | |||
| 13 | return | 19 | return | ||
| 14 | case '\r': | 20 | case '\r': | ||
| 15 | s.Col = 0 | 21 | s.Col = 0 | ||
| @@ -35,7 +41,7 @@ func (s *Screen) putRune(r rune) { | |||||
| 35 | return | 41 | return | ||
| 36 | } | 42 | } | ||
| 37 | if r >= 0x20 { | 43 | if r >= 0x20 { | ||
| 38 | - | s.putPrintable(r) | 44 | + | s.putPrintable(s.translateCharset(r)) |
| 39 | } | 45 | } | ||
| 40 | } | 46 | } | ||
| 41 | } | 47 | } | ||
| @@ -61,6 +67,7 @@ func (s *Screen) putPrintable(r rune) { | |||||
| 61 | if s.frame.Width == 0 || s.frame.Height == 0 { | 67 | if s.frame.Width == 0 || s.frame.Height == 0 { | ||
| 62 | return | 68 | return | ||
| 63 | } | 69 | } | ||
| 70 | + | s.lastPrinted = r | |||
| 64 | // Deferred wrap: cursor sits past the last column. Disabling DECAWM | 71 | // Deferred wrap: cursor sits past the last column. Disabling DECAWM | ||
| 65 | // cancels a pending wrap and leaves subsequent output at the right margin. | 72 | // cancels a pending wrap and leaves subsequent output at the right margin. | ||
| 66 | if s.Col >= s.frame.Width { | 73 | if s.Col >= s.frame.Width { | ||