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,258 @@
|
|
|
1
|
+
"""Animation framework for full-screen plugins."""
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
from typing import Callable, Any
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class AnimationState:
|
|
10
|
+
"""State for an animation."""
|
|
11
|
+
start_value: float
|
|
12
|
+
end_value: float
|
|
13
|
+
duration: float
|
|
14
|
+
start_time: float
|
|
15
|
+
current_time: float = 0.0
|
|
16
|
+
completed: bool = False
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def progress(self) -> float:
|
|
20
|
+
"""Get animation progress (0.0 to 1.0)."""
|
|
21
|
+
if self.duration <= 0:
|
|
22
|
+
return 1.0
|
|
23
|
+
return min(1.0, (self.current_time - self.start_time) / self.duration)
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def is_complete(self) -> bool:
|
|
27
|
+
"""Check if animation is complete."""
|
|
28
|
+
return self.progress >= 1.0 or self.completed
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class EasingFunctions:
|
|
32
|
+
"""Common easing functions for animations."""
|
|
33
|
+
|
|
34
|
+
@staticmethod
|
|
35
|
+
def linear(t: float) -> float:
|
|
36
|
+
"""Linear interpolation."""
|
|
37
|
+
return t
|
|
38
|
+
|
|
39
|
+
@staticmethod
|
|
40
|
+
def ease_in_quad(t: float) -> float:
|
|
41
|
+
"""Quadratic ease-in."""
|
|
42
|
+
return t * t
|
|
43
|
+
|
|
44
|
+
@staticmethod
|
|
45
|
+
def ease_out_quad(t: float) -> float:
|
|
46
|
+
"""Quadratic ease-out."""
|
|
47
|
+
return 1 - (1 - t) * (1 - t)
|
|
48
|
+
|
|
49
|
+
@staticmethod
|
|
50
|
+
def ease_in_out_quad(t: float) -> float:
|
|
51
|
+
"""Quadratic ease-in-out."""
|
|
52
|
+
if t < 0.5:
|
|
53
|
+
return 2 * t * t
|
|
54
|
+
return 1 - pow(-2 * t + 2, 2) / 2
|
|
55
|
+
|
|
56
|
+
@staticmethod
|
|
57
|
+
def ease_in_cubic(t: float) -> float:
|
|
58
|
+
"""Cubic ease-in."""
|
|
59
|
+
return t * t * t
|
|
60
|
+
|
|
61
|
+
@staticmethod
|
|
62
|
+
def ease_out_cubic(t: float) -> float:
|
|
63
|
+
"""Cubic ease-out."""
|
|
64
|
+
return 1 - pow(1 - t, 3)
|
|
65
|
+
|
|
66
|
+
@staticmethod
|
|
67
|
+
def ease_in_out_cubic(t: float) -> float:
|
|
68
|
+
"""Cubic ease-in-out."""
|
|
69
|
+
if t < 0.5:
|
|
70
|
+
return 4 * t * t * t
|
|
71
|
+
return 1 - pow(-2 * t + 2, 3) / 2
|
|
72
|
+
|
|
73
|
+
@staticmethod
|
|
74
|
+
def bounce_out(t: float) -> float:
|
|
75
|
+
"""Bounce ease-out."""
|
|
76
|
+
n1 = 7.5625
|
|
77
|
+
d1 = 2.75
|
|
78
|
+
if t < 1 / d1:
|
|
79
|
+
return n1 * t * t
|
|
80
|
+
elif t < 2 / d1:
|
|
81
|
+
t -= 1.5 / d1
|
|
82
|
+
return n1 * t * t + 0.75
|
|
83
|
+
elif t < 2.5 / d1:
|
|
84
|
+
t -= 2.25 / d1
|
|
85
|
+
return n1 * t * t + 0.9375
|
|
86
|
+
else:
|
|
87
|
+
t -= 2.625 / d1
|
|
88
|
+
return n1 * t * t + 0.984375
|
|
89
|
+
|
|
90
|
+
@staticmethod
|
|
91
|
+
def elastic_out(t: float) -> float:
|
|
92
|
+
"""Elastic ease-out."""
|
|
93
|
+
if t == 0 or t == 1:
|
|
94
|
+
return t
|
|
95
|
+
c4 = (2 * math.pi) / 3
|
|
96
|
+
return pow(2, -10 * t) * math.sin((t * 10 - 0.75) * c4) + 1
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class AnimationFramework:
|
|
100
|
+
"""Framework for managing animations in full-screen plugins."""
|
|
101
|
+
|
|
102
|
+
def __init__(self):
|
|
103
|
+
"""Initialize the animation framework."""
|
|
104
|
+
self.animations = {}
|
|
105
|
+
self.next_id = 0
|
|
106
|
+
|
|
107
|
+
def animate(self, start_value: float, end_value: float, duration: float,
|
|
108
|
+
start_time: float, easing: Callable[[float], float] = EasingFunctions.linear) -> int:
|
|
109
|
+
"""Create a new animation.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
start_value: Starting value
|
|
113
|
+
end_value: Ending value
|
|
114
|
+
duration: Animation duration in seconds
|
|
115
|
+
start_time: Animation start time
|
|
116
|
+
easing: Easing function
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
Animation ID
|
|
120
|
+
"""
|
|
121
|
+
animation_id = self.next_id
|
|
122
|
+
self.next_id += 1
|
|
123
|
+
|
|
124
|
+
self.animations[animation_id] = {
|
|
125
|
+
'state': AnimationState(start_value, end_value, duration, start_time),
|
|
126
|
+
'easing': easing
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return animation_id
|
|
130
|
+
|
|
131
|
+
def update(self, current_time: float):
|
|
132
|
+
"""Update all animations.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
current_time: Current time
|
|
136
|
+
"""
|
|
137
|
+
completed_animations = []
|
|
138
|
+
|
|
139
|
+
for anim_id, animation in self.animations.items():
|
|
140
|
+
state = animation['state']
|
|
141
|
+
state.current_time = current_time
|
|
142
|
+
|
|
143
|
+
if state.is_complete:
|
|
144
|
+
completed_animations.append(anim_id)
|
|
145
|
+
|
|
146
|
+
# Remove completed animations
|
|
147
|
+
for anim_id in completed_animations:
|
|
148
|
+
del self.animations[anim_id]
|
|
149
|
+
|
|
150
|
+
def get_value(self, animation_id: int) -> float:
|
|
151
|
+
"""Get current animated value.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
animation_id: Animation ID
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
Current animated value
|
|
158
|
+
"""
|
|
159
|
+
if animation_id not in self.animations:
|
|
160
|
+
return 0.0
|
|
161
|
+
|
|
162
|
+
animation = self.animations[animation_id]
|
|
163
|
+
state = animation['state']
|
|
164
|
+
easing = animation['easing']
|
|
165
|
+
|
|
166
|
+
if state.is_complete:
|
|
167
|
+
return state.end_value
|
|
168
|
+
|
|
169
|
+
# Apply easing to progress
|
|
170
|
+
eased_progress = easing(state.progress)
|
|
171
|
+
|
|
172
|
+
# Interpolate between start and end values
|
|
173
|
+
return state.start_value + (state.end_value - state.start_value) * eased_progress
|
|
174
|
+
|
|
175
|
+
def is_complete(self, animation_id: int) -> bool:
|
|
176
|
+
"""Check if animation is complete.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
animation_id: Animation ID
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
True if animation is complete or doesn't exist
|
|
183
|
+
"""
|
|
184
|
+
if animation_id not in self.animations:
|
|
185
|
+
return True
|
|
186
|
+
|
|
187
|
+
return self.animations[animation_id]['state'].is_complete
|
|
188
|
+
|
|
189
|
+
def stop_animation(self, animation_id: int):
|
|
190
|
+
"""Stop an animation.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
animation_id: Animation ID to stop
|
|
194
|
+
"""
|
|
195
|
+
if animation_id in self.animations:
|
|
196
|
+
del self.animations[animation_id]
|
|
197
|
+
|
|
198
|
+
def clear_all(self):
|
|
199
|
+
"""Clear all animations."""
|
|
200
|
+
self.animations.clear()
|
|
201
|
+
|
|
202
|
+
def get_active_count(self) -> int:
|
|
203
|
+
"""Get number of active animations.
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
Number of active animations
|
|
207
|
+
"""
|
|
208
|
+
return len(self.animations)
|
|
209
|
+
|
|
210
|
+
# Utility methods for common animation patterns
|
|
211
|
+
def fade_in(self, duration: float, start_time: float) -> int:
|
|
212
|
+
"""Create a fade-in animation (0 to 1).
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
duration: Fade duration
|
|
216
|
+
start_time: Start time
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
Animation ID
|
|
220
|
+
"""
|
|
221
|
+
return self.animate(0.0, 1.0, duration, start_time, EasingFunctions.ease_out_quad)
|
|
222
|
+
|
|
223
|
+
def fade_out(self, duration: float, start_time: float) -> int:
|
|
224
|
+
"""Create a fade-out animation (1 to 0).
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
duration: Fade duration
|
|
228
|
+
start_time: Start time
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
Animation ID
|
|
232
|
+
"""
|
|
233
|
+
return self.animate(1.0, 0.0, duration, start_time, EasingFunctions.ease_in_quad)
|
|
234
|
+
|
|
235
|
+
def slide_in(self, distance: float, duration: float, start_time: float) -> int:
|
|
236
|
+
"""Create a slide-in animation.
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
distance: Distance to slide
|
|
240
|
+
duration: Animation duration
|
|
241
|
+
start_time: Start time
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
Animation ID
|
|
245
|
+
"""
|
|
246
|
+
return self.animate(-distance, 0.0, duration, start_time, EasingFunctions.ease_out_cubic)
|
|
247
|
+
|
|
248
|
+
def bounce_in(self, duration: float, start_time: float) -> int:
|
|
249
|
+
"""Create a bounce-in animation.
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
duration: Animation duration
|
|
253
|
+
start_time: Start time
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
Animation ID
|
|
257
|
+
"""
|
|
258
|
+
return self.animate(0.0, 1.0, duration, start_time, EasingFunctions.bounce_out)
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""Drawing primitives for full-screen plugins."""
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
from typing import Tuple, List, Optional
|
|
5
|
+
from ...io.visual_effects import ColorPalette
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class DrawingPrimitives:
|
|
9
|
+
"""Basic drawing operations for full-screen plugins."""
|
|
10
|
+
|
|
11
|
+
@staticmethod
|
|
12
|
+
def draw_text_centered(renderer, y: int, text: str, color: str = ColorPalette.WHITE):
|
|
13
|
+
"""Draw text centered horizontally.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
renderer: FullScreenRenderer instance
|
|
17
|
+
y: Row position
|
|
18
|
+
text: Text to draw
|
|
19
|
+
color: Text color
|
|
20
|
+
"""
|
|
21
|
+
width, _ = renderer.get_terminal_size()
|
|
22
|
+
x = max(0, (width - len(text)) // 2)
|
|
23
|
+
renderer.write_at(x, y, text, color)
|
|
24
|
+
|
|
25
|
+
@staticmethod
|
|
26
|
+
def draw_border(renderer, x: int, y: int, width: int, height: int,
|
|
27
|
+
border_char: str = "─", corner_chars: Tuple[str, str, str, str] = ("╭", "╮", "╯", "╰"),
|
|
28
|
+
color: str = ColorPalette.WHITE):
|
|
29
|
+
"""Draw a decorative border.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
renderer: FullScreenRenderer instance
|
|
33
|
+
x, y: Top-left position
|
|
34
|
+
width, height: Border dimensions
|
|
35
|
+
border_char: Character for sides
|
|
36
|
+
corner_chars: Tuple of (top-left, top-right, bottom-right, bottom-left)
|
|
37
|
+
color: Border color
|
|
38
|
+
"""
|
|
39
|
+
if width < 2 or height < 2:
|
|
40
|
+
return
|
|
41
|
+
|
|
42
|
+
# Top border
|
|
43
|
+
renderer.write_at(x, y, corner_chars[0], color)
|
|
44
|
+
for i in range(1, width - 1):
|
|
45
|
+
renderer.write_at(x + i, y, border_char, color)
|
|
46
|
+
renderer.write_at(x + width - 1, y, corner_chars[1], color)
|
|
47
|
+
|
|
48
|
+
# Side borders
|
|
49
|
+
for i in range(1, height - 1):
|
|
50
|
+
renderer.write_at(x, y + i, "│", color)
|
|
51
|
+
renderer.write_at(x + width - 1, y + i, "│", color)
|
|
52
|
+
|
|
53
|
+
# Bottom border
|
|
54
|
+
if height > 1:
|
|
55
|
+
renderer.write_at(x, y + height - 1, corner_chars[3], color)
|
|
56
|
+
for i in range(1, width - 1):
|
|
57
|
+
renderer.write_at(x + i, y + height - 1, border_char, color)
|
|
58
|
+
renderer.write_at(x + width - 1, y + height - 1, corner_chars[2], color)
|
|
59
|
+
|
|
60
|
+
@staticmethod
|
|
61
|
+
def draw_progress_bar(renderer, x: int, y: int, width: int, progress: float,
|
|
62
|
+
fill_char: str = "█", empty_char: str = "░",
|
|
63
|
+
color: str = ColorPalette.GREEN):
|
|
64
|
+
"""Draw a progress bar.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
renderer: FullScreenRenderer instance
|
|
68
|
+
x, y: Position
|
|
69
|
+
width: Bar width
|
|
70
|
+
progress: Progress value (0.0 to 1.0)
|
|
71
|
+
fill_char: Character for filled portion
|
|
72
|
+
empty_char: Character for empty portion
|
|
73
|
+
color: Bar color
|
|
74
|
+
"""
|
|
75
|
+
progress = max(0.0, min(1.0, progress))
|
|
76
|
+
filled_width = int(width * progress)
|
|
77
|
+
|
|
78
|
+
bar_text = fill_char * filled_width + empty_char * (width - filled_width)
|
|
79
|
+
renderer.write_at(x, y, bar_text, color)
|
|
80
|
+
|
|
81
|
+
@staticmethod
|
|
82
|
+
def draw_spinner(renderer, x: int, y: int, frame: int,
|
|
83
|
+
frames: Optional[List[str]] = None, color: str = ColorPalette.CYAN):
|
|
84
|
+
"""Draw an animated spinner.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
renderer: FullScreenRenderer instance
|
|
88
|
+
x, y: Position
|
|
89
|
+
frame: Current frame number
|
|
90
|
+
frames: List of spinner frames
|
|
91
|
+
color: Spinner color
|
|
92
|
+
"""
|
|
93
|
+
if frames is None:
|
|
94
|
+
frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧"]
|
|
95
|
+
|
|
96
|
+
char = frames[frame % len(frames)]
|
|
97
|
+
renderer.write_at(x, y, char, color)
|
|
98
|
+
|
|
99
|
+
@staticmethod
|
|
100
|
+
def draw_circle_points(renderer, center_x: int, center_y: int, radius: int,
|
|
101
|
+
char: str = "●", color: str = ColorPalette.WHITE):
|
|
102
|
+
"""Draw points in a circle pattern.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
renderer: FullScreenRenderer instance
|
|
106
|
+
center_x, center_y: Circle center
|
|
107
|
+
radius: Circle radius
|
|
108
|
+
char: Character to draw
|
|
109
|
+
color: Character color
|
|
110
|
+
"""
|
|
111
|
+
width, height = renderer.get_terminal_size()
|
|
112
|
+
|
|
113
|
+
for angle in range(0, 360, 15): # Every 15 degrees
|
|
114
|
+
rad = math.radians(angle)
|
|
115
|
+
x = int(center_x + radius * math.cos(rad))
|
|
116
|
+
y = int(center_y + radius * math.sin(rad) * 0.5) # Adjust for terminal aspect ratio
|
|
117
|
+
|
|
118
|
+
if 0 <= x < width and 0 <= y < height:
|
|
119
|
+
renderer.write_at(x, y, char, color)
|
|
120
|
+
|
|
121
|
+
@staticmethod
|
|
122
|
+
def draw_wave(renderer, y: int, amplitude: int, frequency: float, phase: float,
|
|
123
|
+
char: str = "~", color: str = ColorPalette.BLUE):
|
|
124
|
+
"""Draw a wave pattern.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
renderer: FullScreenRenderer instance
|
|
128
|
+
y: Base row position
|
|
129
|
+
amplitude: Wave height
|
|
130
|
+
frequency: Wave frequency
|
|
131
|
+
phase: Wave phase offset
|
|
132
|
+
char: Character to draw
|
|
133
|
+
color: Wave color
|
|
134
|
+
"""
|
|
135
|
+
width, height = renderer.get_terminal_size()
|
|
136
|
+
|
|
137
|
+
for x in range(width):
|
|
138
|
+
wave_y = int(y + amplitude * math.sin(frequency * x + phase))
|
|
139
|
+
if 0 <= wave_y < height:
|
|
140
|
+
renderer.write_at(x, wave_y, char, color)
|
|
141
|
+
|
|
142
|
+
@staticmethod
|
|
143
|
+
def fill_area(renderer, x: int, y: int, width: int, height: int,
|
|
144
|
+
char: str = " ", color: str = ColorPalette.RESET):
|
|
145
|
+
"""Fill an area with a character.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
renderer: FullScreenRenderer instance
|
|
149
|
+
x, y: Top-left position
|
|
150
|
+
width, height: Area dimensions
|
|
151
|
+
char: Fill character
|
|
152
|
+
color: Fill color
|
|
153
|
+
"""
|
|
154
|
+
term_width, term_height = renderer.get_terminal_size()
|
|
155
|
+
|
|
156
|
+
for row in range(height):
|
|
157
|
+
for col in range(width):
|
|
158
|
+
draw_x, draw_y = x + col, y + row
|
|
159
|
+
if 0 <= draw_x < term_width and 0 <= draw_y < term_height:
|
|
160
|
+
renderer.write_at(draw_x, draw_y, char, color)
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""Matrix rain components for the full-screen framework."""
|
|
2
|
+
|
|
3
|
+
import random
|
|
4
|
+
from typing import List
|
|
5
|
+
from ...io.visual_effects import ColorPalette
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class MatrixColumn:
|
|
9
|
+
"""A single column of falling Matrix characters."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, x: int, height: int):
|
|
12
|
+
"""Initialize Matrix column.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
x: X position (column number)
|
|
16
|
+
height: Terminal height
|
|
17
|
+
"""
|
|
18
|
+
self.x = x
|
|
19
|
+
self.height = height
|
|
20
|
+
self.chars: List[str] = []
|
|
21
|
+
self.positions: List[int] = []
|
|
22
|
+
self.speed = random.uniform(1.5, 4.0)
|
|
23
|
+
self.next_update = 0
|
|
24
|
+
self.length = random.randint(5, 25)
|
|
25
|
+
|
|
26
|
+
# Matrix character set (katakana, numbers, symbols)
|
|
27
|
+
self.matrix_chars = [
|
|
28
|
+
'ア', 'イ', 'ウ', 'エ', 'オ', 'カ', 'キ', 'ク', 'ケ', 'コ',
|
|
29
|
+
'サ', 'シ', 'ス', 'セ', 'ソ', 'タ', 'チ', 'ツ', 'テ', 'ト',
|
|
30
|
+
'ナ', 'ニ', 'ヌ', 'ネ', 'ノ', 'ハ', 'ヒ', 'フ', 'ヘ', 'ホ',
|
|
31
|
+
'マ', 'ミ', 'ム', 'メ', 'モ', 'ヤ', 'ユ', 'ヨ', 'ラ', 'リ',
|
|
32
|
+
'ル', 'レ', 'ロ', 'ワ', 'ヲ', 'ン', '0', '1', '2', '3', '4',
|
|
33
|
+
'5', '6', '7', '8', '9', ':', '.', '"', '=', '*', '+', '<',
|
|
34
|
+
'>', '|', '\\', '/', '[', ']', '{', '}', '(', ')', '-', '_'
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
self._reset()
|
|
38
|
+
|
|
39
|
+
def _reset(self):
|
|
40
|
+
"""Reset column to start falling from top."""
|
|
41
|
+
self.chars = [random.choice(self.matrix_chars) for _ in range(self.length)]
|
|
42
|
+
self.positions = list(range(-self.length, 0))
|
|
43
|
+
self.speed = random.uniform(1.2, 3.5)
|
|
44
|
+
self.next_update = 0
|
|
45
|
+
|
|
46
|
+
def update(self, time: float) -> bool:
|
|
47
|
+
"""Update column positions.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
time: Current time
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
True if column is still active
|
|
54
|
+
"""
|
|
55
|
+
if time < self.next_update:
|
|
56
|
+
return True
|
|
57
|
+
|
|
58
|
+
self.next_update = time + (1.0 / self.speed)
|
|
59
|
+
|
|
60
|
+
# Move all positions down
|
|
61
|
+
for i in range(len(self.positions)):
|
|
62
|
+
self.positions[i] += 1
|
|
63
|
+
|
|
64
|
+
# Remove characters that have fallen off screen
|
|
65
|
+
while self.positions and self.positions[0] >= self.height:
|
|
66
|
+
self.positions.pop(0)
|
|
67
|
+
self.chars.pop(0)
|
|
68
|
+
|
|
69
|
+
# Check if column is done
|
|
70
|
+
if not self.positions:
|
|
71
|
+
# Random chance to restart
|
|
72
|
+
if random.random() < 0.1:
|
|
73
|
+
self._reset()
|
|
74
|
+
return False
|
|
75
|
+
|
|
76
|
+
# Randomly change some characters
|
|
77
|
+
for i in range(len(self.chars)):
|
|
78
|
+
if random.random() < 0.05:
|
|
79
|
+
self.chars[i] = random.choice(self.matrix_chars)
|
|
80
|
+
|
|
81
|
+
return True
|
|
82
|
+
|
|
83
|
+
def render(self, renderer):
|
|
84
|
+
"""Render column using the full-screen renderer.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
renderer: FullScreenRenderer instance
|
|
88
|
+
"""
|
|
89
|
+
for i, (char, pos) in enumerate(zip(self.chars, self.positions)):
|
|
90
|
+
if 0 <= pos < self.height:
|
|
91
|
+
# Brightest character at the head
|
|
92
|
+
if i == len(self.chars) - 1:
|
|
93
|
+
color = ColorPalette.BRIGHT_WHITE
|
|
94
|
+
# Bright green for recent characters
|
|
95
|
+
elif i >= len(self.chars) - 3:
|
|
96
|
+
color = ColorPalette.BRIGHT_GREEN
|
|
97
|
+
# Normal green for middle
|
|
98
|
+
elif i >= len(self.chars) - 8:
|
|
99
|
+
color = ColorPalette.GREEN
|
|
100
|
+
# Dim green for tail
|
|
101
|
+
else:
|
|
102
|
+
color = ColorPalette.DIM_GREEN
|
|
103
|
+
|
|
104
|
+
# Write character at position
|
|
105
|
+
renderer.write_at(self.x, pos, char, color)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class MatrixRenderer:
|
|
109
|
+
"""Renders the complete Matrix rain effect using the full-screen framework."""
|
|
110
|
+
|
|
111
|
+
def __init__(self, terminal_width: int, terminal_height: int):
|
|
112
|
+
"""Initialize Matrix renderer.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
terminal_width: Terminal width in columns
|
|
116
|
+
terminal_height: Terminal height in rows
|
|
117
|
+
"""
|
|
118
|
+
self.terminal_width = terminal_width
|
|
119
|
+
self.terminal_height = terminal_height
|
|
120
|
+
self.columns: List[MatrixColumn] = []
|
|
121
|
+
self.start_time = 0
|
|
122
|
+
|
|
123
|
+
# Create initial columns
|
|
124
|
+
self._create_columns()
|
|
125
|
+
|
|
126
|
+
def _create_columns(self):
|
|
127
|
+
"""Create initial set of Matrix columns."""
|
|
128
|
+
self.columns = []
|
|
129
|
+
for x in range(self.terminal_width):
|
|
130
|
+
if random.random() < 0.5: # 50% chance for each column to be active
|
|
131
|
+
column = MatrixColumn(x, self.terminal_height)
|
|
132
|
+
# Stagger start times
|
|
133
|
+
column.next_update = random.uniform(0, 3.0)
|
|
134
|
+
self.columns.append(column)
|
|
135
|
+
|
|
136
|
+
def update(self, current_time: float):
|
|
137
|
+
"""Update all Matrix columns.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
current_time: Current time for animation
|
|
141
|
+
"""
|
|
142
|
+
# Update all columns
|
|
143
|
+
active_columns = []
|
|
144
|
+
for column in self.columns:
|
|
145
|
+
if column.update(current_time):
|
|
146
|
+
active_columns.append(column)
|
|
147
|
+
|
|
148
|
+
self.columns = active_columns
|
|
149
|
+
|
|
150
|
+
# Add new columns occasionally
|
|
151
|
+
if len(self.columns) < self.terminal_width * 0.4 and random.random() < 0.02:
|
|
152
|
+
x = random.randint(0, self.terminal_width - 1)
|
|
153
|
+
# Make sure we don't have a column too close
|
|
154
|
+
if not any(abs(col.x - x) < 2 for col in self.columns):
|
|
155
|
+
column = MatrixColumn(x, self.terminal_height)
|
|
156
|
+
self.columns.append(column)
|
|
157
|
+
|
|
158
|
+
def render(self, renderer):
|
|
159
|
+
"""Render all Matrix columns.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
renderer: FullScreenRenderer instance
|
|
163
|
+
"""
|
|
164
|
+
# Clear screen
|
|
165
|
+
renderer.clear_screen()
|
|
166
|
+
|
|
167
|
+
# Render all columns
|
|
168
|
+
for column in self.columns:
|
|
169
|
+
column.render(renderer)
|
|
170
|
+
|
|
171
|
+
# Flush output
|
|
172
|
+
renderer.flush()
|
|
173
|
+
|
|
174
|
+
def reset(self):
|
|
175
|
+
"""Reset the Matrix renderer."""
|
|
176
|
+
self._create_columns()
|
|
177
|
+
self.start_time = 0
|