@delorenj/claude-notifications 2.0.0 → 2.1.0

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 (206) hide show
  1. package/DO.md +5 -0
  2. package/FIXES-APPLIED.md +195 -0
  3. package/INTEGRATION.md +445 -0
  4. package/LAYOUT-INTEGRATION.md +191 -0
  5. package/QUICK-REFERENCE.md +195 -0
  6. package/README.md +145 -14
  7. package/TASK.md +15 -0
  8. package/ZELLIJ-NOTIFY.md +523 -0
  9. package/_bmad-output/implementation-artifacts/spec-install-multi-cli-hooks.md +241 -0
  10. package/bin/claude-notifications.js +417 -312
  11. package/bin/claude-notify.js +47 -1
  12. package/bin/zellij-notify.js +346 -0
  13. package/bun.lock +35 -0
  14. package/diagnose-zellij.sh +105 -0
  15. package/examples/settings-with-zellij.json +18 -0
  16. package/examples/settings-zellij-only.json +18 -0
  17. package/examples/zellij-notify-examples.sh +143 -0
  18. package/lib/adapters/_stub.js +35 -0
  19. package/lib/adapters/auggie.js +10 -0
  20. package/lib/adapters/claude-code.js +181 -0
  21. package/lib/adapters/codex.js +10 -0
  22. package/lib/adapters/copilot.js +10 -0
  23. package/lib/adapters/gemini.js +10 -0
  24. package/lib/adapters/index.js +240 -0
  25. package/lib/adapters/kimi.js +10 -0
  26. package/lib/adapters/opencode.js +14 -0
  27. package/lib/adapters/vibe.js +10 -0
  28. package/lib/config.js +44 -8
  29. package/lib/tui.js +115 -0
  30. package/lib/zellij.js +248 -0
  31. package/package.json +6 -4
  32. package/postinstall.js +28 -25
  33. package/preuninstall.js +18 -9
  34. package/test/adapters/claude-code.test.js +144 -0
  35. package/test/adapters/patches.test.js +81 -0
  36. package/test/adapters/registry.test.js +89 -0
  37. package/test/adapters/stubs.test.js +46 -0
  38. package/test/cli-json.test.js +79 -0
  39. package/test/helpers/fake-fs.js +59 -0
  40. package/test-integration.sh +113 -0
  41. package/test-notification-plugin.kdl +34 -0
  42. package/test-updated-layout.sh +75 -0
  43. package/test-zellij-cli.sh +72 -0
  44. package/zellij-plugin/.cargo/config.toml +5 -0
  45. package/zellij-plugin/.github/workflows/ci.yml +97 -0
  46. package/zellij-plugin/Cargo.lock +3558 -0
  47. package/zellij-plugin/Cargo.toml +40 -0
  48. package/zellij-plugin/README.md +290 -0
  49. package/zellij-plugin/build.sh +179 -0
  50. package/zellij-plugin/configs/examples/accessibility.kdl +31 -0
  51. package/zellij-plugin/configs/examples/catppuccin.kdl +32 -0
  52. package/zellij-plugin/configs/examples/default.kdl +34 -0
  53. package/zellij-plugin/configs/examples/minimal.kdl +22 -0
  54. package/zellij-plugin/docs/CONFIGURATION.md +191 -0
  55. package/zellij-plugin/docs/INTEGRATION.md +333 -0
  56. package/zellij-plugin/src/animation.rs +451 -0
  57. package/zellij-plugin/src/colors.rs +407 -0
  58. package/zellij-plugin/src/config.rs +664 -0
  59. package/zellij-plugin/src/event_bridge.rs +339 -0
  60. package/zellij-plugin/src/main.rs +420 -0
  61. package/zellij-plugin/src/notification.rs +466 -0
  62. package/zellij-plugin/src/queue.rs +399 -0
  63. package/zellij-plugin/src/renderer.rs +477 -0
  64. package/zellij-plugin/src/state.rs +338 -0
  65. package/zellij-plugin/src/tests.rs +413 -0
  66. package/.claude/checkpoints/1756392335.json +0 -1
  67. package/.claude/checkpoints/1756392341.json +0 -1
  68. package/.claude/checkpoints/1756392347.json +0 -1
  69. package/.claude/checkpoints/1756392376.json +0 -1
  70. package/.claude/checkpoints/1756392377.json +0 -1
  71. package/.claude/checkpoints/1756392386.json +0 -1
  72. package/.claude/checkpoints/1756392387.json +0 -1
  73. package/.claude/checkpoints/1756392398.json +0 -1
  74. package/.claude/checkpoints/1756392400.json +0 -1
  75. package/.claude/checkpoints/1756392427.json +0 -1
  76. package/.claude/checkpoints/1756392428.json +0 -1
  77. package/.claude/checkpoints/1756392486.json +0 -1
  78. package/.claude/checkpoints/1756392488.json +0 -1
  79. package/.claude/checkpoints/1756392558.json +0 -1
  80. package/.claude/checkpoints/1756392559.json +0 -1
  81. package/.claude/checkpoints/summary-session-20250828-105040.md +0 -57
  82. package/.claude/checkpoints/task-1756392207.json +0 -1
  83. package/.claude/checkpoints/task-1756392742.json +0 -1
  84. package/.claude/commands/analysis/COMMAND_COMPLIANCE_REPORT.md +0 -54
  85. package/.claude/commands/analysis/README.md +0 -9
  86. package/.claude/commands/analysis/bottleneck-detect.md +0 -162
  87. package/.claude/commands/analysis/performance-bottlenecks.md +0 -59
  88. package/.claude/commands/analysis/performance-report.md +0 -25
  89. package/.claude/commands/analysis/token-efficiency.md +0 -45
  90. package/.claude/commands/analysis/token-usage.md +0 -25
  91. package/.claude/commands/automation/README.md +0 -9
  92. package/.claude/commands/automation/auto-agent.md +0 -122
  93. package/.claude/commands/automation/self-healing.md +0 -106
  94. package/.claude/commands/automation/session-memory.md +0 -90
  95. package/.claude/commands/automation/smart-agents.md +0 -73
  96. package/.claude/commands/automation/smart-spawn.md +0 -25
  97. package/.claude/commands/automation/workflow-select.md +0 -25
  98. package/.claude/commands/coordination/README.md +0 -9
  99. package/.claude/commands/coordination/agent-spawn.md +0 -25
  100. package/.claude/commands/coordination/init.md +0 -44
  101. package/.claude/commands/coordination/orchestrate.md +0 -43
  102. package/.claude/commands/coordination/spawn.md +0 -45
  103. package/.claude/commands/coordination/swarm-init.md +0 -85
  104. package/.claude/commands/coordination/task-orchestrate.md +0 -25
  105. package/.claude/commands/github/README.md +0 -11
  106. package/.claude/commands/github/code-review-swarm.md +0 -514
  107. package/.claude/commands/github/code-review.md +0 -25
  108. package/.claude/commands/github/github-modes.md +0 -147
  109. package/.claude/commands/github/github-swarm.md +0 -121
  110. package/.claude/commands/github/issue-tracker.md +0 -292
  111. package/.claude/commands/github/issue-triage.md +0 -25
  112. package/.claude/commands/github/multi-repo-swarm.md +0 -519
  113. package/.claude/commands/github/pr-enhance.md +0 -26
  114. package/.claude/commands/github/pr-manager.md +0 -170
  115. package/.claude/commands/github/project-board-sync.md +0 -471
  116. package/.claude/commands/github/release-manager.md +0 -338
  117. package/.claude/commands/github/release-swarm.md +0 -544
  118. package/.claude/commands/github/repo-analyze.md +0 -25
  119. package/.claude/commands/github/repo-architect.md +0 -367
  120. package/.claude/commands/github/swarm-issue.md +0 -482
  121. package/.claude/commands/github/swarm-pr.md +0 -285
  122. package/.claude/commands/github/sync-coordinator.md +0 -301
  123. package/.claude/commands/github/workflow-automation.md +0 -442
  124. package/.claude/commands/hooks/README.md +0 -11
  125. package/.claude/commands/hooks/overview.md +0 -58
  126. package/.claude/commands/hooks/post-edit.md +0 -117
  127. package/.claude/commands/hooks/post-task.md +0 -112
  128. package/.claude/commands/hooks/pre-edit.md +0 -113
  129. package/.claude/commands/hooks/pre-task.md +0 -111
  130. package/.claude/commands/hooks/session-end.md +0 -118
  131. package/.claude/commands/hooks/setup.md +0 -103
  132. package/.claude/commands/memory/README.md +0 -9
  133. package/.claude/commands/memory/memory-persist.md +0 -25
  134. package/.claude/commands/memory/memory-search.md +0 -25
  135. package/.claude/commands/memory/memory-usage.md +0 -25
  136. package/.claude/commands/memory/neural.md +0 -47
  137. package/.claude/commands/memory/usage.md +0 -46
  138. package/.claude/commands/monitoring/README.md +0 -9
  139. package/.claude/commands/monitoring/agent-metrics.md +0 -25
  140. package/.claude/commands/monitoring/agents.md +0 -44
  141. package/.claude/commands/monitoring/real-time-view.md +0 -25
  142. package/.claude/commands/monitoring/status.md +0 -46
  143. package/.claude/commands/monitoring/swarm-monitor.md +0 -25
  144. package/.claude/commands/optimization/README.md +0 -9
  145. package/.claude/commands/optimization/auto-topology.md +0 -62
  146. package/.claude/commands/optimization/cache-manage.md +0 -25
  147. package/.claude/commands/optimization/parallel-execute.md +0 -25
  148. package/.claude/commands/optimization/parallel-execution.md +0 -50
  149. package/.claude/commands/optimization/topology-optimize.md +0 -25
  150. package/.claude/commands/pair/README.md +0 -261
  151. package/.claude/commands/pair/commands.md +0 -546
  152. package/.claude/commands/pair/config.md +0 -510
  153. package/.claude/commands/pair/examples.md +0 -512
  154. package/.claude/commands/pair/modes.md +0 -348
  155. package/.claude/commands/pair/session.md +0 -407
  156. package/.claude/commands/pair/start.md +0 -209
  157. package/.claude/commands/sparc/analyzer.md +0 -52
  158. package/.claude/commands/sparc/architect.md +0 -53
  159. package/.claude/commands/sparc/batch-executor.md +0 -54
  160. package/.claude/commands/sparc/coder.md +0 -54
  161. package/.claude/commands/sparc/debugger.md +0 -54
  162. package/.claude/commands/sparc/designer.md +0 -53
  163. package/.claude/commands/sparc/documenter.md +0 -54
  164. package/.claude/commands/sparc/innovator.md +0 -54
  165. package/.claude/commands/sparc/memory-manager.md +0 -54
  166. package/.claude/commands/sparc/optimizer.md +0 -54
  167. package/.claude/commands/sparc/orchestrator.md +0 -132
  168. package/.claude/commands/sparc/researcher.md +0 -54
  169. package/.claude/commands/sparc/reviewer.md +0 -54
  170. package/.claude/commands/sparc/sparc-modes.md +0 -174
  171. package/.claude/commands/sparc/swarm-coordinator.md +0 -54
  172. package/.claude/commands/sparc/tdd.md +0 -54
  173. package/.claude/commands/sparc/tester.md +0 -54
  174. package/.claude/commands/sparc/workflow-manager.md +0 -54
  175. package/.claude/commands/stream-chain/pipeline.md +0 -121
  176. package/.claude/commands/stream-chain/run.md +0 -70
  177. package/.claude/commands/swarm/analysis.md +0 -95
  178. package/.claude/commands/swarm/development.md +0 -96
  179. package/.claude/commands/swarm/examples.md +0 -168
  180. package/.claude/commands/swarm/maintenance.md +0 -102
  181. package/.claude/commands/swarm/optimization.md +0 -117
  182. package/.claude/commands/swarm/research.md +0 -136
  183. package/.claude/commands/swarm/testing.md +0 -131
  184. package/.claude/commands/training/README.md +0 -9
  185. package/.claude/commands/training/model-update.md +0 -25
  186. package/.claude/commands/training/neural-patterns.md +0 -74
  187. package/.claude/commands/training/neural-train.md +0 -25
  188. package/.claude/commands/training/pattern-learn.md +0 -25
  189. package/.claude/commands/training/specialization.md +0 -63
  190. package/.claude/commands/truth/start.md +0 -143
  191. package/.claude/commands/verify/check.md +0 -50
  192. package/.claude/commands/verify/start.md +0 -128
  193. package/.claude/commands/workflows/README.md +0 -9
  194. package/.claude/commands/workflows/development.md +0 -78
  195. package/.claude/commands/workflows/research.md +0 -63
  196. package/.claude/commands/workflows/workflow-create.md +0 -25
  197. package/.claude/commands/workflows/workflow-execute.md +0 -25
  198. package/.claude/commands/workflows/workflow-export.md +0 -25
  199. package/.claude/config.json +0 -36
  200. package/.claude/settings.json +0 -162
  201. package/.claude-flow/metrics/agent-metrics.json +0 -1
  202. package/.claude-flow/metrics/performance.json +0 -9
  203. package/.claude-flow/metrics/system-metrics.json +0 -230
  204. package/.claude-flow/metrics/task-metrics.json +0 -10
  205. package/FIXES.md +0 -75
  206. package/test-results.md +0 -163
@@ -0,0 +1,477 @@
1
+ //! Renderer module for Zellij Visual Notifications
2
+ //!
3
+ //! Handles rendering of status bar widgets, pane borders, and badges.
4
+
5
+ use std::collections::BTreeMap;
6
+ use crate::animation::AnimationEngine;
7
+ use crate::colors::ColorManager;
8
+ use crate::config::Config;
9
+ use crate::notification::NotificationType;
10
+ use crate::queue::NotificationQueue;
11
+ use crate::state::VisualState;
12
+
13
+ /// Renderer for visual elements
14
+ #[derive(Debug, Clone)]
15
+ pub struct Renderer {
16
+ /// Show status bar widget
17
+ show_status_bar: bool,
18
+ /// Show border colors
19
+ show_border_colors: bool,
20
+ /// Show tab badges
21
+ show_tab_badges: bool,
22
+ /// Use unicode icons
23
+ use_unicode: bool,
24
+ /// Accessibility mode (patterns instead of colors only)
25
+ use_patterns: bool,
26
+ }
27
+
28
+ impl Default for Renderer {
29
+ fn default() -> Self {
30
+ Self {
31
+ show_status_bar: true,
32
+ show_border_colors: true,
33
+ show_tab_badges: true,
34
+ use_unicode: true,
35
+ use_patterns: true,
36
+ }
37
+ }
38
+ }
39
+
40
+ impl Renderer {
41
+ /// Create a new renderer with configuration
42
+ pub fn new(config: &Config) -> Self {
43
+ Self {
44
+ show_status_bar: config.show_status_bar,
45
+ show_border_colors: config.show_border_colors,
46
+ show_tab_badges: config.show_tab_badges,
47
+ use_unicode: true,
48
+ use_patterns: config.accessibility.use_patterns,
49
+ }
50
+ }
51
+
52
+ /// Render the status bar widget
53
+ pub fn render_status_bar(
54
+ &self,
55
+ rows: usize,
56
+ cols: usize,
57
+ pane_states: &BTreeMap<u32, VisualState>,
58
+ queue: &NotificationQueue,
59
+ color_manager: &ColorManager,
60
+ animation_engine: &AnimationEngine,
61
+ tick: u64,
62
+ ) {
63
+ if !self.show_status_bar || cols < 10 {
64
+ return;
65
+ }
66
+
67
+ // Count active notifications
68
+ let active_count = pane_states.values().filter(|s| s.has_notification()).count();
69
+ let queue_count = queue.len();
70
+
71
+ // Build status bar content
72
+ let content = self.build_status_content(
73
+ active_count,
74
+ queue_count,
75
+ pane_states,
76
+ color_manager,
77
+ animation_engine,
78
+ tick,
79
+ );
80
+
81
+ // Print the status bar (Zellij will capture this)
82
+ print!("{}", content);
83
+ }
84
+
85
+ /// Build the status bar content string
86
+ fn build_status_content(
87
+ &self,
88
+ active_count: usize,
89
+ queue_count: usize,
90
+ pane_states: &BTreeMap<u32, VisualState>,
91
+ color_manager: &ColorManager,
92
+ animation_engine: &AnimationEngine,
93
+ tick: u64,
94
+ ) -> String {
95
+ let mut output = String::new();
96
+
97
+ // Plugin name/icon
98
+ let icon = if self.use_unicode { "\u{1F514}" } else { "[N]" }; // Bell icon
99
+ output.push_str(&format!("{} ", icon));
100
+
101
+ // Show notification counts
102
+ if active_count == 0 && queue_count == 0 {
103
+ output.push_str(&format!("{}No notifications{}",
104
+ color_manager.fg_escape(&color_manager.get_dimmed_color()),
105
+ color_manager.reset_escape()
106
+ ));
107
+ } else {
108
+ // Show active notification indicators
109
+ for (pane_id, state) in pane_states.iter() {
110
+ if let Some(ref notif_type) = state.notification_type {
111
+ if !state.acknowledged {
112
+ let color = color_manager.get_notification_color(notif_type)
113
+ .unwrap_or_else(|| color_manager.get_foreground_color());
114
+
115
+ let brightness = animation_engine.get_brightness(state, tick);
116
+ let adjusted_color = color_manager.apply_brightness(&color, brightness);
117
+
118
+ let icon = self.get_notification_icon(notif_type);
119
+ let pattern = if self.use_patterns {
120
+ self.get_pattern_suffix(notif_type)
121
+ } else {
122
+ ""
123
+ };
124
+
125
+ output.push_str(&format!("{}[{}{}:{}{}]{} ",
126
+ color_manager.fg_escape(&adjusted_color),
127
+ icon,
128
+ pattern,
129
+ pane_id,
130
+ if state.is_animating { "*" } else { "" },
131
+ color_manager.reset_escape()
132
+ ));
133
+ }
134
+ }
135
+ }
136
+
137
+ // Show queue count if any
138
+ if queue_count > 0 {
139
+ output.push_str(&format!("(+{} queued)", queue_count));
140
+ }
141
+ }
142
+
143
+ output
144
+ }
145
+
146
+ /// Get the icon for a notification type
147
+ fn get_notification_icon(&self, notification_type: &NotificationType) -> &'static str {
148
+ if self.use_unicode {
149
+ match notification_type {
150
+ NotificationType::Success => "\u{2714}", // Check mark
151
+ NotificationType::Error => "\u{2718}", // X mark
152
+ NotificationType::Warning => "\u{26A0}", // Warning triangle
153
+ NotificationType::Info => "\u{2139}", // Info symbol
154
+ NotificationType::Progress => "\u{21BB}", // Rotating arrow
155
+ NotificationType::Attention => "\u{2757}", // Exclamation mark
156
+ }
157
+ } else {
158
+ match notification_type {
159
+ NotificationType::Success => "+",
160
+ NotificationType::Error => "X",
161
+ NotificationType::Warning => "!",
162
+ NotificationType::Info => "i",
163
+ NotificationType::Progress => "~",
164
+ NotificationType::Attention => "!",
165
+ }
166
+ }
167
+ }
168
+
169
+ /// Get pattern suffix for accessibility (distinguishes by shape, not just color)
170
+ fn get_pattern_suffix(&self, notification_type: &NotificationType) -> &'static str {
171
+ match notification_type {
172
+ NotificationType::Success => "=", // Double line
173
+ NotificationType::Error => "##", // Hash/blocked
174
+ NotificationType::Warning => "~~", // Wavy
175
+ NotificationType::Info => "..", // Dots
176
+ NotificationType::Progress => "->", // Arrow
177
+ NotificationType::Attention => "!!", // Double exclaim
178
+ }
179
+ }
180
+
181
+ /// Render a pane badge (for tab bar)
182
+ pub fn render_pane_badge(
183
+ &self,
184
+ state: &VisualState,
185
+ color_manager: &ColorManager,
186
+ ) -> Option<String> {
187
+ if !self.show_tab_badges {
188
+ return None;
189
+ }
190
+
191
+ if let Some(ref notif_type) = state.notification_type {
192
+ if !state.acknowledged {
193
+ let icon = self.get_notification_icon(notif_type);
194
+ let color = color_manager.get_notification_color(notif_type)?;
195
+
196
+ return Some(format!("{}{}{}",
197
+ color_manager.fg_escape(&color),
198
+ icon,
199
+ color_manager.reset_escape()
200
+ ));
201
+ }
202
+ }
203
+
204
+ None
205
+ }
206
+
207
+ /// Get border style for a pane
208
+ pub fn get_border_style(
209
+ &self,
210
+ state: &VisualState,
211
+ color_manager: &ColorManager,
212
+ animation_engine: &AnimationEngine,
213
+ tick: u64,
214
+ ) -> Option<BorderStyle> {
215
+ if !self.show_border_colors {
216
+ return None;
217
+ }
218
+
219
+ if let Some(ref notif_type) = state.notification_type {
220
+ if !state.acknowledged {
221
+ let base_color = color_manager.get_notification_color(notif_type)?;
222
+
223
+ // Apply animation brightness
224
+ let brightness = animation_engine.get_brightness(state, tick);
225
+ let color = color_manager.apply_brightness(&base_color, brightness);
226
+
227
+ return Some(BorderStyle {
228
+ color,
229
+ style: if state.is_animating {
230
+ BorderLineStyle::Double
231
+ } else {
232
+ BorderLineStyle::Single
233
+ },
234
+ });
235
+ }
236
+ }
237
+
238
+ None
239
+ }
240
+
241
+ /// Format notification for tooltip/popup
242
+ pub fn format_notification_tooltip(
243
+ &self,
244
+ state: &VisualState,
245
+ _color_manager: &ColorManager,
246
+ ) -> Option<String> {
247
+ if let Some(ref message) = state.notification_message {
248
+ let icon = state.notification_type.as_ref()
249
+ .map(|t| self.get_notification_icon(t))
250
+ .unwrap_or("");
251
+
252
+ Some(format!("{} {}", icon, message))
253
+ } else {
254
+ None
255
+ }
256
+ }
257
+
258
+ /// Create a summary line for multiple notifications
259
+ pub fn render_summary(
260
+ &self,
261
+ pane_states: &BTreeMap<u32, VisualState>,
262
+ color_manager: &ColorManager,
263
+ ) -> String {
264
+ let mut success = 0;
265
+ let mut error = 0;
266
+ let mut warning = 0;
267
+ let mut info = 0;
268
+ let mut attention = 0;
269
+
270
+ for state in pane_states.values() {
271
+ if let Some(ref notif_type) = state.notification_type {
272
+ if !state.acknowledged {
273
+ match notif_type {
274
+ NotificationType::Success => success += 1,
275
+ NotificationType::Error => error += 1,
276
+ NotificationType::Warning => warning += 1,
277
+ NotificationType::Info => info += 1,
278
+ NotificationType::Attention => attention += 1,
279
+ NotificationType::Progress => {}
280
+ }
281
+ }
282
+ }
283
+ }
284
+
285
+ let mut parts = Vec::new();
286
+
287
+ if success > 0 {
288
+ let color = color_manager.get_notification_color(&NotificationType::Success)
289
+ .unwrap_or_default();
290
+ parts.push(format!("{}{}{}{}",
291
+ color_manager.fg_escape(&color),
292
+ self.get_notification_icon(&NotificationType::Success),
293
+ success,
294
+ color_manager.reset_escape()
295
+ ));
296
+ }
297
+ if error > 0 {
298
+ let color = color_manager.get_notification_color(&NotificationType::Error)
299
+ .unwrap_or_default();
300
+ parts.push(format!("{}{}{}{}",
301
+ color_manager.fg_escape(&color),
302
+ self.get_notification_icon(&NotificationType::Error),
303
+ error,
304
+ color_manager.reset_escape()
305
+ ));
306
+ }
307
+ if warning > 0 {
308
+ let color = color_manager.get_notification_color(&NotificationType::Warning)
309
+ .unwrap_or_default();
310
+ parts.push(format!("{}{}{}{}",
311
+ color_manager.fg_escape(&color),
312
+ self.get_notification_icon(&NotificationType::Warning),
313
+ warning,
314
+ color_manager.reset_escape()
315
+ ));
316
+ }
317
+ if attention > 0 {
318
+ let color = color_manager.get_notification_color(&NotificationType::Attention)
319
+ .unwrap_or_default();
320
+ parts.push(format!("{}{}{}{}",
321
+ color_manager.fg_escape(&color),
322
+ self.get_notification_icon(&NotificationType::Attention),
323
+ attention,
324
+ color_manager.reset_escape()
325
+ ));
326
+ }
327
+ if info > 0 {
328
+ let color = color_manager.get_notification_color(&NotificationType::Info)
329
+ .unwrap_or_default();
330
+ parts.push(format!("{}{}{}{}",
331
+ color_manager.fg_escape(&color),
332
+ self.get_notification_icon(&NotificationType::Info),
333
+ info,
334
+ color_manager.reset_escape()
335
+ ));
336
+ }
337
+
338
+ if parts.is_empty() {
339
+ "No notifications".to_string()
340
+ } else {
341
+ parts.join(" ")
342
+ }
343
+ }
344
+ }
345
+
346
+ /// Border style for pane borders
347
+ #[derive(Debug, Clone)]
348
+ pub struct BorderStyle {
349
+ /// Border color (hex)
350
+ pub color: String,
351
+ /// Line style
352
+ pub style: BorderLineStyle,
353
+ }
354
+
355
+ /// Border line styles
356
+ #[derive(Debug, Clone, Copy, PartialEq)]
357
+ pub enum BorderLineStyle {
358
+ /// Single line border
359
+ Single,
360
+ /// Double line border
361
+ Double,
362
+ /// Dashed border
363
+ Dashed,
364
+ /// Dotted border
365
+ Dotted,
366
+ /// Bold/thick border
367
+ Bold,
368
+ }
369
+
370
+ impl BorderLineStyle {
371
+ /// Get the box-drawing characters for this style
372
+ pub fn chars(&self) -> BorderChars {
373
+ match self {
374
+ BorderLineStyle::Single => BorderChars {
375
+ horizontal: '\u{2500}',
376
+ vertical: '\u{2502}',
377
+ top_left: '\u{250C}',
378
+ top_right: '\u{2510}',
379
+ bottom_left: '\u{2514}',
380
+ bottom_right: '\u{2518}',
381
+ },
382
+ BorderLineStyle::Double => BorderChars {
383
+ horizontal: '\u{2550}',
384
+ vertical: '\u{2551}',
385
+ top_left: '\u{2554}',
386
+ top_right: '\u{2557}',
387
+ bottom_left: '\u{255A}',
388
+ bottom_right: '\u{255D}',
389
+ },
390
+ BorderLineStyle::Dashed => BorderChars {
391
+ horizontal: '\u{2504}',
392
+ vertical: '\u{2506}',
393
+ top_left: '\u{250C}',
394
+ top_right: '\u{2510}',
395
+ bottom_left: '\u{2514}',
396
+ bottom_right: '\u{2518}',
397
+ },
398
+ BorderLineStyle::Dotted => BorderChars {
399
+ horizontal: '\u{2508}',
400
+ vertical: '\u{250A}',
401
+ top_left: '\u{250C}',
402
+ top_right: '\u{2510}',
403
+ bottom_left: '\u{2514}',
404
+ bottom_right: '\u{2518}',
405
+ },
406
+ BorderLineStyle::Bold => BorderChars {
407
+ horizontal: '\u{2501}',
408
+ vertical: '\u{2503}',
409
+ top_left: '\u{250F}',
410
+ top_right: '\u{2513}',
411
+ bottom_left: '\u{2517}',
412
+ bottom_right: '\u{251B}',
413
+ },
414
+ }
415
+ }
416
+ }
417
+
418
+ /// Box-drawing characters for borders
419
+ #[derive(Debug, Clone, Copy)]
420
+ pub struct BorderChars {
421
+ pub horizontal: char,
422
+ pub vertical: char,
423
+ pub top_left: char,
424
+ pub top_right: char,
425
+ pub bottom_left: char,
426
+ pub bottom_right: char,
427
+ }
428
+
429
+ #[cfg(test)]
430
+ mod tests {
431
+ use super::*;
432
+
433
+ #[test]
434
+ fn test_renderer_creation() {
435
+ let config = Config::default();
436
+ let renderer = Renderer::new(&config);
437
+ assert!(renderer.show_status_bar);
438
+ assert!(renderer.show_border_colors);
439
+ assert!(renderer.show_tab_badges);
440
+ }
441
+
442
+ #[test]
443
+ fn test_notification_icons() {
444
+ let renderer = Renderer::default();
445
+
446
+ let success_icon = renderer.get_notification_icon(&NotificationType::Success);
447
+ let error_icon = renderer.get_notification_icon(&NotificationType::Error);
448
+
449
+ assert!(!success_icon.is_empty());
450
+ assert!(!error_icon.is_empty());
451
+ assert_ne!(success_icon, error_icon);
452
+ }
453
+
454
+ #[test]
455
+ fn test_border_line_styles() {
456
+ let single = BorderLineStyle::Single;
457
+ let double = BorderLineStyle::Double;
458
+
459
+ let single_chars = single.chars();
460
+ let double_chars = double.chars();
461
+
462
+ assert_ne!(single_chars.horizontal, double_chars.horizontal);
463
+ assert_ne!(single_chars.vertical, double_chars.vertical);
464
+ }
465
+
466
+ #[test]
467
+ fn test_pattern_suffix() {
468
+ let renderer = Renderer::default();
469
+
470
+ let success_pattern = renderer.get_pattern_suffix(&NotificationType::Success);
471
+ let error_pattern = renderer.get_pattern_suffix(&NotificationType::Error);
472
+
473
+ assert!(!success_pattern.is_empty());
474
+ assert!(!error_pattern.is_empty());
475
+ assert_ne!(success_pattern, error_pattern);
476
+ }
477
+ }