From 5b591b0b4265b4a1bf2c1b8b8eb7914fcb9ce517 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Sun, 25 Jan 2026 15:26:36 +0100 Subject: [PATCH 1/3] Cleanup: remove unused field --- view.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/view.go b/view.go index ead9aa9..f2c43c5 100644 --- a/view.go +++ b/view.go @@ -167,9 +167,6 @@ type View struct { // If HasLoader is true, the message will be appended with a spinning loader animation HasLoader bool - // IgnoreCarriageReturns tells us whether to ignore '\r' characters - IgnoreCarriageReturns bool - // ParentView is the view which catches events bubbled up from the given view if there's no matching handler ParentView *View From f2e01ccce872b4b7df80887a9af1f6f0ad12142a Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Sun, 25 Jan 2026 15:18:29 +0100 Subject: [PATCH 2/3] Fix rendering of CRLF sequence ('\r\n') The FirstGraphemeCluster call returns this as a single character; we want to treat it the same way as a single \n. This would be a problem if e.g. a progress bar used \r repeatedly to paint over the same line, and then printed a \n to move on to the next line; the last pair of \r and \n was swallowed. Another scenario where this was a problem was if you stream output of a command to the log, and the command used \r\n as line feeds. This happens for example for a background fetch that fails with an error; in that case we print the combined output (stdout plus stderr) to the log after the command finished, and for some reason it uses \r\n in that case (I can't actually explain why; when I do `git fetch --all | xxd` I see only bare \n characters). All output would appear on one line then. --- view.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/view.go b/view.go index f2c43c5..9ba8b7e 100644 --- a/view.go +++ b/view.go @@ -462,6 +462,10 @@ func characterEquals(chr []byte, b byte) bool { return len(chr) == 1 && chr[0] == b } +func isCRLF(chr []byte) bool { + return len(chr) == 2 && chr[0] == '\r' && chr[1] == '\n' +} + // String returns a string from a given cell slice. func (l lineType) String() string { var str strings.Builder @@ -837,7 +841,7 @@ func (v *View) write(p []byte) { chr, remaining, width, state = uniseg.FirstGraphemeCluster(remaining, state) switch { - case characterEquals(chr, '\n'): + case characterEquals(chr, '\n') || isCRLF(chr): finishLine() advanceToNextLine() case characterEquals(chr, '\r'): From fec19755170198e676471ff8c6c255bd2a52206b Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Mon, 26 Jan 2026 09:30:41 +0100 Subject: [PATCH 3/3] Parse and skip character-set designation sequences The escape sequences `ESC ( ...`, `ESC ) ...`, `ESC * ...`, and `ESC + ...` are all character set designation sequences (or G-set designations). There's nothing useful we can do with them, so we just skip them. In practice, the only one that you are likely to see is `ESC ( B`, which is sent as part of `tput sgr0`, which is sometimes used in scripts to reset all graphics attributes to defaults. --- escape.go | 11 +++++++++++ escape_test.go | 20 ++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/escape.go b/escape.go index 7c500d9..cb557f0 100644 --- a/escape.go +++ b/escape.go @@ -39,6 +39,7 @@ func (self noInstruction) isInstruction() {} const ( stateNone escapeState = iota stateEscape + stateCharacterSetDesignation stateCSI stateParams stateOSC @@ -144,9 +145,19 @@ func (ei *escapeInterpreter) parseOne(ch []byte) (isEscape bool, err error) { case characterEquals(ch, ']'): ei.state = stateOSC return true, nil + case characterEquals(ch, '('), + characterEquals(ch, ')'), + characterEquals(ch, '*'), + characterEquals(ch, '+'): + ei.state = stateCharacterSetDesignation + return true, nil default: return false, errNotCSI } + case stateCharacterSetDesignation: + // Not supported, so just skip it + ei.state = stateNone + return true, nil case stateCSI: switch { case len(ch) == 1 && ch[0] >= '0' && ch[0] <= '9': diff --git a/escape_test.go b/escape_test.go index b217cc0..0073463 100644 --- a/escape_test.go +++ b/escape_test.go @@ -28,6 +28,26 @@ func TestParseOne(t *testing.T) { parseEscRunes(t, ei, "\x1b[1K") _, ok = ei.instruction.(noInstruction) assert.Equal(t, true, ok) + + ei = newEscapeInterpreter(OutputNormal) + parseEscRunes(t, ei, "\x1b(B") + _, ok = ei.instruction.(noInstruction) + assert.Equal(t, true, ok) + + ei = newEscapeInterpreter(OutputNormal) + parseEscRunes(t, ei, "\x1b)0") + _, ok = ei.instruction.(noInstruction) + assert.Equal(t, true, ok) + + ei = newEscapeInterpreter(OutputNormal) + parseEscRunes(t, ei, "\x1b*A") + _, ok = ei.instruction.(noInstruction) + assert.Equal(t, true, ok) + + ei = newEscapeInterpreter(OutputNormal) + parseEscRunes(t, ei, "\x1b+K") + _, ok = ei.instruction.(noInstruction) + assert.Equal(t, true, ok) } func TestParseOneColours(t *testing.T) {