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.
Files changed (283) hide show
  1. tau/__init__.py +0 -0
  2. tau/agent/__init__.py +11 -0
  3. tau/agent/prompt/__init__.py +10 -0
  4. tau/agent/prompt/builder.py +302 -0
  5. tau/agent/prompt/types.py +33 -0
  6. tau/agent/service.py +369 -0
  7. tau/agent/types.py +61 -0
  8. tau/auth/manager.py +247 -0
  9. tau/auth/storage.py +82 -0
  10. tau/auth/types.py +41 -0
  11. tau/builtins/__init__.py +4 -0
  12. tau/builtins/__pycache__/__init__.cpython-313.pyc +0 -0
  13. tau/builtins/__pycache__/__init__.cpython-314.pyc +0 -0
  14. tau/builtins/commands/__init__.py +41 -0
  15. tau/builtins/commands/__pycache__/__init__.cpython-313.pyc +0 -0
  16. tau/builtins/commands/__pycache__/__init__.cpython-314.pyc +0 -0
  17. tau/builtins/commands/__pycache__/clear.cpython-313.pyc +0 -0
  18. tau/builtins/commands/__pycache__/clear.cpython-314.pyc +0 -0
  19. tau/builtins/commands/__pycache__/compact.cpython-313.pyc +0 -0
  20. tau/builtins/commands/__pycache__/compact.cpython-314.pyc +0 -0
  21. tau/builtins/commands/__pycache__/reload.cpython-313.pyc +0 -0
  22. tau/builtins/commands/__pycache__/reload.cpython-314.pyc +0 -0
  23. tau/builtins/commands/__pycache__/session.cpython-313.pyc +0 -0
  24. tau/builtins/commands/__pycache__/session.cpython-314.pyc +0 -0
  25. tau/builtins/commands/clear.py +16 -0
  26. tau/builtins/commands/compact.py +28 -0
  27. tau/builtins/commands/reload.py +27 -0
  28. tau/builtins/commands/session.py +19 -0
  29. tau/builtins/extensions/footer/__init__.py +76 -0
  30. tau/builtins/extensions/footer/__pycache__/__init__.cpython-313.pyc +0 -0
  31. tau/builtins/extensions/footer/__pycache__/git.cpython-313.pyc +0 -0
  32. tau/builtins/extensions/footer/__pycache__/model.cpython-313.pyc +0 -0
  33. tau/builtins/extensions/footer/__pycache__/utils.cpython-313.pyc +0 -0
  34. tau/builtins/extensions/footer/git.py +26 -0
  35. tau/builtins/extensions/footer/model.py +69 -0
  36. tau/builtins/extensions/footer/utils.py +44 -0
  37. tau/builtins/extensions/header/__init__.py +18 -0
  38. tau/builtins/extensions/header/__pycache__/__init__.cpython-313.pyc +0 -0
  39. tau/builtins/models/__init__.py +0 -0
  40. tau/builtins/models/__pycache__/__init__.cpython-313.pyc +0 -0
  41. tau/builtins/models/__pycache__/text.cpython-313.pyc +0 -0
  42. tau/builtins/models/audio.py +43 -0
  43. tau/builtins/models/image.py +43 -0
  44. tau/builtins/models/text.py +482 -0
  45. tau/builtins/models/video.py +40 -0
  46. tau/builtins/prompts/commit.md +7 -0
  47. tau/builtins/prompts/docs.md +7 -0
  48. tau/builtins/prompts/explain.md +7 -0
  49. tau/builtins/prompts/fix.md +7 -0
  50. tau/builtins/prompts/refactor.md +7 -0
  51. tau/builtins/prompts/review.md +7 -0
  52. tau/builtins/prompts/test.md +7 -0
  53. tau/builtins/providers/__init__.py +0 -0
  54. tau/builtins/providers/__pycache__/__init__.cpython-313.pyc +0 -0
  55. tau/builtins/providers/__pycache__/text.cpython-313.pyc +0 -0
  56. tau/builtins/providers/audio.py +10 -0
  57. tau/builtins/providers/image.py +9 -0
  58. tau/builtins/providers/text.py +33 -0
  59. tau/builtins/providers/video.py +6 -0
  60. tau/builtins/skills/code-review/SKILL.md +4 -0
  61. tau/builtins/skills/debug/SKILL.md +4 -0
  62. tau/builtins/skills/git-commit/SKILL.md +4 -0
  63. tau/builtins/themes/dark.yaml +1 -0
  64. tau/builtins/themes/light.yaml +46 -0
  65. tau/builtins/tools/__init__.py +73 -0
  66. tau/builtins/tools/__pycache__/__init__.cpython-313.pyc +0 -0
  67. tau/builtins/tools/__pycache__/__init__.cpython-314.pyc +0 -0
  68. tau/builtins/tools/__pycache__/bash.cpython-313.pyc +0 -0
  69. tau/builtins/tools/__pycache__/bash.cpython-314.pyc +0 -0
  70. tau/builtins/tools/__pycache__/edit.cpython-313.pyc +0 -0
  71. tau/builtins/tools/__pycache__/edit.cpython-314.pyc +0 -0
  72. tau/builtins/tools/__pycache__/glob.cpython-313.pyc +0 -0
  73. tau/builtins/tools/__pycache__/glob.cpython-314.pyc +0 -0
  74. tau/builtins/tools/__pycache__/grep.cpython-313.pyc +0 -0
  75. tau/builtins/tools/__pycache__/grep.cpython-314.pyc +0 -0
  76. tau/builtins/tools/__pycache__/ls.cpython-313.pyc +0 -0
  77. tau/builtins/tools/__pycache__/ls.cpython-314.pyc +0 -0
  78. tau/builtins/tools/__pycache__/read.cpython-313.pyc +0 -0
  79. tau/builtins/tools/__pycache__/read.cpython-314.pyc +0 -0
  80. tau/builtins/tools/__pycache__/terminal.cpython-313.pyc +0 -0
  81. tau/builtins/tools/__pycache__/terminal.cpython-314.pyc +0 -0
  82. tau/builtins/tools/__pycache__/write.cpython-313.pyc +0 -0
  83. tau/builtins/tools/__pycache__/write.cpython-314.pyc +0 -0
  84. tau/builtins/tools/edit.py +215 -0
  85. tau/builtins/tools/glob.py +112 -0
  86. tau/builtins/tools/grep.py +146 -0
  87. tau/builtins/tools/ls.py +135 -0
  88. tau/builtins/tools/read.py +122 -0
  89. tau/builtins/tools/terminal.py +150 -0
  90. tau/builtins/tools/write.py +105 -0
  91. tau/commands/__init__.py +10 -0
  92. tau/commands/registry.py +71 -0
  93. tau/commands/types.py +33 -0
  94. tau/console/__init__.py +0 -0
  95. tau/console/cli.py +266 -0
  96. tau/console/commands/__init__.py +0 -0
  97. tau/console/commands/auth.py +193 -0
  98. tau/console/commands/packages.py +104 -0
  99. tau/console/commands/update.py +76 -0
  100. tau/core/__init__.py +0 -0
  101. tau/core/registry.py +102 -0
  102. tau/engine/__init__.py +47 -0
  103. tau/engine/service.py +768 -0
  104. tau/engine/types.py +163 -0
  105. tau/extensions/__init__.py +28 -0
  106. tau/extensions/api.py +928 -0
  107. tau/extensions/context.py +462 -0
  108. tau/extensions/events.py +70 -0
  109. tau/extensions/loader.py +386 -0
  110. tau/extensions/runtime.py +184 -0
  111. tau/extensions/settings.py +137 -0
  112. tau/hooks/__init__.py +112 -0
  113. tau/hooks/engine.py +237 -0
  114. tau/hooks/inference.py +21 -0
  115. tau/hooks/runtime.py +126 -0
  116. tau/hooks/service.py +121 -0
  117. tau/hooks/session.py +117 -0
  118. tau/hooks/tui.py +61 -0
  119. tau/hooks/types.py +72 -0
  120. tau/inference/__init__.py +180 -0
  121. tau/inference/api/__init__.py +0 -0
  122. tau/inference/api/audio/__init__.py +0 -0
  123. tau/inference/api/audio/base.py +29 -0
  124. tau/inference/api/audio/builtins.py +15 -0
  125. tau/inference/api/audio/elevenlabs_audio.py +183 -0
  126. tau/inference/api/audio/gemini_audio.py +95 -0
  127. tau/inference/api/audio/openai_audio.py +159 -0
  128. tau/inference/api/audio/registry.py +15 -0
  129. tau/inference/api/audio/sarvam_audio.py +163 -0
  130. tau/inference/api/audio/service.py +103 -0
  131. tau/inference/api/audio/utils.py +47 -0
  132. tau/inference/api/image/__init__.py +0 -0
  133. tau/inference/api/image/base.py +17 -0
  134. tau/inference/api/image/builtins.py +8 -0
  135. tau/inference/api/image/gemini_image.py +77 -0
  136. tau/inference/api/image/openai_image.py +103 -0
  137. tau/inference/api/image/openrouter.py +144 -0
  138. tau/inference/api/image/registry.py +15 -0
  139. tau/inference/api/image/service.py +71 -0
  140. tau/inference/api/registry.py +82 -0
  141. tau/inference/api/text/__init__.py +0 -0
  142. tau/inference/api/text/anthropic_claude_code.py +222 -0
  143. tau/inference/api/text/anthropic_messages.py +196 -0
  144. tau/inference/api/text/base.py +40 -0
  145. tau/inference/api/text/builtins.py +19 -0
  146. tau/inference/api/text/gemini_generate.py +234 -0
  147. tau/inference/api/text/github_copilot_chat.py +172 -0
  148. tau/inference/api/text/google_antigravity.py +522 -0
  149. tau/inference/api/text/mistral_chat.py +284 -0
  150. tau/inference/api/text/ollama_chat.py +200 -0
  151. tau/inference/api/text/openai_codex_responses.py +497 -0
  152. tau/inference/api/text/openai_completions.py +227 -0
  153. tau/inference/api/text/openai_responses.py +235 -0
  154. tau/inference/api/text/registry.py +50 -0
  155. tau/inference/api/text/service.py +297 -0
  156. tau/inference/api/text/types.py +7 -0
  157. tau/inference/api/text/utils.py +228 -0
  158. tau/inference/api/video/__init__.py +0 -0
  159. tau/inference/api/video/base.py +26 -0
  160. tau/inference/api/video/builtins.py +7 -0
  161. tau/inference/api/video/fal_video.py +119 -0
  162. tau/inference/api/video/openrouter_video.py +142 -0
  163. tau/inference/api/video/registry.py +15 -0
  164. tau/inference/api/video/service.py +72 -0
  165. tau/inference/model/__init__.py +0 -0
  166. tau/inference/model/registry.py +102 -0
  167. tau/inference/model/types.py +65 -0
  168. tau/inference/provider/__init__.py +0 -0
  169. tau/inference/provider/oauth/__init__.py +35 -0
  170. tau/inference/provider/oauth/anthropic_claude_code.py +286 -0
  171. tau/inference/provider/oauth/github_copilot.py +333 -0
  172. tau/inference/provider/oauth/google_antigravity.py +258 -0
  173. tau/inference/provider/oauth/openai_codex.py +309 -0
  174. tau/inference/provider/oauth/pkce.py +14 -0
  175. tau/inference/provider/oauth/types.py +46 -0
  176. tau/inference/provider/oauth/utils.py +154 -0
  177. tau/inference/provider/registry.py +141 -0
  178. tau/inference/provider/types.py +114 -0
  179. tau/inference/types.py +549 -0
  180. tau/inference/utils.py +219 -0
  181. tau/message/__init__.py +0 -0
  182. tau/message/types.py +482 -0
  183. tau/message/utils.py +178 -0
  184. tau/packages/__init__.py +11 -0
  185. tau/packages/manager.py +190 -0
  186. tau/packages/types.py +20 -0
  187. tau/packages/utils.py +67 -0
  188. tau/prompts/expand.py +58 -0
  189. tau/prompts/loader.py +69 -0
  190. tau/prompts/registry.py +45 -0
  191. tau/prompts/types.py +24 -0
  192. tau/rpc/__init__.py +8 -0
  193. tau/rpc/mode.py +783 -0
  194. tau/rpc/types.py +252 -0
  195. tau/runtime/service.py +759 -0
  196. tau/runtime/types.py +303 -0
  197. tau/session/branch_summarization.py +312 -0
  198. tau/session/compaction.py +646 -0
  199. tau/session/manager.py +652 -0
  200. tau/session/types.py +188 -0
  201. tau/session/utils.py +233 -0
  202. tau/settings/manager.py +1077 -0
  203. tau/settings/paths.py +150 -0
  204. tau/settings/storage.py +63 -0
  205. tau/settings/types.py +173 -0
  206. tau/settings/utils.py +25 -0
  207. tau/skills/loader.py +91 -0
  208. tau/skills/registry.py +70 -0
  209. tau/skills/types.py +25 -0
  210. tau/themes/loader.py +238 -0
  211. tau/themes/registry.py +108 -0
  212. tau/themes/types.py +19 -0
  213. tau/tool/__init__.py +3 -0
  214. tau/tool/registry.py +117 -0
  215. tau/tool/render.py +21 -0
  216. tau/tool/types.py +244 -0
  217. tau/trust/__init__.py +13 -0
  218. tau/trust/manager.py +80 -0
  219. tau/trust/types.py +14 -0
  220. tau/trust/utils.py +72 -0
  221. tau/tui/__init__.py +54 -0
  222. tau/tui/agent_hooks.py +346 -0
  223. tau/tui/ansi.py +330 -0
  224. tau/tui/app.py +540 -0
  225. tau/tui/autocomplete.py +33 -0
  226. tau/tui/capabilities.py +119 -0
  227. tau/tui/commands/__init__.py +3 -0
  228. tau/tui/commands/appearance.py +498 -0
  229. tau/tui/commands/auth.py +232 -0
  230. tau/tui/commands/context.py +38 -0
  231. tau/tui/commands/misc.py +82 -0
  232. tau/tui/commands/model.py +118 -0
  233. tau/tui/commands/session.py +464 -0
  234. tau/tui/component.py +268 -0
  235. tau/tui/components/__init__.py +0 -0
  236. tau/tui/components/autocomplete_manager.py +267 -0
  237. tau/tui/components/autocomplete_picker.py +143 -0
  238. tau/tui/components/box.py +90 -0
  239. tau/tui/components/command_palette.py +144 -0
  240. tau/tui/components/dynamic_border.py +19 -0
  241. tau/tui/components/file_picker.py +233 -0
  242. tau/tui/components/image.py +181 -0
  243. tau/tui/components/inline_selector.py +71 -0
  244. tau/tui/components/layout.py +1194 -0
  245. tau/tui/components/message_list.py +692 -0
  246. tau/tui/components/modal.py +97 -0
  247. tau/tui/components/model_palette.py +204 -0
  248. tau/tui/components/picker_overlay.py +174 -0
  249. tau/tui/components/prompt_overlay.py +236 -0
  250. tau/tui/components/resume_modal.py +372 -0
  251. tau/tui/components/select_list.py +222 -0
  252. tau/tui/components/settings_modal.py +274 -0
  253. tau/tui/components/settings_schema.py +203 -0
  254. tau/tui/components/spinner.py +119 -0
  255. tau/tui/components/text_input.py +396 -0
  256. tau/tui/components/text_prompt.py +82 -0
  257. tau/tui/components/tree_select_list.py +580 -0
  258. tau/tui/components/trust_screen.py +97 -0
  259. tau/tui/diff.py +114 -0
  260. tau/tui/fuzzy.py +99 -0
  261. tau/tui/input.py +496 -0
  262. tau/tui/input_handler.py +716 -0
  263. tau/tui/keybindings.py +87 -0
  264. tau/tui/markdown.py +286 -0
  265. tau/tui/message_renderers.py +31 -0
  266. tau/tui/overlay.py +326 -0
  267. tau/tui/renderer.py +378 -0
  268. tau/tui/terminal.py +499 -0
  269. tau/tui/theme.py +148 -0
  270. tau/tui/tui.py +544 -0
  271. tau/tui/ui_context.py +768 -0
  272. tau/tui/utils.py +20 -0
  273. tau/utils/__init__.py +0 -0
  274. tau/utils/http_proxy.py +221 -0
  275. tau/utils/image_processing.py +172 -0
  276. tau/utils/secrets.py +59 -0
  277. tau/utils/version_check.py +60 -0
  278. tau_coding_agent-0.1.0.dist-info/METADATA +177 -0
  279. tau_coding_agent-0.1.0.dist-info/RECORD +283 -0
  280. tau_coding_agent-0.1.0.dist-info/WHEEL +5 -0
  281. tau_coding_agent-0.1.0.dist-info/entry_points.txt +2 -0
  282. tau_coding_agent-0.1.0.dist-info/licenses/LICENSE +21 -0
  283. 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)