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
core/utils/dict_utils.py
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"""Dictionary utility functions for configuration and data manipulation."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Any, Dict, List, Optional, Union
|
|
5
|
+
|
|
6
|
+
logger = logging.getLogger(__name__)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def deep_merge(target: Dict[str, Any], source: Dict[str, Any]) -> Dict[str, Any]:
|
|
10
|
+
"""Deep merge source dictionary into target dictionary.
|
|
11
|
+
|
|
12
|
+
Recursively merges dictionaries, with source values taking precedence.
|
|
13
|
+
Non-dict values in source will overwrite target values.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
target: Target dictionary to merge into (not modified).
|
|
17
|
+
source: Source dictionary to merge from.
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
New dictionary with merged values.
|
|
21
|
+
|
|
22
|
+
Example:
|
|
23
|
+
>>> target = {"a": {"b": 1, "c": 2}, "d": 3}
|
|
24
|
+
>>> source = {"a": {"b": 10, "e": 4}, "f": 5}
|
|
25
|
+
>>> deep_merge(target, source)
|
|
26
|
+
{"a": {"b": 10, "c": 2, "e": 4}, "d": 3, "f": 5}
|
|
27
|
+
"""
|
|
28
|
+
if not isinstance(target, dict):
|
|
29
|
+
logger.warning(f"Target is not a dict: {type(target)}")
|
|
30
|
+
return source if isinstance(source, dict) else {}
|
|
31
|
+
|
|
32
|
+
if not isinstance(source, dict):
|
|
33
|
+
logger.warning(f"Source is not a dict: {type(source)}")
|
|
34
|
+
return target.copy()
|
|
35
|
+
|
|
36
|
+
result = target.copy()
|
|
37
|
+
|
|
38
|
+
for key, value in source.items():
|
|
39
|
+
if (key in result and
|
|
40
|
+
isinstance(result[key], dict) and
|
|
41
|
+
isinstance(value, dict)):
|
|
42
|
+
# Recursively merge nested dictionaries
|
|
43
|
+
result[key] = deep_merge(result[key], value)
|
|
44
|
+
else:
|
|
45
|
+
# Overwrite with source value
|
|
46
|
+
result[key] = value
|
|
47
|
+
|
|
48
|
+
return result
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def safe_get(data: Dict[str, Any], key_path: str, default: Any = None) -> Any:
|
|
52
|
+
"""Safely get a value from nested dictionary using dot notation.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
data: Dictionary to search in.
|
|
56
|
+
key_path: Dot-separated path to the value (e.g., "section.subsection.key").
|
|
57
|
+
default: Default value if key not found.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
Value at key_path or default if not found.
|
|
61
|
+
|
|
62
|
+
Example:
|
|
63
|
+
>>> data = {"terminal": {"render_fps": 20}}
|
|
64
|
+
>>> safe_get(data, "terminal.render_fps")
|
|
65
|
+
20
|
|
66
|
+
>>> safe_get(data, "missing.key", "fallback")
|
|
67
|
+
"fallback"
|
|
68
|
+
"""
|
|
69
|
+
if not isinstance(data, dict):
|
|
70
|
+
return default
|
|
71
|
+
|
|
72
|
+
if not key_path:
|
|
73
|
+
return default
|
|
74
|
+
|
|
75
|
+
keys = key_path.split('.')
|
|
76
|
+
current = data
|
|
77
|
+
|
|
78
|
+
for key in keys:
|
|
79
|
+
if not isinstance(current, dict) or key not in current:
|
|
80
|
+
return default
|
|
81
|
+
current = current[key]
|
|
82
|
+
|
|
83
|
+
return current
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def safe_set(data: Dict[str, Any], key_path: str, value: Any) -> bool:
|
|
87
|
+
"""Safely set a value in nested dictionary using dot notation.
|
|
88
|
+
|
|
89
|
+
Creates intermediate dictionaries as needed.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
data: Dictionary to modify (modified in place).
|
|
93
|
+
key_path: Dot-separated path to set (e.g., "section.subsection.key").
|
|
94
|
+
value: Value to set.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
True if successful, False otherwise.
|
|
98
|
+
|
|
99
|
+
Example:
|
|
100
|
+
>>> data = {}
|
|
101
|
+
>>> safe_set(data, "terminal.render_fps", 30)
|
|
102
|
+
True
|
|
103
|
+
>>> data
|
|
104
|
+
{"terminal": {"render_fps": 30}}
|
|
105
|
+
"""
|
|
106
|
+
if not isinstance(data, dict):
|
|
107
|
+
logger.error(f"Cannot set key in non-dict: {type(data)}")
|
|
108
|
+
return False
|
|
109
|
+
|
|
110
|
+
if not key_path:
|
|
111
|
+
logger.error("Empty key_path provided")
|
|
112
|
+
return False
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
keys = key_path.split('.')
|
|
116
|
+
current = data
|
|
117
|
+
|
|
118
|
+
# Navigate to parent of target key, creating dicts as needed
|
|
119
|
+
for key in keys[:-1]:
|
|
120
|
+
if key not in current:
|
|
121
|
+
current[key] = {}
|
|
122
|
+
elif not isinstance(current[key], dict):
|
|
123
|
+
logger.warning(f"Overwriting non-dict value at key '{key}'")
|
|
124
|
+
current[key] = {}
|
|
125
|
+
current = current[key]
|
|
126
|
+
|
|
127
|
+
# Set the final value
|
|
128
|
+
current[keys[-1]] = value
|
|
129
|
+
return True
|
|
130
|
+
|
|
131
|
+
except Exception as e:
|
|
132
|
+
logger.error(f"Failed to set key '{key_path}': {e}")
|
|
133
|
+
return False
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def merge_multiple(configs: List[Dict[str, Any]]) -> Dict[str, Any]:
|
|
137
|
+
"""Merge multiple dictionaries in order.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
configs: List of dictionaries to merge (later configs take precedence).
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
Merged dictionary.
|
|
144
|
+
|
|
145
|
+
Example:
|
|
146
|
+
>>> configs = [{"a": 1}, {"b": 2}, {"a": 10}]
|
|
147
|
+
>>> merge_multiple(configs)
|
|
148
|
+
{"a": 10, "b": 2}
|
|
149
|
+
"""
|
|
150
|
+
if not configs:
|
|
151
|
+
return {}
|
|
152
|
+
|
|
153
|
+
result = {}
|
|
154
|
+
for config in configs:
|
|
155
|
+
if isinstance(config, dict):
|
|
156
|
+
result = deep_merge(result, config)
|
|
157
|
+
else:
|
|
158
|
+
logger.warning(f"Skipping non-dict config: {type(config)}")
|
|
159
|
+
|
|
160
|
+
return result
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def flatten_dict(data: Dict[str, Any], prefix: str = "", separator: str = ".") -> Dict[str, Any]:
|
|
164
|
+
"""Flatten nested dictionary to dot-notation keys.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
data: Dictionary to flatten.
|
|
168
|
+
prefix: Prefix for keys (used internally for recursion).
|
|
169
|
+
separator: Separator for nested keys.
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
Flattened dictionary.
|
|
173
|
+
|
|
174
|
+
Example:
|
|
175
|
+
>>> data = {"a": {"b": 1, "c": 2}, "d": 3}
|
|
176
|
+
>>> flatten_dict(data)
|
|
177
|
+
{"a.b": 1, "a.c": 2, "d": 3}
|
|
178
|
+
"""
|
|
179
|
+
result = {}
|
|
180
|
+
|
|
181
|
+
for key, value in data.items():
|
|
182
|
+
new_key = f"{prefix}{separator}{key}" if prefix else key
|
|
183
|
+
|
|
184
|
+
if isinstance(value, dict):
|
|
185
|
+
result.update(flatten_dict(value, new_key, separator))
|
|
186
|
+
else:
|
|
187
|
+
result[new_key] = value
|
|
188
|
+
|
|
189
|
+
return result
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def unflatten_dict(data: Dict[str, Any], separator: str = ".") -> Dict[str, Any]:
|
|
193
|
+
"""Unflatten dot-notation keys to nested dictionary.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
data: Dictionary with dot-notation keys.
|
|
197
|
+
separator: Separator used in keys.
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
Nested dictionary.
|
|
201
|
+
|
|
202
|
+
Example:
|
|
203
|
+
>>> data = {"a.b": 1, "a.c": 2, "d": 3}
|
|
204
|
+
>>> unflatten_dict(data)
|
|
205
|
+
{"a": {"b": 1, "c": 2}, "d": 3}
|
|
206
|
+
"""
|
|
207
|
+
result = {}
|
|
208
|
+
|
|
209
|
+
for key, value in data.items():
|
|
210
|
+
safe_set(result, key.replace(separator, "."), value)
|
|
211
|
+
|
|
212
|
+
return result
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
"""Error handling utility functions for consistent error management."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import functools
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Any, Callable, Optional, TypeVar, Union
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
T = TypeVar('T')
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def log_and_continue(operation_logger: logging.Logger, operation: str, exception: Exception) -> None:
|
|
14
|
+
"""Log an error and continue execution.
|
|
15
|
+
|
|
16
|
+
Standardized error logging format for non-fatal errors.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
operation_logger: Logger to use for error message.
|
|
20
|
+
operation: Description of the operation that failed.
|
|
21
|
+
exception: Exception that occurred.
|
|
22
|
+
|
|
23
|
+
Example:
|
|
24
|
+
>>> try:
|
|
25
|
+
... risky_operation()
|
|
26
|
+
... except Exception as e:
|
|
27
|
+
... log_and_continue(logger, "loading plugin config", e)
|
|
28
|
+
"""
|
|
29
|
+
operation_logger.error(f"Failed {operation}: {exception}")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def safe_execute(func: Callable[[], T], error_msg: str, default: T = None,
|
|
33
|
+
logger_instance: logging.Logger = None) -> T:
|
|
34
|
+
"""Execute function with error handling and default return value.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
func: Function to execute.
|
|
38
|
+
error_msg: Error message to log if function fails.
|
|
39
|
+
default: Default value to return on error.
|
|
40
|
+
logger_instance: Logger to use (defaults to module logger).
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
Function result or default value on error.
|
|
44
|
+
|
|
45
|
+
Example:
|
|
46
|
+
>>> def risky_function():
|
|
47
|
+
... return 1 / 0
|
|
48
|
+
>>> result = safe_execute(risky_function, "dividing by zero", default=42)
|
|
49
|
+
>>> result # Returns 42 since function raises exception
|
|
50
|
+
42
|
|
51
|
+
"""
|
|
52
|
+
log = logger_instance or logger
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
return func()
|
|
56
|
+
except Exception as e:
|
|
57
|
+
log.error(f"{error_msg}: {e}")
|
|
58
|
+
return default
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
async def safe_execute_async(func: Callable[[], T], error_msg: str, default: T = None,
|
|
62
|
+
logger_instance: logging.Logger = None, timeout: float = None) -> T:
|
|
63
|
+
"""Execute async function with error handling and default return value.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
func: Async function to execute.
|
|
67
|
+
error_msg: Error message to log if function fails.
|
|
68
|
+
default: Default value to return on error.
|
|
69
|
+
logger_instance: Logger to use (defaults to module logger).
|
|
70
|
+
timeout: Optional timeout in seconds.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
Function result or default value on error/timeout.
|
|
74
|
+
|
|
75
|
+
Example:
|
|
76
|
+
>>> async def risky_async_function():
|
|
77
|
+
... await asyncio.sleep(10) # Long operation
|
|
78
|
+
>>> result = await safe_execute_async(
|
|
79
|
+
... risky_async_function,
|
|
80
|
+
... "long operation",
|
|
81
|
+
... default="timed_out",
|
|
82
|
+
... timeout=1.0
|
|
83
|
+
... )
|
|
84
|
+
>>> result # Returns "timed_out" due to timeout
|
|
85
|
+
"timed_out"
|
|
86
|
+
"""
|
|
87
|
+
log = logger_instance or logger
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
if timeout is not None:
|
|
91
|
+
return await asyncio.wait_for(func(), timeout=timeout)
|
|
92
|
+
else:
|
|
93
|
+
return await func()
|
|
94
|
+
except asyncio.TimeoutError:
|
|
95
|
+
log.warning(f"{error_msg}: operation timed out after {timeout}s")
|
|
96
|
+
return default
|
|
97
|
+
except Exception as e:
|
|
98
|
+
log.error(f"{error_msg}: {e}")
|
|
99
|
+
return default
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def retry_on_failure(max_attempts: int = 3, delay: float = 0.1,
|
|
103
|
+
backoff_multiplier: float = 2.0,
|
|
104
|
+
logger_instance: logging.Logger = None):
|
|
105
|
+
"""Decorator to retry function on failure with exponential backoff.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
max_attempts: Maximum number of attempts.
|
|
109
|
+
delay: Initial delay between attempts in seconds.
|
|
110
|
+
backoff_multiplier: Multiplier for delay after each failure.
|
|
111
|
+
logger_instance: Logger to use for retry messages.
|
|
112
|
+
|
|
113
|
+
Example:
|
|
114
|
+
>>> @retry_on_failure(max_attempts=3, delay=0.1)
|
|
115
|
+
... def flaky_function():
|
|
116
|
+
... import random
|
|
117
|
+
... if random.random() < 0.7: # 70% chance of failure
|
|
118
|
+
... raise Exception("Random failure")
|
|
119
|
+
... return "success"
|
|
120
|
+
"""
|
|
121
|
+
def decorator(func: Callable) -> Callable:
|
|
122
|
+
@functools.wraps(func)
|
|
123
|
+
def wrapper(*args, **kwargs):
|
|
124
|
+
log = logger_instance or logger
|
|
125
|
+
current_delay = delay
|
|
126
|
+
|
|
127
|
+
for attempt in range(max_attempts):
|
|
128
|
+
try:
|
|
129
|
+
return func(*args, **kwargs)
|
|
130
|
+
except Exception as e:
|
|
131
|
+
if attempt == max_attempts - 1:
|
|
132
|
+
# Last attempt, re-raise the exception
|
|
133
|
+
log.error(f"Function {func.__name__} failed after {max_attempts} attempts: {e}")
|
|
134
|
+
raise
|
|
135
|
+
else:
|
|
136
|
+
log.warning(f"Function {func.__name__} failed (attempt {attempt + 1}/{max_attempts}): {e}")
|
|
137
|
+
if current_delay > 0:
|
|
138
|
+
import time
|
|
139
|
+
time.sleep(current_delay)
|
|
140
|
+
current_delay *= backoff_multiplier
|
|
141
|
+
|
|
142
|
+
return wrapper
|
|
143
|
+
return decorator
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class ErrorAccumulator:
|
|
147
|
+
"""Accumulate errors during batch operations for later reporting."""
|
|
148
|
+
|
|
149
|
+
def __init__(self, logger_instance: logging.Logger = None):
|
|
150
|
+
"""Initialize error accumulator.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
logger_instance: Logger to use for error reporting.
|
|
154
|
+
"""
|
|
155
|
+
self.errors = []
|
|
156
|
+
self.warnings = []
|
|
157
|
+
self.logger = logger_instance or logger
|
|
158
|
+
|
|
159
|
+
def add_error(self, operation: str, error: Union[str, Exception]) -> None:
|
|
160
|
+
"""Add an error to the accumulator.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
operation: Description of operation that failed.
|
|
164
|
+
error: Error message or exception.
|
|
165
|
+
"""
|
|
166
|
+
error_msg = str(error)
|
|
167
|
+
self.errors.append(f"{operation}: {error_msg}")
|
|
168
|
+
self.logger.error(f"Accumulated error - {operation}: {error_msg}")
|
|
169
|
+
|
|
170
|
+
def add_warning(self, operation: str, warning: Union[str, Exception]) -> None:
|
|
171
|
+
"""Add a warning to the accumulator.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
operation: Description of operation that had issues.
|
|
175
|
+
warning: Warning message or exception.
|
|
176
|
+
"""
|
|
177
|
+
warning_msg = str(warning)
|
|
178
|
+
self.warnings.append(f"{operation}: {warning_msg}")
|
|
179
|
+
self.logger.warning(f"Accumulated warning - {operation}: {warning_msg}")
|
|
180
|
+
|
|
181
|
+
def has_errors(self) -> bool:
|
|
182
|
+
"""Check if any errors were accumulated."""
|
|
183
|
+
return len(self.errors) > 0
|
|
184
|
+
|
|
185
|
+
def has_warnings(self) -> bool:
|
|
186
|
+
"""Check if any warnings were accumulated."""
|
|
187
|
+
return len(self.warnings) > 0
|
|
188
|
+
|
|
189
|
+
def get_summary(self) -> str:
|
|
190
|
+
"""Get summary of accumulated errors and warnings."""
|
|
191
|
+
parts = []
|
|
192
|
+
if self.errors:
|
|
193
|
+
parts.append(f"{len(self.errors)} errors")
|
|
194
|
+
if self.warnings:
|
|
195
|
+
parts.append(f"{len(self.warnings)} warnings")
|
|
196
|
+
|
|
197
|
+
if not parts:
|
|
198
|
+
return "No issues"
|
|
199
|
+
|
|
200
|
+
return ", ".join(parts)
|
|
201
|
+
|
|
202
|
+
def report_summary(self) -> None:
|
|
203
|
+
"""Log a summary of accumulated errors and warnings."""
|
|
204
|
+
if self.errors:
|
|
205
|
+
self.logger.error(f"Batch operation completed with {len(self.errors)} errors:")
|
|
206
|
+
for error in self.errors:
|
|
207
|
+
self.logger.error(f" - {error}")
|
|
208
|
+
|
|
209
|
+
if self.warnings:
|
|
210
|
+
self.logger.warning(f"Batch operation completed with {len(self.warnings)} warnings:")
|
|
211
|
+
for warning in self.warnings:
|
|
212
|
+
self.logger.warning(f" - {warning}")
|
|
213
|
+
|
|
214
|
+
if not self.errors and not self.warnings:
|
|
215
|
+
self.logger.info("Batch operation completed successfully")
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def handle_startup_errors(operation_name: str, logger_instance: logging.Logger = None):
|
|
219
|
+
"""Decorator for startup operations that should not crash the application.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
operation_name: Name of the startup operation.
|
|
223
|
+
logger_instance: Logger to use for error reporting.
|
|
224
|
+
|
|
225
|
+
Example:
|
|
226
|
+
>>> @handle_startup_errors("plugin initialization")
|
|
227
|
+
... def initialize_plugin():
|
|
228
|
+
... # Plugin initialization code that might fail
|
|
229
|
+
... pass
|
|
230
|
+
"""
|
|
231
|
+
def decorator(func: Callable) -> Callable:
|
|
232
|
+
@functools.wraps(func)
|
|
233
|
+
def wrapper(*args, **kwargs):
|
|
234
|
+
log = logger_instance or logger
|
|
235
|
+
try:
|
|
236
|
+
return func(*args, **kwargs)
|
|
237
|
+
except Exception as e:
|
|
238
|
+
log.error(f"Startup operation '{operation_name}' failed: {e}")
|
|
239
|
+
log.info(f"Continuing startup despite {operation_name} failure")
|
|
240
|
+
return None
|
|
241
|
+
|
|
242
|
+
return wrapper
|
|
243
|
+
return decorator
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def validate_and_log(condition: bool, error_msg: str,
|
|
247
|
+
logger_instance: logging.Logger = None,
|
|
248
|
+
raise_on_failure: bool = False) -> bool:
|
|
249
|
+
"""Validate condition and log error if validation fails.
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
condition: Condition to validate.
|
|
253
|
+
error_msg: Error message to log if condition is False.
|
|
254
|
+
logger_instance: Logger to use for error reporting.
|
|
255
|
+
raise_on_failure: Whether to raise exception on validation failure.
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
True if condition is True, False otherwise.
|
|
259
|
+
|
|
260
|
+
Raises:
|
|
261
|
+
ValueError: If condition is False and raise_on_failure is True.
|
|
262
|
+
|
|
263
|
+
Example:
|
|
264
|
+
>>> validate_and_log(len("test") > 10, "String too short")
|
|
265
|
+
False # Logs error and returns False
|
|
266
|
+
>>> validate_and_log(len("test") > 0, "String empty")
|
|
267
|
+
True # Returns True, no logging
|
|
268
|
+
"""
|
|
269
|
+
if not condition:
|
|
270
|
+
log = logger_instance or logger
|
|
271
|
+
log.error(error_msg)
|
|
272
|
+
if raise_on_failure:
|
|
273
|
+
raise ValueError(error_msg)
|
|
274
|
+
return False
|
|
275
|
+
return True
|
core/utils/key_reader.py
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Key reader utility for debugging terminal input sequences.
|
|
3
|
+
|
|
4
|
+
This tool helps debug what actual key sequences are sent by the terminal
|
|
5
|
+
when pressing various key combinations. Useful for implementing new
|
|
6
|
+
keyboard shortcuts and understanding terminal behavior.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
python core/utils/key_reader.py
|
|
10
|
+
|
|
11
|
+
Press keys to see their sequences, Ctrl+C to exit.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import sys
|
|
15
|
+
import signal
|
|
16
|
+
|
|
17
|
+
# Platform-specific imports
|
|
18
|
+
IS_WINDOWS = sys.platform == "win32"
|
|
19
|
+
|
|
20
|
+
if IS_WINDOWS:
|
|
21
|
+
import msvcrt
|
|
22
|
+
else:
|
|
23
|
+
import tty
|
|
24
|
+
import termios
|
|
25
|
+
import select
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def signal_handler(sig, frame):
|
|
29
|
+
"""Handle Ctrl+C gracefully."""
|
|
30
|
+
print('\n\nExiting key reader...')
|
|
31
|
+
sys.exit(0)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def main_unix():
|
|
35
|
+
"""Main key reader loop for Unix systems."""
|
|
36
|
+
print("Key Reader (Unix) - Press keys to see their sequences (Ctrl+C to exit)")
|
|
37
|
+
print("=" * 60)
|
|
38
|
+
|
|
39
|
+
# Set up signal handler for graceful exit
|
|
40
|
+
signal.signal(signal.SIGINT, signal_handler)
|
|
41
|
+
|
|
42
|
+
# Save terminal settings
|
|
43
|
+
fd = sys.stdin.fileno()
|
|
44
|
+
old_settings = termios.tcgetattr(fd)
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
# Set terminal to raw mode
|
|
48
|
+
tty.setraw(sys.stdin.fileno())
|
|
49
|
+
|
|
50
|
+
key_count = 0
|
|
51
|
+
|
|
52
|
+
while True:
|
|
53
|
+
# Read one character
|
|
54
|
+
char = sys.stdin.read(1)
|
|
55
|
+
key_count += 1
|
|
56
|
+
|
|
57
|
+
# Get character info
|
|
58
|
+
ascii_code = ord(char)
|
|
59
|
+
hex_code = hex(ascii_code)
|
|
60
|
+
|
|
61
|
+
# Determine key name
|
|
62
|
+
if ascii_code == 3: # Ctrl+C
|
|
63
|
+
print(f"\n\r[{key_count:03d}] Key: 'CTRL+C' | ASCII: {ascii_code} | Hex: {hex_code} | Raw: {repr(char)}")
|
|
64
|
+
break
|
|
65
|
+
elif ascii_code == 27: # ESC or start of escape sequence
|
|
66
|
+
key_name = "ESC"
|
|
67
|
+
# Try to read more characters for escape sequences
|
|
68
|
+
try:
|
|
69
|
+
# Set a short timeout to see if more chars follow
|
|
70
|
+
if select.select([sys.stdin], [], [], 0.1)[0]:
|
|
71
|
+
sequence = char
|
|
72
|
+
while select.select([sys.stdin], [], [], 0.05)[0]:
|
|
73
|
+
next_char = sys.stdin.read(1)
|
|
74
|
+
sequence += next_char
|
|
75
|
+
if len(sequence) > 10: # Prevent infinite sequences
|
|
76
|
+
break
|
|
77
|
+
|
|
78
|
+
# Update display info
|
|
79
|
+
char = sequence
|
|
80
|
+
ascii_code = f"ESC sequence"
|
|
81
|
+
hex_code = " ".join(hex(ord(c)) for c in sequence)
|
|
82
|
+
key_name = f"ESC_SEQ({sequence[1:]})" if len(sequence) > 1 else "ESC"
|
|
83
|
+
except:
|
|
84
|
+
pass
|
|
85
|
+
elif 1 <= ascii_code <= 26: # Ctrl+A through Ctrl+Z
|
|
86
|
+
key_name = f"CTRL+{chr(ascii_code + 64)}"
|
|
87
|
+
elif ascii_code == 127:
|
|
88
|
+
key_name = "BACKSPACE"
|
|
89
|
+
elif 32 <= ascii_code <= 126:
|
|
90
|
+
key_name = f"'{char}'"
|
|
91
|
+
else:
|
|
92
|
+
key_name = f"SPECIAL({ascii_code})"
|
|
93
|
+
|
|
94
|
+
# Display the key info
|
|
95
|
+
print(f"\r[{key_count:03d}] Key: {key_name} | ASCII: {ascii_code} | Hex: {hex_code} | Raw: {repr(char)}")
|
|
96
|
+
|
|
97
|
+
finally:
|
|
98
|
+
# Restore terminal settings
|
|
99
|
+
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def main_windows():
|
|
103
|
+
"""Main key reader loop for Windows systems."""
|
|
104
|
+
print("Key Reader (Windows) - Press keys to see their sequences (Ctrl+C to exit)")
|
|
105
|
+
print("=" * 60)
|
|
106
|
+
|
|
107
|
+
key_count = 0
|
|
108
|
+
|
|
109
|
+
try:
|
|
110
|
+
while True:
|
|
111
|
+
if msvcrt.kbhit():
|
|
112
|
+
# Read a key
|
|
113
|
+
char = msvcrt.getch()
|
|
114
|
+
key_count += 1
|
|
115
|
+
|
|
116
|
+
# Get character info
|
|
117
|
+
ascii_code = char[0] if isinstance(char, bytes) else ord(char)
|
|
118
|
+
hex_code = hex(ascii_code)
|
|
119
|
+
|
|
120
|
+
# Handle special keys (arrow keys, function keys, etc.)
|
|
121
|
+
if ascii_code in (0, 224): # Extended key prefix
|
|
122
|
+
# Read the actual key code
|
|
123
|
+
ext_char = msvcrt.getch()
|
|
124
|
+
ext_code = ext_char[0] if isinstance(ext_char, bytes) else ord(ext_char)
|
|
125
|
+
key_name = f"EXTENDED({ext_code})"
|
|
126
|
+
# Map common extended keys
|
|
127
|
+
ext_key_map = {
|
|
128
|
+
72: "ArrowUp", 80: "ArrowDown",
|
|
129
|
+
75: "ArrowLeft", 77: "ArrowRight",
|
|
130
|
+
71: "Home", 79: "End",
|
|
131
|
+
73: "PageUp", 81: "PageDown",
|
|
132
|
+
82: "Insert", 83: "Delete",
|
|
133
|
+
59: "F1", 60: "F2", 61: "F3", 62: "F4",
|
|
134
|
+
63: "F5", 64: "F6", 65: "F7", 66: "F8",
|
|
135
|
+
67: "F9", 68: "F10", 133: "F11", 134: "F12",
|
|
136
|
+
}
|
|
137
|
+
if ext_code in ext_key_map:
|
|
138
|
+
key_name = ext_key_map[ext_code]
|
|
139
|
+
print(f"[{key_count:03d}] Key: {key_name} | Code: {ascii_code},{ext_code} | Hex: {hex_code},{hex(ext_code)}")
|
|
140
|
+
elif ascii_code == 3: # Ctrl+C
|
|
141
|
+
print(f"[{key_count:03d}] Key: 'CTRL+C' | ASCII: {ascii_code} | Hex: {hex_code}")
|
|
142
|
+
break
|
|
143
|
+
elif ascii_code == 27: # ESC
|
|
144
|
+
print(f"[{key_count:03d}] Key: 'ESC' | ASCII: {ascii_code} | Hex: {hex_code}")
|
|
145
|
+
elif 1 <= ascii_code <= 26: # Ctrl+A through Ctrl+Z
|
|
146
|
+
key_name = f"CTRL+{chr(ascii_code + 64)}"
|
|
147
|
+
print(f"[{key_count:03d}] Key: {key_name} | ASCII: {ascii_code} | Hex: {hex_code}")
|
|
148
|
+
elif ascii_code == 8:
|
|
149
|
+
print(f"[{key_count:03d}] Key: BACKSPACE | ASCII: {ascii_code} | Hex: {hex_code}")
|
|
150
|
+
elif ascii_code == 13:
|
|
151
|
+
print(f"[{key_count:03d}] Key: ENTER | ASCII: {ascii_code} | Hex: {hex_code}")
|
|
152
|
+
elif 32 <= ascii_code <= 126:
|
|
153
|
+
char_str = chr(ascii_code)
|
|
154
|
+
print(f"[{key_count:03d}] Key: '{char_str}' | ASCII: {ascii_code} | Hex: {hex_code}")
|
|
155
|
+
else:
|
|
156
|
+
print(f"[{key_count:03d}] Key: SPECIAL({ascii_code}) | Hex: {hex_code}")
|
|
157
|
+
|
|
158
|
+
except KeyboardInterrupt:
|
|
159
|
+
print('\n\nExiting key reader...')
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def main():
|
|
163
|
+
"""Main entry point - selects platform-specific implementation."""
|
|
164
|
+
if IS_WINDOWS:
|
|
165
|
+
main_windows()
|
|
166
|
+
else:
|
|
167
|
+
main_unix()
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
if __name__ == "__main__":
|
|
171
|
+
main()
|