From 2b1e688838915588d751d3836f700d9e7db0dac4 Mon Sep 17 00:00:00 2001 From: "Calmcacil B." <716671+calmcacil@users.noreply.github.com> Date: Thu, 16 Jul 2026 23:43:08 +0200 Subject: [PATCH] fix(anime): render frames correctly in raw terminals --- internal/anisearch/anisearch_test.go | 20 ++++++++++++++++++++ internal/anisearch/input.go | 8 ++++---- internal/anisearch/search.go | 19 ++++++++++++++----- 3 files changed, 38 insertions(+), 9 deletions(-) diff --git a/internal/anisearch/anisearch_test.go b/internal/anisearch/anisearch_test.go index 8484c7d..0af2396 100644 --- a/internal/anisearch/anisearch_test.go +++ b/internal/anisearch/anisearch_test.go @@ -731,6 +731,26 @@ func TestRenderSearchResultsLayout(t *testing.T) { } } +func TestWriteTerminalFrameUsesCRLFInRawMode(t *testing.T) { + var output bytes.Buffer + writeTerminalFrame(&output, "first\nsecond\n", true) + + want := "\033[H\033[Jfirst\r\nsecond\r\n" + if got := output.String(); got != want { + t.Fatalf("writeTerminalFrame() = %q, want %q", got, want) + } +} + +func TestWriteTerminalFramePreservesNewlinesOutsideRawMode(t *testing.T) { + var output bytes.Buffer + writeTerminalFrame(&output, "first\nsecond\n", false) + + want := "\033[H\033[Jfirst\nsecond\n" + if got := output.String(); got != want { + t.Fatalf("writeTerminalFrame() = %q, want %q", got, want) + } +} + func TestFormatEpisodes(t *testing.T) { tests := []struct { name string diff --git a/internal/anisearch/input.go b/internal/anisearch/input.go index d6affde..f7b9486 100644 --- a/internal/anisearch/input.go +++ b/internal/anisearch/input.go @@ -73,24 +73,24 @@ func readKey() KeyEvent { // runInRawTerminal executes f while stdin is in raw terminal mode. // It restores the terminal state before returning. -func runInRawTerminal(f func()) { +func runInRawTerminal(f func(raw bool)) { fd := int(os.Stdin.Fd()) if !term.IsTerminal(fd) { // Not a terminal; just run f directly. - f() + f(false) return } oldState, err := term.MakeRaw(fd) if err != nil { // Cannot set raw mode; run f directly anyway. - f() + f(false) return } defer term.Restore(fd, oldState) //nolint:errcheck - f() + f(true) } // PromptForQuery reads a search query from stdin. diff --git a/internal/anisearch/search.go b/internal/anisearch/search.go index d31e114..327d8b3 100644 --- a/internal/anisearch/search.go +++ b/internal/anisearch/search.go @@ -3,7 +3,9 @@ package anisearch import ( "context" "fmt" + "io" "os" + "strings" "time" "github.com/calmcacil/CalmsToolkit/internal/config" @@ -132,11 +134,11 @@ func runInteractive(ctx context.Context, client *AniListClient, query string, cu selected := 0 currentPage := 1 - runInRawTerminal(func() { + runInRawTerminal(func(raw bool) { for { // Render search results. output := renderSearchResults(query, currentResult, selected, mapping, cfg) - fmt.Fprint(os.Stdout, "\033[H\033[J"+output) + writeTerminalFrame(os.Stdout, output, raw) // Read key. key := readKey() @@ -152,7 +154,7 @@ func runInteractive(ctx context.Context, client *AniListClient, query string, cu } case KeyEnter: if selected >= 0 && selected < len(currentResult.Media) { - runDetailView(currentResult.Media[selected], mapping, cfg) + runDetailView(currentResult.Media[selected], mapping, cfg, raw) } case KeyNext: if currentResult.PageInfo.HasNextPage { @@ -186,14 +188,14 @@ func runInteractive(ctx context.Context, client *AniListClient, query string, cu } // runDetailView shows the detail for a single show until the user presses B or Q. -func runDetailView(show Show, mapping *AnibridgeMapping, cfg ToolConfig) { +func runDetailView(show Show, mapping *AnibridgeMapping, cfg ToolConfig, raw bool) { tvdbID := 0 if mapping != nil { tvdbID, _ = mapping.LookupByAniList(show.ID) } output := renderDetail(show, tvdbID, cfg) - fmt.Fprint(os.Stdout, "\033[H\033[J"+output) + writeTerminalFrame(os.Stdout, output, raw) for { key := readKey() @@ -207,3 +209,10 @@ func runDetailView(show Show, mapping *AnibridgeMapping, cfg ToolConfig) { } } } + +func writeTerminalFrame(w io.Writer, frame string, raw bool) { + if raw { + frame = strings.ReplaceAll(frame, "\n", "\r\n") + } + fmt.Fprint(w, "\033[H\033[J"+frame) +}