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,136 @@
|
|
|
1
|
+
"""Base widget class for modal UI components."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from typing import Any, List
|
|
5
|
+
from ...io.key_parser import KeyPress
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class BaseWidget(ABC):
|
|
9
|
+
"""Base class for all modal widgets.
|
|
10
|
+
|
|
11
|
+
Provides common functionality for rendering, input handling, and value management
|
|
12
|
+
across all widget types including checkboxes, dropdowns, text inputs, and sliders.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, config: dict, config_path: str, config_service=None):
|
|
16
|
+
"""Initialize base widget.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
config: Widget configuration dictionary containing display options.
|
|
20
|
+
config_path: Dot-notation path to config value (e.g., "core.llm.temperature").
|
|
21
|
+
config_service: ConfigService instance for reading/writing config values.
|
|
22
|
+
"""
|
|
23
|
+
self.config = config
|
|
24
|
+
self.config_path = config_path
|
|
25
|
+
self.config_service = config_service
|
|
26
|
+
self.focused = False
|
|
27
|
+
self._pending_value = None
|
|
28
|
+
|
|
29
|
+
@abstractmethod
|
|
30
|
+
def render(self) -> List[str]:
|
|
31
|
+
"""Render widget using existing ColorPalette.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
List of strings representing widget display lines.
|
|
35
|
+
"""
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
@abstractmethod
|
|
39
|
+
def handle_input(self, key_press: KeyPress) -> bool:
|
|
40
|
+
"""Handle input, return True if consumed.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
key_press: Key press event to handle.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
True if the key press was handled by this widget.
|
|
47
|
+
"""
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
def get_value(self) -> Any:
|
|
51
|
+
"""Get current value from config system.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
Current configuration value for this widget's config path.
|
|
55
|
+
"""
|
|
56
|
+
# Try to get real value from config service
|
|
57
|
+
if self.config_service:
|
|
58
|
+
try:
|
|
59
|
+
value = self.config_service.get(self.config_path)
|
|
60
|
+
# If we got a value, return it
|
|
61
|
+
if value is not None:
|
|
62
|
+
return value
|
|
63
|
+
except Exception:
|
|
64
|
+
# Fall through to defaults if config access fails
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
# Fallback to defaults for testing or when config service is unavailable
|
|
68
|
+
# Use reasonable defaults based on widget type
|
|
69
|
+
widget_type = self.__class__.__name__.lower()
|
|
70
|
+
|
|
71
|
+
if "checkbox" in widget_type:
|
|
72
|
+
return True
|
|
73
|
+
elif "slider" in widget_type:
|
|
74
|
+
# For sliders, check config for min/max and return middle value
|
|
75
|
+
min_val = self.config.get("min_value", 0)
|
|
76
|
+
max_val = self.config.get("max_value", 1)
|
|
77
|
+
return (min_val + max_val) / 2
|
|
78
|
+
elif "dropdown" in widget_type:
|
|
79
|
+
options = self.config.get("options", [])
|
|
80
|
+
return options[0] if options else "Unknown"
|
|
81
|
+
elif "text_input" in widget_type:
|
|
82
|
+
placeholder = self.config.get("placeholder", "")
|
|
83
|
+
return placeholder
|
|
84
|
+
else:
|
|
85
|
+
return ""
|
|
86
|
+
|
|
87
|
+
def set_value(self, value: Any):
|
|
88
|
+
"""Set value (will be saved in Phase 3).
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
value: New value to set for this widget.
|
|
92
|
+
"""
|
|
93
|
+
self._pending_value = value
|
|
94
|
+
|
|
95
|
+
def get_pending_value(self) -> Any:
|
|
96
|
+
"""Get pending value if set, otherwise current value.
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
Pending value if available, otherwise current config value.
|
|
100
|
+
"""
|
|
101
|
+
return self._pending_value if self._pending_value is not None else self.get_value()
|
|
102
|
+
|
|
103
|
+
def has_pending_changes(self) -> bool:
|
|
104
|
+
"""Check if widget has unsaved changes.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
True if there are pending changes to save.
|
|
108
|
+
"""
|
|
109
|
+
return self._pending_value is not None
|
|
110
|
+
|
|
111
|
+
def set_focus(self, focused: bool):
|
|
112
|
+
"""Set widget focus state.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
focused: Whether widget should be focused.
|
|
116
|
+
"""
|
|
117
|
+
self.focused = focused
|
|
118
|
+
|
|
119
|
+
def get_label(self) -> str:
|
|
120
|
+
"""Get widget label from config.
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
Label text for display.
|
|
124
|
+
"""
|
|
125
|
+
return self.config.get("label", "Widget")
|
|
126
|
+
|
|
127
|
+
def is_valid_value(self, value: Any) -> bool:
|
|
128
|
+
"""Validate if a value is acceptable for this widget.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
value: Value to validate.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
True if value is valid for this widget type.
|
|
135
|
+
"""
|
|
136
|
+
return True # Base implementation accepts any value
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Checkbox widget for modal UI components."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import List
|
|
5
|
+
from .base_widget import BaseWidget
|
|
6
|
+
from ...io.key_parser import KeyPress
|
|
7
|
+
from ...io.visual_effects import ColorPalette
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CheckboxWidget(BaseWidget):
|
|
13
|
+
"""Interactive checkbox widget with ✓ symbol.
|
|
14
|
+
|
|
15
|
+
Displays as [ ] or [✓] and toggles state on Enter or Space key press.
|
|
16
|
+
Uses ColorPalette.BRIGHT for focus highlighting.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, config: dict, config_path: str, config_service=None):
|
|
20
|
+
"""Initialize checkbox widget.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
config: Widget configuration dictionary.
|
|
24
|
+
config_path: Dot-notation path to config value.
|
|
25
|
+
config_service: ConfigService instance for reading/writing config values.
|
|
26
|
+
"""
|
|
27
|
+
super().__init__(config, config_path, config_service)
|
|
28
|
+
|
|
29
|
+
def render(self) -> List[str]:
|
|
30
|
+
"""Render checkbox with current state.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
List containing single checkbox display line.
|
|
34
|
+
"""
|
|
35
|
+
# Get current value (prefer pending value if available)
|
|
36
|
+
current_value = self.get_pending_value()
|
|
37
|
+
check = "✓" if current_value else " "
|
|
38
|
+
label = self.get_label()
|
|
39
|
+
|
|
40
|
+
logger.info(f"Checkbox render: value={current_value}, check='{check}', focused={self.focused}")
|
|
41
|
+
|
|
42
|
+
# Apply focus highlighting using existing ColorPalette
|
|
43
|
+
if self.focused:
|
|
44
|
+
rendered = f"{ColorPalette.BRIGHT_WHITE} [{check}] {label}{ColorPalette.RESET}"
|
|
45
|
+
else:
|
|
46
|
+
rendered = f" [{check}] {label}"
|
|
47
|
+
|
|
48
|
+
logger.info(f"Checkbox rendered as: '{rendered}'")
|
|
49
|
+
return [rendered]
|
|
50
|
+
|
|
51
|
+
def handle_input(self, key_press: KeyPress) -> bool:
|
|
52
|
+
"""Handle checkbox input - toggle on Enter or Space.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
key_press: Key press event to handle.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
True if key was handled (Enter or Space).
|
|
59
|
+
"""
|
|
60
|
+
# Check for Enter key (name="Enter" or char="\r" or char="\n")
|
|
61
|
+
is_enter = key_press.name == "Enter" or key_press.char in ("\r", "\n")
|
|
62
|
+
# Check for Space key (name="Space" or char=" ")
|
|
63
|
+
is_space = key_press.name == "Space" or key_press.char == " "
|
|
64
|
+
|
|
65
|
+
logger.info(f"🔘 Checkbox handle_input: name={key_press.name}, char={repr(key_press.char)}, is_enter={is_enter}, is_space={is_space}")
|
|
66
|
+
|
|
67
|
+
if is_enter or is_space:
|
|
68
|
+
current_value = self.get_pending_value()
|
|
69
|
+
new_value = not current_value
|
|
70
|
+
logger.info(f"🔘 Checkbox TOGGLING: {current_value} → {new_value}")
|
|
71
|
+
self.set_value(new_value)
|
|
72
|
+
logger.info(f"🔘 Checkbox value after set: {self.get_pending_value()}, _pending={self._pending_value}")
|
|
73
|
+
return True
|
|
74
|
+
return False
|
|
75
|
+
|
|
76
|
+
def is_valid_value(self, value) -> bool:
|
|
77
|
+
"""Validate checkbox value - must be boolean.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
value: Value to validate.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
True if value is boolean.
|
|
84
|
+
"""
|
|
85
|
+
return isinstance(value, bool)
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""Dropdown widget for modal UI components."""
|
|
2
|
+
|
|
3
|
+
from typing import List, Any
|
|
4
|
+
from .base_widget import BaseWidget
|
|
5
|
+
from ...io.key_parser import KeyPress
|
|
6
|
+
from ...io.visual_effects import ColorPalette
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class DropdownWidget(BaseWidget):
|
|
10
|
+
"""Interactive dropdown widget with ▼ indicator.
|
|
11
|
+
|
|
12
|
+
Displays as [value ▼] format and cycles through options on Enter.
|
|
13
|
+
Uses ColorPalette.BRIGHT for focus highlighting.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, config: dict, config_path: str, config_service=None):
|
|
17
|
+
"""Initialize dropdown widget.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
config: Widget configuration containing 'options' list.
|
|
21
|
+
config_path: Dot-notation path to config value.
|
|
22
|
+
config_service: ConfigService instance for reading/writing config values.
|
|
23
|
+
"""
|
|
24
|
+
super().__init__(config, config_path, config_service)
|
|
25
|
+
self.options = config.get("options", [])
|
|
26
|
+
self._expanded = False
|
|
27
|
+
|
|
28
|
+
def render(self) -> List[str]:
|
|
29
|
+
"""Render dropdown with current selection.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
List containing dropdown display line(s).
|
|
33
|
+
"""
|
|
34
|
+
current_value = self.get_pending_value()
|
|
35
|
+
label = self.get_label()
|
|
36
|
+
|
|
37
|
+
# Display current selection with dropdown indicator
|
|
38
|
+
display_value = str(current_value) if current_value is not None else "None"
|
|
39
|
+
|
|
40
|
+
if self.focused:
|
|
41
|
+
main_line = f"{ColorPalette.BRIGHT_WHITE} {label}: [{display_value} ▼]{ColorPalette.RESET}"
|
|
42
|
+
else:
|
|
43
|
+
main_line = f" {label}: [{display_value} ▼]"
|
|
44
|
+
|
|
45
|
+
lines = [main_line]
|
|
46
|
+
|
|
47
|
+
# If expanded, show options (Phase 2B feature)
|
|
48
|
+
if self._expanded and self.focused:
|
|
49
|
+
for i, option in enumerate(self.options):
|
|
50
|
+
prefix = " > " if option == current_value else " "
|
|
51
|
+
option_line = f"{ColorPalette.DIM}{prefix}{option}{ColorPalette.RESET}"
|
|
52
|
+
lines.append(option_line)
|
|
53
|
+
|
|
54
|
+
return lines
|
|
55
|
+
|
|
56
|
+
def handle_input(self, key_press: KeyPress) -> bool:
|
|
57
|
+
"""Handle dropdown input - cycle options or toggle expansion.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
key_press: Key press event to handle.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
True if key was handled.
|
|
64
|
+
"""
|
|
65
|
+
if key_press.name == "Enter":
|
|
66
|
+
if not self._expanded:
|
|
67
|
+
# Expand dropdown to show options
|
|
68
|
+
self._expanded = True
|
|
69
|
+
return True
|
|
70
|
+
else:
|
|
71
|
+
# Collapse dropdown
|
|
72
|
+
self._expanded = False
|
|
73
|
+
return True
|
|
74
|
+
|
|
75
|
+
elif key_press.name == "ArrowUp" and self._expanded:
|
|
76
|
+
# Navigate to previous option
|
|
77
|
+
self._cycle_option(-1)
|
|
78
|
+
return True
|
|
79
|
+
|
|
80
|
+
elif key_press.name == "ArrowDown" and self._expanded:
|
|
81
|
+
# Navigate to next option
|
|
82
|
+
self._cycle_option(1)
|
|
83
|
+
return True
|
|
84
|
+
|
|
85
|
+
elif key_press.name == "Escape" and self._expanded:
|
|
86
|
+
# Collapse dropdown without changing value
|
|
87
|
+
self._expanded = False
|
|
88
|
+
return True
|
|
89
|
+
|
|
90
|
+
# Quick cycling without expansion (for compact interaction)
|
|
91
|
+
elif key_press.name == "ArrowRight":
|
|
92
|
+
self._cycle_option(1)
|
|
93
|
+
return True
|
|
94
|
+
|
|
95
|
+
elif key_press.name == "ArrowLeft":
|
|
96
|
+
self._cycle_option(-1)
|
|
97
|
+
return True
|
|
98
|
+
|
|
99
|
+
return False
|
|
100
|
+
|
|
101
|
+
def _cycle_option(self, direction: int):
|
|
102
|
+
"""Cycle through available options.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
direction: 1 for next, -1 for previous.
|
|
106
|
+
"""
|
|
107
|
+
if not self.options:
|
|
108
|
+
return
|
|
109
|
+
|
|
110
|
+
current_value = self.get_pending_value()
|
|
111
|
+
|
|
112
|
+
try:
|
|
113
|
+
current_index = self.options.index(current_value)
|
|
114
|
+
except ValueError:
|
|
115
|
+
# Current value not in options, start from beginning
|
|
116
|
+
current_index = -1 if direction == 1 else 0
|
|
117
|
+
|
|
118
|
+
new_index = (current_index + direction) % len(self.options)
|
|
119
|
+
self.set_value(self.options[new_index])
|
|
120
|
+
|
|
121
|
+
def set_focus(self, focused: bool):
|
|
122
|
+
"""Set focus and collapse dropdown when losing focus.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
focused: Whether widget should be focused.
|
|
126
|
+
"""
|
|
127
|
+
super().set_focus(focused)
|
|
128
|
+
if not focused:
|
|
129
|
+
self._expanded = False
|
|
130
|
+
|
|
131
|
+
def is_valid_value(self, value: Any) -> bool:
|
|
132
|
+
"""Validate dropdown value - must be in options list.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
value: Value to validate.
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
True if value is in the options list.
|
|
139
|
+
"""
|
|
140
|
+
return value in self.options or not self.options
|
core/ui/widgets/label.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Label widget for read-only value display."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, List, Optional
|
|
4
|
+
from .base_widget import BaseWidget
|
|
5
|
+
from ...io.visual_effects import ColorPalette
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class LabelWidget(BaseWidget):
|
|
9
|
+
"""Read-only label widget for displaying status values.
|
|
10
|
+
|
|
11
|
+
Unlike other widgets, this doesn't allow user interaction -
|
|
12
|
+
it simply displays a label and value pair.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, label: str, value: str = "", help_text: str = "",
|
|
16
|
+
config_path: str = "", current_value: Any = None, **kwargs):
|
|
17
|
+
"""Initialize label widget.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
label: Display label text.
|
|
21
|
+
value: Value to display (can also be set via current_value).
|
|
22
|
+
help_text: Optional help text.
|
|
23
|
+
config_path: Config path (usually empty for labels).
|
|
24
|
+
current_value: Alternative way to set value.
|
|
25
|
+
**kwargs: Additional configuration.
|
|
26
|
+
"""
|
|
27
|
+
config = {
|
|
28
|
+
"label": label,
|
|
29
|
+
"value": value or str(current_value or ""),
|
|
30
|
+
"help": help_text,
|
|
31
|
+
**kwargs
|
|
32
|
+
}
|
|
33
|
+
super().__init__(config, config_path, None)
|
|
34
|
+
self._value = value or str(current_value or "")
|
|
35
|
+
|
|
36
|
+
def render(self) -> List[str]:
|
|
37
|
+
"""Render the label widget.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
List containing single label display line.
|
|
41
|
+
"""
|
|
42
|
+
label = self.config.get("label", "")
|
|
43
|
+
value = self._value
|
|
44
|
+
|
|
45
|
+
# Format: " Label: Value" (matching other widgets' indentation)
|
|
46
|
+
if self.focused:
|
|
47
|
+
rendered = f"{ColorPalette.BRIGHT_WHITE} {label}: {value}{ColorPalette.RESET}"
|
|
48
|
+
else:
|
|
49
|
+
rendered = f" {label}: {value}"
|
|
50
|
+
|
|
51
|
+
return [rendered]
|
|
52
|
+
|
|
53
|
+
def handle_input(self, key_press) -> bool:
|
|
54
|
+
"""Handle input (no-op for labels).
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
key_press: Key press event.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
False - labels don't consume input.
|
|
61
|
+
"""
|
|
62
|
+
return False
|
|
63
|
+
|
|
64
|
+
def get_value(self) -> str:
|
|
65
|
+
"""Get the label value.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Current value string.
|
|
69
|
+
"""
|
|
70
|
+
return self._value
|
|
71
|
+
|
|
72
|
+
def set_value(self, value: Any) -> None:
|
|
73
|
+
"""Set the label value.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
value: New value to display.
|
|
77
|
+
"""
|
|
78
|
+
self._value = str(value)
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""Slider widget for modal UI components."""
|
|
2
|
+
|
|
3
|
+
from typing import List
|
|
4
|
+
from .base_widget import BaseWidget
|
|
5
|
+
from ...io.key_parser import KeyPress
|
|
6
|
+
from ...io.visual_effects import ColorPalette
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SliderWidget(BaseWidget):
|
|
10
|
+
"""Interactive slider widget with █░ visual bar.
|
|
11
|
+
|
|
12
|
+
Displays numeric value with visual progress bar and responds to arrow keys.
|
|
13
|
+
Uses ColorPalette.BRIGHT for focus highlighting.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, config: dict, config_path: str, config_service=None):
|
|
17
|
+
"""Initialize slider widget.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
config: Widget configuration with min_value, max_value, step values.
|
|
21
|
+
config_path: Dot-notation path to config value.
|
|
22
|
+
config_service: ConfigService instance for reading/writing config values.
|
|
23
|
+
"""
|
|
24
|
+
super().__init__(config, config_path, config_service)
|
|
25
|
+
# Support both naming conventions: min/max and min_value/max_value
|
|
26
|
+
self.min_value = config.get("min_value") or config.get("min", 0.0)
|
|
27
|
+
self.max_value = config.get("max_value") or config.get("max", 1.0)
|
|
28
|
+
self.step = config.get("step", 0.1)
|
|
29
|
+
self.bar_width = config.get("bar_width", 20)
|
|
30
|
+
self.decimal_places = config.get("decimal_places", 1)
|
|
31
|
+
|
|
32
|
+
def render(self) -> List[str]:
|
|
33
|
+
"""Render slider with visual progress bar.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
List containing slider display line.
|
|
37
|
+
"""
|
|
38
|
+
# Get current value and ensure it's numeric
|
|
39
|
+
current_value = self.get_pending_value()
|
|
40
|
+
try:
|
|
41
|
+
value = float(current_value) if current_value is not None else self.min_value
|
|
42
|
+
except (TypeError, ValueError):
|
|
43
|
+
value = self.min_value
|
|
44
|
+
|
|
45
|
+
# Clamp value to valid range
|
|
46
|
+
value = max(self.min_value, min(self.max_value, value))
|
|
47
|
+
|
|
48
|
+
label = self.get_label()
|
|
49
|
+
|
|
50
|
+
# Create visual slider bar using existing characters
|
|
51
|
+
progress = (value - self.min_value) / max(0.001, self.max_value - self.min_value)
|
|
52
|
+
filled = int(progress * self.bar_width)
|
|
53
|
+
bar = "█" * filled + "░" * (self.bar_width - filled)
|
|
54
|
+
|
|
55
|
+
# Format value display - show as integer if no decimal places needed
|
|
56
|
+
if self.decimal_places == 0:
|
|
57
|
+
value_display = f"{int(value)}"
|
|
58
|
+
else:
|
|
59
|
+
value_display = f"{value:.{self.decimal_places}f}"
|
|
60
|
+
|
|
61
|
+
# Show range info when focused
|
|
62
|
+
if self.focused:
|
|
63
|
+
range_info = f" ({self.min_value}–{self.max_value})"
|
|
64
|
+
main_line = f"{ColorPalette.BRIGHT_WHITE} {label}: {value_display} [{bar}]{range_info}{ColorPalette.RESET}"
|
|
65
|
+
else:
|
|
66
|
+
main_line = f" {label}: {value_display} [{bar}]"
|
|
67
|
+
|
|
68
|
+
return [main_line]
|
|
69
|
+
|
|
70
|
+
def handle_input(self, key_press: KeyPress) -> bool:
|
|
71
|
+
"""Handle slider input - arrow keys adjust value.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
key_press: Key press event to handle.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
True if key was handled.
|
|
78
|
+
"""
|
|
79
|
+
current_value = self.get_pending_value()
|
|
80
|
+
try:
|
|
81
|
+
value = float(current_value) if current_value is not None else self.min_value
|
|
82
|
+
except (TypeError, ValueError):
|
|
83
|
+
value = self.min_value
|
|
84
|
+
|
|
85
|
+
# Clamp current value to valid range
|
|
86
|
+
value = max(self.min_value, min(self.max_value, value))
|
|
87
|
+
|
|
88
|
+
if key_press.name == "ArrowRight" or key_press.name == "ArrowUp":
|
|
89
|
+
# Increase value
|
|
90
|
+
new_value = min(self.max_value, value + self.step)
|
|
91
|
+
self.set_value(new_value)
|
|
92
|
+
return True
|
|
93
|
+
|
|
94
|
+
elif key_press.name == "ArrowLeft" or key_press.name == "ArrowDown":
|
|
95
|
+
# Decrease value
|
|
96
|
+
new_value = max(self.min_value, value - self.step)
|
|
97
|
+
self.set_value(new_value)
|
|
98
|
+
return True
|
|
99
|
+
|
|
100
|
+
elif key_press.name == "Home":
|
|
101
|
+
# Set to minimum value
|
|
102
|
+
self.set_value(self.min_value)
|
|
103
|
+
return True
|
|
104
|
+
|
|
105
|
+
elif key_press.name == "End":
|
|
106
|
+
# Set to maximum value
|
|
107
|
+
self.set_value(self.max_value)
|
|
108
|
+
return True
|
|
109
|
+
|
|
110
|
+
elif key_press.name == "Ctrl+ArrowRight":
|
|
111
|
+
# Large step increase
|
|
112
|
+
large_step = self.step * 10
|
|
113
|
+
new_value = min(self.max_value, value + large_step)
|
|
114
|
+
self.set_value(new_value)
|
|
115
|
+
return True
|
|
116
|
+
|
|
117
|
+
elif key_press.name == "Ctrl+ArrowLeft":
|
|
118
|
+
# Large step decrease
|
|
119
|
+
large_step = self.step * 10
|
|
120
|
+
new_value = max(self.min_value, value - large_step)
|
|
121
|
+
self.set_value(new_value)
|
|
122
|
+
return True
|
|
123
|
+
|
|
124
|
+
# Handle direct numeric input for precise values
|
|
125
|
+
elif key_press.char and isinstance(key_press.char, str) and key_press.char.isdigit():
|
|
126
|
+
# Allow typing numbers (basic implementation)
|
|
127
|
+
try:
|
|
128
|
+
# Convert single digit to step-based movement
|
|
129
|
+
digit = int(key_press.char)
|
|
130
|
+
target_progress = digit / 10.0 # 0-9 maps to 0%-90%
|
|
131
|
+
target_value = self.min_value + target_progress * (self.max_value - self.min_value)
|
|
132
|
+
target_value = max(self.min_value, min(self.max_value, target_value))
|
|
133
|
+
self.set_value(target_value)
|
|
134
|
+
return True
|
|
135
|
+
except ValueError:
|
|
136
|
+
pass
|
|
137
|
+
|
|
138
|
+
return False
|
|
139
|
+
|
|
140
|
+
def set_value(self, value):
|
|
141
|
+
"""Set slider value with validation and clamping.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
value: New numeric value.
|
|
145
|
+
"""
|
|
146
|
+
try:
|
|
147
|
+
numeric_value = float(value)
|
|
148
|
+
# Clamp to valid range
|
|
149
|
+
clamped_value = max(self.min_value, min(self.max_value, numeric_value))
|
|
150
|
+
# Round to step precision
|
|
151
|
+
stepped_value = round(clamped_value / self.step) * self.step
|
|
152
|
+
super().set_value(stepped_value)
|
|
153
|
+
except (TypeError, ValueError):
|
|
154
|
+
# Invalid value, set to minimum
|
|
155
|
+
super().set_value(self.min_value)
|
|
156
|
+
|
|
157
|
+
def is_valid_value(self, value) -> bool:
|
|
158
|
+
"""Validate slider value - must be numeric and in range.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
value: Value to validate.
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
True if value is numeric and within min/max range.
|
|
165
|
+
"""
|
|
166
|
+
try:
|
|
167
|
+
numeric_value = float(value)
|
|
168
|
+
return self.min_value <= numeric_value <= self.max_value
|
|
169
|
+
except (TypeError, ValueError):
|
|
170
|
+
return False
|
|
171
|
+
|
|
172
|
+
def get_progress_percentage(self) -> float:
|
|
173
|
+
"""Get current value as percentage of range.
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
Progress as percentage (0.0 to 1.0).
|
|
177
|
+
"""
|
|
178
|
+
current_value = self.get_pending_value()
|
|
179
|
+
try:
|
|
180
|
+
value = float(current_value) if current_value is not None else self.min_value
|
|
181
|
+
except (TypeError, ValueError):
|
|
182
|
+
value = self.min_value
|
|
183
|
+
|
|
184
|
+
value = max(self.min_value, min(self.max_value, value))
|
|
185
|
+
return (value - self.min_value) / max(0.001, self.max_value - self.min_value)
|