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,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
@@ -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()