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