@delorenj/claude-notifications 1.2.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 (67) 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 +424 -305
  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 +121 -5
  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/test.sh +1 -1
  45. package/zellij-plugin/.cargo/config.toml +5 -0
  46. package/zellij-plugin/.github/workflows/ci.yml +97 -0
  47. package/zellij-plugin/Cargo.lock +3558 -0
  48. package/zellij-plugin/Cargo.toml +40 -0
  49. package/zellij-plugin/README.md +290 -0
  50. package/zellij-plugin/build.sh +179 -0
  51. package/zellij-plugin/configs/examples/accessibility.kdl +31 -0
  52. package/zellij-plugin/configs/examples/catppuccin.kdl +32 -0
  53. package/zellij-plugin/configs/examples/default.kdl +34 -0
  54. package/zellij-plugin/configs/examples/minimal.kdl +22 -0
  55. package/zellij-plugin/docs/CONFIGURATION.md +191 -0
  56. package/zellij-plugin/docs/INTEGRATION.md +333 -0
  57. package/zellij-plugin/src/animation.rs +451 -0
  58. package/zellij-plugin/src/colors.rs +407 -0
  59. package/zellij-plugin/src/config.rs +664 -0
  60. package/zellij-plugin/src/event_bridge.rs +339 -0
  61. package/zellij-plugin/src/main.rs +420 -0
  62. package/zellij-plugin/src/notification.rs +466 -0
  63. package/zellij-plugin/src/queue.rs +399 -0
  64. package/zellij-plugin/src/renderer.rs +477 -0
  65. package/zellij-plugin/src/state.rs +338 -0
  66. package/zellij-plugin/src/tests.rs +413 -0
  67. package/ruv-swarm-mcp.db +0 -0
@@ -0,0 +1,407 @@
1
+ //! Color management module for Zellij Visual Notifications
2
+ //!
3
+ //! Handles terminal color capabilities, theme colors, and color interpolation for animations.
4
+
5
+ use crate::config::ThemeConfig;
6
+ use crate::notification::NotificationType;
7
+
8
+ /// Color manager for handling terminal colors
9
+ #[derive(Debug, Clone)]
10
+ pub struct ColorManager {
11
+ /// Current theme configuration
12
+ theme: ThemeConfig,
13
+ /// Detected color capability
14
+ color_capability: ColorCapability,
15
+ /// High contrast mode enabled
16
+ high_contrast: bool,
17
+ }
18
+
19
+ impl Default for ColorManager {
20
+ fn default() -> Self {
21
+ Self {
22
+ theme: ThemeConfig::default(),
23
+ color_capability: ColorCapability::TrueColor,
24
+ high_contrast: false,
25
+ }
26
+ }
27
+ }
28
+
29
+ impl ColorManager {
30
+ /// Create a new color manager with the given theme
31
+ pub fn new(theme: &ThemeConfig) -> Self {
32
+ Self {
33
+ theme: theme.clone(),
34
+ color_capability: Self::detect_capability(),
35
+ high_contrast: false,
36
+ }
37
+ }
38
+
39
+ /// Detect terminal color capability
40
+ fn detect_capability() -> ColorCapability {
41
+ // In WASM environment, we can't directly check environment variables
42
+ // Default to TrueColor as Zellij supports it
43
+ ColorCapability::TrueColor
44
+ }
45
+
46
+ /// Set high contrast mode
47
+ pub fn set_high_contrast(&mut self, enabled: bool) {
48
+ self.high_contrast = enabled;
49
+ }
50
+
51
+ /// Get the notification color based on type
52
+ pub fn get_notification_color(&self, notification_type: &NotificationType) -> Option<String> {
53
+ let base_color = match notification_type {
54
+ NotificationType::Success => &self.theme.success_color,
55
+ NotificationType::Error => &self.theme.error_color,
56
+ NotificationType::Warning => &self.theme.warning_color,
57
+ NotificationType::Info => &self.theme.info_color,
58
+ NotificationType::Progress => &self.theme.highlight_color,
59
+ NotificationType::Attention => &self.theme.warning_color,
60
+ };
61
+
62
+ Some(self.adjust_for_capability(base_color))
63
+ }
64
+
65
+ /// Get the background color
66
+ pub fn get_background_color(&self) -> String {
67
+ self.adjust_for_capability(&self.theme.background_color)
68
+ }
69
+
70
+ /// Get the foreground color
71
+ pub fn get_foreground_color(&self) -> String {
72
+ self.adjust_for_capability(&self.theme.foreground_color)
73
+ }
74
+
75
+ /// Get the dimmed color
76
+ pub fn get_dimmed_color(&self) -> String {
77
+ self.adjust_for_capability(&self.theme.dimmed_color)
78
+ }
79
+
80
+ /// Adjust color based on terminal capability and high contrast mode
81
+ fn adjust_for_capability(&self, hex_color: &str) -> String {
82
+ let color = Color::from_hex(hex_color);
83
+
84
+ if self.high_contrast {
85
+ // Increase contrast
86
+ let adjusted = color.increase_contrast();
87
+ return match self.color_capability {
88
+ ColorCapability::TrueColor => adjusted.to_hex(),
89
+ ColorCapability::Color256 => adjusted.to_ansi256().to_string(),
90
+ ColorCapability::Color16 => adjusted.to_ansi16().to_string(),
91
+ };
92
+ }
93
+
94
+ match self.color_capability {
95
+ ColorCapability::TrueColor => hex_color.to_string(),
96
+ ColorCapability::Color256 => color.to_ansi256().to_string(),
97
+ ColorCapability::Color16 => color.to_ansi16().to_string(),
98
+ }
99
+ }
100
+
101
+ /// Interpolate between two colors based on a factor (0.0 - 1.0)
102
+ pub fn interpolate(&self, color1: &str, color2: &str, factor: f32) -> String {
103
+ let c1 = Color::from_hex(color1);
104
+ let c2 = Color::from_hex(color2);
105
+ let result = c1.interpolate(&c2, factor);
106
+ result.to_hex()
107
+ }
108
+
109
+ /// Apply brightness to a color
110
+ pub fn apply_brightness(&self, hex_color: &str, brightness: f32) -> String {
111
+ let color = Color::from_hex(hex_color);
112
+ let adjusted = color.apply_brightness(brightness);
113
+ adjusted.to_hex()
114
+ }
115
+
116
+ /// Get ANSI escape sequence for setting foreground color
117
+ pub fn fg_escape(&self, hex_color: &str) -> String {
118
+ let color = Color::from_hex(hex_color);
119
+ match self.color_capability {
120
+ ColorCapability::TrueColor => {
121
+ format!("\x1b[38;2;{};{};{}m", color.r, color.g, color.b)
122
+ }
123
+ ColorCapability::Color256 => {
124
+ format!("\x1b[38;5;{}m", color.to_ansi256())
125
+ }
126
+ ColorCapability::Color16 => {
127
+ format!("\x1b[{}m", color.to_ansi16())
128
+ }
129
+ }
130
+ }
131
+
132
+ /// Get ANSI escape sequence for setting background color
133
+ pub fn bg_escape(&self, hex_color: &str) -> String {
134
+ let color = Color::from_hex(hex_color);
135
+ match self.color_capability {
136
+ ColorCapability::TrueColor => {
137
+ format!("\x1b[48;2;{};{};{}m", color.r, color.g, color.b)
138
+ }
139
+ ColorCapability::Color256 => {
140
+ format!("\x1b[48;5;{}m", color.to_ansi256())
141
+ }
142
+ ColorCapability::Color16 => {
143
+ format!("\x1b[{}m", color.to_ansi16() + 10)
144
+ }
145
+ }
146
+ }
147
+
148
+ /// Get ANSI reset escape sequence
149
+ pub fn reset_escape(&self) -> &'static str {
150
+ "\x1b[0m"
151
+ }
152
+ }
153
+
154
+ /// Terminal color capability levels
155
+ #[derive(Debug, Clone, Copy, PartialEq)]
156
+ pub enum ColorCapability {
157
+ /// True color (24-bit RGB)
158
+ TrueColor,
159
+ /// 256 color mode
160
+ Color256,
161
+ /// 16 color mode (basic ANSI)
162
+ Color16,
163
+ }
164
+
165
+ /// RGB Color representation
166
+ #[derive(Debug, Clone, Copy, Default)]
167
+ pub struct Color {
168
+ pub r: u8,
169
+ pub g: u8,
170
+ pub b: u8,
171
+ }
172
+
173
+ impl Color {
174
+ /// Create a new color from RGB values
175
+ pub fn new(r: u8, g: u8, b: u8) -> Self {
176
+ Self { r, g, b }
177
+ }
178
+
179
+ /// Parse color from hex string (supports #RRGGBB and RRGGBB)
180
+ pub fn from_hex(hex: &str) -> Self {
181
+ let hex = hex.trim_start_matches('#');
182
+ if hex.len() != 6 {
183
+ return Self::default();
184
+ }
185
+
186
+ let r = u8::from_str_radix(&hex[0..2], 16).unwrap_or(0);
187
+ let g = u8::from_str_radix(&hex[2..4], 16).unwrap_or(0);
188
+ let b = u8::from_str_radix(&hex[4..6], 16).unwrap_or(0);
189
+
190
+ Self { r, g, b }
191
+ }
192
+
193
+ /// Convert to hex string
194
+ pub fn to_hex(&self) -> String {
195
+ format!("#{:02x}{:02x}{:02x}", self.r, self.g, self.b)
196
+ }
197
+
198
+ /// Convert to ANSI 256 color code
199
+ pub fn to_ansi256(&self) -> u8 {
200
+ // If it's a grayscale color
201
+ if self.r == self.g && self.g == self.b {
202
+ if self.r < 8 {
203
+ return 16;
204
+ }
205
+ if self.r > 248 {
206
+ return 231;
207
+ }
208
+ return ((self.r as f32 - 8.0) / 247.0 * 24.0) as u8 + 232;
209
+ }
210
+
211
+ // Convert to 6x6x6 color cube
212
+ let r = (self.r as f32 / 255.0 * 5.0).round() as u8;
213
+ let g = (self.g as f32 / 255.0 * 5.0).round() as u8;
214
+ let b = (self.b as f32 / 255.0 * 5.0).round() as u8;
215
+
216
+ 16 + 36 * r + 6 * g + b
217
+ }
218
+
219
+ /// Convert to ANSI 16 color code
220
+ pub fn to_ansi16(&self) -> u8 {
221
+ let value = self.r.max(self.g).max(self.b);
222
+
223
+ // If very dark, use black
224
+ if value < 64 {
225
+ return 30;
226
+ }
227
+
228
+ let mut code = 30;
229
+ if self.r > 127 {
230
+ code += 1;
231
+ }
232
+ if self.g > 127 {
233
+ code += 2;
234
+ }
235
+ if self.b > 127 {
236
+ code += 4;
237
+ }
238
+
239
+ // Use bright variants for light colors
240
+ if value > 192 {
241
+ code += 60;
242
+ }
243
+
244
+ code
245
+ }
246
+
247
+ /// Interpolate between two colors
248
+ pub fn interpolate(&self, other: &Color, factor: f32) -> Color {
249
+ let factor = factor.clamp(0.0, 1.0);
250
+ Color {
251
+ r: (self.r as f32 + (other.r as f32 - self.r as f32) * factor) as u8,
252
+ g: (self.g as f32 + (other.g as f32 - self.g as f32) * factor) as u8,
253
+ b: (self.b as f32 + (other.b as f32 - self.b as f32) * factor) as u8,
254
+ }
255
+ }
256
+
257
+ /// Apply brightness multiplier (0.0 = black, 1.0 = original, >1.0 = brighter)
258
+ pub fn apply_brightness(&self, brightness: f32) -> Color {
259
+ Color {
260
+ r: (self.r as f32 * brightness).min(255.0) as u8,
261
+ g: (self.g as f32 * brightness).min(255.0) as u8,
262
+ b: (self.b as f32 * brightness).min(255.0) as u8,
263
+ }
264
+ }
265
+
266
+ /// Increase contrast (move towards white or black)
267
+ pub fn increase_contrast(&self) -> Color {
268
+ let luminance = 0.299 * self.r as f32 + 0.587 * self.g as f32 + 0.114 * self.b as f32;
269
+
270
+ if luminance > 127.0 {
271
+ // Make lighter
272
+ Color {
273
+ r: (self.r as f32 * 1.2).min(255.0) as u8,
274
+ g: (self.g as f32 * 1.2).min(255.0) as u8,
275
+ b: (self.b as f32 * 1.2).min(255.0) as u8,
276
+ }
277
+ } else {
278
+ // Make darker or more saturated
279
+ Color {
280
+ r: (self.r as f32 * 0.9) as u8,
281
+ g: (self.g as f32 * 0.9) as u8,
282
+ b: (self.b as f32 * 0.9) as u8,
283
+ }
284
+ }
285
+ }
286
+
287
+ /// Calculate luminance (0.0 - 1.0)
288
+ pub fn luminance(&self) -> f32 {
289
+ (0.299 * self.r as f32 + 0.587 * self.g as f32 + 0.114 * self.b as f32) / 255.0
290
+ }
291
+
292
+ /// Check if color is considered "light"
293
+ pub fn is_light(&self) -> bool {
294
+ self.luminance() > 0.5
295
+ }
296
+ }
297
+
298
+ /// Predefined colors for quick access
299
+ pub mod colors {
300
+ use super::Color;
301
+
302
+ pub const BLACK: Color = Color { r: 0, g: 0, b: 0 };
303
+ pub const WHITE: Color = Color { r: 255, g: 255, b: 255 };
304
+ pub const RED: Color = Color { r: 255, g: 0, b: 0 };
305
+ pub const GREEN: Color = Color { r: 0, g: 255, b: 0 };
306
+ pub const BLUE: Color = Color { r: 0, g: 0, b: 255 };
307
+ pub const YELLOW: Color = Color { r: 255, g: 255, b: 0 };
308
+ pub const CYAN: Color = Color { r: 0, g: 255, b: 255 };
309
+ pub const MAGENTA: Color = Color { r: 255, g: 0, b: 255 };
310
+ }
311
+
312
+ /// Generate a color gradient for animations
313
+ pub fn generate_gradient(start: &Color, end: &Color, steps: usize) -> Vec<Color> {
314
+ (0..steps)
315
+ .map(|i| {
316
+ let factor = i as f32 / (steps - 1) as f32;
317
+ start.interpolate(end, factor)
318
+ })
319
+ .collect()
320
+ }
321
+
322
+ /// Generate a pulse gradient (start -> end -> start)
323
+ pub fn generate_pulse_gradient(base: &Color, bright: &Color, steps: usize) -> Vec<Color> {
324
+ let half_steps = steps / 2;
325
+ let mut gradient = Vec::with_capacity(steps);
326
+
327
+ // First half: base -> bright
328
+ for i in 0..half_steps {
329
+ let factor = i as f32 / half_steps as f32;
330
+ gradient.push(base.interpolate(bright, factor));
331
+ }
332
+
333
+ // Second half: bright -> base
334
+ for i in 0..(steps - half_steps) {
335
+ let factor = i as f32 / (steps - half_steps) as f32;
336
+ gradient.push(bright.interpolate(base, factor));
337
+ }
338
+
339
+ gradient
340
+ }
341
+
342
+ #[cfg(test)]
343
+ mod tests {
344
+ use super::*;
345
+
346
+ #[test]
347
+ fn test_color_from_hex() {
348
+ let color = Color::from_hex("#ff5500");
349
+ assert_eq!(color.r, 255);
350
+ assert_eq!(color.g, 85);
351
+ assert_eq!(color.b, 0);
352
+
353
+ let color2 = Color::from_hex("00ff00");
354
+ assert_eq!(color2.r, 0);
355
+ assert_eq!(color2.g, 255);
356
+ assert_eq!(color2.b, 0);
357
+ }
358
+
359
+ #[test]
360
+ fn test_color_to_hex() {
361
+ let color = Color::new(255, 128, 64);
362
+ assert_eq!(color.to_hex(), "#ff8040");
363
+ }
364
+
365
+ #[test]
366
+ fn test_color_interpolation() {
367
+ let black = Color::new(0, 0, 0);
368
+ let white = Color::new(255, 255, 255);
369
+
370
+ let mid = black.interpolate(&white, 0.5);
371
+ assert!(mid.r > 120 && mid.r < 135);
372
+ assert!(mid.g > 120 && mid.g < 135);
373
+ assert!(mid.b > 120 && mid.b < 135);
374
+ }
375
+
376
+ #[test]
377
+ fn test_color_brightness() {
378
+ let color = Color::new(100, 100, 100);
379
+ let brighter = color.apply_brightness(1.5);
380
+ assert_eq!(brighter.r, 150);
381
+
382
+ let darker = color.apply_brightness(0.5);
383
+ assert_eq!(darker.r, 50);
384
+ }
385
+
386
+ #[test]
387
+ fn test_ansi256_conversion() {
388
+ let red = Color::new(255, 0, 0);
389
+ let ansi = red.to_ansi256();
390
+ assert!(ansi >= 16 && ansi <= 231);
391
+
392
+ let gray = Color::new(128, 128, 128);
393
+ let ansi_gray = gray.to_ansi256();
394
+ assert!(ansi_gray >= 232 || (ansi_gray >= 16 && ansi_gray <= 231));
395
+ }
396
+
397
+ #[test]
398
+ fn test_gradient_generation() {
399
+ let start = Color::new(0, 0, 0);
400
+ let end = Color::new(255, 255, 255);
401
+ let gradient = generate_gradient(&start, &end, 5);
402
+
403
+ assert_eq!(gradient.len(), 5);
404
+ assert_eq!(gradient[0].r, 0);
405
+ assert_eq!(gradient[4].r, 255);
406
+ }
407
+ }