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