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/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)
|