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,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
|
+
]
|