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,282 @@
1
+ """Full-screen renderer for plugin terminal output."""
2
+
3
+ import sys
4
+ import shutil
5
+ import logging
6
+ from typing import Tuple, Optional, Any
7
+ from dataclasses import dataclass
8
+
9
+ # Platform-specific imports for terminal control
10
+ IS_WINDOWS = sys.platform == "win32"
11
+
12
+ if IS_WINDOWS:
13
+ import ctypes
14
+ kernel32 = ctypes.windll.kernel32
15
+ STD_OUTPUT_HANDLE = -11
16
+ STD_INPUT_HANDLE = -10
17
+ ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004
18
+ ENABLE_VIRTUAL_TERMINAL_INPUT = 0x0200
19
+ ENABLE_ECHO_INPUT = 0x0004
20
+ ENABLE_LINE_INPUT = 0x0002
21
+ ENABLE_PROCESSED_INPUT = 0x0001
22
+ else:
23
+ import termios
24
+ import tty
25
+
26
+ from ..io.visual_effects import ColorPalette
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+
31
+ @dataclass
32
+ class TerminalSnapshot:
33
+ """Snapshot of terminal state for restoration."""
34
+ termios_settings: Any = None # Unix termios settings
35
+ console_mode: Any = None # Windows console mode
36
+ cursor_visible: bool = True
37
+ terminal_size: Tuple[int, int] = (80, 24)
38
+
39
+
40
+ class FullScreenRenderer:
41
+ """Handles full-screen rendering with alternate buffer management.
42
+
43
+ This class provides direct terminal control for plugins, including
44
+ alternate buffer management, cursor control, and direct output.
45
+ """
46
+
47
+ def __init__(self):
48
+ """Initialize the full-screen renderer."""
49
+ self.active = False
50
+ self.terminal_snapshot: Optional[TerminalSnapshot] = None
51
+ self.terminal_width = 80
52
+ self.terminal_height = 24
53
+
54
+ logger.info("FullScreenRenderer initialized")
55
+
56
+ def setup_terminal(self) -> bool:
57
+ """Setup terminal for full-screen rendering.
58
+
59
+ Returns:
60
+ True if setup was successful, False otherwise.
61
+ """
62
+ try:
63
+ # Get terminal size
64
+ size = shutil.get_terminal_size()
65
+ self.terminal_width = size.columns
66
+ self.terminal_height = size.lines
67
+
68
+ # Save current terminal state
69
+ self.terminal_snapshot = TerminalSnapshot()
70
+ # CRITICAL FIX: Don't set raw mode when running within main application
71
+ # The main app already handles input properly via InputHandler
72
+ if sys.stdin.isatty():
73
+ if IS_WINDOWS:
74
+ # Windows: Save console mode
75
+ stdin_handle = kernel32.GetStdHandle(STD_INPUT_HANDLE)
76
+ mode = ctypes.c_ulong()
77
+ kernel32.GetConsoleMode(stdin_handle, ctypes.byref(mode))
78
+ self.terminal_snapshot.console_mode = mode.value
79
+
80
+ # Enable VT processing for ANSI escape sequences
81
+ stdout_handle = kernel32.GetStdHandle(STD_OUTPUT_HANDLE)
82
+ out_mode = ctypes.c_ulong()
83
+ kernel32.GetConsoleMode(stdout_handle, ctypes.byref(out_mode))
84
+ kernel32.SetConsoleMode(
85
+ stdout_handle,
86
+ out_mode.value | ENABLE_VIRTUAL_TERMINAL_PROCESSING
87
+ )
88
+ else:
89
+ # Unix: Save termios settings
90
+ self.terminal_snapshot.termios_settings = termios.tcgetattr(sys.stdin.fileno())
91
+ # Skip tty.setraw() - let main application handle input
92
+
93
+ # Enter alternate buffer and setup
94
+ sys.stdout.write("\033[?1049h") # Enter alternate buffer
95
+ sys.stdout.flush()
96
+
97
+ # Clear and setup alternate buffer
98
+ self.clear_screen()
99
+ self.hide_cursor()
100
+ sys.stdout.flush()
101
+
102
+ # Fill screen with spaces to ensure clean state
103
+ for row in range(self.terminal_height):
104
+ sys.stdout.write(f"\033[{row+1};1H{' ' * self.terminal_width}")
105
+ sys.stdout.write("\033[H") # Return to home
106
+ sys.stdout.flush()
107
+
108
+ self.active = True
109
+ logger.info(f"Terminal setup complete: {self.terminal_width}x{self.terminal_height}")
110
+ return True
111
+
112
+ except Exception as e:
113
+ logger.error(f"Failed to setup terminal: {e}")
114
+ return False
115
+
116
+ def restore_terminal(self) -> bool:
117
+ """Restore terminal to original state.
118
+
119
+ Returns:
120
+ True if restoration was successful, False otherwise.
121
+ """
122
+ try:
123
+ if not self.active:
124
+ return True
125
+
126
+ # Clear alternate buffer before exiting
127
+ self.clear_screen()
128
+ # Don't call show_cursor() - normal mode keeps cursor hidden anyway
129
+
130
+ # Exit alternate buffer (automatically restores screen and cursor position)
131
+ sys.stdout.write("\033[?1049l") # Exit alternate buffer
132
+ sys.stdout.flush()
133
+
134
+ # CRITICAL FIX: Don't clear screen or move cursor - alternate buffer already restored everything
135
+ # Removed: sys.stdout.write("\033[2J") and sys.stdout.write("\033[H")
136
+ # The alternate buffer automatically restores the exact screen and cursor position
137
+
138
+ # Restore terminal settings
139
+ if self.terminal_snapshot and sys.stdin.isatty():
140
+ if IS_WINDOWS:
141
+ # Windows: Restore console mode
142
+ if self.terminal_snapshot.console_mode is not None:
143
+ stdin_handle = kernel32.GetStdHandle(STD_INPUT_HANDLE)
144
+ kernel32.SetConsoleMode(
145
+ stdin_handle,
146
+ self.terminal_snapshot.console_mode
147
+ )
148
+ else:
149
+ # Unix: Restore termios settings
150
+ if self.terminal_snapshot.termios_settings:
151
+ termios.tcsetattr(
152
+ sys.stdin.fileno(),
153
+ termios.TCSADRAIN,
154
+ self.terminal_snapshot.termios_settings
155
+ )
156
+
157
+ self.active = False
158
+ self.terminal_snapshot = None
159
+ logger.info("Terminal restored successfully")
160
+ return True
161
+
162
+ except Exception as e:
163
+ logger.error(f"Failed to restore terminal: {e}")
164
+ return False
165
+
166
+ def clear_screen(self):
167
+ """Clear the entire screen."""
168
+ sys.stdout.write("\033[2J\033[H")
169
+ sys.stdout.flush()
170
+
171
+ def clear_line(self, row: int):
172
+ """Clear a specific line.
173
+
174
+ Args:
175
+ row: Row number (1-based).
176
+ """
177
+ sys.stdout.write(f"\033[{row};1H\033[K")
178
+ sys.stdout.flush()
179
+
180
+ def move_cursor(self, x: int, y: int):
181
+ """Move cursor to specific position.
182
+
183
+ Args:
184
+ x: Column position (0-based).
185
+ y: Row position (0-based).
186
+ """
187
+ sys.stdout.write(f"\033[{y+1};{x+1}H")
188
+
189
+ def hide_cursor(self):
190
+ """Hide the cursor."""
191
+ sys.stdout.write("\033[?25l")
192
+
193
+ def show_cursor(self):
194
+ """Show the cursor."""
195
+ sys.stdout.write("\033[?25h")
196
+
197
+ def write_raw(self, text: str):
198
+ """Write raw text to terminal.
199
+
200
+ Args:
201
+ text: Text to write (can include ANSI codes).
202
+ """
203
+ sys.stdout.write(text)
204
+ sys.stdout.flush()
205
+
206
+ def write_at(self, x: int, y: int, text: str, color: str = ColorPalette.RESET):
207
+ """Write text at specific position with optional color.
208
+
209
+ Args:
210
+ x: Column position (0-based).
211
+ y: Row position (0-based).
212
+ text: Text to write.
213
+ color: ANSI color code.
214
+ """
215
+ if 0 <= x < self.terminal_width and 0 <= y < self.terminal_height:
216
+ self.move_cursor(x, y)
217
+ sys.stdout.write(f"{color}{text}{ColorPalette.RESET}")
218
+ sys.stdout.flush()
219
+
220
+ def draw_box(self, x: int, y: int, width: int, height: int,
221
+ border_color: str = ColorPalette.WHITE,
222
+ fill_color: str = ColorPalette.RESET):
223
+ """Draw a box at the specified position.
224
+
225
+ Args:
226
+ x: Left column (0-based).
227
+ y: Top row (0-based).
228
+ width: Box width.
229
+ height: Box height.
230
+ border_color: Color for border.
231
+ fill_color: Color for interior.
232
+ """
233
+ # Draw top border
234
+ self.write_at(x, y, f"{border_color}╭{'─' * (width-2)}╮{ColorPalette.RESET}")
235
+
236
+ # Draw sides and interior
237
+ for row in range(1, height-1):
238
+ self.write_at(x, y + row, f"{border_color}│{fill_color}{' ' * (width-2)}{border_color}│{ColorPalette.RESET}")
239
+
240
+ # Draw bottom border
241
+ if height > 1:
242
+ self.write_at(x, y + height - 1, f"{border_color}╰{'─' * (width-2)}╯{ColorPalette.RESET}")
243
+
244
+ def draw_line(self, x1: int, y1: int, x2: int, y2: int,
245
+ char: str = "─", color: str = ColorPalette.WHITE):
246
+ """Draw a line between two points.
247
+
248
+ Args:
249
+ x1, y1: Start position.
250
+ x2, y2: End position.
251
+ char: Character to use for line.
252
+ color: Line color.
253
+ """
254
+ # Simple horizontal/vertical line implementation
255
+ if y1 == y2: # Horizontal line
256
+ start_x, end_x = min(x1, x2), max(x1, x2)
257
+ line_text = char * (end_x - start_x + 1)
258
+ self.write_at(start_x, y1, line_text, color)
259
+ elif x1 == x2: # Vertical line
260
+ start_y, end_y = min(y1, y2), max(y1, y2)
261
+ for y in range(start_y, end_y + 1):
262
+ self.write_at(x1, y, char, color)
263
+
264
+ def get_terminal_size(self) -> Tuple[int, int]:
265
+ """Get current terminal size.
266
+
267
+ Returns:
268
+ Tuple of (width, height).
269
+ """
270
+ return (self.terminal_width, self.terminal_height)
271
+
272
+ def is_active(self) -> bool:
273
+ """Check if renderer is currently active.
274
+
275
+ Returns:
276
+ True if renderer is active, False otherwise.
277
+ """
278
+ return self.active
279
+
280
+ def flush(self):
281
+ """Flush output buffer."""
282
+ sys.stdout.flush()
@@ -0,0 +1,324 @@
1
+ """Full-screen session management."""
2
+
3
+ import asyncio
4
+ import logging
5
+ import sys
6
+ from typing import Optional
7
+ from dataclasses import dataclass
8
+
9
+ # Platform-specific imports for input handling
10
+ IS_WINDOWS = sys.platform == "win32"
11
+
12
+ if IS_WINDOWS:
13
+ import msvcrt
14
+ else:
15
+ import select
16
+
17
+ from ..io.key_parser import KeyParser, KeyPress
18
+ from .plugin import FullScreenPlugin
19
+ from .renderer import FullScreenRenderer
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ @dataclass
25
+ class SessionStats:
26
+ """Statistics for a full-screen session."""
27
+ start_time: float
28
+ end_time: Optional[float] = None
29
+ frame_count: int = 0
30
+ input_events: int = 0
31
+ average_fps: float = 0.0
32
+
33
+ @property
34
+ def duration(self) -> float:
35
+ """Get session duration in seconds."""
36
+ end = self.end_time or asyncio.get_event_loop().time()
37
+ return end - self.start_time
38
+
39
+
40
+ class FullScreenSession:
41
+ """Manages a full-screen plugin execution session.
42
+
43
+ This class handles the complete lifecycle of running a full-screen plugin,
44
+ including terminal setup, input handling, rendering loop, and cleanup.
45
+ """
46
+
47
+ def __init__(self, plugin: FullScreenPlugin, event_bus=None, target_fps: float = 60.0):
48
+ """Initialize a full-screen session.
49
+
50
+ Args:
51
+ plugin: The plugin to run.
52
+ event_bus: Event bus for input routing (optional, falls back to direct stdin).
53
+ target_fps: Target frame rate for rendering.
54
+ """
55
+ self.plugin = plugin
56
+ self.event_bus = event_bus
57
+ self.target_fps = target_fps
58
+ self.frame_delay = 1.0 / target_fps
59
+
60
+ # Session state
61
+ self.running = False
62
+ self.renderer = FullScreenRenderer()
63
+ self.key_parser = KeyParser()
64
+ self.stats = SessionStats(start_time=0)
65
+
66
+ # CRITICAL FIX: Input routing through events
67
+ self.pending_input = asyncio.Queue()
68
+ self.input_hook_registered = False
69
+
70
+ logger.info(f"Created session for plugin: {plugin.name}")
71
+
72
+ async def _register_input_hook(self):
73
+ """Register hook to receive input from InputHandler."""
74
+ if self.event_bus and not self.input_hook_registered:
75
+ try:
76
+ from ..events.models import Hook, HookPriority, EventType
77
+ hook = Hook(
78
+ name="fullscreen_input",
79
+ plugin_name=f"fullscreen_session_{self.plugin.name}",
80
+ event_type=EventType.FULLSCREEN_INPUT,
81
+ priority=HookPriority.DISPLAY.value,
82
+ callback=self._handle_fullscreen_input
83
+ )
84
+ success = await self.event_bus.register_hook(hook)
85
+ if success:
86
+ self.input_hook_registered = True
87
+ logger.info(f"✅ Registered FULLSCREEN_INPUT hook for {self.plugin.name}")
88
+ else:
89
+ logger.error(f"❌ Failed to register FULLSCREEN_INPUT hook for {self.plugin.name}")
90
+ except Exception as e:
91
+ logger.error(f"Error registering fullscreen input hook: {e}")
92
+
93
+ async def _handle_fullscreen_input(self, event_data, context=None):
94
+ """Handle FULLSCREEN_INPUT events from InputHandler."""
95
+ try:
96
+ print(f"🎯 CRITICAL: Session received FULLSCREEN_INPUT event: {event_data}")
97
+ key_press = event_data.get('key_press')
98
+ if key_press:
99
+ print(f"🎯 CRITICAL: Adding key to pending_input queue: {key_press.name} ({key_press.char})")
100
+ logger.info(f"🎯 Session received input: {key_press.name}")
101
+ await self.pending_input.put(key_press)
102
+ return {"success": True, "handled": True}
103
+ print(f"❌ CRITICAL: No key_press in event data: {event_data}")
104
+ return {"success": False, "error": "No key_press in event data"}
105
+ except Exception as e:
106
+ print(f"❌ CRITICAL: Exception in _handle_fullscreen_input: {e}")
107
+ logger.error(f"Error handling fullscreen input: {e}")
108
+ return {"success": False, "error": str(e)}
109
+
110
+ async def run(self) -> bool:
111
+ """Run the full-screen session.
112
+
113
+ Returns:
114
+ True if session completed successfully, False if error occurred.
115
+ """
116
+ try:
117
+ # Initialize session
118
+ if not await self._initialize():
119
+ return False
120
+
121
+ logger.info(f"Starting full-screen session for {self.plugin.name}")
122
+ self.running = True
123
+ self.stats.start_time = asyncio.get_event_loop().time()
124
+
125
+ # Main session loop
126
+ await self._session_loop()
127
+
128
+ logger.info(f"Session completed for {self.plugin.name}")
129
+ return True
130
+
131
+ except Exception as e:
132
+ logger.error(f"Session error for {self.plugin.name}: {e}")
133
+ return False
134
+
135
+ finally:
136
+ await self._cleanup()
137
+
138
+ async def _initialize(self) -> bool:
139
+ """Initialize the session.
140
+
141
+ Returns:
142
+ True if initialization successful, False otherwise.
143
+ """
144
+ try:
145
+ # CRITICAL FIX: Register input hook before terminal setup
146
+ try:
147
+ await self._register_input_hook()
148
+ except Exception as e:
149
+ logger.warning(f"Input hook registration failed: {e}")
150
+ # Continue anyway, input might still work via fallback
151
+
152
+ # Setup terminal
153
+ if not self.renderer.setup_terminal():
154
+ logger.error("Failed to setup terminal")
155
+ return False
156
+
157
+ # Initialize plugin
158
+ if not await self.plugin.initialize(self.renderer):
159
+ logger.error(f"Failed to initialize plugin {self.plugin.name}")
160
+ return False
161
+
162
+ # Start plugin
163
+ await self.plugin.on_start()
164
+
165
+ return True
166
+
167
+ except Exception as e:
168
+ logger.error(f"Session initialization failed: {e}")
169
+ return False
170
+
171
+ async def _session_loop(self):
172
+ """Main session loop handling rendering and input."""
173
+ last_frame_time = asyncio.get_event_loop().time()
174
+
175
+ while self.running:
176
+ current_time = asyncio.get_event_loop().time()
177
+ delta_time = current_time - last_frame_time
178
+
179
+ try:
180
+ # Handle input
181
+ if await self._handle_input():
182
+ break # Exit requested
183
+
184
+ # Render frame
185
+ if not await self.plugin.render_frame(delta_time):
186
+ break # Plugin requested exit
187
+
188
+ self.stats.frame_count += 1
189
+ last_frame_time = current_time
190
+
191
+ # Frame rate limiting
192
+ await asyncio.sleep(max(0, self.frame_delay - delta_time))
193
+
194
+ except Exception as e:
195
+ logger.error(f"Error in session loop: {e}")
196
+ break
197
+
198
+ async def _handle_input(self) -> bool:
199
+ """Handle input events.
200
+
201
+ Returns:
202
+ True if exit was requested, False otherwise.
203
+ """
204
+ try:
205
+ # Check for available input (non-blocking)
206
+ if not sys.stdin.isatty():
207
+ return False
208
+
209
+ # Platform-specific input checking
210
+ has_input = False
211
+ char = None
212
+
213
+ if IS_WINDOWS:
214
+ # Windows: Use msvcrt to check for input
215
+ if msvcrt.kbhit():
216
+ has_input = True
217
+ char_bytes = msvcrt.getch()
218
+ char = char_bytes.decode("utf-8", errors="ignore") if char_bytes else None
219
+
220
+ # Handle extended keys on Windows (arrow keys, etc.)
221
+ if char_bytes and char_bytes[0] in (0, 224):
222
+ ext_char = msvcrt.getch()
223
+ ext_code = ext_char[0] if ext_char else 0
224
+ # Map Windows extended key codes to escape sequences
225
+ win_key_map = {
226
+ 72: "\x1b[A", # ArrowUp
227
+ 80: "\x1b[B", # ArrowDown
228
+ 75: "\x1b[D", # ArrowLeft
229
+ 77: "\x1b[C", # ArrowRight
230
+ 71: "\x1b[H", # Home
231
+ 79: "\x1b[F", # End
232
+ 83: "\x1b[3~", # Delete
233
+ }
234
+ if ext_code in win_key_map:
235
+ # Process escape sequence character by character
236
+ for c in win_key_map[ext_code]:
237
+ if not hasattr(self, '_key_parser'):
238
+ self._key_parser = KeyParser()
239
+ key_press = self._key_parser.parse_char(c)
240
+ if key_press:
241
+ self.stats.input_events += 1
242
+ return await self.plugin.handle_input(key_press)
243
+ return False
244
+ else:
245
+ # Unix: Use select to check for input without blocking
246
+ ready, _, _ = select.select([sys.stdin], [], [], 0)
247
+ if ready:
248
+ has_input = True
249
+ char = sys.stdin.read(1)
250
+
251
+ if not has_input or not char:
252
+ return False
253
+
254
+ # Use the actual key parser for proper parsing
255
+ if not hasattr(self, '_key_parser'):
256
+ self._key_parser = KeyParser()
257
+
258
+ # Parse the input character properly
259
+ key_press = self._key_parser.parse_char(char)
260
+ if not key_press:
261
+ # Check for standalone ESC key
262
+ key_press = self._key_parser.check_for_standalone_escape()
263
+ if not key_press:
264
+ return False
265
+
266
+ self.stats.input_events += 1
267
+
268
+ # Let plugin handle input (plugin decides if it wants to exit)
269
+ return await self.plugin.handle_input(key_press)
270
+
271
+ except Exception as e:
272
+ logger.error(f"Error handling input: {e}")
273
+ return False
274
+
275
+ async def _cleanup(self):
276
+ """Clean up session resources."""
277
+ try:
278
+ self.running = False
279
+ self.stats.end_time = asyncio.get_event_loop().time()
280
+
281
+ # Calculate final stats
282
+ if self.stats.duration > 0:
283
+ self.stats.average_fps = self.stats.frame_count / self.stats.duration
284
+
285
+ # Stop plugin
286
+ await self.plugin.on_stop()
287
+
288
+ # Restore terminal
289
+ self.renderer.restore_terminal()
290
+
291
+ # Unregister input hook if it was registered
292
+ if self.input_hook_registered and self.event_bus:
293
+ try:
294
+ hook_id = f"fullscreen_session_{self.plugin.name}.fullscreen_input"
295
+ await self.event_bus.unregister_hook(hook_id)
296
+ self.input_hook_registered = False
297
+ logger.info(f"✅ Unregistered FULLSCREEN_INPUT hook for {self.plugin.name}")
298
+ except Exception as e:
299
+ logger.error(f"Error unregistering hook: {e}")
300
+
301
+ # Cleanup plugin
302
+ await self.plugin.cleanup()
303
+
304
+ logger.info(f"Session cleanup complete for {self.plugin.name}")
305
+ logger.info(f"Session stats: {self.stats.frame_count} frames, "
306
+ f"{self.stats.average_fps:.1f} fps, "
307
+ f"{self.stats.input_events} inputs, "
308
+ f"{self.stats.duration:.1f}s duration")
309
+
310
+ except Exception as e:
311
+ logger.error(f"Error during session cleanup: {e}")
312
+
313
+ def stop(self):
314
+ """Stop the session gracefully."""
315
+ self.running = False
316
+ logger.info(f"Stop requested for session: {self.plugin.name}")
317
+
318
+ def get_stats(self) -> SessionStats:
319
+ """Get current session statistics.
320
+
321
+ Returns:
322
+ Current session statistics.
323
+ """
324
+ return self.stats
core/io/__init__.py ADDED
@@ -0,0 +1,52 @@
1
+ """Input/Output subsystem for Kollabor CLI."""
2
+
3
+ from .input_handler import InputHandler
4
+ from .terminal_renderer import TerminalRenderer
5
+ from .key_parser import KeyParser, KeyPress, KeyType
6
+ from .buffer_manager import BufferManager
7
+ from .input_errors import InputErrorHandler, ErrorType, ErrorSeverity
8
+ from .visual_effects import VisualEffects, ColorPalette, EffectType
9
+ from .terminal_state import TerminalState, TerminalCapabilities, TerminalMode
10
+ from .layout import LayoutManager, LayoutArea, ThinkingAnimationManager
11
+ from .status_renderer import StatusRenderer, StatusMetric, StatusFormat
12
+ from .message_renderer import (
13
+ MessageRenderer,
14
+ ConversationMessage,
15
+ MessageType,
16
+ MessageFormat,
17
+ )
18
+
19
+ __all__ = [
20
+ # Core components
21
+ "InputHandler",
22
+ "TerminalRenderer",
23
+ # Input handling
24
+ "KeyParser",
25
+ "KeyPress",
26
+ "KeyType",
27
+ "BufferManager",
28
+ "InputErrorHandler",
29
+ "ErrorType",
30
+ "ErrorSeverity",
31
+ # Visual effects
32
+ "VisualEffects",
33
+ "ColorPalette",
34
+ "EffectType",
35
+ # Terminal management
36
+ "TerminalState",
37
+ "TerminalCapabilities",
38
+ "TerminalMode",
39
+ # Layout management
40
+ "LayoutManager",
41
+ "LayoutArea",
42
+ "ThinkingAnimationManager",
43
+ # Status rendering
44
+ "StatusRenderer",
45
+ "StatusMetric",
46
+ "StatusFormat",
47
+ # Message rendering
48
+ "MessageRenderer",
49
+ "ConversationMessage",
50
+ "MessageType",
51
+ "MessageFormat",
52
+ ]