From 800d3755fab1c79821867adb30931534e5666ee7 Mon Sep 17 00:00:00 2001 From: stlim Date: Fri, 21 Aug 2026 10:52:54 +0900 Subject: [PATCH] refactor: Retire the git diff theme --- README.md | 4 +- cmd/termtype/menu_test.go | 7 ++ internal/themes/git_diff_test.go | 61 ------------ internal/themes/git_diff_theme.go | 152 ------------------------------ 4 files changed, 8 insertions(+), 216 deletions(-) delete mode 100644 internal/themes/git_diff_test.go delete mode 100644 internal/themes/git_diff_theme.go diff --git a/README.md b/README.md index cf5d6c3..bc7d6bd 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,7 @@ Typing practice in your terminal. ![TermType — the cozy theme](docs/termtype-cozy.gif) - **Seven themes** — from a warm minimal screen (`cozy`, the default) to a - log stream, a Matrix rain, a git diff, a hex editor, and a live Claude - Code session. + log stream, a Matrix rain, a hex editor, and a live Claude Code session. - **Sentences or words** — the built-in sentence pool, or a random stream of common English words. - **Normal and Time Attack** — untimed rounds, or a 15/30/60-second race. @@ -103,7 +102,6 @@ It is auto-enabled for non-UTF-8 locales, or force it with - `simple` — a plain, clean screen. - `matrix` — green rain, inspired by The Matrix. - `hex` — mimics a hex editor. -- `diff` — looks like a git diff. - `claude` — composing a message in a live Claude Code session. - `tutor` — a touch-typing tutor: a finger-zone-colored keyboard with the next key highlighted, and a pair of hands pointing at the finger to use. diff --git a/cmd/termtype/menu_test.go b/cmd/termtype/menu_test.go index 0e21f26..e978784 100644 --- a/cmd/termtype/menu_test.go +++ b/cmd/termtype/menu_test.go @@ -97,3 +97,10 @@ func TestRestoresSavedTheme(t *testing.T) { t.Fatalf("idx = %d, want %d (saved theme not restored)", got, want) } } + +func TestRemovedThemeFallsBackToCozy(t *testing.T) { + m := newMenuModel("diff") // 구버전 config에 남아 있을 수 있는 값 + if got := m.themes[m.idx]; got != "cozy" { + t.Errorf("removed theme should fall back to cozy, got %q", got) + } +} diff --git a/internal/themes/git_diff_test.go b/internal/themes/git_diff_test.go deleted file mode 100644 index 91dadda..0000000 --- a/internal/themes/git_diff_test.go +++ /dev/null @@ -1,61 +0,0 @@ -package themes - -import ( - "strings" - "testing" - - "github.com/gdamore/tcell/v2" - "github.com/namest504/termtype/internal/domain" - "github.com/namest504/termtype/internal/ui" -) - -func rowText(ss tcell.SimulationScreen, y int) string { - cells, w, _ := ss.GetContents() - var b strings.Builder - for x := 0; x < w; x++ { - c := cells[y*w+x] - if len(c.Runes) > 0 && c.Runes[0] != 0 { - b.WriteRune(c.Runes[0]) - } else { - b.WriteByte(' ') - } - } - return strings.TrimRight(b.String(), " ") -} - -// At a narrow width the diff theme must wrap a long target into several "+ " -// lines instead of letting it run off the edge, and each wrapped row must stay -// within the terminal width. -func TestDiff_WrapsLongTarget(t *testing.T) { - ss := tcell.NewSimulationScreen("") - if err := ss.Init(); err != nil { - t.Fatal(err) - } - const w = 24 - ss.SetSize(w, 14) - - gs := &domain.GameState{Sentences: domain.Sentences} - th := Themes["diff"] - th.ResetState(gs) - gs.TargetSentence = "The quick brown fox jumps over the lazy dog." - gs.UserInput = "" - th.UpdateScreen(ui.NewRenderer(ss), gs) - ss.Show() - - plusLines := 0 - cells, cw, ch := ss.GetContents() - for y := 0; y < ch; y++ { - row := rowText(ss, y) - if strings.HasPrefix(row, "+ ") { - plusLines++ - } - // Guard against drawing past the right edge. - if len([]rune(row)) > cw { - t.Errorf("row %d overflows width %d: %q", y, cw, row) - } - } - _ = cells - if plusLines < 2 { - t.Errorf("expected the target to wrap into >=2 '+ ' lines, got %d", plusLines) - } -} diff --git a/internal/themes/git_diff_theme.go b/internal/themes/git_diff_theme.go deleted file mode 100644 index 5c0fed2..0000000 --- a/internal/themes/git_diff_theme.go +++ /dev/null @@ -1,152 +0,0 @@ -package themes - -import ( - "github.com/gdamore/tcell/v2" - "github.com/mattn/go-runewidth" - "github.com/namest504/termtype/internal/domain" - "github.com/namest504/termtype/internal/ui" -) - -func init() { - Themes["diff"] = &DiffTheme{} -} - -// DiffTheme mimics a git diff UI: the target is the added hunk, typed over -// a plausible bit of code with a couple of removed lines above it. -type DiffTheme struct{} - -// diffLine is one non-target row of the fake hunk. -type diffLine struct { - mark rune // ' ' unchanged context, '-' removed - text string -} - -var diffLead = []diffLine{ - {' ', "func handleRequest(w http.ResponseWriter, r *http.Request) {"}, - {' ', " defer r.Body.Close()"}, - {'-', " data, _ := io.ReadAll(r.Body)"}, - {'-', " process(data)"}, -} - -var diffTail = []diffLine{ - {' ', " w.WriteHeader(http.StatusOK)"}, - {' ', "}"}, -} - -func (t *DiffTheme) ResetState(gs *domain.GameState) { - gs.ResetCommon() - gs.TargetSentence = gs.RandomSentence() -} - -func (t *DiffTheme) UpdateScreen(renderer domain.Renderer, gs *domain.GameState) { - renderer.Clear() - w, h := renderer.Size() - - dim := tcell.StyleDefault.Foreground(tcell.ColorDimGray) - renderer.DrawText(0, 0, dim, "diff --git a/server/handler.go b/server/handler.go") - renderer.DrawText(0, 1, dim, "--- a/server/handler.go") - renderer.DrawText(0, 2, dim, "+++ b/server/handler.go") - renderer.DrawText(0, 3, tcell.StyleDefault.Foreground(tcell.ColorBlue), "@@ -12,8 +12,9 @@") - - y := t.drawContext(renderer, diffLead, 4, w) - y = t.drawTarget(renderer, gs, y, w, h) - y = t.drawContext(renderer, diffTail, y, w) - - if gs.IsFinished { - renderer.HideCursor() - renderer.DrawText(0, y+1, tcell.StyleDefault, ui.Truncate(ui.ResultText(gs), w)) - } - - renderer.Show() -} - -// drawContext renders unchanged and removed hunk lines and returns the next -// free row. -func (t *DiffTheme) drawContext(renderer domain.Renderer, lines []diffLine, y, w int) int { - removed := tcell.StyleDefault.Foreground(tcell.ColorRed) - for _, ln := range lines { - style, mark := tcell.StyleDefault, " " - if ln.mark == '-' { - style, mark = removed, "-" - } - renderer.DrawText(0, y, style, ui.Truncate(mark+" "+ln.text, w)) - y++ - } - return y -} - -// drawTarget renders the target as one or more "+ " diff lines, wrapping to the -// terminal width and coloring each typed rune. Long targets (the words stream) -// scroll inside a window that follows the cursor. It returns the next free row -// and positions the cursor (unless the round is finished). Glyph placement -// advances by display width so wide runes (e.g. Hangul) align with the cursor. -func (t *DiffTheme) drawTarget(renderer domain.Renderer, gs *domain.GameState, startY, w, h int) int { - green := tcell.StyleDefault.Foreground(tcell.ColorGreen) - red := tcell.StyleDefault.Foreground(tcell.ColorRed).Background(tcell.ColorDarkRed) - - inputRunes := []rune(gs.UserInput) - wrapWidth := w - 3 // "+ " prefix plus a cell of right padding - if wrapWidth < 1 { - wrapWidth = 1 - } - allLines := ui.WrapText(gs.TargetSentence, wrapWidth) - - // Window the "+" hunk so it never runs off the screen. - visible := h - startY - 4 // leave room for the trailing context and result - if visible > 8 { - visible = 8 - } - if visible < 1 { - visible = 1 - } - winStart := 0 - lines := allLines - if len(allLines) > visible { - winStart = ui.WindowStart(len(allLines), ui.LineOfRune(allLines, len(inputRunes)), visible) - lines = allLines[winStart : winStart+visible] - } - - offset := 0 - for i := 0; i < winStart; i++ { - offset += len([]rune(allLines[i])) - } - - cursorX, cursorY := 2, startY - foundCursor := false - y := startY - for _, line := range lines { - lineRunes := []rune(line) - renderer.DrawText(0, y, green, "+ ") - col := 2 - for ci, r := range lineRunes { - idx := offset + ci - style := green - if idx < len(inputRunes) && inputRunes[idx] != r { - style = red - } - renderer.SetContent(col, y, r, style) - col += runewidth.RuneWidth(r) - } - if !foundCursor && len(inputRunes) >= offset && len(inputRunes) < offset+len(lineRunes) { - rel := len(inputRunes) - offset - cursorX = 2 + runewidth.StringWidth(string(lineRunes[:rel])) - cursorY = y - foundCursor = true - } - offset += len(lineRunes) - y++ - } - if !foundCursor && len(lines) > 0 { - // The cursor sits past the window's last rune (end of the target, - // or the input has consumed the whole visible window). - last := []rune(lines[len(lines)-1]) - cursorX = 2 + runewidth.StringWidth(string(last)) - cursorY = startY + len(lines) - 1 - } - if !gs.IsFinished { - renderer.ShowCursor(cursorX, cursorY) - } - return y -} - -func (t *DiffTheme) OnTick(gs *domain.GameState) {}