diff --git a/tai.py b/tai.py index 9892b3c..bee31f3 100755 --- a/tai.py +++ b/tai.py @@ -97,7 +97,7 @@ def _fromisoformat(text): return parsed.replace(tzinfo=tzinfo) raise ValueError("invalid datetime, use ISO like 2026-10-08T09:00") -VERSION = "1.33.0" +VERSION = "1.33.1" WORKER_STEPS = 12 CREATE_SKILL_STEPS = 40 FORK_MAX_DEPTH = 2 @@ -13255,9 +13255,12 @@ class TuiApp: self._need_paint = True def set_theme(self, name): - """Switch the global theme and redraw everything. Raises + """Switch the global theme and repaint every cell. Cells are diffed + by style key (role names), which does not change with the theme, so + the compositor must forget what the terminal shows. Raises ValueError naming the available themes when unknown.""" tui_set_theme(name) + self.compositor.invalidate() self.dirty_paint() return tui_theme() diff --git a/test_tai.py b/test_tai.py index 406ae85..dbcb128 100644 --- a/test_tai.py +++ b/test_tai.py @@ -5861,6 +5861,22 @@ class TuiThemeTests(unittest.TestCase): with self.assertRaises(ValueError): app.set_theme("nope") + def test_set_theme_repaints_every_cell_on_the_terminal(self): + """Regression: after a theme switch only cells whose text changed got the + new colors, leaving old-theme speckles on spaces and unchanged glyphs.""" + def screen(theme, switch_to=None): + tai.tui_set_theme(theme) + app = tai.TuiApp() + app.rows, app.cols = 10, 40 + app.mount(tai.TuiText("hello world and more words", style=tai.TuiStyle(fg="role:fg", bg="role:bubble_in"))) + vt = tai.TuiVtScreen(10, 40) + vt.feed(app.frame_bytes().encode("utf-8")) + if switch_to: + app.set_theme(switch_to) + vt.feed(app.frame_bytes().encode("utf-8")) + return vt.grid + self.assertEqual(screen("dark", "gruvbox"), screen("gruvbox")) + def test_cycle_order(self): self.assertEqual(tai.tui_cycle_theme_name("dark"), "light") self.assertEqual(tai.tui_cycle_theme_name("ansi"), "dark") @@ -6207,8 +6223,10 @@ class TuiChatTests(unittest.TestCase): app.offload(work, lambda value, error: done.append((value, error))) for _ in range(300): await asyncio.sleep(0.01) + app._fire_timers(time.time()) if done: await asyncio.sleep(0.2) + app._fire_timers(time.time()) break app.layout() self.shown = app.render_text() diff --git a/tui.md b/tui.md index fdf8e74..0656829 100644 --- a/tui.md +++ b/tui.md @@ -242,8 +242,10 @@ render uncolored rather than crashing. Selection precedence: `--theme` flag, `TAI_THEME` env var, `dark` default; an unknown name prints the available list and exits 2. -`App.set_theme(name)` swaps the global theme and redraws everything; -F4 cycles registry order in the chat scene. To change a color, edit +`App.set_theme(name)` swaps the global theme and invalidates the compositor, +because cells are diffed by style key (role names), which a theme switch does +not change; without the invalidation only cells whose text changed got the +new colors and old-theme speckles stayed on spaces. F4 cycles registry order in the chat scene. To change a color, edit one value in `_tui_builtin_themes()` or register a custom theme. ## Chat scene (`--tui`)