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/input.py ADDED
@@ -0,0 +1,496 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+
6
+ # ── Key event ─────────────────────────────────────────────────────────────────
7
+
8
+ @dataclass
9
+ class KeyEvent:
10
+ """A single parsed keyboard event."""
11
+ key: str # canonical name: "a", "enter", "up", "f1", etc.
12
+ char: str | None = None # printable character if the key produces one
13
+ ctrl: bool = False
14
+ alt: bool = False
15
+ shift: bool = False
16
+ released: bool = False # True on key-up (Kitty keyboard protocol only)
17
+ raw: str = "" # original bytes received from stdin
18
+
19
+ def __str__(self) -> str:
20
+ parts = []
21
+ if self.ctrl:
22
+ parts.append("ctrl")
23
+ if self.alt:
24
+ parts.append("alt")
25
+ if self.shift:
26
+ parts.append("shift")
27
+ parts.append(self.key)
28
+ return "+".join(parts)
29
+
30
+ def matches(self, *keys: str) -> bool:
31
+ """Check if this event matches any of the given key combos ('ctrl+c', 'enter', 'up')."""
32
+ combo = str(self)
33
+ return combo in keys or self.key in keys
34
+
35
+
36
+ @dataclass
37
+ class PasteEvent:
38
+ """Bracketed paste — text pasted into the terminal."""
39
+ text: str
40
+ raw: str = ""
41
+
42
+
43
+ @dataclass
44
+ class MouseEvent:
45
+ """Mouse button press or release."""
46
+ x: int
47
+ y: int
48
+ button: int
49
+ pressed: bool
50
+ raw: str = ""
51
+
52
+
53
+ @dataclass
54
+ class BgColorEvent:
55
+ """Terminal background color response (OSC 11).
56
+
57
+ Emitted when the terminal replies to an ``\\x1b]11;?\\x1b\\\\`` query.
58
+ Each channel is normalised to 0–255.
59
+ """
60
+ r: int
61
+ g: int
62
+ b: int
63
+
64
+ @property
65
+ def is_dark(self) -> bool:
66
+ """True when luminance < 0.5 (dark background)."""
67
+ return (0.2126 * self.r + 0.7152 * self.g + 0.0722 * self.b) < 128
68
+
69
+
70
+ InputEvent = KeyEvent | PasteEvent | MouseEvent | BgColorEvent
71
+
72
+
73
+ # ── CSI / SS3 lookup tables ───────────────────────────────────────────────────
74
+
75
+ # CSI final-byte → key name (no parameters)
76
+ _CSI_SIMPLE: dict[str, str] = {
77
+ "A": "up",
78
+ "B": "down",
79
+ "C": "right",
80
+ "D": "left",
81
+ "H": "home",
82
+ "F": "end",
83
+ "Z": "tab", # shift+tab (also sets shift below)
84
+ "P": "f1",
85
+ "Q": "f2",
86
+ "R": "f3",
87
+ "S": "f4",
88
+ }
89
+
90
+ # CSI tilde number → key name
91
+ _CSI_TILDE: dict[int, str] = {
92
+ 1: "home",
93
+ 2: "insert",
94
+ 3: "delete",
95
+ 4: "end",
96
+ 5: "page_up",
97
+ 6: "page_down",
98
+ 7: "home",
99
+ 8: "end",
100
+ 11: "f1",
101
+ 12: "f2",
102
+ 13: "f3",
103
+ 14: "f4",
104
+ 15: "f5",
105
+ 17: "f6",
106
+ 18: "f7",
107
+ 19: "f8",
108
+ 20: "f9",
109
+ 21: "f10",
110
+ 23: "f11",
111
+ 24: "f12",
112
+ }
113
+
114
+ # SS3 (ESC O) final-byte → key name
115
+ _SS3: dict[str, str] = {
116
+ "P": "f1",
117
+ "Q": "f2",
118
+ "R": "f3",
119
+ "S": "f4",
120
+ "H": "home",
121
+ "F": "end",
122
+ "A": "up",
123
+ "B": "down",
124
+ "C": "right",
125
+ "D": "left",
126
+ "M": "enter",
127
+ }
128
+
129
+ # Kitty / CSI modifier byte → (shift, alt, ctrl)
130
+ # The modifier is encoded as: (value - 1) with bit flags
131
+ # bit 0 = shift, bit 1 = alt, bit 2 = ctrl, bit 3 = super
132
+ def _decode_modifier(mod: int) -> tuple[bool, bool, bool]:
133
+ m = mod - 1
134
+ return bool(m & 1), bool(m & 2), bool(m & 4)
135
+
136
+
137
+ # Control characters → key names
138
+ _CTRL_CHARS: dict[str, tuple[str, bool]] = {
139
+ "\x00": ("space", True), # ctrl+space / ctrl+@
140
+ "\x01": ("a", True),
141
+ "\x02": ("b", True),
142
+ "\x03": ("c", True),
143
+ "\x04": ("d", True),
144
+ "\x05": ("e", True),
145
+ "\x06": ("f", True),
146
+ "\x07": ("g", True),
147
+ "\x08": ("backspace", False), # ctrl+h / backspace on some terminals
148
+ "\x09": ("tab", False),
149
+ "\x0a": ("enter", False),
150
+ "\x0b": ("k", True),
151
+ "\x0c": ("l", True),
152
+ "\x0d": ("enter", False),
153
+ "\x0e": ("n", True),
154
+ "\x0f": ("o", True),
155
+ "\x10": ("p", True),
156
+ "\x11": ("q", True),
157
+ "\x12": ("r", True),
158
+ "\x13": ("s", True),
159
+ "\x14": ("t", True),
160
+ "\x15": ("u", True),
161
+ "\x16": ("v", True),
162
+ "\x17": ("w", True),
163
+ "\x18": ("x", True),
164
+ "\x19": ("y", True),
165
+ "\x1a": ("z", True),
166
+ "\x1c": ("\\", True),
167
+ "\x1d": ("]", True),
168
+ "\x1e": ("6", True),
169
+ "\x1f": ("-", True),
170
+ "\x7f": ("backspace", False),
171
+ }
172
+
173
+
174
+ # ── Sequence completeness ─────────────────────────────────────────────────────
175
+
176
+ def _is_complete(buf: str) -> bool | None:
177
+ """
178
+ Return True if buf is a complete escape sequence,
179
+ False if it will never be one, None if it's incomplete (needs more data).
180
+ """
181
+ if not buf.startswith("\x1b"):
182
+ return True # not an escape — single char, always complete
183
+
184
+ if len(buf) == 1:
185
+ return None # bare ESC, could be start of sequence
186
+
187
+ second = buf[1]
188
+
189
+ # SS3: ESC O <char>
190
+ if second == "O":
191
+ if len(buf) < 3:
192
+ return None
193
+ return True
194
+
195
+ # CSI: ESC [ <params> <final>
196
+ if second == "[":
197
+ if len(buf) < 3:
198
+ return None
199
+ # Basic mouse: ESC [ M <button> <col> <row> — exactly 6 bytes.
200
+ # Must be checked before the generic final-byte scan because 'M' is a
201
+ # valid CSI final byte and would otherwise terminate prematurely,
202
+ # leaving the 3 parameter bytes to be parsed as text.
203
+ if buf[2] == "M":
204
+ return len(buf) >= 6
205
+ # Final byte is 0x40–0x7E (@–~)
206
+ for i in range(2, len(buf)):
207
+ c = buf[i]
208
+ code = ord(c)
209
+ if 0x40 <= code <= 0x7E:
210
+ return True
211
+ return None # still accumulating params
212
+
213
+ # APC: ESC _ ... ST (zero-width cursor markers etc.)
214
+ if second == "_":
215
+ if buf.endswith("\x1b\\") or buf.endswith("\x07"):
216
+ return True
217
+ return None
218
+
219
+ # OSC: ESC ] ... BEL or ST
220
+ if second == "]":
221
+ if buf.endswith("\x07") or buf.endswith("\x1b\\"):
222
+ return True
223
+ return None
224
+
225
+ # DCS / PM / SOS / APC variants: ESC P/X/^ ... ST
226
+ if second in ("P", "X", "^"):
227
+ if buf.endswith("\x1b\\"):
228
+ return True
229
+ return None
230
+
231
+ # Other 2-char sequences (ESC <char>): complete
232
+ return True
233
+
234
+
235
+ # ── Parser ────────────────────────────────────────────────────────────────────
236
+
237
+ class InputParser:
238
+ """
239
+ Stateful parser that converts raw stdin bytes into InputEvent objects.
240
+
241
+ Usage:
242
+ parser = InputParser()
243
+ for event in parser.feed(raw_bytes):
244
+ handle(event)
245
+ """
246
+
247
+ def __init__(self) -> None:
248
+ self._buf = ""
249
+
250
+ def feed(self, data: str) -> list[InputEvent]:
251
+ self._buf += data
252
+ events: list[InputEvent] = []
253
+ while self._buf:
254
+ complete = _is_complete(self._buf)
255
+ if complete is None:
256
+ break # need more data
257
+ event = self._parse_one(self._buf)
258
+ if event is not None:
259
+ events.append(event)
260
+ self._buf = ""
261
+ return events
262
+
263
+ def flush(self) -> list[InputEvent]:
264
+ """
265
+ Force-parse whatever remains in the buffer.
266
+ Called after a short read timeout to emit a bare ESC rather than
267
+ waiting indefinitely for a sequence that never arrives.
268
+ """
269
+ if not self._buf:
270
+ return []
271
+ event = self._parse_one(self._buf)
272
+ self._buf = ""
273
+ return [event] if event is not None else []
274
+
275
+ def _parse_one(self, raw: str) -> InputEvent | None:
276
+ # ── Bracketed paste ───────────────────────────────────────────────────
277
+ if raw.startswith("\x1b[200~"):
278
+ # strip opening and closing markers
279
+ text = raw[6:]
280
+ if text.endswith("\x1b[201~"):
281
+ text = text[:-6]
282
+ return PasteEvent(text=text, raw=raw)
283
+
284
+ # ── Control characters ────────────────────────────────────────────────
285
+ if len(raw) == 1 and raw in _CTRL_CHARS:
286
+ name, is_ctrl = _CTRL_CHARS[raw]
287
+ return KeyEvent(key=name, char=None, ctrl=is_ctrl, raw=raw)
288
+
289
+ # ── Printable ASCII / Unicode ─────────────────────────────────────────
290
+ if len(raw) == 1 and raw >= " " and raw != "\x7f":
291
+ return KeyEvent(key=raw.lower(), char=raw, shift=raw.isupper(), raw=raw)
292
+
293
+ if not raw.startswith("\x1b"):
294
+ # Multi-byte UTF-8 character (emoji, CJK, etc.)
295
+ return KeyEvent(key=raw, char=raw, raw=raw)
296
+
297
+ # ── Bare ESC ──────────────────────────────────────────────────────────
298
+ if raw == "\x1b":
299
+ return KeyEvent(key="escape", char=None, raw=raw)
300
+
301
+ second = raw[1] if len(raw) > 1 else ""
302
+
303
+ # ── Alt + char: ESC <char> ────────────────────────────────────────────
304
+ if len(raw) == 2 and second not in ("[", "O", "_", "]", "P", "X", "^"):
305
+ inner = self._parse_one(second)
306
+ if inner is not None and isinstance(inner, KeyEvent):
307
+ inner.alt = True
308
+ inner.raw = raw
309
+ return inner
310
+ return KeyEvent(key=second, char=second, alt=True, raw=raw)
311
+
312
+ # ── SS3: ESC O <char> ─────────────────────────────────────────────────
313
+ if second == "O" and len(raw) == 3:
314
+ final = raw[2]
315
+ name = _SS3.get(final)
316
+ if name:
317
+ return KeyEvent(key=name, char=None, raw=raw)
318
+
319
+ # ── CSI: ESC [ ... ───────────────────────────────────────────────────
320
+ if second == "[":
321
+ return self._parse_csi(raw)
322
+
323
+ # ── OSC 11 — terminal background-colour response ─────────────────────
324
+ # Reply format: ESC ] 11 ; rgb:RRRR/GGGG/BBBB BEL-or-ST
325
+ if second == "]":
326
+ body = raw[2:]
327
+ if body.endswith("\x07"):
328
+ body = body[:-1]
329
+ elif body.endswith("\x1b\\"):
330
+ body = body[:-2]
331
+ if body.startswith("11;rgb:"):
332
+ try:
333
+ parts = body[7:].split("/")
334
+ r = int(parts[0], 16) >> 8 # 16-bit → 8-bit
335
+ g = int(parts[1], 16) >> 8
336
+ b = int(parts[2], 16) >> 8
337
+ return BgColorEvent(r=r, g=g, b=b)
338
+ except (ValueError, IndexError):
339
+ pass
340
+
341
+ # ── APC / other non-key sequences (ignore) ───────────────────────────
342
+ return None
343
+
344
+ def _parse_csi(self, raw: str) -> InputEvent | None:
345
+ # raw = ESC [ <params> <final>
346
+ payload = raw[2:] # strip ESC [
347
+ if not payload:
348
+ return None
349
+
350
+ final = payload[-1]
351
+ params_str = payload[:-1]
352
+
353
+ # ── Mouse: SGR — ESC [ < button ; col ; row M/m ─────────────────────
354
+ if params_str.startswith("<"):
355
+ return self._parse_mouse(raw, params_str[1:], final)
356
+
357
+ # ── Mouse: basic — ESC [ M <button+32> <col+32> <row+32> ─────────────
358
+ # Terminals that don't support SGR (mode 1006) fall back to this 6-byte
359
+ # form. We must parse it here; if left unhandled, the 3 raw bytes leak
360
+ # into the event queue as printable characters and corrupt the editor.
361
+ if final == "M" and not params_str and len(raw) == 6:
362
+ b = ord(raw[3]) - 32
363
+ x = max(1, ord(raw[4]) - 32)
364
+ y = max(1, ord(raw[5]) - 32)
365
+ button = b & 0x03
366
+ if b & 0x40:
367
+ button += 64 # scroll wheel
368
+ return MouseEvent(x=x, y=y, button=button, pressed=True, raw=raw)
369
+
370
+ # ── Kitty protocol: ESC [ <cp> ; <mods> u ────────────────────────────
371
+ if final == "u":
372
+ return self._parse_kitty(raw, params_str)
373
+
374
+ # ── Shift+Tab: ESC [ Z ────────────────────────────────────────────────
375
+ if final == "Z" and not params_str:
376
+ return KeyEvent(key="tab", char=None, shift=True, raw=raw)
377
+
378
+ # ── Simple CSI: ESC [ <letter> (no params) ───────────────────────────
379
+ if not params_str:
380
+ name = _CSI_SIMPLE.get(final)
381
+ if name:
382
+ return KeyEvent(key=name, char=None, raw=raw)
383
+ return None
384
+
385
+ # ── CSI with params ───────────────────────────────────────────────────
386
+ parts = params_str.split(";")
387
+
388
+ # ESC [ 1 ; <mod> <letter> — modified arrow/navigation
389
+ if len(parts) == 2 and parts[0] == "1":
390
+ try:
391
+ mod = int(parts[1])
392
+ except ValueError:
393
+ return None
394
+ shift, alt, ctrl = _decode_modifier(mod)
395
+ name = _CSI_SIMPLE.get(final)
396
+ if name:
397
+ return KeyEvent(key=name, char=None, shift=shift, alt=alt, ctrl=ctrl, raw=raw)
398
+
399
+ # ESC [ <n> ~ — tilde sequences
400
+ if final == "~" and len(parts) == 1:
401
+ try:
402
+ n = int(parts[0])
403
+ except ValueError:
404
+ return None
405
+ name = _CSI_TILDE.get(n)
406
+ if name:
407
+ return KeyEvent(key=name, char=None, raw=raw)
408
+
409
+ # ESC [ <n> ; <mod> ~ — modified tilde sequences
410
+ if final == "~" and len(parts) == 2:
411
+ try:
412
+ n, mod = int(parts[0]), int(parts[1])
413
+ except ValueError:
414
+ return None
415
+ name = _CSI_TILDE.get(n)
416
+ if name:
417
+ shift, alt, ctrl = _decode_modifier(mod)
418
+ return KeyEvent(key=name, char=None, shift=shift, alt=alt, ctrl=ctrl, raw=raw)
419
+
420
+ return None
421
+
422
+ def _parse_kitty(self, raw: str, params_str: str) -> KeyEvent | None:
423
+ # ESC [ <codepoint> u
424
+ # ESC [ <codepoint> ; <mods> u
425
+ # ESC [ <codepoint> ; <mods> ; <event_type> u (Kitty keyboard protocol)
426
+ # event_type: 1=press 2=repeat 3=release
427
+ parts = params_str.split(";")
428
+ try:
429
+ codepoint = int(parts[0])
430
+ except ValueError:
431
+ return None
432
+
433
+ shift = alt = ctrl = False
434
+ if len(parts) >= 2 and parts[1]:
435
+ try:
436
+ shift, alt, ctrl = _decode_modifier(int(parts[1]))
437
+ except ValueError:
438
+ pass
439
+
440
+ released = False
441
+ if len(parts) >= 3 and parts[2]:
442
+ try:
443
+ released = int(parts[2]) == 3 # 3 = key-up
444
+ except ValueError:
445
+ pass
446
+
447
+ # Map codepoint to key name
448
+ try:
449
+ ch = chr(codepoint)
450
+ except (ValueError, OverflowError):
451
+ return None
452
+
453
+ # Special codepoints
454
+ _kitty_special: dict[int, str] = {
455
+ 27: "escape",
456
+ 13: "enter",
457
+ 9: "tab",
458
+ 127: "backspace",
459
+ 57358: "caps_lock",
460
+ 57359: "scroll_lock",
461
+ 57360: "num_lock",
462
+ 57361: "print_screen",
463
+ 57362: "pause",
464
+ 57363: "menu",
465
+ 57376: "f13",
466
+ 57377: "f14",
467
+ }
468
+ # Arrow / navigation codepoints (Kitty uses Unicode private area)
469
+ _kitty_nav: dict[int, str] = {
470
+ 57352: "up", 57353: "down", 57354: "right", 57355: "left",
471
+ 57356: "end", 57357: "home", 57358: "page_up", 57359: "page_down",
472
+ 57399: "kp0", 57400: "kp1", 57401: "kp2", 57402: "kp3",
473
+ }
474
+
475
+ if codepoint in _kitty_special:
476
+ key = _kitty_special[codepoint]
477
+ return KeyEvent(key=key, char=None, shift=shift, alt=alt, ctrl=ctrl, released=released, raw=raw)
478
+
479
+ if codepoint in _kitty_nav:
480
+ key = _kitty_nav[codepoint]
481
+ return KeyEvent(key=key, char=None, shift=shift, alt=alt, ctrl=ctrl, released=released, raw=raw)
482
+
483
+ # Regular character
484
+ key = ch.lower()
485
+ char = ch if ch.isprintable() else None
486
+ return KeyEvent(key=key, char=char, shift=shift, alt=alt, ctrl=ctrl, released=released, raw=raw)
487
+
488
+ def _parse_mouse(self, raw: str, params: str, final: str) -> MouseEvent | None:
489
+ # SGR mouse: ESC [ < button ; col ; row M/m
490
+ try:
491
+ parts = params.split(";")
492
+ button, x, y = int(parts[0]), int(parts[1]), int(parts[2])
493
+ except (ValueError, IndexError):
494
+ return None
495
+ pressed = final == "M"
496
+ return MouseEvent(x=x, y=y, button=button, pressed=pressed, raw=raw)