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.
Files changed (128) hide show
  1. core/__init__.py +18 -0
  2. core/application.py +578 -0
  3. core/cli.py +193 -0
  4. core/commands/__init__.py +43 -0
  5. core/commands/executor.py +277 -0
  6. core/commands/menu_renderer.py +319 -0
  7. core/commands/parser.py +186 -0
  8. core/commands/registry.py +331 -0
  9. core/commands/system_commands.py +479 -0
  10. core/config/__init__.py +7 -0
  11. core/config/llm_task_config.py +110 -0
  12. core/config/loader.py +501 -0
  13. core/config/manager.py +112 -0
  14. core/config/plugin_config_manager.py +346 -0
  15. core/config/plugin_schema.py +424 -0
  16. core/config/service.py +399 -0
  17. core/effects/__init__.py +1 -0
  18. core/events/__init__.py +12 -0
  19. core/events/bus.py +129 -0
  20. core/events/executor.py +154 -0
  21. core/events/models.py +258 -0
  22. core/events/processor.py +176 -0
  23. core/events/registry.py +289 -0
  24. core/fullscreen/__init__.py +19 -0
  25. core/fullscreen/command_integration.py +290 -0
  26. core/fullscreen/components/__init__.py +12 -0
  27. core/fullscreen/components/animation.py +258 -0
  28. core/fullscreen/components/drawing.py +160 -0
  29. core/fullscreen/components/matrix_components.py +177 -0
  30. core/fullscreen/manager.py +302 -0
  31. core/fullscreen/plugin.py +204 -0
  32. core/fullscreen/renderer.py +282 -0
  33. core/fullscreen/session.py +324 -0
  34. core/io/__init__.py +52 -0
  35. core/io/buffer_manager.py +362 -0
  36. core/io/config_status_view.py +272 -0
  37. core/io/core_status_views.py +410 -0
  38. core/io/input_errors.py +313 -0
  39. core/io/input_handler.py +2655 -0
  40. core/io/input_mode_manager.py +402 -0
  41. core/io/key_parser.py +344 -0
  42. core/io/layout.py +587 -0
  43. core/io/message_coordinator.py +204 -0
  44. core/io/message_renderer.py +601 -0
  45. core/io/modal_interaction_handler.py +315 -0
  46. core/io/raw_input_processor.py +946 -0
  47. core/io/status_renderer.py +845 -0
  48. core/io/terminal_renderer.py +586 -0
  49. core/io/terminal_state.py +551 -0
  50. core/io/visual_effects.py +734 -0
  51. core/llm/__init__.py +26 -0
  52. core/llm/api_communication_service.py +863 -0
  53. core/llm/conversation_logger.py +473 -0
  54. core/llm/conversation_manager.py +414 -0
  55. core/llm/file_operations_executor.py +1401 -0
  56. core/llm/hook_system.py +402 -0
  57. core/llm/llm_service.py +1629 -0
  58. core/llm/mcp_integration.py +386 -0
  59. core/llm/message_display_service.py +450 -0
  60. core/llm/model_router.py +214 -0
  61. core/llm/plugin_sdk.py +396 -0
  62. core/llm/response_parser.py +848 -0
  63. core/llm/response_processor.py +364 -0
  64. core/llm/tool_executor.py +520 -0
  65. core/logging/__init__.py +19 -0
  66. core/logging/setup.py +208 -0
  67. core/models/__init__.py +5 -0
  68. core/models/base.py +23 -0
  69. core/plugins/__init__.py +13 -0
  70. core/plugins/collector.py +212 -0
  71. core/plugins/discovery.py +386 -0
  72. core/plugins/factory.py +263 -0
  73. core/plugins/registry.py +152 -0
  74. core/storage/__init__.py +5 -0
  75. core/storage/state_manager.py +84 -0
  76. core/ui/__init__.py +6 -0
  77. core/ui/config_merger.py +176 -0
  78. core/ui/config_widgets.py +369 -0
  79. core/ui/live_modal_renderer.py +276 -0
  80. core/ui/modal_actions.py +162 -0
  81. core/ui/modal_overlay_renderer.py +373 -0
  82. core/ui/modal_renderer.py +591 -0
  83. core/ui/modal_state_manager.py +443 -0
  84. core/ui/widget_integration.py +222 -0
  85. core/ui/widgets/__init__.py +27 -0
  86. core/ui/widgets/base_widget.py +136 -0
  87. core/ui/widgets/checkbox.py +85 -0
  88. core/ui/widgets/dropdown.py +140 -0
  89. core/ui/widgets/label.py +78 -0
  90. core/ui/widgets/slider.py +185 -0
  91. core/ui/widgets/text_input.py +224 -0
  92. core/utils/__init__.py +11 -0
  93. core/utils/config_utils.py +656 -0
  94. core/utils/dict_utils.py +212 -0
  95. core/utils/error_utils.py +275 -0
  96. core/utils/key_reader.py +171 -0
  97. core/utils/plugin_utils.py +267 -0
  98. core/utils/prompt_renderer.py +151 -0
  99. kollabor-0.4.9.dist-info/METADATA +298 -0
  100. kollabor-0.4.9.dist-info/RECORD +128 -0
  101. kollabor-0.4.9.dist-info/WHEEL +5 -0
  102. kollabor-0.4.9.dist-info/entry_points.txt +2 -0
  103. kollabor-0.4.9.dist-info/licenses/LICENSE +21 -0
  104. kollabor-0.4.9.dist-info/top_level.txt +4 -0
  105. kollabor_cli_main.py +20 -0
  106. plugins/__init__.py +1 -0
  107. plugins/enhanced_input/__init__.py +18 -0
  108. plugins/enhanced_input/box_renderer.py +103 -0
  109. plugins/enhanced_input/box_styles.py +142 -0
  110. plugins/enhanced_input/color_engine.py +165 -0
  111. plugins/enhanced_input/config.py +150 -0
  112. plugins/enhanced_input/cursor_manager.py +72 -0
  113. plugins/enhanced_input/geometry.py +81 -0
  114. plugins/enhanced_input/state.py +130 -0
  115. plugins/enhanced_input/text_processor.py +115 -0
  116. plugins/enhanced_input_plugin.py +385 -0
  117. plugins/fullscreen/__init__.py +9 -0
  118. plugins/fullscreen/example_plugin.py +327 -0
  119. plugins/fullscreen/matrix_plugin.py +132 -0
  120. plugins/hook_monitoring_plugin.py +1299 -0
  121. plugins/query_enhancer_plugin.py +350 -0
  122. plugins/save_conversation_plugin.py +502 -0
  123. plugins/system_commands_plugin.py +93 -0
  124. plugins/tmux_plugin.py +795 -0
  125. plugins/workflow_enforcement_plugin.py +629 -0
  126. system_prompt/default.md +1286 -0
  127. system_prompt/default_win.md +265 -0
  128. 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
+ }