fix: mascot animation, makes syntax highlighting blink

This commit is contained in:
2026-07-30 14:37:39 +02:00
parent 0d2af19c6d
commit 590a863e26
5 changed files with 91 additions and 3 deletions

View File

@@ -72,6 +72,52 @@ func TestDiffletStylesOnlyDiffSigns(t *testing.T) {
} }
} }
func TestDiffletAnimationDoesNotChangeThreadCommentRows(t *testing.T) {
if err := applyTheme("dark"); err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = applyTheme("dark") })
app := NewAppWithSettings(
&recordingService{}, "owner", "repository", false, 10, 10,
AppSettings{Mascot: true, MascotAnimated: true},
)
app.screen = threadScreen
app.loading = false
app.details = PRDetails{
PullRequest: PullRequest{
Owner: "owner", Repository: "repository",
RepoWithOwner: "owner/repository", Number: 42,
Title: "Stable highlighted comments",
},
Threads: []ReviewThread{{
ID: "thread", Path: "main.go",
Comments: []ReviewComment{{
ID: "comment", Author: "reviewer",
Body: "```go\nfunc main() {\n\tprintln(\"stable\")\n}\n```",
}},
}},
}
updated, _ := app.Update(tea.WindowSizeMsg{Width: 100, Height: 30})
app = updated.(App)
before := strings.Split(app.View(), "\n")
generation := app.difflet.generation
updated, _ = app.Update(diffletTickMsg{generation: generation})
app = updated.(App)
after := strings.Split(app.View(), "\n")
if len(before) != len(after) {
t.Fatalf("animation changed frame height from %d to %d", len(before), len(after))
}
for row := diffletHeight; row < len(before); row++ {
if before[row] != after[row] {
t.Fatalf("animation changed non-mascot row %d:\nbefore %q\nafter %q",
row, before[row], after[row])
}
}
}
func TestDiffletEnabledSitsRightOfWrappingHeader(t *testing.T) { func TestDiffletEnabledSitsRightOfWrappingHeader(t *testing.T) {
app := NewAppWithSettings( app := NewAppWithSettings(
&recordingService{}, "owner", "repository", false, 10, 10, &recordingService{}, "owner", "repository", false, 10, 10,

View File

@@ -97,6 +97,7 @@ func commentMarkdownRenderer(width int) (*glamour.TermRenderer, error) {
style.Code.Suffix = "" style.Code.Suffix = ""
renderer, err := glamour.NewTermRenderer( renderer, err := glamour.NewTermRenderer(
glamour.WithStyles(style), glamour.WithStyles(style),
glamour.WithChromaFormatter("terminal16m"),
glamour.WithWordWrap(width), glamour.WithWordWrap(width),
glamour.WithTableWrap(true), glamour.WithTableWrap(true),
glamour.WithPreservedNewLines(), glamour.WithPreservedNewLines(),

View File

@@ -49,6 +49,21 @@ func (o *terminalCursorOutput) Write(value []byte) (int, error) {
o.mu.Lock() o.mu.Lock()
defer o.mu.Unlock() defer o.mu.Unlock()
// Bubble Tea v1 can expose intermediate rows from an animated partial
// repaint. This is especially visible when unchanged Markdown code blocks
// below the changed rows contain dense ANSI styling. Terminals that support
// synchronized output hold the completed frame until the reset sequence;
// terminals that do not support it safely ignore both sequences.
if !bytes.Equal(value, []byte(ansi.ShowCursor)) &&
!bytes.Equal(value, []byte(ansi.HideCursor)) {
if _, err := io.WriteString(o.file, ansi.SetSynchronizedOutputMode); err != nil {
return 0, err
}
defer func() {
_, _ = io.WriteString(o.file, ansi.ResetSynchronizedOutputMode)
}()
}
written, err := o.file.Write(value) written, err := o.file.Write(value)
if err != nil || written != len(value) { if err != nil || written != len(value) {
return written, err return written, err

View File

@@ -25,7 +25,7 @@ func TestTerminalCursorOutputPositionsHardwareBarAfterFrame(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
wantSuffix := ansi.SetCursorStyle(5) + ansi.CursorPosition(7, 4) + ansi.ShowCursor wantSuffix := ansi.SetCursorStyle(5) + ansi.CursorPosition(7, 4) + ansi.ShowCursor
if !strings.HasSuffix(string(content), wantSuffix) { if !strings.HasSuffix(string(content), wantSuffix+ansi.ResetSynchronizedOutputMode) {
t.Fatalf("cursor output = %q, want suffix %q", content, wantSuffix) t.Fatalf("cursor output = %q, want suffix %q", content, wantSuffix)
} }
} }
@@ -46,7 +46,33 @@ func TestTerminalCursorOutputHidesCursorOutsideInsertMode(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if !strings.HasSuffix(string(content), ansi.HideCursor) { if !strings.HasSuffix(
string(content),
ansi.HideCursor+ansi.ResetSynchronizedOutputMode,
) {
t.Fatalf("cursor output did not hide cursor: %q", content) t.Fatalf("cursor output did not hide cursor: %q", content)
} }
} }
func TestTerminalCursorOutputSynchronizesCompletedFrames(t *testing.T) {
file, err := os.CreateTemp(t.TempDir(), "cursor-output")
if err != nil {
t.Fatal(err)
}
defer file.Close()
output := newTerminalCursorOutput(file)
output.SetCursor(false, 0, 0)
if _, err := output.Write([]byte("animated frame")); err != nil {
t.Fatal(err)
}
content, err := os.ReadFile(file.Name())
if err != nil {
t.Fatal(err)
}
want := ansi.SetSynchronizedOutputMode + "animated frame" +
ansi.HideCursor + ansi.ResetSynchronizedOutputMode
if string(content) != want {
t.Fatalf("synchronized frame output = %q, want %q", content, want)
}
}

View File

@@ -1,3 +1,3 @@
package main package main
const dipleVersion = "0.1.1" const dipleVersion = "0.1.2"