kollabor 0.4.9__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.
- core/__init__.py +18 -0
- core/application.py +578 -0
- core/cli.py +193 -0
- core/commands/__init__.py +43 -0
- core/commands/executor.py +277 -0
- core/commands/menu_renderer.py +319 -0
- core/commands/parser.py +186 -0
- core/commands/registry.py +331 -0
- core/commands/system_commands.py +479 -0
- core/config/__init__.py +7 -0
- core/config/llm_task_config.py +110 -0
- core/config/loader.py +501 -0
- core/config/manager.py +112 -0
- core/config/plugin_config_manager.py +346 -0
- core/config/plugin_schema.py +424 -0
- core/config/service.py +399 -0
- core/effects/__init__.py +1 -0
- core/events/__init__.py +12 -0
- core/events/bus.py +129 -0
- core/events/executor.py +154 -0
- core/events/models.py +258 -0
- core/events/processor.py +176 -0
- core/events/registry.py +289 -0
- core/fullscreen/__init__.py +19 -0
- core/fullscreen/command_integration.py +290 -0
- core/fullscreen/components/__init__.py +12 -0
- core/fullscreen/components/animation.py +258 -0
- core/fullscreen/components/drawing.py +160 -0
- core/fullscreen/components/matrix_components.py +177 -0
- core/fullscreen/manager.py +302 -0
- core/fullscreen/plugin.py +204 -0
- core/fullscreen/renderer.py +282 -0
- core/fullscreen/session.py +324 -0
- core/io/__init__.py +52 -0
- core/io/buffer_manager.py +362 -0
- core/io/config_status_view.py +272 -0
- core/io/core_status_views.py +410 -0
- core/io/input_errors.py +313 -0
- core/io/input_handler.py +2655 -0
- core/io/input_mode_manager.py +402 -0
- core/io/key_parser.py +344 -0
- core/io/layout.py +587 -0
- core/io/message_coordinator.py +204 -0
- core/io/message_renderer.py +601 -0
- core/io/modal_interaction_handler.py +315 -0
- core/io/raw_input_processor.py +946 -0
- core/io/status_renderer.py +845 -0
- core/io/terminal_renderer.py +586 -0
- core/io/terminal_state.py +551 -0
- core/io/visual_effects.py +734 -0
- core/llm/__init__.py +26 -0
- core/llm/api_communication_service.py +863 -0
- core/llm/conversation_logger.py +473 -0
- core/llm/conversation_manager.py +414 -0
- core/llm/file_operations_executor.py +1401 -0
- core/llm/hook_system.py +402 -0
- core/llm/llm_service.py +1629 -0
- core/llm/mcp_integration.py +386 -0
- core/llm/message_display_service.py +450 -0
- core/llm/model_router.py +214 -0
- core/llm/plugin_sdk.py +396 -0
- core/llm/response_parser.py +848 -0
- core/llm/response_processor.py +364 -0
- core/llm/tool_executor.py +520 -0
- core/logging/__init__.py +19 -0
- core/logging/setup.py +208 -0
- core/models/__init__.py +5 -0
- core/models/base.py +23 -0
- core/plugins/__init__.py +13 -0
- core/plugins/collector.py +212 -0
- core/plugins/discovery.py +386 -0
- core/plugins/factory.py +263 -0
- core/plugins/registry.py +152 -0
- core/storage/__init__.py +5 -0
- core/storage/state_manager.py +84 -0
- core/ui/__init__.py +6 -0
- core/ui/config_merger.py +176 -0
- core/ui/config_widgets.py +369 -0
- core/ui/live_modal_renderer.py +276 -0
- core/ui/modal_actions.py +162 -0
- core/ui/modal_overlay_renderer.py +373 -0
- core/ui/modal_renderer.py +591 -0
- core/ui/modal_state_manager.py +443 -0
- core/ui/widget_integration.py +222 -0
- core/ui/widgets/__init__.py +27 -0
- core/ui/widgets/base_widget.py +136 -0
- core/ui/widgets/checkbox.py +85 -0
- core/ui/widgets/dropdown.py +140 -0
- core/ui/widgets/label.py +78 -0
- core/ui/widgets/slider.py +185 -0
- core/ui/widgets/text_input.py +224 -0
- core/utils/__init__.py +11 -0
- core/utils/config_utils.py +656 -0
- core/utils/dict_utils.py +212 -0
- core/utils/error_utils.py +275 -0
- core/utils/key_reader.py +171 -0
- core/utils/plugin_utils.py +267 -0
- core/utils/prompt_renderer.py +151 -0
- kollabor-0.4.9.dist-info/METADATA +298 -0
- kollabor-0.4.9.dist-info/RECORD +128 -0
- kollabor-0.4.9.dist-info/WHEEL +5 -0
- kollabor-0.4.9.dist-info/entry_points.txt +2 -0
- kollabor-0.4.9.dist-info/licenses/LICENSE +21 -0
- kollabor-0.4.9.dist-info/top_level.txt +4 -0
- kollabor_cli_main.py +20 -0
- plugins/__init__.py +1 -0
- plugins/enhanced_input/__init__.py +18 -0
- plugins/enhanced_input/box_renderer.py +103 -0
- plugins/enhanced_input/box_styles.py +142 -0
- plugins/enhanced_input/color_engine.py +165 -0
- plugins/enhanced_input/config.py +150 -0
- plugins/enhanced_input/cursor_manager.py +72 -0
- plugins/enhanced_input/geometry.py +81 -0
- plugins/enhanced_input/state.py +130 -0
- plugins/enhanced_input/text_processor.py +115 -0
- plugins/enhanced_input_plugin.py +385 -0
- plugins/fullscreen/__init__.py +9 -0
- plugins/fullscreen/example_plugin.py +327 -0
- plugins/fullscreen/matrix_plugin.py +132 -0
- plugins/hook_monitoring_plugin.py +1299 -0
- plugins/query_enhancer_plugin.py +350 -0
- plugins/save_conversation_plugin.py +502 -0
- plugins/system_commands_plugin.py +93 -0
- plugins/tmux_plugin.py +795 -0
- plugins/workflow_enforcement_plugin.py +629 -0
- system_prompt/default.md +1286 -0
- system_prompt/default_win.md +265 -0
- system_prompt/example_with_trender.md +47 -0
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
"""Terminal state management for rendering system."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
import shutil
|
|
6
|
+
import sys
|
|
7
|
+
import time
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from enum import Enum
|
|
10
|
+
from typing import Optional, Any, Dict
|
|
11
|
+
|
|
12
|
+
# Platform-specific imports for terminal control
|
|
13
|
+
IS_WINDOWS = sys.platform == "win32"
|
|
14
|
+
|
|
15
|
+
if IS_WINDOWS:
|
|
16
|
+
import msvcrt
|
|
17
|
+
import ctypes
|
|
18
|
+
from ctypes import wintypes
|
|
19
|
+
|
|
20
|
+
# Windows console mode constants
|
|
21
|
+
ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004
|
|
22
|
+
ENABLE_VIRTUAL_TERMINAL_INPUT = 0x0200
|
|
23
|
+
ENABLE_ECHO_INPUT = 0x0004
|
|
24
|
+
ENABLE_LINE_INPUT = 0x0002
|
|
25
|
+
ENABLE_PROCESSED_INPUT = 0x0001
|
|
26
|
+
|
|
27
|
+
# Get handles
|
|
28
|
+
kernel32 = ctypes.windll.kernel32
|
|
29
|
+
STD_OUTPUT_HANDLE = -11
|
|
30
|
+
STD_INPUT_HANDLE = -10
|
|
31
|
+
else:
|
|
32
|
+
import signal
|
|
33
|
+
import termios
|
|
34
|
+
import tty
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
logger = logging.getLogger(__name__)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class TerminalMode(Enum):
|
|
41
|
+
"""Terminal operating modes."""
|
|
42
|
+
|
|
43
|
+
NORMAL = "normal"
|
|
44
|
+
RAW = "raw"
|
|
45
|
+
COOKED = "cooked"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class TerminalCapabilities:
|
|
50
|
+
"""Terminal capability detection results."""
|
|
51
|
+
|
|
52
|
+
has_color: bool = False
|
|
53
|
+
has_256_color: bool = False
|
|
54
|
+
has_truecolor: bool = False
|
|
55
|
+
width: int = 80
|
|
56
|
+
height: int = 24
|
|
57
|
+
cursor_support: bool = True
|
|
58
|
+
mouse_support: bool = False
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def color_level(self) -> str:
|
|
62
|
+
"""Get the color support level description."""
|
|
63
|
+
if self.has_truecolor:
|
|
64
|
+
return "truecolor"
|
|
65
|
+
elif self.has_256_color:
|
|
66
|
+
return "256color"
|
|
67
|
+
elif self.has_color:
|
|
68
|
+
return "basic"
|
|
69
|
+
else:
|
|
70
|
+
return "monochrome"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class TerminalDetector:
|
|
74
|
+
"""Detects terminal capabilities and features."""
|
|
75
|
+
|
|
76
|
+
@staticmethod
|
|
77
|
+
def detect_capabilities() -> TerminalCapabilities:
|
|
78
|
+
"""Detect terminal capabilities.
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
Terminal capabilities information.
|
|
82
|
+
"""
|
|
83
|
+
caps = TerminalCapabilities()
|
|
84
|
+
|
|
85
|
+
# Detect terminal size
|
|
86
|
+
try:
|
|
87
|
+
size = shutil.get_terminal_size()
|
|
88
|
+
caps.width = size.columns
|
|
89
|
+
caps.height = size.lines
|
|
90
|
+
except Exception as e:
|
|
91
|
+
logger.debug(f"Could not get terminal size: {e}")
|
|
92
|
+
caps.width = 80
|
|
93
|
+
caps.height = 24
|
|
94
|
+
|
|
95
|
+
# Detect color support from environment variables
|
|
96
|
+
term = os.environ.get("TERM", "").lower()
|
|
97
|
+
colorterm = os.environ.get("COLORTERM", "").lower()
|
|
98
|
+
|
|
99
|
+
# Basic color detection
|
|
100
|
+
caps.has_color = (
|
|
101
|
+
"color" in term
|
|
102
|
+
or "xterm" in term
|
|
103
|
+
or "screen" in term
|
|
104
|
+
or "tmux" in term
|
|
105
|
+
or sys.stdout.isatty()
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# 256 color detection
|
|
109
|
+
caps.has_256_color = (
|
|
110
|
+
"256" in term
|
|
111
|
+
or "256color" in colorterm
|
|
112
|
+
or term in ["xterm-256color", "screen-256color"]
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
# True color (24-bit) detection
|
|
116
|
+
caps.has_truecolor = (
|
|
117
|
+
colorterm in ["truecolor", "24bit"]
|
|
118
|
+
or "truecolor" in term
|
|
119
|
+
or os.environ.get("TERM_PROGRAM") in ["iTerm.app", "vscode"]
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# Cursor support (assume yes unless proven otherwise)
|
|
123
|
+
caps.cursor_support = sys.stdout.isatty()
|
|
124
|
+
|
|
125
|
+
logger.debug(
|
|
126
|
+
f"Detected terminal capabilities: {caps.color_level} color, "
|
|
127
|
+
f"{caps.width}x{caps.height}"
|
|
128
|
+
)
|
|
129
|
+
return caps
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class TerminalState:
|
|
133
|
+
"""Manages terminal state, mode, and low-level operations."""
|
|
134
|
+
|
|
135
|
+
def __init__(self):
|
|
136
|
+
"""Initialize terminal state manager."""
|
|
137
|
+
self.current_mode = TerminalMode.NORMAL
|
|
138
|
+
self.original_termios: Optional[Any] = None
|
|
139
|
+
self.original_console_mode: Optional[int] = None # Windows console mode
|
|
140
|
+
self.is_terminal = False
|
|
141
|
+
self.capabilities = TerminalCapabilities()
|
|
142
|
+
|
|
143
|
+
# State tracking
|
|
144
|
+
self._cursor_hidden = False
|
|
145
|
+
self._last_size = (0, 0)
|
|
146
|
+
self._resize_occurred = False # Track if terminal resize happened
|
|
147
|
+
self._last_resize_time = 0 # Track when last resize signal arrived
|
|
148
|
+
self._resize_debounce_delay = 0.2 # Wait 200ms for resize to settle
|
|
149
|
+
|
|
150
|
+
# Initialize terminal state
|
|
151
|
+
self._initialize_terminal()
|
|
152
|
+
|
|
153
|
+
# Set up SIGWINCH handler for terminal resize detection (Unix only)
|
|
154
|
+
self._setup_resize_handler()
|
|
155
|
+
|
|
156
|
+
def _initialize_terminal(self) -> None:
|
|
157
|
+
"""Initialize terminal and detect capabilities."""
|
|
158
|
+
# Save original terminal settings
|
|
159
|
+
try:
|
|
160
|
+
if sys.stdin.isatty():
|
|
161
|
+
if IS_WINDOWS:
|
|
162
|
+
# Windows: Save console mode and enable VT processing
|
|
163
|
+
self._setup_windows_console()
|
|
164
|
+
else:
|
|
165
|
+
# Unix: Save termios settings
|
|
166
|
+
self.original_termios = termios.tcgetattr(sys.stdin)
|
|
167
|
+
self.is_terminal = True
|
|
168
|
+
logger.info("Terminal mode detected and settings saved")
|
|
169
|
+
else:
|
|
170
|
+
logger.info("Non-terminal mode detected")
|
|
171
|
+
self.is_terminal = False
|
|
172
|
+
except Exception as e:
|
|
173
|
+
logger.warning(f"Could not save terminal settings: {e}")
|
|
174
|
+
self.is_terminal = False
|
|
175
|
+
self.original_termios = None
|
|
176
|
+
self.original_console_mode = None
|
|
177
|
+
|
|
178
|
+
# Detect terminal capabilities
|
|
179
|
+
self.capabilities = TerminalDetector.detect_capabilities()
|
|
180
|
+
|
|
181
|
+
def _setup_windows_console(self) -> None:
|
|
182
|
+
"""Setup Windows console for VT100 processing."""
|
|
183
|
+
if not IS_WINDOWS:
|
|
184
|
+
return
|
|
185
|
+
|
|
186
|
+
try:
|
|
187
|
+
# Get stdout handle and enable VT processing
|
|
188
|
+
stdout_handle = kernel32.GetStdHandle(STD_OUTPUT_HANDLE)
|
|
189
|
+
mode = ctypes.c_ulong()
|
|
190
|
+
kernel32.GetConsoleMode(stdout_handle, ctypes.byref(mode))
|
|
191
|
+
kernel32.SetConsoleMode(
|
|
192
|
+
stdout_handle,
|
|
193
|
+
mode.value | ENABLE_VIRTUAL_TERMINAL_PROCESSING
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
# Get stdin handle and save original mode
|
|
197
|
+
stdin_handle = kernel32.GetStdHandle(STD_INPUT_HANDLE)
|
|
198
|
+
input_mode = ctypes.c_ulong()
|
|
199
|
+
kernel32.GetConsoleMode(stdin_handle, ctypes.byref(input_mode))
|
|
200
|
+
self.original_console_mode = input_mode.value
|
|
201
|
+
|
|
202
|
+
# Enable VT input processing
|
|
203
|
+
kernel32.SetConsoleMode(
|
|
204
|
+
stdin_handle,
|
|
205
|
+
input_mode.value | ENABLE_VIRTUAL_TERMINAL_INPUT
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
logger.info("Windows console VT processing enabled")
|
|
209
|
+
except Exception as e:
|
|
210
|
+
logger.warning(f"Could not setup Windows console: {e}")
|
|
211
|
+
|
|
212
|
+
def _setup_resize_handler(self) -> None:
|
|
213
|
+
"""Set up SIGWINCH signal handler for terminal resize detection."""
|
|
214
|
+
if IS_WINDOWS:
|
|
215
|
+
# Windows doesn't have SIGWINCH - rely on polling terminal size
|
|
216
|
+
logger.debug("Windows platform - using size polling instead of SIGWINCH")
|
|
217
|
+
return
|
|
218
|
+
|
|
219
|
+
try:
|
|
220
|
+
def handle_resize(signum, frame):
|
|
221
|
+
"""Handle SIGWINCH signal (terminal resize) with debouncing."""
|
|
222
|
+
current_time = time.time()
|
|
223
|
+
self._last_resize_time = current_time
|
|
224
|
+
logger.debug(f"Terminal resize signal received at {current_time}")
|
|
225
|
+
|
|
226
|
+
signal.signal(signal.SIGWINCH, handle_resize)
|
|
227
|
+
logger.debug("SIGWINCH handler registered successfully")
|
|
228
|
+
except Exception as e:
|
|
229
|
+
logger.warning(f"Could not set up resize handler: {e}")
|
|
230
|
+
|
|
231
|
+
def check_and_clear_resize_flag(self) -> bool:
|
|
232
|
+
"""Check if resize occurred and clear the flag (with debouncing).
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
True if resize occurred and settled, False otherwise.
|
|
236
|
+
"""
|
|
237
|
+
current_time = time.time()
|
|
238
|
+
|
|
239
|
+
# Check if resize signal was received
|
|
240
|
+
if self._last_resize_time > 0:
|
|
241
|
+
# Check if enough time has passed since last resize signal (debouncing)
|
|
242
|
+
time_since_resize = current_time - self._last_resize_time
|
|
243
|
+
|
|
244
|
+
if time_since_resize >= self._resize_debounce_delay:
|
|
245
|
+
# Resize has settled - return True and reset
|
|
246
|
+
logger.debug(f"Resize settled after {time_since_resize:.3f}s")
|
|
247
|
+
self._last_resize_time = 0
|
|
248
|
+
return True
|
|
249
|
+
else:
|
|
250
|
+
# Still within debounce window - resize not settled yet
|
|
251
|
+
logger.debug(
|
|
252
|
+
f"Resize in progress, waiting... ({time_since_resize:.3f}s elapsed)"
|
|
253
|
+
)
|
|
254
|
+
return False
|
|
255
|
+
|
|
256
|
+
return False
|
|
257
|
+
|
|
258
|
+
def enter_raw_mode(self) -> bool:
|
|
259
|
+
"""Enter raw terminal mode for character-by-character input.
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
True if successful, False otherwise.
|
|
263
|
+
"""
|
|
264
|
+
if not self.is_terminal or self.current_mode == TerminalMode.RAW:
|
|
265
|
+
return False
|
|
266
|
+
|
|
267
|
+
try:
|
|
268
|
+
if IS_WINDOWS:
|
|
269
|
+
# Windows: Disable line input and echo
|
|
270
|
+
stdin_handle = kernel32.GetStdHandle(STD_INPUT_HANDLE)
|
|
271
|
+
mode = ctypes.c_ulong()
|
|
272
|
+
kernel32.GetConsoleMode(stdin_handle, ctypes.byref(mode))
|
|
273
|
+
# Disable echo and line input, keep VT processing
|
|
274
|
+
new_mode = mode.value & ~(ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT)
|
|
275
|
+
new_mode |= ENABLE_VIRTUAL_TERMINAL_INPUT
|
|
276
|
+
kernel32.SetConsoleMode(stdin_handle, new_mode)
|
|
277
|
+
else:
|
|
278
|
+
# Unix: Use tty.setraw
|
|
279
|
+
tty.setraw(sys.stdin.fileno())
|
|
280
|
+
self.current_mode = TerminalMode.RAW
|
|
281
|
+
logger.debug("Entered raw terminal mode")
|
|
282
|
+
return True
|
|
283
|
+
except Exception as e:
|
|
284
|
+
logger.error(f"Failed to enter raw mode: {e}")
|
|
285
|
+
return False
|
|
286
|
+
|
|
287
|
+
def exit_raw_mode(self) -> bool:
|
|
288
|
+
"""Exit raw terminal mode and restore normal settings.
|
|
289
|
+
|
|
290
|
+
Returns:
|
|
291
|
+
True if successful, False otherwise.
|
|
292
|
+
"""
|
|
293
|
+
if not self.is_terminal:
|
|
294
|
+
return False
|
|
295
|
+
|
|
296
|
+
try:
|
|
297
|
+
if IS_WINDOWS:
|
|
298
|
+
# Windows: Restore original console mode
|
|
299
|
+
if self.original_console_mode is not None:
|
|
300
|
+
stdin_handle = kernel32.GetStdHandle(STD_INPUT_HANDLE)
|
|
301
|
+
kernel32.SetConsoleMode(stdin_handle, self.original_console_mode)
|
|
302
|
+
else:
|
|
303
|
+
# Unix: Restore termios settings
|
|
304
|
+
if self.original_termios:
|
|
305
|
+
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, self.original_termios)
|
|
306
|
+
self.current_mode = TerminalMode.NORMAL
|
|
307
|
+
logger.debug("Exited raw terminal mode")
|
|
308
|
+
return True
|
|
309
|
+
except Exception as e:
|
|
310
|
+
logger.error(f"Failed to exit raw mode: {e}")
|
|
311
|
+
return False
|
|
312
|
+
|
|
313
|
+
def write_raw(self, text: str) -> bool:
|
|
314
|
+
"""Write text directly to terminal using low-level operations.
|
|
315
|
+
|
|
316
|
+
Args:
|
|
317
|
+
text: Text to write.
|
|
318
|
+
|
|
319
|
+
Returns:
|
|
320
|
+
True if successful, False otherwise.
|
|
321
|
+
"""
|
|
322
|
+
try:
|
|
323
|
+
if self.is_terminal:
|
|
324
|
+
os.write(sys.stdout.fileno(), text.encode("utf-8"))
|
|
325
|
+
else:
|
|
326
|
+
sys.stdout.write(text)
|
|
327
|
+
sys.stdout.flush()
|
|
328
|
+
return True
|
|
329
|
+
except Exception as e:
|
|
330
|
+
logger.error(f"Failed to write to terminal: {e}")
|
|
331
|
+
return False
|
|
332
|
+
|
|
333
|
+
def hide_cursor(self) -> bool:
|
|
334
|
+
"""Hide the terminal cursor.
|
|
335
|
+
|
|
336
|
+
Returns:
|
|
337
|
+
True if successful, False otherwise.
|
|
338
|
+
"""
|
|
339
|
+
if self._cursor_hidden or not self.capabilities.cursor_support:
|
|
340
|
+
return True
|
|
341
|
+
|
|
342
|
+
success = self.write_raw("\033[?25l")
|
|
343
|
+
if success:
|
|
344
|
+
self._cursor_hidden = True
|
|
345
|
+
logger.debug("Cursor hidden")
|
|
346
|
+
return success
|
|
347
|
+
|
|
348
|
+
def show_cursor(self) -> bool:
|
|
349
|
+
"""Show the terminal cursor.
|
|
350
|
+
|
|
351
|
+
Returns:
|
|
352
|
+
True if successful, False otherwise.
|
|
353
|
+
"""
|
|
354
|
+
if not self._cursor_hidden or not self.capabilities.cursor_support:
|
|
355
|
+
return True
|
|
356
|
+
|
|
357
|
+
success = self.write_raw("\033[?25h")
|
|
358
|
+
if success:
|
|
359
|
+
self._cursor_hidden = False
|
|
360
|
+
logger.debug("Cursor shown")
|
|
361
|
+
return success
|
|
362
|
+
|
|
363
|
+
def clear_line(self) -> bool:
|
|
364
|
+
"""Clear the current line.
|
|
365
|
+
|
|
366
|
+
Returns:
|
|
367
|
+
True if successful, False otherwise.
|
|
368
|
+
"""
|
|
369
|
+
return self.write_raw("\r\033[2K")
|
|
370
|
+
|
|
371
|
+
def move_cursor_up(self, lines: int = 1) -> bool:
|
|
372
|
+
"""Move cursor up by specified number of lines.
|
|
373
|
+
|
|
374
|
+
Args:
|
|
375
|
+
lines: Number of lines to move up.
|
|
376
|
+
|
|
377
|
+
Returns:
|
|
378
|
+
True if successful, False otherwise.
|
|
379
|
+
"""
|
|
380
|
+
if lines <= 0:
|
|
381
|
+
return True
|
|
382
|
+
return self.write_raw(f"\033[{lines}A")
|
|
383
|
+
|
|
384
|
+
def move_cursor_down(self, lines: int = 1) -> bool:
|
|
385
|
+
"""Move cursor down by specified number of lines.
|
|
386
|
+
|
|
387
|
+
Args:
|
|
388
|
+
lines: Number of lines to move down.
|
|
389
|
+
|
|
390
|
+
Returns:
|
|
391
|
+
True if successful, False otherwise.
|
|
392
|
+
"""
|
|
393
|
+
if lines <= 0:
|
|
394
|
+
return True
|
|
395
|
+
return self.write_raw(f"\033[{lines}B")
|
|
396
|
+
|
|
397
|
+
def move_cursor_to_column(self, column: int) -> bool:
|
|
398
|
+
"""Move cursor to specified column.
|
|
399
|
+
|
|
400
|
+
Args:
|
|
401
|
+
column: Column number (1-based).
|
|
402
|
+
|
|
403
|
+
Returns:
|
|
404
|
+
True if successful, False otherwise.
|
|
405
|
+
"""
|
|
406
|
+
if column <= 0:
|
|
407
|
+
column = 1
|
|
408
|
+
return self.write_raw(f"\033[{column}G")
|
|
409
|
+
|
|
410
|
+
def save_cursor_position(self) -> bool:
|
|
411
|
+
"""Save current cursor position.
|
|
412
|
+
|
|
413
|
+
Returns:
|
|
414
|
+
True if successful, False otherwise.
|
|
415
|
+
"""
|
|
416
|
+
return self.write_raw("\033[s")
|
|
417
|
+
|
|
418
|
+
def restore_cursor_position(self) -> bool:
|
|
419
|
+
"""Restore previously saved cursor position.
|
|
420
|
+
|
|
421
|
+
Returns:
|
|
422
|
+
True if successful, False otherwise.
|
|
423
|
+
"""
|
|
424
|
+
return self.write_raw("\033[u")
|
|
425
|
+
|
|
426
|
+
def clear_screen_from_cursor(self) -> bool:
|
|
427
|
+
"""Clear screen from cursor position to end.
|
|
428
|
+
|
|
429
|
+
Returns:
|
|
430
|
+
True if successful, False otherwise.
|
|
431
|
+
"""
|
|
432
|
+
return self.write_raw("\033[0J")
|
|
433
|
+
|
|
434
|
+
def update_size(self) -> bool:
|
|
435
|
+
"""Update terminal size information.
|
|
436
|
+
|
|
437
|
+
Returns:
|
|
438
|
+
True if size changed, False otherwise.
|
|
439
|
+
"""
|
|
440
|
+
try:
|
|
441
|
+
size = shutil.get_terminal_size()
|
|
442
|
+
new_size = (size.columns, size.lines)
|
|
443
|
+
|
|
444
|
+
if new_size != self._last_size:
|
|
445
|
+
self.capabilities.width = size.columns
|
|
446
|
+
self.capabilities.height = size.lines
|
|
447
|
+
self._last_size = new_size
|
|
448
|
+
logger.debug(f"Terminal size updated: {size.columns}x{size.lines}")
|
|
449
|
+
return True
|
|
450
|
+
except Exception as e:
|
|
451
|
+
logger.debug(f"Could not update terminal size: {e}")
|
|
452
|
+
|
|
453
|
+
return False
|
|
454
|
+
|
|
455
|
+
def get_size(self) -> tuple[int, int]:
|
|
456
|
+
"""Get current terminal size.
|
|
457
|
+
|
|
458
|
+
Returns:
|
|
459
|
+
Tuple of (width, height).
|
|
460
|
+
"""
|
|
461
|
+
return (self.capabilities.width, self.capabilities.height)
|
|
462
|
+
|
|
463
|
+
def supports_color(self, color_type: str = "basic") -> bool:
|
|
464
|
+
"""Check if terminal supports specified color type.
|
|
465
|
+
|
|
466
|
+
Args:
|
|
467
|
+
color_type: Color type to check ("basic", "256", "truecolor").
|
|
468
|
+
|
|
469
|
+
Returns:
|
|
470
|
+
True if color type is supported.
|
|
471
|
+
"""
|
|
472
|
+
if color_type == "truecolor":
|
|
473
|
+
return self.capabilities.has_truecolor
|
|
474
|
+
elif color_type == "256":
|
|
475
|
+
return self.capabilities.has_256_color
|
|
476
|
+
elif color_type == "basic":
|
|
477
|
+
return self.capabilities.has_color
|
|
478
|
+
else:
|
|
479
|
+
return False
|
|
480
|
+
|
|
481
|
+
def cleanup(self) -> None:
|
|
482
|
+
"""Cleanup terminal state and restore settings."""
|
|
483
|
+
try:
|
|
484
|
+
# Show cursor if hidden
|
|
485
|
+
if self._cursor_hidden:
|
|
486
|
+
self.show_cursor()
|
|
487
|
+
|
|
488
|
+
# Exit raw mode if active
|
|
489
|
+
if self.current_mode == TerminalMode.RAW:
|
|
490
|
+
self.exit_raw_mode()
|
|
491
|
+
|
|
492
|
+
logger.debug("Terminal state cleanup completed")
|
|
493
|
+
except Exception as e:
|
|
494
|
+
logger.error(f"Error during terminal cleanup: {e}")
|
|
495
|
+
|
|
496
|
+
def get_cursor_position(self) -> tuple[int, int]:
|
|
497
|
+
"""Query current cursor position from terminal.
|
|
498
|
+
|
|
499
|
+
Returns:
|
|
500
|
+
Tuple of (row, col) position, or (0, 0) if query fails.
|
|
501
|
+
"""
|
|
502
|
+
try:
|
|
503
|
+
# Send cursor position query
|
|
504
|
+
sys.stdout.write("\033[6n")
|
|
505
|
+
sys.stdout.flush()
|
|
506
|
+
|
|
507
|
+
# Read response (should be \033[row;colR)
|
|
508
|
+
response = ""
|
|
509
|
+
while True:
|
|
510
|
+
char = sys.stdin.read(1)
|
|
511
|
+
response += char
|
|
512
|
+
if char == "R":
|
|
513
|
+
break
|
|
514
|
+
if len(response) > 20: # Safety limit
|
|
515
|
+
break
|
|
516
|
+
|
|
517
|
+
# Parse response: \033[row;colR
|
|
518
|
+
if response.startswith("\033[") and response.endswith("R"):
|
|
519
|
+
coords = response[2:-1] # Remove \033[ and R
|
|
520
|
+
if ";" in coords:
|
|
521
|
+
row, col = coords.split(";")
|
|
522
|
+
return (int(row), int(col))
|
|
523
|
+
|
|
524
|
+
logger.warning(
|
|
525
|
+
f"Failed to parse cursor position response: {repr(response)}"
|
|
526
|
+
)
|
|
527
|
+
return (0, 0)
|
|
528
|
+
|
|
529
|
+
except Exception as e:
|
|
530
|
+
logger.error(f"Error querying cursor position: {e}")
|
|
531
|
+
return (0, 0)
|
|
532
|
+
|
|
533
|
+
def get_status(self) -> Dict[str, Any]:
|
|
534
|
+
"""Get terminal state status information.
|
|
535
|
+
|
|
536
|
+
Returns:
|
|
537
|
+
Dictionary with terminal status information.
|
|
538
|
+
"""
|
|
539
|
+
return {
|
|
540
|
+
"mode": getattr(self.current_mode, "value", self.current_mode),
|
|
541
|
+
"is_terminal": self.is_terminal,
|
|
542
|
+
"cursor_hidden": self._cursor_hidden,
|
|
543
|
+
"capabilities": {
|
|
544
|
+
"color_level": self.capabilities.color_level,
|
|
545
|
+
"width": self.capabilities.width,
|
|
546
|
+
"height": self.capabilities.height,
|
|
547
|
+
"cursor_support": self.capabilities.cursor_support,
|
|
548
|
+
"mouse_support": self.capabilities.mouse_support,
|
|
549
|
+
},
|
|
550
|
+
"last_size": self._last_size,
|
|
551
|
+
}
|