superqode 0.1.5__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 (288) hide show
  1. superqode/__init__.py +33 -0
  2. superqode/acp/__init__.py +23 -0
  3. superqode/acp/client.py +913 -0
  4. superqode/acp/permission_screen.py +457 -0
  5. superqode/acp/types.py +480 -0
  6. superqode/acp_discovery.py +856 -0
  7. superqode/agent/__init__.py +22 -0
  8. superqode/agent/edit_strategies.py +334 -0
  9. superqode/agent/loop.py +892 -0
  10. superqode/agent/qe_report_templates.py +39 -0
  11. superqode/agent/system_prompts.py +353 -0
  12. superqode/agent_output.py +721 -0
  13. superqode/agent_stream.py +953 -0
  14. superqode/agents/__init__.py +59 -0
  15. superqode/agents/acp_registry.py +305 -0
  16. superqode/agents/client.py +249 -0
  17. superqode/agents/data/augmentcode.com.toml +51 -0
  18. superqode/agents/data/cagent.dev.toml +51 -0
  19. superqode/agents/data/claude.com.toml +60 -0
  20. superqode/agents/data/codeassistant.dev.toml +51 -0
  21. superqode/agents/data/codex.openai.com.toml +57 -0
  22. superqode/agents/data/fastagent.ai.toml +66 -0
  23. superqode/agents/data/geminicli.com.toml +77 -0
  24. superqode/agents/data/goose.block.xyz.toml +54 -0
  25. superqode/agents/data/junie.jetbrains.com.toml +56 -0
  26. superqode/agents/data/kimi.moonshot.cn.toml +57 -0
  27. superqode/agents/data/llmlingagent.dev.toml +51 -0
  28. superqode/agents/data/molt.bot.toml +49 -0
  29. superqode/agents/data/opencode.ai.toml +60 -0
  30. superqode/agents/data/stakpak.dev.toml +51 -0
  31. superqode/agents/data/vtcode.dev.toml +51 -0
  32. superqode/agents/discovery.py +266 -0
  33. superqode/agents/messaging.py +160 -0
  34. superqode/agents/persona.py +166 -0
  35. superqode/agents/registry.py +421 -0
  36. superqode/agents/schema.py +72 -0
  37. superqode/agents/unified.py +367 -0
  38. superqode/app/__init__.py +111 -0
  39. superqode/app/constants.py +314 -0
  40. superqode/app/css.py +366 -0
  41. superqode/app/models.py +118 -0
  42. superqode/app/suggester.py +125 -0
  43. superqode/app/widgets.py +1591 -0
  44. superqode/app_enhanced.py +399 -0
  45. superqode/app_main.py +17187 -0
  46. superqode/approval.py +312 -0
  47. superqode/atomic.py +296 -0
  48. superqode/commands/__init__.py +1 -0
  49. superqode/commands/acp.py +965 -0
  50. superqode/commands/agents.py +180 -0
  51. superqode/commands/auth.py +278 -0
  52. superqode/commands/config.py +374 -0
  53. superqode/commands/init.py +826 -0
  54. superqode/commands/providers.py +819 -0
  55. superqode/commands/qe.py +1145 -0
  56. superqode/commands/roles.py +380 -0
  57. superqode/commands/serve.py +172 -0
  58. superqode/commands/suggestions.py +127 -0
  59. superqode/commands/superqe.py +460 -0
  60. superqode/config/__init__.py +51 -0
  61. superqode/config/loader.py +812 -0
  62. superqode/config/schema.py +498 -0
  63. superqode/core/__init__.py +111 -0
  64. superqode/core/roles.py +281 -0
  65. superqode/danger.py +386 -0
  66. superqode/data/superqode-template.yaml +1522 -0
  67. superqode/design_system.py +1080 -0
  68. superqode/dialogs/__init__.py +6 -0
  69. superqode/dialogs/base.py +39 -0
  70. superqode/dialogs/model.py +130 -0
  71. superqode/dialogs/provider.py +870 -0
  72. superqode/diff_view.py +919 -0
  73. superqode/enterprise.py +21 -0
  74. superqode/evaluation/__init__.py +25 -0
  75. superqode/evaluation/adapters.py +93 -0
  76. superqode/evaluation/behaviors.py +89 -0
  77. superqode/evaluation/engine.py +209 -0
  78. superqode/evaluation/scenarios.py +96 -0
  79. superqode/execution/__init__.py +36 -0
  80. superqode/execution/linter.py +538 -0
  81. superqode/execution/modes.py +347 -0
  82. superqode/execution/resolver.py +283 -0
  83. superqode/execution/runner.py +642 -0
  84. superqode/file_explorer.py +811 -0
  85. superqode/file_viewer.py +471 -0
  86. superqode/flash.py +183 -0
  87. superqode/guidance/__init__.py +58 -0
  88. superqode/guidance/config.py +203 -0
  89. superqode/guidance/prompts.py +71 -0
  90. superqode/harness/__init__.py +54 -0
  91. superqode/harness/accelerator.py +291 -0
  92. superqode/harness/config.py +319 -0
  93. superqode/harness/validator.py +147 -0
  94. superqode/history.py +279 -0
  95. superqode/integrations/superopt_runner.py +124 -0
  96. superqode/logging/__init__.py +49 -0
  97. superqode/logging/adapters.py +219 -0
  98. superqode/logging/formatter.py +923 -0
  99. superqode/logging/integration.py +341 -0
  100. superqode/logging/sinks.py +170 -0
  101. superqode/logging/unified_log.py +417 -0
  102. superqode/lsp/__init__.py +26 -0
  103. superqode/lsp/client.py +544 -0
  104. superqode/main.py +1069 -0
  105. superqode/mcp/__init__.py +89 -0
  106. superqode/mcp/auth_storage.py +380 -0
  107. superqode/mcp/client.py +1236 -0
  108. superqode/mcp/config.py +319 -0
  109. superqode/mcp/integration.py +337 -0
  110. superqode/mcp/oauth.py +436 -0
  111. superqode/mcp/oauth_callback.py +385 -0
  112. superqode/mcp/types.py +290 -0
  113. superqode/memory/__init__.py +31 -0
  114. superqode/memory/feedback.py +342 -0
  115. superqode/memory/store.py +522 -0
  116. superqode/notifications.py +369 -0
  117. superqode/optimization/__init__.py +5 -0
  118. superqode/optimization/config.py +33 -0
  119. superqode/permissions/__init__.py +25 -0
  120. superqode/permissions/rules.py +488 -0
  121. superqode/plan.py +323 -0
  122. superqode/providers/__init__.py +33 -0
  123. superqode/providers/gateway/__init__.py +165 -0
  124. superqode/providers/gateway/base.py +228 -0
  125. superqode/providers/gateway/litellm_gateway.py +1170 -0
  126. superqode/providers/gateway/openresponses_gateway.py +436 -0
  127. superqode/providers/health.py +297 -0
  128. superqode/providers/huggingface/__init__.py +74 -0
  129. superqode/providers/huggingface/downloader.py +472 -0
  130. superqode/providers/huggingface/endpoints.py +442 -0
  131. superqode/providers/huggingface/hub.py +531 -0
  132. superqode/providers/huggingface/inference.py +394 -0
  133. superqode/providers/huggingface/transformers_runner.py +516 -0
  134. superqode/providers/local/__init__.py +100 -0
  135. superqode/providers/local/base.py +438 -0
  136. superqode/providers/local/discovery.py +418 -0
  137. superqode/providers/local/lmstudio.py +256 -0
  138. superqode/providers/local/mlx.py +457 -0
  139. superqode/providers/local/ollama.py +486 -0
  140. superqode/providers/local/sglang.py +268 -0
  141. superqode/providers/local/tgi.py +260 -0
  142. superqode/providers/local/tool_support.py +477 -0
  143. superqode/providers/local/vllm.py +258 -0
  144. superqode/providers/manager.py +1338 -0
  145. superqode/providers/models.py +1016 -0
  146. superqode/providers/models_dev.py +578 -0
  147. superqode/providers/openresponses/__init__.py +87 -0
  148. superqode/providers/openresponses/converters/__init__.py +17 -0
  149. superqode/providers/openresponses/converters/messages.py +343 -0
  150. superqode/providers/openresponses/converters/tools.py +268 -0
  151. superqode/providers/openresponses/schema/__init__.py +56 -0
  152. superqode/providers/openresponses/schema/models.py +585 -0
  153. superqode/providers/openresponses/streaming/__init__.py +5 -0
  154. superqode/providers/openresponses/streaming/parser.py +338 -0
  155. superqode/providers/openresponses/tools/__init__.py +21 -0
  156. superqode/providers/openresponses/tools/apply_patch.py +352 -0
  157. superqode/providers/openresponses/tools/code_interpreter.py +290 -0
  158. superqode/providers/openresponses/tools/file_search.py +333 -0
  159. superqode/providers/openresponses/tools/mcp_adapter.py +252 -0
  160. superqode/providers/registry.py +716 -0
  161. superqode/providers/usage.py +332 -0
  162. superqode/pure_mode.py +384 -0
  163. superqode/qr/__init__.py +23 -0
  164. superqode/qr/dashboard.py +781 -0
  165. superqode/qr/generator.py +1018 -0
  166. superqode/qr/templates.py +135 -0
  167. superqode/safety/__init__.py +41 -0
  168. superqode/safety/sandbox.py +413 -0
  169. superqode/safety/warnings.py +256 -0
  170. superqode/server/__init__.py +33 -0
  171. superqode/server/lsp_server.py +775 -0
  172. superqode/server/web.py +250 -0
  173. superqode/session/__init__.py +25 -0
  174. superqode/session/persistence.py +580 -0
  175. superqode/session/sharing.py +477 -0
  176. superqode/session.py +475 -0
  177. superqode/sidebar.py +2991 -0
  178. superqode/stream_view.py +648 -0
  179. superqode/styles/__init__.py +3 -0
  180. superqode/superqe/__init__.py +184 -0
  181. superqode/superqe/acp_runner.py +1064 -0
  182. superqode/superqe/constitution/__init__.py +62 -0
  183. superqode/superqe/constitution/evaluator.py +308 -0
  184. superqode/superqe/constitution/loader.py +432 -0
  185. superqode/superqe/constitution/schema.py +250 -0
  186. superqode/superqe/events.py +591 -0
  187. superqode/superqe/frameworks/__init__.py +65 -0
  188. superqode/superqe/frameworks/base.py +234 -0
  189. superqode/superqe/frameworks/e2e.py +263 -0
  190. superqode/superqe/frameworks/executor.py +237 -0
  191. superqode/superqe/frameworks/javascript.py +409 -0
  192. superqode/superqe/frameworks/python.py +373 -0
  193. superqode/superqe/frameworks/registry.py +92 -0
  194. superqode/superqe/mcp_tools/__init__.py +47 -0
  195. superqode/superqe/mcp_tools/core_tools.py +418 -0
  196. superqode/superqe/mcp_tools/registry.py +230 -0
  197. superqode/superqe/mcp_tools/testing_tools.py +167 -0
  198. superqode/superqe/noise.py +89 -0
  199. superqode/superqe/orchestrator.py +778 -0
  200. superqode/superqe/roles.py +609 -0
  201. superqode/superqe/session.py +713 -0
  202. superqode/superqe/skills/__init__.py +57 -0
  203. superqode/superqe/skills/base.py +106 -0
  204. superqode/superqe/skills/core_skills.py +899 -0
  205. superqode/superqe/skills/registry.py +90 -0
  206. superqode/superqe/verifier.py +101 -0
  207. superqode/superqe_cli.py +76 -0
  208. superqode/tool_call.py +358 -0
  209. superqode/tools/__init__.py +93 -0
  210. superqode/tools/agent_tools.py +496 -0
  211. superqode/tools/base.py +324 -0
  212. superqode/tools/batch_tool.py +133 -0
  213. superqode/tools/diagnostics.py +311 -0
  214. superqode/tools/edit_tools.py +653 -0
  215. superqode/tools/enhanced_base.py +515 -0
  216. superqode/tools/file_tools.py +269 -0
  217. superqode/tools/file_tracking.py +45 -0
  218. superqode/tools/lsp_tools.py +610 -0
  219. superqode/tools/network_tools.py +350 -0
  220. superqode/tools/permissions.py +400 -0
  221. superqode/tools/question_tool.py +324 -0
  222. superqode/tools/search_tools.py +598 -0
  223. superqode/tools/shell_tools.py +259 -0
  224. superqode/tools/todo_tools.py +121 -0
  225. superqode/tools/validation.py +80 -0
  226. superqode/tools/web_tools.py +639 -0
  227. superqode/tui.py +1152 -0
  228. superqode/tui_integration.py +875 -0
  229. superqode/tui_widgets/__init__.py +27 -0
  230. superqode/tui_widgets/widgets/__init__.py +18 -0
  231. superqode/tui_widgets/widgets/progress.py +185 -0
  232. superqode/tui_widgets/widgets/tool_display.py +188 -0
  233. superqode/undo_manager.py +574 -0
  234. superqode/utils/__init__.py +5 -0
  235. superqode/utils/error_handling.py +323 -0
  236. superqode/utils/fuzzy.py +257 -0
  237. superqode/widgets/__init__.py +477 -0
  238. superqode/widgets/agent_collab.py +390 -0
  239. superqode/widgets/agent_store.py +936 -0
  240. superqode/widgets/agent_switcher.py +395 -0
  241. superqode/widgets/animation_manager.py +284 -0
  242. superqode/widgets/code_context.py +356 -0
  243. superqode/widgets/command_palette.py +412 -0
  244. superqode/widgets/connection_status.py +537 -0
  245. superqode/widgets/conversation_history.py +470 -0
  246. superqode/widgets/diff_indicator.py +155 -0
  247. superqode/widgets/enhanced_status_bar.py +385 -0
  248. superqode/widgets/enhanced_toast.py +476 -0
  249. superqode/widgets/file_browser.py +809 -0
  250. superqode/widgets/file_reference.py +585 -0
  251. superqode/widgets/issue_timeline.py +340 -0
  252. superqode/widgets/leader_key.py +264 -0
  253. superqode/widgets/mode_switcher.py +445 -0
  254. superqode/widgets/model_picker.py +234 -0
  255. superqode/widgets/permission_preview.py +1205 -0
  256. superqode/widgets/prompt.py +358 -0
  257. superqode/widgets/provider_connect.py +725 -0
  258. superqode/widgets/pty_shell.py +587 -0
  259. superqode/widgets/qe_dashboard.py +321 -0
  260. superqode/widgets/resizable_sidebar.py +377 -0
  261. superqode/widgets/response_changes.py +218 -0
  262. superqode/widgets/response_display.py +528 -0
  263. superqode/widgets/rich_tool_display.py +613 -0
  264. superqode/widgets/sidebar_panels.py +1180 -0
  265. superqode/widgets/slash_complete.py +356 -0
  266. superqode/widgets/split_view.py +612 -0
  267. superqode/widgets/status_bar.py +273 -0
  268. superqode/widgets/superqode_display.py +786 -0
  269. superqode/widgets/thinking_display.py +815 -0
  270. superqode/widgets/throbber.py +87 -0
  271. superqode/widgets/toast.py +206 -0
  272. superqode/widgets/unified_output.py +1073 -0
  273. superqode/workspace/__init__.py +75 -0
  274. superqode/workspace/artifacts.py +472 -0
  275. superqode/workspace/coordinator.py +353 -0
  276. superqode/workspace/diff_tracker.py +429 -0
  277. superqode/workspace/git_guard.py +373 -0
  278. superqode/workspace/git_snapshot.py +526 -0
  279. superqode/workspace/manager.py +750 -0
  280. superqode/workspace/snapshot.py +357 -0
  281. superqode/workspace/watcher.py +535 -0
  282. superqode/workspace/worktree.py +440 -0
  283. superqode-0.1.5.dist-info/METADATA +204 -0
  284. superqode-0.1.5.dist-info/RECORD +288 -0
  285. superqode-0.1.5.dist-info/WHEEL +5 -0
  286. superqode-0.1.5.dist-info/entry_points.txt +3 -0
  287. superqode-0.1.5.dist-info/licenses/LICENSE +648 -0
  288. superqode-0.1.5.dist-info/top_level.txt +1 -0
@@ -0,0 +1,587 @@
1
+ """
2
+ PTY Shell Widget - Interactive Terminal in TUI.
3
+
4
+ Provides a full pseudo-terminal (PTY) for running interactive
5
+ shell commands within the SuperQode TUI.
6
+
7
+ Features:
8
+ - True terminal emulation (ncurses, vim, etc.)
9
+ - Resize support
10
+ - Input/output streaming
11
+ - Multiple shell sessions
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import asyncio
17
+ import os
18
+ import pty
19
+ import select
20
+ import signal
21
+ import struct
22
+ import sys
23
+ import termios
24
+ import fcntl
25
+ from dataclasses import dataclass
26
+ from datetime import datetime
27
+ from pathlib import Path
28
+ from typing import Callable, Dict, List, Optional, Tuple
29
+ import threading
30
+
31
+ from rich.console import RenderableType
32
+ from rich.panel import Panel
33
+ from rich.text import Text
34
+ from textual.reactive import reactive
35
+ from textual.widgets import Static
36
+ from textual.timer import Timer
37
+ from textual import events
38
+
39
+
40
+ @dataclass
41
+ class ShellSession:
42
+ """A shell session with PTY."""
43
+
44
+ id: str
45
+ pid: int
46
+ fd: int
47
+ cwd: Path
48
+ created_at: datetime
49
+ title: str = "Shell"
50
+
51
+ @property
52
+ def is_running(self) -> bool:
53
+ """Check if the shell process is still running."""
54
+ try:
55
+ os.kill(self.pid, 0)
56
+ return True
57
+ except OSError:
58
+ return False
59
+
60
+
61
+ class PTYShell:
62
+ """
63
+ Pseudo-terminal shell manager.
64
+
65
+ Manages shell sessions with proper PTY handling for
66
+ interactive terminal applications.
67
+
68
+ Usage:
69
+ shell = PTYShell()
70
+ shell.start()
71
+
72
+ # Send input
73
+ shell.write("ls -la\\n")
74
+
75
+ # Read output
76
+ output = shell.read()
77
+
78
+ # Resize
79
+ shell.resize(80, 24)
80
+
81
+ shell.stop()
82
+ """
83
+
84
+ DEFAULT_SHELL = os.environ.get("SHELL", "/bin/bash")
85
+
86
+ def __init__(
87
+ self,
88
+ working_directory: Optional[Path] = None,
89
+ shell: Optional[str] = None,
90
+ env: Optional[Dict[str, str]] = None,
91
+ ):
92
+ self.working_directory = working_directory or Path.cwd()
93
+ self.shell = shell or self.DEFAULT_SHELL
94
+ self.env = env or dict(os.environ)
95
+
96
+ # PTY state
97
+ self._master_fd: Optional[int] = None
98
+ self._slave_fd: Optional[int] = None
99
+ self._pid: Optional[int] = None
100
+ self._running = False
101
+
102
+ # Output buffer
103
+ self._output_buffer: List[str] = []
104
+ self._output_lock = threading.Lock()
105
+
106
+ # Callbacks
107
+ self._on_output: Optional[Callable[[str], None]] = None
108
+ self._on_exit: Optional[Callable[[int], None]] = None
109
+
110
+ # Reader thread
111
+ self._reader_thread: Optional[threading.Thread] = None
112
+
113
+ # Terminal size
114
+ self._rows = 24
115
+ self._cols = 80
116
+
117
+ @property
118
+ def is_running(self) -> bool:
119
+ """Check if shell is running."""
120
+ return self._running and self._pid is not None
121
+
122
+ @property
123
+ def pid(self) -> Optional[int]:
124
+ """Get the shell process ID."""
125
+ return self._pid
126
+
127
+ def start(self) -> bool:
128
+ """Start the shell session."""
129
+ if self._running:
130
+ return True
131
+
132
+ try:
133
+ # Create pseudo-terminal
134
+ self._master_fd, self._slave_fd = pty.openpty()
135
+
136
+ # Set terminal size
137
+ self._set_window_size(self._rows, self._cols)
138
+
139
+ # Fork process
140
+ self._pid = os.fork()
141
+
142
+ if self._pid == 0:
143
+ # Child process
144
+ self._child_process()
145
+ else:
146
+ # Parent process
147
+ os.close(self._slave_fd)
148
+ self._slave_fd = None
149
+
150
+ # Make master non-blocking
151
+ flags = fcntl.fcntl(self._master_fd, fcntl.F_GETFL)
152
+ fcntl.fcntl(self._master_fd, fcntl.F_SETFL, flags | os.O_NONBLOCK)
153
+
154
+ self._running = True
155
+
156
+ # Start reader thread
157
+ self._reader_thread = threading.Thread(
158
+ target=self._read_loop,
159
+ daemon=True,
160
+ )
161
+ self._reader_thread.start()
162
+
163
+ return True
164
+
165
+ except Exception as e:
166
+ self._cleanup()
167
+ raise RuntimeError(f"Failed to start shell: {e}")
168
+
169
+ def _child_process(self) -> None:
170
+ """Set up and exec shell in child process."""
171
+ # Create new session
172
+ os.setsid()
173
+
174
+ # Set controlling terminal
175
+ os.dup2(self._slave_fd, 0)
176
+ os.dup2(self._slave_fd, 1)
177
+ os.dup2(self._slave_fd, 2)
178
+
179
+ if self._slave_fd > 2:
180
+ os.close(self._slave_fd)
181
+
182
+ # Change to working directory
183
+ try:
184
+ os.chdir(self.working_directory)
185
+ except OSError:
186
+ pass
187
+
188
+ # Set environment
189
+ self.env["TERM"] = "xterm-256color"
190
+ self.env["COLORTERM"] = "truecolor"
191
+
192
+ # Exec shell
193
+ try:
194
+ os.execvpe(self.shell, [self.shell], self.env)
195
+ except Exception:
196
+ os._exit(1)
197
+
198
+ def _set_window_size(self, rows: int, cols: int) -> None:
199
+ """Set terminal window size."""
200
+ if self._master_fd is not None:
201
+ winsize = struct.pack("HHHH", rows, cols, 0, 0)
202
+ fcntl.ioctl(self._master_fd, termios.TIOCSWINSZ, winsize)
203
+
204
+ def _read_loop(self) -> None:
205
+ """Background thread to read PTY output."""
206
+ while self._running:
207
+ try:
208
+ # Wait for data with timeout
209
+ r, _, _ = select.select([self._master_fd], [], [], 0.1)
210
+
211
+ if self._master_fd in r:
212
+ try:
213
+ data = os.read(self._master_fd, 4096)
214
+ if data:
215
+ text = data.decode("utf-8", errors="replace")
216
+
217
+ with self._output_lock:
218
+ self._output_buffer.append(text)
219
+
220
+ if self._on_output:
221
+ self._on_output(text)
222
+ else:
223
+ # EOF - shell exited
224
+ self._handle_exit()
225
+ break
226
+ except OSError:
227
+ self._handle_exit()
228
+ break
229
+
230
+ except (ValueError, OSError):
231
+ # FD closed
232
+ break
233
+
234
+ self._running = False
235
+
236
+ def _handle_exit(self) -> None:
237
+ """Handle shell exit."""
238
+ self._running = False
239
+
240
+ exit_code = 0
241
+ if self._pid:
242
+ try:
243
+ _, status = os.waitpid(self._pid, os.WNOHANG)
244
+ if os.WIFEXITED(status):
245
+ exit_code = os.WEXITSTATUS(status)
246
+ except ChildProcessError:
247
+ pass
248
+
249
+ if self._on_exit:
250
+ self._on_exit(exit_code)
251
+
252
+ def write(self, data: str) -> int:
253
+ """Write data to the shell."""
254
+ if not self._running or self._master_fd is None:
255
+ return 0
256
+
257
+ try:
258
+ return os.write(self._master_fd, data.encode("utf-8"))
259
+ except OSError:
260
+ return 0
261
+
262
+ def read(self) -> str:
263
+ """Read buffered output from the shell."""
264
+ with self._output_lock:
265
+ output = "".join(self._output_buffer)
266
+ self._output_buffer.clear()
267
+ return output
268
+
269
+ def resize(self, rows: int, cols: int) -> None:
270
+ """Resize the terminal."""
271
+ self._rows = rows
272
+ self._cols = cols
273
+
274
+ if self._running:
275
+ self._set_window_size(rows, cols)
276
+
277
+ def send_signal(self, sig: int) -> None:
278
+ """Send a signal to the shell process."""
279
+ if self._pid:
280
+ try:
281
+ os.kill(self._pid, sig)
282
+ except OSError:
283
+ pass
284
+
285
+ def interrupt(self) -> None:
286
+ """Send interrupt signal (Ctrl+C)."""
287
+ self.write("\x03")
288
+
289
+ def stop(self) -> None:
290
+ """Stop the shell session."""
291
+ if not self._running:
292
+ return
293
+
294
+ self._running = False
295
+
296
+ # Kill the process
297
+ if self._pid:
298
+ try:
299
+ os.kill(self._pid, signal.SIGTERM)
300
+ os.waitpid(self._pid, 0)
301
+ except (OSError, ChildProcessError):
302
+ pass
303
+
304
+ self._cleanup()
305
+
306
+ def _cleanup(self) -> None:
307
+ """Clean up resources."""
308
+ if self._master_fd is not None:
309
+ try:
310
+ os.close(self._master_fd)
311
+ except OSError:
312
+ pass
313
+ self._master_fd = None
314
+
315
+ if self._slave_fd is not None:
316
+ try:
317
+ os.close(self._slave_fd)
318
+ except OSError:
319
+ pass
320
+ self._slave_fd = None
321
+
322
+ self._pid = None
323
+
324
+ def on_output(self, callback: Callable[[str], None]) -> None:
325
+ """Set callback for output events."""
326
+ self._on_output = callback
327
+
328
+ def on_exit(self, callback: Callable[[int], None]) -> None:
329
+ """Set callback for exit events."""
330
+ self._on_exit = callback
331
+
332
+ def __enter__(self) -> "PTYShell":
333
+ self.start()
334
+ return self
335
+
336
+ def __exit__(self, *args) -> None:
337
+ self.stop()
338
+
339
+
340
+ class PTYShellWidget(Static):
341
+ """
342
+ Textual widget for PTY shell.
343
+
344
+ Displays an interactive terminal within the TUI.
345
+
346
+ Usage:
347
+ shell_widget = PTYShellWidget(working_directory=Path.cwd())
348
+ # Add to your app's compose()
349
+ """
350
+
351
+ DEFAULT_CSS = """
352
+ PTYShellWidget {
353
+ height: 100%;
354
+ border: solid #3f3f46;
355
+ background: #0f0f0f;
356
+ padding: 0 1;
357
+ }
358
+
359
+ PTYShellWidget:focus {
360
+ border: solid #3b82f6;
361
+ }
362
+ """
363
+
364
+ # Reactive state
365
+ is_active: reactive[bool] = reactive(False)
366
+
367
+ def __init__(
368
+ self,
369
+ working_directory: Optional[Path] = None,
370
+ shell: Optional[str] = None,
371
+ title: str = "Terminal",
372
+ **kwargs,
373
+ ):
374
+ super().__init__(**kwargs)
375
+ self.title = title
376
+ self._shell = PTYShell(
377
+ working_directory=working_directory,
378
+ shell=shell,
379
+ )
380
+ self._output_lines: List[str] = []
381
+ self._max_lines = 1000
382
+ self._scroll_offset = 0
383
+ self._timer: Optional[Timer] = None
384
+
385
+ def on_mount(self) -> None:
386
+ """Start shell when mounted."""
387
+ # Set up callbacks
388
+ self._shell.on_output(self._handle_output)
389
+ self._shell.on_exit(self._handle_exit)
390
+
391
+ # Start shell
392
+ try:
393
+ self._shell.start()
394
+ self.is_active = True
395
+ except RuntimeError as e:
396
+ self._output_lines.append(f"[ERROR] {e}")
397
+
398
+ # Start refresh timer
399
+ self._timer = self.set_interval(0.1, self._refresh_output)
400
+
401
+ def on_unmount(self) -> None:
402
+ """Stop shell when unmounted."""
403
+ if self._timer:
404
+ self._timer.stop()
405
+ self._shell.stop()
406
+
407
+ def on_resize(self, event: events.Resize) -> None:
408
+ """Handle resize events."""
409
+ # Account for borders and padding
410
+ rows = max(1, event.size.height - 2)
411
+ cols = max(1, event.size.width - 4)
412
+ self._shell.resize(rows, cols)
413
+
414
+ def on_key(self, event: events.Key) -> None:
415
+ """Handle key events."""
416
+ if not self.is_active:
417
+ return
418
+
419
+ # Convert key to terminal sequence
420
+ key_map = {
421
+ "enter": "\r",
422
+ "tab": "\t",
423
+ "backspace": "\x7f",
424
+ "delete": "\x1b[3~",
425
+ "escape": "\x1b",
426
+ "up": "\x1b[A",
427
+ "down": "\x1b[B",
428
+ "right": "\x1b[C",
429
+ "left": "\x1b[D",
430
+ "home": "\x1b[H",
431
+ "end": "\x1b[F",
432
+ "pageup": "\x1b[5~",
433
+ "pagedown": "\x1b[6~",
434
+ "f1": "\x1bOP",
435
+ "f2": "\x1bOQ",
436
+ "f3": "\x1bOR",
437
+ "f4": "\x1bOS",
438
+ }
439
+
440
+ # Ctrl key combinations
441
+ if event.key.startswith("ctrl+"):
442
+ char = event.key[5:]
443
+ if len(char) == 1:
444
+ code = ord(char.upper()) - 64
445
+ if 1 <= code <= 26:
446
+ self._shell.write(chr(code))
447
+ event.prevent_default()
448
+ return
449
+
450
+ # Special keys
451
+ if event.key in key_map:
452
+ self._shell.write(key_map[event.key])
453
+ event.prevent_default()
454
+ return
455
+
456
+ # Regular characters
457
+ if event.character and len(event.character) == 1:
458
+ self._shell.write(event.character)
459
+ event.prevent_default()
460
+
461
+ def _handle_output(self, text: str) -> None:
462
+ """Handle output from shell."""
463
+ # Split into lines and add to buffer
464
+ lines = text.split("\n")
465
+
466
+ for i, line in enumerate(lines):
467
+ if i == 0 and self._output_lines:
468
+ # Append to last line
469
+ self._output_lines[-1] += line
470
+ else:
471
+ self._output_lines.append(line)
472
+
473
+ # Limit buffer size
474
+ if len(self._output_lines) > self._max_lines:
475
+ self._output_lines = self._output_lines[-self._max_lines :]
476
+
477
+ def _handle_exit(self, exit_code: int) -> None:
478
+ """Handle shell exit."""
479
+ self.is_active = False
480
+ self._output_lines.append(f"\n[Process exited with code {exit_code}]")
481
+ self.refresh()
482
+
483
+ def _refresh_output(self) -> None:
484
+ """Refresh the display."""
485
+ if self.is_active:
486
+ self.refresh()
487
+
488
+ def send_command(self, command: str) -> None:
489
+ """Send a command to the shell."""
490
+ if self.is_active:
491
+ self._shell.write(command + "\n")
492
+
493
+ def clear(self) -> None:
494
+ """Clear the output buffer."""
495
+ self._output_lines.clear()
496
+ self.refresh()
497
+
498
+ def render(self) -> RenderableType:
499
+ """Render the terminal output."""
500
+ content = Text()
501
+
502
+ # Get visible lines (based on widget height)
503
+ visible_lines = self._output_lines[-50:] # Show last 50 lines
504
+
505
+ for line in visible_lines:
506
+ # Basic ANSI code stripping for now
507
+ # TODO: Full ANSI parsing for colors
508
+ clean_line = line
509
+ content.append(clean_line + "\n", style="#e2e8f0")
510
+
511
+ if not self.is_active:
512
+ content.append("\n[Shell not running]", style="#ef4444")
513
+
514
+ border_style = "#3b82f6" if self.has_focus else "#3f3f46"
515
+
516
+ return Panel(
517
+ content,
518
+ title=f"[bold #3b82f6]{self.title}[/]",
519
+ border_style=border_style,
520
+ padding=(0, 0),
521
+ )
522
+
523
+
524
+ class ShellManager:
525
+ """
526
+ Manages multiple shell sessions.
527
+
528
+ Usage:
529
+ manager = ShellManager()
530
+
531
+ # Create a new shell
532
+ shell_id = manager.create_shell(Path.cwd())
533
+
534
+ # Get shell
535
+ shell = manager.get_shell(shell_id)
536
+
537
+ # List shells
538
+ shells = manager.list_shells()
539
+
540
+ # Close shell
541
+ manager.close_shell(shell_id)
542
+ """
543
+
544
+ def __init__(self):
545
+ self._shells: Dict[str, PTYShell] = {}
546
+ self._counter = 0
547
+
548
+ def create_shell(
549
+ self,
550
+ working_directory: Optional[Path] = None,
551
+ shell: Optional[str] = None,
552
+ title: str = "Shell",
553
+ ) -> str:
554
+ """Create a new shell session."""
555
+ self._counter += 1
556
+ shell_id = f"shell-{self._counter}"
557
+
558
+ pty_shell = PTYShell(
559
+ working_directory=working_directory,
560
+ shell=shell,
561
+ )
562
+ pty_shell.start()
563
+
564
+ self._shells[shell_id] = pty_shell
565
+ return shell_id
566
+
567
+ def get_shell(self, shell_id: str) -> Optional[PTYShell]:
568
+ """Get a shell by ID."""
569
+ return self._shells.get(shell_id)
570
+
571
+ def close_shell(self, shell_id: str) -> bool:
572
+ """Close a shell session."""
573
+ shell = self._shells.pop(shell_id, None)
574
+ if shell:
575
+ shell.stop()
576
+ return True
577
+ return False
578
+
579
+ def list_shells(self) -> List[str]:
580
+ """List all shell IDs."""
581
+ return list(self._shells.keys())
582
+
583
+ def close_all(self) -> None:
584
+ """Close all shell sessions."""
585
+ for shell in self._shells.values():
586
+ shell.stop()
587
+ self._shells.clear()