tau-coding-agent 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tau/__init__.py +0 -0
- tau/agent/__init__.py +11 -0
- tau/agent/prompt/__init__.py +10 -0
- tau/agent/prompt/builder.py +302 -0
- tau/agent/prompt/types.py +33 -0
- tau/agent/service.py +369 -0
- tau/agent/types.py +61 -0
- tau/auth/manager.py +247 -0
- tau/auth/storage.py +82 -0
- tau/auth/types.py +41 -0
- tau/builtins/__init__.py +4 -0
- tau/builtins/__pycache__/__init__.cpython-313.pyc +0 -0
- tau/builtins/__pycache__/__init__.cpython-314.pyc +0 -0
- tau/builtins/commands/__init__.py +41 -0
- tau/builtins/commands/__pycache__/__init__.cpython-313.pyc +0 -0
- tau/builtins/commands/__pycache__/__init__.cpython-314.pyc +0 -0
- tau/builtins/commands/__pycache__/clear.cpython-313.pyc +0 -0
- tau/builtins/commands/__pycache__/clear.cpython-314.pyc +0 -0
- tau/builtins/commands/__pycache__/compact.cpython-313.pyc +0 -0
- tau/builtins/commands/__pycache__/compact.cpython-314.pyc +0 -0
- tau/builtins/commands/__pycache__/reload.cpython-313.pyc +0 -0
- tau/builtins/commands/__pycache__/reload.cpython-314.pyc +0 -0
- tau/builtins/commands/__pycache__/session.cpython-313.pyc +0 -0
- tau/builtins/commands/__pycache__/session.cpython-314.pyc +0 -0
- tau/builtins/commands/clear.py +16 -0
- tau/builtins/commands/compact.py +28 -0
- tau/builtins/commands/reload.py +27 -0
- tau/builtins/commands/session.py +19 -0
- tau/builtins/extensions/footer/__init__.py +76 -0
- tau/builtins/extensions/footer/__pycache__/__init__.cpython-313.pyc +0 -0
- tau/builtins/extensions/footer/__pycache__/git.cpython-313.pyc +0 -0
- tau/builtins/extensions/footer/__pycache__/model.cpython-313.pyc +0 -0
- tau/builtins/extensions/footer/__pycache__/utils.cpython-313.pyc +0 -0
- tau/builtins/extensions/footer/git.py +26 -0
- tau/builtins/extensions/footer/model.py +69 -0
- tau/builtins/extensions/footer/utils.py +44 -0
- tau/builtins/extensions/header/__init__.py +18 -0
- tau/builtins/extensions/header/__pycache__/__init__.cpython-313.pyc +0 -0
- tau/builtins/models/__init__.py +0 -0
- tau/builtins/models/__pycache__/__init__.cpython-313.pyc +0 -0
- tau/builtins/models/__pycache__/text.cpython-313.pyc +0 -0
- tau/builtins/models/audio.py +43 -0
- tau/builtins/models/image.py +43 -0
- tau/builtins/models/text.py +482 -0
- tau/builtins/models/video.py +40 -0
- tau/builtins/prompts/commit.md +7 -0
- tau/builtins/prompts/docs.md +7 -0
- tau/builtins/prompts/explain.md +7 -0
- tau/builtins/prompts/fix.md +7 -0
- tau/builtins/prompts/refactor.md +7 -0
- tau/builtins/prompts/review.md +7 -0
- tau/builtins/prompts/test.md +7 -0
- tau/builtins/providers/__init__.py +0 -0
- tau/builtins/providers/__pycache__/__init__.cpython-313.pyc +0 -0
- tau/builtins/providers/__pycache__/text.cpython-313.pyc +0 -0
- tau/builtins/providers/audio.py +10 -0
- tau/builtins/providers/image.py +9 -0
- tau/builtins/providers/text.py +33 -0
- tau/builtins/providers/video.py +6 -0
- tau/builtins/skills/code-review/SKILL.md +4 -0
- tau/builtins/skills/debug/SKILL.md +4 -0
- tau/builtins/skills/git-commit/SKILL.md +4 -0
- tau/builtins/themes/dark.yaml +1 -0
- tau/builtins/themes/light.yaml +46 -0
- tau/builtins/tools/__init__.py +73 -0
- tau/builtins/tools/__pycache__/__init__.cpython-313.pyc +0 -0
- tau/builtins/tools/__pycache__/__init__.cpython-314.pyc +0 -0
- tau/builtins/tools/__pycache__/bash.cpython-313.pyc +0 -0
- tau/builtins/tools/__pycache__/bash.cpython-314.pyc +0 -0
- tau/builtins/tools/__pycache__/edit.cpython-313.pyc +0 -0
- tau/builtins/tools/__pycache__/edit.cpython-314.pyc +0 -0
- tau/builtins/tools/__pycache__/glob.cpython-313.pyc +0 -0
- tau/builtins/tools/__pycache__/glob.cpython-314.pyc +0 -0
- tau/builtins/tools/__pycache__/grep.cpython-313.pyc +0 -0
- tau/builtins/tools/__pycache__/grep.cpython-314.pyc +0 -0
- tau/builtins/tools/__pycache__/ls.cpython-313.pyc +0 -0
- tau/builtins/tools/__pycache__/ls.cpython-314.pyc +0 -0
- tau/builtins/tools/__pycache__/read.cpython-313.pyc +0 -0
- tau/builtins/tools/__pycache__/read.cpython-314.pyc +0 -0
- tau/builtins/tools/__pycache__/terminal.cpython-313.pyc +0 -0
- tau/builtins/tools/__pycache__/terminal.cpython-314.pyc +0 -0
- tau/builtins/tools/__pycache__/write.cpython-313.pyc +0 -0
- tau/builtins/tools/__pycache__/write.cpython-314.pyc +0 -0
- tau/builtins/tools/edit.py +215 -0
- tau/builtins/tools/glob.py +112 -0
- tau/builtins/tools/grep.py +146 -0
- tau/builtins/tools/ls.py +135 -0
- tau/builtins/tools/read.py +122 -0
- tau/builtins/tools/terminal.py +150 -0
- tau/builtins/tools/write.py +105 -0
- tau/commands/__init__.py +10 -0
- tau/commands/registry.py +71 -0
- tau/commands/types.py +33 -0
- tau/console/__init__.py +0 -0
- tau/console/cli.py +266 -0
- tau/console/commands/__init__.py +0 -0
- tau/console/commands/auth.py +193 -0
- tau/console/commands/packages.py +104 -0
- tau/console/commands/update.py +76 -0
- tau/core/__init__.py +0 -0
- tau/core/registry.py +102 -0
- tau/engine/__init__.py +47 -0
- tau/engine/service.py +768 -0
- tau/engine/types.py +163 -0
- tau/extensions/__init__.py +28 -0
- tau/extensions/api.py +928 -0
- tau/extensions/context.py +462 -0
- tau/extensions/events.py +70 -0
- tau/extensions/loader.py +386 -0
- tau/extensions/runtime.py +184 -0
- tau/extensions/settings.py +137 -0
- tau/hooks/__init__.py +112 -0
- tau/hooks/engine.py +237 -0
- tau/hooks/inference.py +21 -0
- tau/hooks/runtime.py +126 -0
- tau/hooks/service.py +121 -0
- tau/hooks/session.py +117 -0
- tau/hooks/tui.py +61 -0
- tau/hooks/types.py +72 -0
- tau/inference/__init__.py +180 -0
- tau/inference/api/__init__.py +0 -0
- tau/inference/api/audio/__init__.py +0 -0
- tau/inference/api/audio/base.py +29 -0
- tau/inference/api/audio/builtins.py +15 -0
- tau/inference/api/audio/elevenlabs_audio.py +183 -0
- tau/inference/api/audio/gemini_audio.py +95 -0
- tau/inference/api/audio/openai_audio.py +159 -0
- tau/inference/api/audio/registry.py +15 -0
- tau/inference/api/audio/sarvam_audio.py +163 -0
- tau/inference/api/audio/service.py +103 -0
- tau/inference/api/audio/utils.py +47 -0
- tau/inference/api/image/__init__.py +0 -0
- tau/inference/api/image/base.py +17 -0
- tau/inference/api/image/builtins.py +8 -0
- tau/inference/api/image/gemini_image.py +77 -0
- tau/inference/api/image/openai_image.py +103 -0
- tau/inference/api/image/openrouter.py +144 -0
- tau/inference/api/image/registry.py +15 -0
- tau/inference/api/image/service.py +71 -0
- tau/inference/api/registry.py +82 -0
- tau/inference/api/text/__init__.py +0 -0
- tau/inference/api/text/anthropic_claude_code.py +222 -0
- tau/inference/api/text/anthropic_messages.py +196 -0
- tau/inference/api/text/base.py +40 -0
- tau/inference/api/text/builtins.py +19 -0
- tau/inference/api/text/gemini_generate.py +234 -0
- tau/inference/api/text/github_copilot_chat.py +172 -0
- tau/inference/api/text/google_antigravity.py +522 -0
- tau/inference/api/text/mistral_chat.py +284 -0
- tau/inference/api/text/ollama_chat.py +200 -0
- tau/inference/api/text/openai_codex_responses.py +497 -0
- tau/inference/api/text/openai_completions.py +227 -0
- tau/inference/api/text/openai_responses.py +235 -0
- tau/inference/api/text/registry.py +50 -0
- tau/inference/api/text/service.py +297 -0
- tau/inference/api/text/types.py +7 -0
- tau/inference/api/text/utils.py +228 -0
- tau/inference/api/video/__init__.py +0 -0
- tau/inference/api/video/base.py +26 -0
- tau/inference/api/video/builtins.py +7 -0
- tau/inference/api/video/fal_video.py +119 -0
- tau/inference/api/video/openrouter_video.py +142 -0
- tau/inference/api/video/registry.py +15 -0
- tau/inference/api/video/service.py +72 -0
- tau/inference/model/__init__.py +0 -0
- tau/inference/model/registry.py +102 -0
- tau/inference/model/types.py +65 -0
- tau/inference/provider/__init__.py +0 -0
- tau/inference/provider/oauth/__init__.py +35 -0
- tau/inference/provider/oauth/anthropic_claude_code.py +286 -0
- tau/inference/provider/oauth/github_copilot.py +333 -0
- tau/inference/provider/oauth/google_antigravity.py +258 -0
- tau/inference/provider/oauth/openai_codex.py +309 -0
- tau/inference/provider/oauth/pkce.py +14 -0
- tau/inference/provider/oauth/types.py +46 -0
- tau/inference/provider/oauth/utils.py +154 -0
- tau/inference/provider/registry.py +141 -0
- tau/inference/provider/types.py +114 -0
- tau/inference/types.py +549 -0
- tau/inference/utils.py +219 -0
- tau/message/__init__.py +0 -0
- tau/message/types.py +482 -0
- tau/message/utils.py +178 -0
- tau/packages/__init__.py +11 -0
- tau/packages/manager.py +190 -0
- tau/packages/types.py +20 -0
- tau/packages/utils.py +67 -0
- tau/prompts/expand.py +58 -0
- tau/prompts/loader.py +69 -0
- tau/prompts/registry.py +45 -0
- tau/prompts/types.py +24 -0
- tau/rpc/__init__.py +8 -0
- tau/rpc/mode.py +783 -0
- tau/rpc/types.py +252 -0
- tau/runtime/service.py +759 -0
- tau/runtime/types.py +303 -0
- tau/session/branch_summarization.py +312 -0
- tau/session/compaction.py +646 -0
- tau/session/manager.py +652 -0
- tau/session/types.py +188 -0
- tau/session/utils.py +233 -0
- tau/settings/manager.py +1077 -0
- tau/settings/paths.py +150 -0
- tau/settings/storage.py +63 -0
- tau/settings/types.py +173 -0
- tau/settings/utils.py +25 -0
- tau/skills/loader.py +91 -0
- tau/skills/registry.py +70 -0
- tau/skills/types.py +25 -0
- tau/themes/loader.py +238 -0
- tau/themes/registry.py +108 -0
- tau/themes/types.py +19 -0
- tau/tool/__init__.py +3 -0
- tau/tool/registry.py +117 -0
- tau/tool/render.py +21 -0
- tau/tool/types.py +244 -0
- tau/trust/__init__.py +13 -0
- tau/trust/manager.py +80 -0
- tau/trust/types.py +14 -0
- tau/trust/utils.py +72 -0
- tau/tui/__init__.py +54 -0
- tau/tui/agent_hooks.py +346 -0
- tau/tui/ansi.py +330 -0
- tau/tui/app.py +540 -0
- tau/tui/autocomplete.py +33 -0
- tau/tui/capabilities.py +119 -0
- tau/tui/commands/__init__.py +3 -0
- tau/tui/commands/appearance.py +498 -0
- tau/tui/commands/auth.py +232 -0
- tau/tui/commands/context.py +38 -0
- tau/tui/commands/misc.py +82 -0
- tau/tui/commands/model.py +118 -0
- tau/tui/commands/session.py +464 -0
- tau/tui/component.py +268 -0
- tau/tui/components/__init__.py +0 -0
- tau/tui/components/autocomplete_manager.py +267 -0
- tau/tui/components/autocomplete_picker.py +143 -0
- tau/tui/components/box.py +90 -0
- tau/tui/components/command_palette.py +144 -0
- tau/tui/components/dynamic_border.py +19 -0
- tau/tui/components/file_picker.py +233 -0
- tau/tui/components/image.py +181 -0
- tau/tui/components/inline_selector.py +71 -0
- tau/tui/components/layout.py +1194 -0
- tau/tui/components/message_list.py +692 -0
- tau/tui/components/modal.py +97 -0
- tau/tui/components/model_palette.py +204 -0
- tau/tui/components/picker_overlay.py +174 -0
- tau/tui/components/prompt_overlay.py +236 -0
- tau/tui/components/resume_modal.py +372 -0
- tau/tui/components/select_list.py +222 -0
- tau/tui/components/settings_modal.py +274 -0
- tau/tui/components/settings_schema.py +203 -0
- tau/tui/components/spinner.py +119 -0
- tau/tui/components/text_input.py +396 -0
- tau/tui/components/text_prompt.py +82 -0
- tau/tui/components/tree_select_list.py +580 -0
- tau/tui/components/trust_screen.py +97 -0
- tau/tui/diff.py +114 -0
- tau/tui/fuzzy.py +99 -0
- tau/tui/input.py +496 -0
- tau/tui/input_handler.py +716 -0
- tau/tui/keybindings.py +87 -0
- tau/tui/markdown.py +286 -0
- tau/tui/message_renderers.py +31 -0
- tau/tui/overlay.py +326 -0
- tau/tui/renderer.py +378 -0
- tau/tui/terminal.py +499 -0
- tau/tui/theme.py +148 -0
- tau/tui/tui.py +544 -0
- tau/tui/ui_context.py +768 -0
- tau/tui/utils.py +20 -0
- tau/utils/__init__.py +0 -0
- tau/utils/http_proxy.py +221 -0
- tau/utils/image_processing.py +172 -0
- tau/utils/secrets.py +59 -0
- tau/utils/version_check.py +60 -0
- tau_coding_agent-0.1.0.dist-info/METADATA +177 -0
- tau_coding_agent-0.1.0.dist-info/RECORD +283 -0
- tau_coding_agent-0.1.0.dist-info/WHEEL +5 -0
- tau_coding_agent-0.1.0.dist-info/entry_points.txt +2 -0
- tau_coding_agent-0.1.0.dist-info/licenses/LICENSE +21 -0
- tau_coding_agent-0.1.0.dist-info/top_level.txt +1 -0
tau/tui/terminal.py
ADDED
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
import tty
|
|
7
|
+
import termios
|
|
8
|
+
import signal
|
|
9
|
+
from typing import Any, Callable
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Terminal:
|
|
13
|
+
"""
|
|
14
|
+
Owns stdin/stdout in raw mode for the duration of the TUI session.
|
|
15
|
+
|
|
16
|
+
This class is a wrapper around low-level terminal operations using ANSI escape codes.
|
|
17
|
+
It manages terminal initialization, cleanup, input/output, cursor control, and screen rendering.
|
|
18
|
+
|
|
19
|
+
Key responsibilities:
|
|
20
|
+
- Enter/restore raw mode (immediate keyboard input, no echo, no line buffering)
|
|
21
|
+
- Switch to/from alternate screen buffer (clean workspace)
|
|
22
|
+
- Write buffered output to terminal with flushing
|
|
23
|
+
- Track terminal size and fire callbacks on resize
|
|
24
|
+
- Hide/show cursor and control cursor position
|
|
25
|
+
- Clear screen portions and manage display
|
|
26
|
+
- Handle mouse events and special paste mode
|
|
27
|
+
- Synchronize screen updates to prevent flicker
|
|
28
|
+
|
|
29
|
+
Typical usage:
|
|
30
|
+
with Terminal() as terminal:
|
|
31
|
+
terminal.hide_cursor()
|
|
32
|
+
# Build TUI display
|
|
33
|
+
terminal.write("Hello")
|
|
34
|
+
terminal.flush()
|
|
35
|
+
# Read keyboard input
|
|
36
|
+
key = terminal.read_raw()
|
|
37
|
+
|
|
38
|
+
The Terminal class uses ANSI escape sequences (\x1b[...) to communicate with the terminal.
|
|
39
|
+
These sequences tell the terminal what to do (move cursor, change colors, clear screen, etc).
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(self) -> None:
|
|
43
|
+
"""
|
|
44
|
+
Initialize the Terminal object.
|
|
45
|
+
|
|
46
|
+
Sets up internal state:
|
|
47
|
+
- _original_termios: Saves original terminal settings (restored on exit)
|
|
48
|
+
- _resize_callbacks: List of functions to call when terminal is resized
|
|
49
|
+
- _prev_sigwinch: Saves original resize signal handler
|
|
50
|
+
- width/height: Current terminal dimensions in characters
|
|
51
|
+
"""
|
|
52
|
+
self._original_termios: list | None = None
|
|
53
|
+
self._resize_callbacks: list[Callable[[], None]] = []
|
|
54
|
+
self._prev_sigwinch: Any = None
|
|
55
|
+
self.width, self.height = self._get_size()
|
|
56
|
+
|
|
57
|
+
# -------------------------------------------------------------------------
|
|
58
|
+
# Lifecycle
|
|
59
|
+
# -------------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
def enter_raw_mode(self) -> None:
|
|
62
|
+
"""
|
|
63
|
+
Switch stdin to raw mode and install SIGWINCH handler.
|
|
64
|
+
|
|
65
|
+
Raw mode disables line buffering, echo, and signal processing.
|
|
66
|
+
Characters are sent immediately without waiting for Enter.
|
|
67
|
+
Also saves the original terminal settings and signal handlers for later restoration.
|
|
68
|
+
"""
|
|
69
|
+
fd = sys.stdin.fileno()
|
|
70
|
+
self._original_termios = termios.tcgetattr(fd) # Save original settings
|
|
71
|
+
tty.setraw(fd) # Switch to raw mode (no echo, no buffering)
|
|
72
|
+
self._prev_sigwinch = signal.signal(signal.SIGWINCH, self._on_resize) # Detect terminal resize
|
|
73
|
+
|
|
74
|
+
def exit_raw_mode(self) -> None:
|
|
75
|
+
"""
|
|
76
|
+
Restore terminal to its original state.
|
|
77
|
+
|
|
78
|
+
Reverses the changes made by enter_raw_mode():
|
|
79
|
+
- Restores original terminal settings (echo, buffering, signals re-enabled)
|
|
80
|
+
- Restores the original signal handler for terminal resize events
|
|
81
|
+
"""
|
|
82
|
+
if self._original_termios is not None:
|
|
83
|
+
# TCSADRAIN: wait for pending output to finish before restoring
|
|
84
|
+
termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, self._original_termios)
|
|
85
|
+
self._original_termios = None
|
|
86
|
+
if self._prev_sigwinch is not None:
|
|
87
|
+
# Restore the previous resize signal handler
|
|
88
|
+
signal.signal(signal.SIGWINCH, self._prev_sigwinch)
|
|
89
|
+
self._prev_sigwinch = None
|
|
90
|
+
|
|
91
|
+
def enter_alt_screen(self) -> None:
|
|
92
|
+
"""
|
|
93
|
+
Switch to alternate screen buffer (like vim or less does).
|
|
94
|
+
|
|
95
|
+
This creates a clean workspace separate from the main terminal.
|
|
96
|
+
Your terminal history/scrollback is preserved underneath.
|
|
97
|
+
Sends three ANSI codes:
|
|
98
|
+
- \x1b[?1049h: Enable alternate screen buffer
|
|
99
|
+
- \x1b[2J: Clear the screen completely
|
|
100
|
+
- \x1b[H: Move cursor to top-left corner (home position)
|
|
101
|
+
"""
|
|
102
|
+
self.write_flush("\x1b[?1049h\x1b[2J\x1b[H")
|
|
103
|
+
|
|
104
|
+
def exit_alt_screen(self) -> None:
|
|
105
|
+
"""
|
|
106
|
+
Switch back to the main screen buffer.
|
|
107
|
+
|
|
108
|
+
Restores your terminal to how it was before enter_alt_screen().
|
|
109
|
+
Your terminal history/scrollback is still there.
|
|
110
|
+
Sends ANSI code:
|
|
111
|
+
- \x1b[?1049l: Disable alternate screen buffer (restore main)
|
|
112
|
+
"""
|
|
113
|
+
self.write_flush("\x1b[?1049l")
|
|
114
|
+
|
|
115
|
+
def __enter__(self) -> Terminal:
|
|
116
|
+
"""
|
|
117
|
+
Context manager entry: set up the terminal for TUI use.
|
|
118
|
+
|
|
119
|
+
Enables raw mode only — no alternate screen. Content renders into
|
|
120
|
+
the main buffer so the terminal's native scrollback works.
|
|
121
|
+
"""
|
|
122
|
+
self.enter_raw_mode()
|
|
123
|
+
return self
|
|
124
|
+
|
|
125
|
+
def __exit__(self, *_: object) -> None:
|
|
126
|
+
"""
|
|
127
|
+
Context manager exit: restore the terminal to normal state.
|
|
128
|
+
|
|
129
|
+
Shows the cursor and restores raw mode. The caller is responsible
|
|
130
|
+
for moving the cursor past the last rendered line before calling
|
|
131
|
+
this so the shell prompt appears below the TUI output.
|
|
132
|
+
"""
|
|
133
|
+
self.show_cursor()
|
|
134
|
+
self.exit_raw_mode()
|
|
135
|
+
|
|
136
|
+
# -------------------------------------------------------------------------
|
|
137
|
+
# Output
|
|
138
|
+
# -------------------------------------------------------------------------
|
|
139
|
+
|
|
140
|
+
def write(self, data: str) -> None:
|
|
141
|
+
"""
|
|
142
|
+
Write data to the output buffer (doesn't send to screen yet).
|
|
143
|
+
|
|
144
|
+
The data is stored in a temporary buffer and waits for a flush() call.
|
|
145
|
+
This allows batching multiple writes before sending to screen.
|
|
146
|
+
"""
|
|
147
|
+
sys.stdout.write(data)
|
|
148
|
+
|
|
149
|
+
def flush(self) -> None:
|
|
150
|
+
"""
|
|
151
|
+
Send all buffered output to the terminal immediately.
|
|
152
|
+
|
|
153
|
+
Forces everything in the buffer to be displayed on screen right away.
|
|
154
|
+
Clears the buffer after sending.
|
|
155
|
+
"""
|
|
156
|
+
sys.stdout.flush()
|
|
157
|
+
|
|
158
|
+
def write_flush(self, data: str) -> None:
|
|
159
|
+
"""
|
|
160
|
+
Write data and immediately send it to the terminal (convenience method).
|
|
161
|
+
|
|
162
|
+
Combines write() + flush() in one call.
|
|
163
|
+
Use this when you need instant output (status messages, clearing, etc).
|
|
164
|
+
"""
|
|
165
|
+
self.write(data)
|
|
166
|
+
self.flush()
|
|
167
|
+
|
|
168
|
+
# -------------------------------------------------------------------------
|
|
169
|
+
# Cursor
|
|
170
|
+
# -------------------------------------------------------------------------
|
|
171
|
+
|
|
172
|
+
def hide_cursor(self) -> None:
|
|
173
|
+
"""
|
|
174
|
+
Hide the cursor on the terminal.
|
|
175
|
+
|
|
176
|
+
Makes the blinking cursor invisible. Used during TUI rendering
|
|
177
|
+
to prevent visual distraction.
|
|
178
|
+
Sends ANSI code: \x1b[?25l (disable cursor visibility)
|
|
179
|
+
"""
|
|
180
|
+
self.write("\x1b[?25l")
|
|
181
|
+
|
|
182
|
+
def show_cursor(self) -> None:
|
|
183
|
+
"""
|
|
184
|
+
Show the cursor on the terminal.
|
|
185
|
+
|
|
186
|
+
Makes the cursor visible again. Called when TUI exits.
|
|
187
|
+
Sends ANSI code: \x1b[?25h (enable cursor visibility)
|
|
188
|
+
"""
|
|
189
|
+
self.write("\x1b[?25h")
|
|
190
|
+
|
|
191
|
+
def move_cursor(self, row: int, col: int = 0) -> str:
|
|
192
|
+
"""
|
|
193
|
+
Return ANSI sequence to move cursor to row (0-indexed), col (0-indexed).
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
row: Row number (0 = top, increases downward)
|
|
197
|
+
col: Column number (0 = left, increases rightward)
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
ANSI escape sequence to move cursor to specified position.
|
|
201
|
+
Example: move_cursor(5, 10) returns \x1b[6;11H (converts to 1-indexed)
|
|
202
|
+
"""
|
|
203
|
+
return f"\x1b[{row + 1};{col + 1}H"
|
|
204
|
+
|
|
205
|
+
def move_up(self, n: int) -> str:
|
|
206
|
+
"""
|
|
207
|
+
Return ANSI sequence to move cursor up n lines.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
n: Number of lines to move up
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
ANSI escape sequence, or empty string if n <= 0
|
|
214
|
+
Example: move_up(3) returns \x1b[3A (move up 3 lines)
|
|
215
|
+
"""
|
|
216
|
+
return f"\x1b[{n}A" if n > 0 else ""
|
|
217
|
+
|
|
218
|
+
def move_down(self, n: int) -> str:
|
|
219
|
+
"""
|
|
220
|
+
Return ANSI sequence to move cursor down n lines.
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
n: Number of lines to move down
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
ANSI escape sequence, or empty string if n <= 0
|
|
227
|
+
Example: move_down(2) returns \x1b[2B (move down 2 lines)
|
|
228
|
+
"""
|
|
229
|
+
return f"\x1b[{n}B" if n > 0 else ""
|
|
230
|
+
|
|
231
|
+
# -------------------------------------------------------------------------
|
|
232
|
+
# Screen
|
|
233
|
+
# -------------------------------------------------------------------------
|
|
234
|
+
|
|
235
|
+
def clear_screen(self) -> str:
|
|
236
|
+
"""
|
|
237
|
+
Return ANSI sequence to clear the entire screen.
|
|
238
|
+
|
|
239
|
+
Wipes all content and moves cursor to top-left corner (home).
|
|
240
|
+
Returns: \x1b[2J (clear screen) + \x1b[H (move to home)
|
|
241
|
+
"""
|
|
242
|
+
return "\x1b[2J\x1b[H"
|
|
243
|
+
|
|
244
|
+
def clear_line(self) -> str:
|
|
245
|
+
"""
|
|
246
|
+
Return ANSI sequence to clear the entire current line.
|
|
247
|
+
|
|
248
|
+
Erases all characters on the line where the cursor is.
|
|
249
|
+
Returns: \x1b[2K (clear entire line)
|
|
250
|
+
"""
|
|
251
|
+
return "\x1b[2K"
|
|
252
|
+
|
|
253
|
+
def clear_to_end_of_line(self) -> str:
|
|
254
|
+
"""
|
|
255
|
+
Return ANSI sequence to clear from cursor to end of line.
|
|
256
|
+
|
|
257
|
+
Erases all characters from cursor position to the end of the current line.
|
|
258
|
+
Cursor position stays the same.
|
|
259
|
+
Returns: \x1b[K
|
|
260
|
+
"""
|
|
261
|
+
return "\x1b[K"
|
|
262
|
+
|
|
263
|
+
def clear_to_end_of_screen(self) -> str:
|
|
264
|
+
"""
|
|
265
|
+
Return ANSI sequence to clear from cursor to end of screen.
|
|
266
|
+
|
|
267
|
+
Erases all characters from cursor position to the bottom-right of screen.
|
|
268
|
+
Everything above the cursor stays intact.
|
|
269
|
+
Returns: \x1b[J
|
|
270
|
+
"""
|
|
271
|
+
return "\x1b[J"
|
|
272
|
+
|
|
273
|
+
def clear_scrollback(self) -> str:
|
|
274
|
+
"""
|
|
275
|
+
Return ANSI sequence to clear the scrollback/history buffer.
|
|
276
|
+
|
|
277
|
+
Deletes the terminal's scroll history (things you could scroll up to see).
|
|
278
|
+
Note: Not supported on all terminals.
|
|
279
|
+
Returns: \x1b[3J
|
|
280
|
+
"""
|
|
281
|
+
return "\x1b[3J"
|
|
282
|
+
|
|
283
|
+
# -------------------------------------------------------------------------
|
|
284
|
+
# Bracketed paste
|
|
285
|
+
# -------------------------------------------------------------------------
|
|
286
|
+
|
|
287
|
+
def enable_bracketed_paste(self) -> None:
|
|
288
|
+
"""
|
|
289
|
+
Enable bracketed paste mode on the terminal.
|
|
290
|
+
|
|
291
|
+
When enabled, pasted text is wrapped with special markers so the app
|
|
292
|
+
can distinguish between pasted text and manually typed characters.
|
|
293
|
+
This allows handling large pastes differently than typing.
|
|
294
|
+
Sends ANSI code: \x1b[?2004h (enable bracketed paste)
|
|
295
|
+
"""
|
|
296
|
+
self.write("\x1b[?2004h")
|
|
297
|
+
|
|
298
|
+
def disable_bracketed_paste(self) -> None:
|
|
299
|
+
"""
|
|
300
|
+
Disable bracketed paste mode on the terminal.
|
|
301
|
+
|
|
302
|
+
Turns off the special paste markers. Pasted text is treated like typing.
|
|
303
|
+
Sends ANSI code: \x1b[?2004l (disable bracketed paste)
|
|
304
|
+
"""
|
|
305
|
+
self.write("\x1b[?2004l")
|
|
306
|
+
|
|
307
|
+
# -------------------------------------------------------------------------
|
|
308
|
+
# Auto-wrap (DECAWM)
|
|
309
|
+
# -------------------------------------------------------------------------
|
|
310
|
+
|
|
311
|
+
def disable_autowrap(self) -> None:
|
|
312
|
+
"""
|
|
313
|
+
Turn off the terminal's auto-wrap (DECAWM).
|
|
314
|
+
|
|
315
|
+
The renderer positions the cursor manually with relative moves and
|
|
316
|
+
inserts its own line breaks, truncating every line to the terminal
|
|
317
|
+
width. With auto-wrap left on, a line that fills the last column makes
|
|
318
|
+
the terminal insert a phantom physical row, desynchronising the
|
|
319
|
+
renderer's logical cursor from the real one — which strands content
|
|
320
|
+
(e.g. the streaming spinner) on the wrong row until a full redraw. With
|
|
321
|
+
auto-wrap off the cursor simply stays at the last column, so manual
|
|
322
|
+
positioning stays exact.
|
|
323
|
+
Sends ANSI code: \x1b[?7l (reset DECAWM)
|
|
324
|
+
"""
|
|
325
|
+
self.write("\x1b[?7l")
|
|
326
|
+
|
|
327
|
+
def enable_autowrap(self) -> None:
|
|
328
|
+
"""
|
|
329
|
+
Restore the terminal's auto-wrap (DECAWM) — pairs with disable_autowrap.
|
|
330
|
+
Sends ANSI code: \x1b[?7h (set DECAWM)
|
|
331
|
+
"""
|
|
332
|
+
self.write("\x1b[?7h")
|
|
333
|
+
|
|
334
|
+
# -------------------------------------------------------------------------
|
|
335
|
+
# Mouse tracking (SGR extended, needed for scroll wheel)
|
|
336
|
+
# -------------------------------------------------------------------------
|
|
337
|
+
|
|
338
|
+
def query_background_color(self) -> None:
|
|
339
|
+
"""Send an OSC 11 query to the terminal.
|
|
340
|
+
|
|
341
|
+
The reply arrives asynchronously on stdin as
|
|
342
|
+
``ESC ] 11 ; rgb:RRRR/GGGG/BBBB BEL-or-ST``.
|
|
343
|
+
``InputParser`` converts it to a ``BgColorEvent``; ``TUI._dispatch``
|
|
344
|
+
stores the result in ``tui.background_color``.
|
|
345
|
+
"""
|
|
346
|
+
self.write_flush("\x1b]11;?\x1b\\")
|
|
347
|
+
|
|
348
|
+
def enable_kitty_keyboard(self) -> None:
|
|
349
|
+
"""Enable Kitty keyboard protocol (progressive enhancement level 1).
|
|
350
|
+
|
|
351
|
+
Level 1 adds:
|
|
352
|
+
* Key-release events (``KeyEvent.released=True``)
|
|
353
|
+
* Unambiguous encoding of modifier combinations
|
|
354
|
+
|
|
355
|
+
Non-Kitty terminals silently ignore this sequence.
|
|
356
|
+
"""
|
|
357
|
+
self.write("\x1b[>1u")
|
|
358
|
+
|
|
359
|
+
def disable_kitty_keyboard(self) -> None:
|
|
360
|
+
"""Restore the keyboard protocol to the terminal default."""
|
|
361
|
+
self.write("\x1b[<1u")
|
|
362
|
+
|
|
363
|
+
def enable_mouse_tracking(self) -> None:
|
|
364
|
+
"""
|
|
365
|
+
Enable mouse tracking on the terminal.
|
|
366
|
+
|
|
367
|
+
Allows the TUI to detect mouse clicks, movement, and scroll wheel events.
|
|
368
|
+
Sends two ANSI codes:
|
|
369
|
+
- \x1b[?1000h: Enable basic mouse tracking
|
|
370
|
+
- \x1b[?1006h: Enable SGR extended format (needed for scroll wheel)
|
|
371
|
+
"""
|
|
372
|
+
self.write("\x1b[?1000h\x1b[?1006h")
|
|
373
|
+
|
|
374
|
+
def disable_mouse_tracking(self) -> None:
|
|
375
|
+
"""
|
|
376
|
+
Disable mouse tracking on the terminal.
|
|
377
|
+
|
|
378
|
+
Turns off mouse event detection. Mouse input is no longer reported to app.
|
|
379
|
+
Sends two ANSI codes:
|
|
380
|
+
- \x1b[?1006l: Disable SGR extended format
|
|
381
|
+
- \x1b[?1000l: Disable basic mouse tracking
|
|
382
|
+
"""
|
|
383
|
+
self.write("\x1b[?1006l\x1b[?1000l")
|
|
384
|
+
|
|
385
|
+
# -------------------------------------------------------------------------
|
|
386
|
+
# Synchronized output (flicker prevention)
|
|
387
|
+
# -------------------------------------------------------------------------
|
|
388
|
+
|
|
389
|
+
def begin_sync(self) -> str:
|
|
390
|
+
"""
|
|
391
|
+
Return ANSI sequence to begin synchronized output.
|
|
392
|
+
|
|
393
|
+
Tells the terminal to buffer all output and NOT display it yet.
|
|
394
|
+
Prevents visual flicker when redrawing the entire screen.
|
|
395
|
+
Everything written after this is held until end_sync() is called.
|
|
396
|
+
Returns: \x1b[?2026h (enable synchronized update mode)
|
|
397
|
+
"""
|
|
398
|
+
return "\x1b[?2026h"
|
|
399
|
+
|
|
400
|
+
def end_sync(self) -> str:
|
|
401
|
+
"""
|
|
402
|
+
Return ANSI sequence to end synchronized output.
|
|
403
|
+
|
|
404
|
+
Tells the terminal to flush and display all buffered output atomically.
|
|
405
|
+
Everything between begin_sync() and end_sync() appears on screen at once.
|
|
406
|
+
This prevents seeing partial/incomplete renders.
|
|
407
|
+
Returns: \x1b[?2026l (disable synchronized update mode)
|
|
408
|
+
"""
|
|
409
|
+
return "\x1b[?2026l"
|
|
410
|
+
|
|
411
|
+
# -------------------------------------------------------------------------
|
|
412
|
+
# Terminal title
|
|
413
|
+
# -------------------------------------------------------------------------
|
|
414
|
+
|
|
415
|
+
def set_title(self, title: str) -> None:
|
|
416
|
+
"""
|
|
417
|
+
Set the terminal window title.
|
|
418
|
+
|
|
419
|
+
Changes what appears in the terminal window's title bar.
|
|
420
|
+
Format: ESC ] 0 ; title BELL
|
|
421
|
+
- \x1b]0; : Start of title sequence
|
|
422
|
+
- title : Your custom title text
|
|
423
|
+
- \x07 : BELL character (marks end of sequence)
|
|
424
|
+
|
|
425
|
+
Args:
|
|
426
|
+
title: The new window title text
|
|
427
|
+
"""
|
|
428
|
+
self.write(f"\x1b]0;{title}\x07")
|
|
429
|
+
|
|
430
|
+
# -------------------------------------------------------------------------
|
|
431
|
+
# Size
|
|
432
|
+
# -------------------------------------------------------------------------
|
|
433
|
+
|
|
434
|
+
def on_resize(self, callback: Callable[[], None]) -> None:
|
|
435
|
+
"""
|
|
436
|
+
Register a callback function to run when the terminal is resized.
|
|
437
|
+
|
|
438
|
+
The callback will be called whenever the user resizes their terminal window.
|
|
439
|
+
Multiple callbacks can be registered; all will be called on resize.
|
|
440
|
+
|
|
441
|
+
Args:
|
|
442
|
+
callback: A function that takes no arguments and returns None
|
|
443
|
+
Example: on_resize(lambda: print("Resized!"))
|
|
444
|
+
"""
|
|
445
|
+
self._resize_callbacks.append(callback)
|
|
446
|
+
|
|
447
|
+
def _on_resize(self, *_: object) -> None:
|
|
448
|
+
"""
|
|
449
|
+
Internal handler called when terminal is resized (SIGWINCH signal).
|
|
450
|
+
|
|
451
|
+
Updates width and height, then safely calls all registered callbacks.
|
|
452
|
+
Uses event loop to prevent issues with concurrent screen updates.
|
|
453
|
+
"""
|
|
454
|
+
self.width, self.height = self._get_size()
|
|
455
|
+
# Defer callbacks to the event loop — calling them inline from a signal
|
|
456
|
+
# handler causes reentrant stdout writes if a render is already in flight.
|
|
457
|
+
try:
|
|
458
|
+
loop = asyncio.get_running_loop()
|
|
459
|
+
for cb in self._resize_callbacks:
|
|
460
|
+
loop.call_soon_threadsafe(cb)
|
|
461
|
+
except RuntimeError:
|
|
462
|
+
for cb in self._resize_callbacks:
|
|
463
|
+
cb()
|
|
464
|
+
|
|
465
|
+
@staticmethod
|
|
466
|
+
def _get_size() -> tuple[int, int]:
|
|
467
|
+
"""
|
|
468
|
+
Get the current terminal dimensions (width and height).
|
|
469
|
+
|
|
470
|
+
Returns:
|
|
471
|
+
Tuple of (width, height) in characters
|
|
472
|
+
Falls back to 80x24 if terminal size can't be determined
|
|
473
|
+
"""
|
|
474
|
+
try:
|
|
475
|
+
size = os.get_terminal_size()
|
|
476
|
+
return size.columns, size.lines
|
|
477
|
+
except OSError:
|
|
478
|
+
return 80, 24
|
|
479
|
+
|
|
480
|
+
# -------------------------------------------------------------------------
|
|
481
|
+
# Input
|
|
482
|
+
# -------------------------------------------------------------------------
|
|
483
|
+
|
|
484
|
+
def read_raw(self, n: int = 64) -> str:
|
|
485
|
+
"""
|
|
486
|
+
Read keyboard input from stdin without waiting for Enter.
|
|
487
|
+
|
|
488
|
+
In raw mode, characters are sent immediately without buffering.
|
|
489
|
+
This is how the TUI gets instant keyboard input.
|
|
490
|
+
|
|
491
|
+
Args:
|
|
492
|
+
n: Maximum number of bytes to read (default 64)
|
|
493
|
+
|
|
494
|
+
Returns:
|
|
495
|
+
The input as a string. Invalid UTF-8 is replaced with replacement character.
|
|
496
|
+
Example: User presses 'h' -> returns "h"
|
|
497
|
+
User presses left arrow -> returns "\x1b[D" (ANSI code)
|
|
498
|
+
"""
|
|
499
|
+
return os.read(sys.stdin.fileno(), n).decode("utf-8", errors="replace")
|
tau/tui/theme.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Callable
|
|
5
|
+
|
|
6
|
+
from tau.tui.ansi import (
|
|
7
|
+
RESET, BOLD, DIM, ITALIC,
|
|
8
|
+
BRIGHT_CYAN, BRIGHT_GREEN, BRIGHT_YELLOW, BRIGHT_RED, BRIGHT_WHITE, BRIGHT_BLACK,
|
|
9
|
+
fg,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
# A color function wraps a string in ANSI codes and returns the styled string.
|
|
13
|
+
ColorFn = Callable[[str], str]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# ---------------------------------------------------------------------------
|
|
17
|
+
# Color-function builders — public helpers for writing custom themes
|
|
18
|
+
# ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
def color(ansi_code: str) -> ColorFn:
|
|
21
|
+
"""Wrap text in any ANSI SGR code followed by RESET."""
|
|
22
|
+
return lambda s: ansi_code + s + RESET
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def rgb(r: int, g: int, b: int) -> ColorFn:
|
|
26
|
+
"""Truecolor (24-bit) foreground ColorFn."""
|
|
27
|
+
return lambda s: fg(r, g, b) + s + RESET
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def rgb_bold(r: int, g: int, b: int) -> ColorFn:
|
|
31
|
+
"""Bold + truecolor foreground ColorFn."""
|
|
32
|
+
return lambda s: BOLD + fg(r, g, b) + s + RESET
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def rgb_italic(r: int, g: int, b: int) -> ColorFn:
|
|
36
|
+
"""Italic + truecolor foreground ColorFn."""
|
|
37
|
+
return lambda s: ITALIC + fg(r, g, b) + s + RESET
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _wrap(code: str) -> ColorFn:
|
|
41
|
+
return color(code)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# ---------------------------------------------------------------------------
|
|
45
|
+
# Per-component theme dataclasses
|
|
46
|
+
# ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class SpinnerTheme:
|
|
50
|
+
"""Controls the animated spinner appearance."""
|
|
51
|
+
frames: list[str] = field(default_factory=lambda: ["▖", "▘", "▝", "▗"])
|
|
52
|
+
interval_ms: int = 120
|
|
53
|
+
frame_color: ColorFn = field(default_factory=lambda: _wrap(BRIGHT_CYAN))
|
|
54
|
+
label_color: ColorFn = field(default_factory=lambda: lambda s: s)
|
|
55
|
+
label_thinking: str = "Thinking…"
|
|
56
|
+
label_streaming: str = "Streaming…"
|
|
57
|
+
label_tool_calling: str = "Tool Calling…"
|
|
58
|
+
label_compacting: str = "Compacting…"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class MarkdownTheme:
|
|
63
|
+
"""Controls colours for rendered markdown inside assistant messages."""
|
|
64
|
+
heading: ColorFn = field(default_factory=lambda: lambda s: BOLD + BRIGHT_CYAN + s + RESET)
|
|
65
|
+
code_inline: ColorFn = field(default_factory=lambda: _wrap(BRIGHT_YELLOW))
|
|
66
|
+
code_block: ColorFn = field(default_factory=lambda: _wrap(BRIGHT_GREEN))
|
|
67
|
+
code_block_border: ColorFn = field(default_factory=lambda: _wrap(BRIGHT_BLACK))
|
|
68
|
+
quote: ColorFn = field(default_factory=lambda: lambda s: "\x1b[3m" + s + RESET) # italic
|
|
69
|
+
quote_border: ColorFn = field(default_factory=lambda: _wrap(BRIGHT_BLACK))
|
|
70
|
+
hr: ColorFn = field(default_factory=lambda: _wrap(BRIGHT_BLACK))
|
|
71
|
+
list_bullet: ColorFn = field(default_factory=lambda: _wrap(BRIGHT_CYAN))
|
|
72
|
+
bold: ColorFn = field(default_factory=lambda: lambda s: BOLD + s + RESET)
|
|
73
|
+
italic: ColorFn = field(default_factory=lambda: lambda s: "\x1b[3m" + s + RESET)
|
|
74
|
+
strikethrough: ColorFn = field(default_factory=lambda: lambda s: "\x1b[9m" + s + RESET)
|
|
75
|
+
link_text: ColorFn = field(default_factory=lambda: _wrap(BRIGHT_CYAN))
|
|
76
|
+
link_url: ColorFn = field(default_factory=lambda: _wrap(BRIGHT_BLACK))
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass
|
|
80
|
+
class MessageTheme:
|
|
81
|
+
"""Controls all colors used when rendering chat messages."""
|
|
82
|
+
you_label: ColorFn = field(default_factory=lambda: lambda s: BOLD + BRIGHT_CYAN + s + RESET)
|
|
83
|
+
assistant_label: ColorFn = field(default_factory=lambda: lambda s: BOLD + BRIGHT_GREEN + s + RESET)
|
|
84
|
+
tool_arrow: ColorFn = field(default_factory=lambda: _wrap(BRIGHT_YELLOW))
|
|
85
|
+
tool_result_ok: ColorFn = field(default_factory=lambda: lambda s: s)
|
|
86
|
+
tool_result_err: ColorFn = field(default_factory=lambda: _wrap(BRIGHT_RED))
|
|
87
|
+
thinking: ColorFn = field(default_factory=lambda: lambda s: DIM + ITALIC + s + RESET)
|
|
88
|
+
error_label: ColorFn = field(default_factory=lambda: lambda s: BOLD + BRIGHT_RED + s + RESET)
|
|
89
|
+
dim: ColorFn = field(default_factory=lambda: _wrap(DIM))
|
|
90
|
+
stream_cursor: ColorFn = field(default_factory=lambda: _wrap(BRIGHT_WHITE))
|
|
91
|
+
diff_added: ColorFn = field(default_factory=lambda: _wrap(BRIGHT_GREEN))
|
|
92
|
+
diff_removed: ColorFn = field(default_factory=lambda: _wrap(BRIGHT_RED))
|
|
93
|
+
diff_context: ColorFn = field(default_factory=lambda: _wrap(BRIGHT_BLACK))
|
|
94
|
+
diff_hunk: ColorFn = field(default_factory=lambda: _wrap(BRIGHT_YELLOW))
|
|
95
|
+
diff_inverse: ColorFn = field(default_factory=lambda: lambda s: "\x1b[7m" + s + "\x1b[27m")
|
|
96
|
+
markdown: MarkdownTheme = field(default_factory=MarkdownTheme)
|
|
97
|
+
show_thinking: bool = True
|
|
98
|
+
show_tool_calls: bool = True
|
|
99
|
+
show_images: bool = True
|
|
100
|
+
thinking_label: str = "thinking…"
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@dataclass
|
|
104
|
+
class InputTheme:
|
|
105
|
+
"""Controls the text-input prompt appearance."""
|
|
106
|
+
prefix: str = "❯ "
|
|
107
|
+
placeholder: str = ""
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@dataclass
|
|
111
|
+
class SelectListTheme:
|
|
112
|
+
"""Controls appearance of the SelectList / CommandPalette component."""
|
|
113
|
+
selected_label: ColorFn = field(default_factory=lambda: lambda s: BOLD + BRIGHT_WHITE + s + RESET)
|
|
114
|
+
selected_desc: ColorFn = field(default_factory=lambda: _wrap(BRIGHT_BLACK))
|
|
115
|
+
normal_label: ColorFn = field(default_factory=lambda: _wrap(BRIGHT_BLACK))
|
|
116
|
+
normal_desc: ColorFn = field(default_factory=lambda: _wrap(BRIGHT_BLACK))
|
|
117
|
+
indicator: ColorFn = field(default_factory=lambda: _wrap(BRIGHT_BLACK))
|
|
118
|
+
empty: ColorFn = field(default_factory=lambda: _wrap(BRIGHT_BLACK))
|
|
119
|
+
# Optional full-line background for the selected row (None = no background)
|
|
120
|
+
selected_bg: ColorFn | None = field(default_factory=lambda: None)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@dataclass
|
|
124
|
+
class LayoutTheme:
|
|
125
|
+
"""
|
|
126
|
+
Top-level theme that wires together all sub-themes.
|
|
127
|
+
|
|
128
|
+
Pass a custom instance to App.create() or Layout() to restyle the whole UI:
|
|
129
|
+
|
|
130
|
+
from tau.tui.theme import LayoutTheme, SpinnerTheme, MessageTheme
|
|
131
|
+
from tau.tui.ansi import BRIGHT_MAGENTA, RESET
|
|
132
|
+
|
|
133
|
+
theme = LayoutTheme(
|
|
134
|
+
divider=lambda s: BRIGHT_MAGENTA + s + RESET,
|
|
135
|
+
spinner=SpinnerTheme(
|
|
136
|
+
frames=["◐", "◓", "◑", "◒"],
|
|
137
|
+
interval_ms=100,
|
|
138
|
+
),
|
|
139
|
+
)
|
|
140
|
+
app = await App.create(config, theme=theme)
|
|
141
|
+
"""
|
|
142
|
+
divider: ColorFn = field(default_factory=lambda: _wrap(BRIGHT_BLACK))
|
|
143
|
+
divider_command: ColorFn = field(default_factory=lambda: _wrap(BRIGHT_CYAN))
|
|
144
|
+
divider_execute: ColorFn = field(default_factory=lambda: _wrap(BRIGHT_YELLOW))
|
|
145
|
+
spinner: SpinnerTheme = field(default_factory=SpinnerTheme)
|
|
146
|
+
message: MessageTheme = field(default_factory=MessageTheme)
|
|
147
|
+
input: InputTheme = field(default_factory=InputTheme)
|
|
148
|
+
select_list: SelectListTheme = field(default_factory=SelectListTheme)
|