@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,664 @@
1
+ //! Configuration module for Zellij Visual Notifications
2
+ //!
3
+ //! Handles KDL configuration parsing, validation, and hot-reload functionality.
4
+
5
+ use serde::{Deserialize, Serialize};
6
+ use std::collections::BTreeMap;
7
+
8
+ /// Main plugin configuration
9
+ #[derive(Debug, Clone, Serialize, Deserialize)]
10
+ pub struct Config {
11
+ /// Enable/disable the plugin
12
+ pub enabled: bool,
13
+ /// Theme configuration
14
+ pub theme: ThemeConfig,
15
+ /// Animation configuration
16
+ pub animation: AnimationConfig,
17
+ /// Accessibility configuration
18
+ pub accessibility: AccessibilityConfig,
19
+ /// Notification timeout in milliseconds
20
+ pub notification_timeout_ms: u64,
21
+ /// Maximum queue size
22
+ pub queue_max_size: usize,
23
+ /// Enable status bar widget
24
+ pub show_status_bar: bool,
25
+ /// Enable pane border colors
26
+ pub show_border_colors: bool,
27
+ /// Enable tab badges
28
+ pub show_tab_badges: bool,
29
+ /// IPC socket path (for external communication)
30
+ pub ipc_socket_path: Option<String>,
31
+ /// Debug mode
32
+ pub debug: bool,
33
+ }
34
+
35
+ impl Default for Config {
36
+ fn default() -> Self {
37
+ Self {
38
+ enabled: true,
39
+ theme: ThemeConfig::default(),
40
+ animation: AnimationConfig::default(),
41
+ accessibility: AccessibilityConfig::default(),
42
+ notification_timeout_ms: 300_000, // 5 minutes
43
+ queue_max_size: 100,
44
+ show_status_bar: true,
45
+ show_border_colors: true,
46
+ show_tab_badges: true,
47
+ ipc_socket_path: None,
48
+ debug: false,
49
+ }
50
+ }
51
+ }
52
+
53
+ impl Config {
54
+ /// Create configuration from Zellij plugin configuration map
55
+ pub fn from_plugin_config(config_map: &BTreeMap<String, String>) -> Self {
56
+ let mut config = Config::default();
57
+
58
+ // Parse boolean options
59
+ if let Some(enabled) = config_map.get("enabled") {
60
+ config.enabled = enabled.parse().unwrap_or(true);
61
+ }
62
+ if let Some(debug) = config_map.get("debug") {
63
+ config.debug = debug.parse().unwrap_or(false);
64
+ }
65
+ if let Some(show_status_bar) = config_map.get("show_status_bar") {
66
+ config.show_status_bar = show_status_bar.parse().unwrap_or(true);
67
+ }
68
+ if let Some(show_border_colors) = config_map.get("show_border_colors") {
69
+ config.show_border_colors = show_border_colors.parse().unwrap_or(true);
70
+ }
71
+ if let Some(show_tab_badges) = config_map.get("show_tab_badges") {
72
+ config.show_tab_badges = show_tab_badges.parse().unwrap_or(true);
73
+ }
74
+
75
+ // Parse numeric options
76
+ if let Some(timeout) = config_map.get("notification_timeout_ms") {
77
+ config.notification_timeout_ms = timeout.parse().unwrap_or(300_000);
78
+ }
79
+ if let Some(max_size) = config_map.get("queue_max_size") {
80
+ config.queue_max_size = max_size.parse().unwrap_or(100);
81
+ }
82
+
83
+ // Parse theme
84
+ if let Some(theme_name) = config_map.get("theme") {
85
+ config.theme = ThemeConfig::from_preset(theme_name);
86
+ }
87
+
88
+ // Parse individual colors
89
+ if let Some(success_color) = config_map.get("success_color") {
90
+ config.theme.success_color = success_color.clone();
91
+ }
92
+ if let Some(error_color) = config_map.get("error_color") {
93
+ config.theme.error_color = error_color.clone();
94
+ }
95
+ if let Some(warning_color) = config_map.get("warning_color") {
96
+ config.theme.warning_color = warning_color.clone();
97
+ }
98
+ if let Some(info_color) = config_map.get("info_color") {
99
+ config.theme.info_color = info_color.clone();
100
+ }
101
+
102
+ // Parse animation settings
103
+ if let Some(animation_enabled) = config_map.get("animation_enabled") {
104
+ config.animation.enabled = animation_enabled.parse().unwrap_or(true);
105
+ }
106
+ if let Some(animation_style) = config_map.get("animation_style") {
107
+ config.animation.style = AnimationStyle::from_str(animation_style);
108
+ }
109
+ if let Some(animation_speed) = config_map.get("animation_speed") {
110
+ config.animation.speed = animation_speed.parse().unwrap_or(50);
111
+ }
112
+ if let Some(animation_cycles) = config_map.get("animation_cycles") {
113
+ config.animation.cycles = animation_cycles.parse().unwrap_or(3);
114
+ }
115
+
116
+ // Parse accessibility settings
117
+ if let Some(high_contrast) = config_map.get("high_contrast") {
118
+ config.accessibility.high_contrast = high_contrast.parse().unwrap_or(false);
119
+ }
120
+ if let Some(reduced_motion) = config_map.get("reduced_motion") {
121
+ config.accessibility.reduced_motion = reduced_motion.parse().unwrap_or(false);
122
+ if config.accessibility.reduced_motion {
123
+ config.animation.enabled = false;
124
+ }
125
+ }
126
+
127
+ // Parse IPC socket path
128
+ if let Some(ipc_path) = config_map.get("ipc_socket_path") {
129
+ config.ipc_socket_path = Some(ipc_path.clone());
130
+ }
131
+
132
+ config
133
+ }
134
+
135
+ /// Validate the configuration
136
+ pub fn validate(&self) -> Result<(), String> {
137
+ if self.notification_timeout_ms < 1000 {
138
+ return Err("notification_timeout_ms must be at least 1000ms".to_string());
139
+ }
140
+ if self.queue_max_size < 1 {
141
+ return Err("queue_max_size must be at least 1".to_string());
142
+ }
143
+ if self.animation.speed < 1 || self.animation.speed > 100 {
144
+ return Err("animation_speed must be between 1 and 100".to_string());
145
+ }
146
+ if self.animation.cycles < 1 || self.animation.cycles > 10 {
147
+ return Err("animation_cycles must be between 1 and 10".to_string());
148
+ }
149
+ Ok(())
150
+ }
151
+ }
152
+
153
+ /// Theme configuration
154
+ #[derive(Debug, Clone, Serialize, Deserialize)]
155
+ pub struct ThemeConfig {
156
+ /// Theme name/preset
157
+ pub name: String,
158
+ /// Success notification color (green by default)
159
+ pub success_color: String,
160
+ /// Error notification color (red by default)
161
+ pub error_color: String,
162
+ /// Warning notification color (yellow by default)
163
+ pub warning_color: String,
164
+ /// Info notification color (blue by default)
165
+ pub info_color: String,
166
+ /// Background color for status bar
167
+ pub background_color: String,
168
+ /// Foreground/text color
169
+ pub foreground_color: String,
170
+ /// Border highlight color
171
+ pub highlight_color: String,
172
+ /// Dimmed/muted color
173
+ pub dimmed_color: String,
174
+ }
175
+
176
+ impl Default for ThemeConfig {
177
+ fn default() -> Self {
178
+ Self {
179
+ name: "default".to_string(),
180
+ success_color: "#22c55e".to_string(), // Green
181
+ error_color: "#ef4444".to_string(), // Red
182
+ warning_color: "#eab308".to_string(), // Yellow
183
+ info_color: "#3b82f6".to_string(), // Blue
184
+ background_color: "#1e1e2e".to_string(),
185
+ foreground_color: "#cdd6f4".to_string(),
186
+ highlight_color: "#89b4fa".to_string(),
187
+ dimmed_color: "#6c7086".to_string(),
188
+ }
189
+ }
190
+ }
191
+
192
+ impl ThemeConfig {
193
+ /// Create a theme from a preset name
194
+ pub fn from_preset(name: &str) -> Self {
195
+ match name.to_lowercase().as_str() {
196
+ "dracula" => Self::dracula(),
197
+ "nord" => Self::nord(),
198
+ "solarized" | "solarized-dark" => Self::solarized_dark(),
199
+ "solarized-light" => Self::solarized_light(),
200
+ "catppuccin" | "catppuccin-mocha" => Self::catppuccin_mocha(),
201
+ "catppuccin-latte" => Self::catppuccin_latte(),
202
+ "gruvbox" | "gruvbox-dark" => Self::gruvbox_dark(),
203
+ "gruvbox-light" => Self::gruvbox_light(),
204
+ "tokyo-night" => Self::tokyo_night(),
205
+ "one-dark" => Self::one_dark(),
206
+ _ => Self::default(),
207
+ }
208
+ }
209
+
210
+ /// Dracula theme
211
+ fn dracula() -> Self {
212
+ Self {
213
+ name: "dracula".to_string(),
214
+ success_color: "#50fa7b".to_string(),
215
+ error_color: "#ff5555".to_string(),
216
+ warning_color: "#f1fa8c".to_string(),
217
+ info_color: "#8be9fd".to_string(),
218
+ background_color: "#282a36".to_string(),
219
+ foreground_color: "#f8f8f2".to_string(),
220
+ highlight_color: "#bd93f9".to_string(),
221
+ dimmed_color: "#6272a4".to_string(),
222
+ }
223
+ }
224
+
225
+ /// Nord theme
226
+ fn nord() -> Self {
227
+ Self {
228
+ name: "nord".to_string(),
229
+ success_color: "#a3be8c".to_string(),
230
+ error_color: "#bf616a".to_string(),
231
+ warning_color: "#ebcb8b".to_string(),
232
+ info_color: "#81a1c1".to_string(),
233
+ background_color: "#2e3440".to_string(),
234
+ foreground_color: "#eceff4".to_string(),
235
+ highlight_color: "#88c0d0".to_string(),
236
+ dimmed_color: "#4c566a".to_string(),
237
+ }
238
+ }
239
+
240
+ /// Solarized Dark theme
241
+ fn solarized_dark() -> Self {
242
+ Self {
243
+ name: "solarized-dark".to_string(),
244
+ success_color: "#859900".to_string(),
245
+ error_color: "#dc322f".to_string(),
246
+ warning_color: "#b58900".to_string(),
247
+ info_color: "#268bd2".to_string(),
248
+ background_color: "#002b36".to_string(),
249
+ foreground_color: "#839496".to_string(),
250
+ highlight_color: "#2aa198".to_string(),
251
+ dimmed_color: "#586e75".to_string(),
252
+ }
253
+ }
254
+
255
+ /// Solarized Light theme
256
+ fn solarized_light() -> Self {
257
+ Self {
258
+ name: "solarized-light".to_string(),
259
+ success_color: "#859900".to_string(),
260
+ error_color: "#dc322f".to_string(),
261
+ warning_color: "#b58900".to_string(),
262
+ info_color: "#268bd2".to_string(),
263
+ background_color: "#fdf6e3".to_string(),
264
+ foreground_color: "#657b83".to_string(),
265
+ highlight_color: "#2aa198".to_string(),
266
+ dimmed_color: "#93a1a1".to_string(),
267
+ }
268
+ }
269
+
270
+ /// Catppuccin Mocha theme
271
+ fn catppuccin_mocha() -> Self {
272
+ Self {
273
+ name: "catppuccin-mocha".to_string(),
274
+ success_color: "#a6e3a1".to_string(),
275
+ error_color: "#f38ba8".to_string(),
276
+ warning_color: "#f9e2af".to_string(),
277
+ info_color: "#89b4fa".to_string(),
278
+ background_color: "#1e1e2e".to_string(),
279
+ foreground_color: "#cdd6f4".to_string(),
280
+ highlight_color: "#cba6f7".to_string(),
281
+ dimmed_color: "#6c7086".to_string(),
282
+ }
283
+ }
284
+
285
+ /// Catppuccin Latte theme (light)
286
+ fn catppuccin_latte() -> Self {
287
+ Self {
288
+ name: "catppuccin-latte".to_string(),
289
+ success_color: "#40a02b".to_string(),
290
+ error_color: "#d20f39".to_string(),
291
+ warning_color: "#df8e1d".to_string(),
292
+ info_color: "#1e66f5".to_string(),
293
+ background_color: "#eff1f5".to_string(),
294
+ foreground_color: "#4c4f69".to_string(),
295
+ highlight_color: "#8839ef".to_string(),
296
+ dimmed_color: "#9ca0b0".to_string(),
297
+ }
298
+ }
299
+
300
+ /// Gruvbox Dark theme
301
+ fn gruvbox_dark() -> Self {
302
+ Self {
303
+ name: "gruvbox-dark".to_string(),
304
+ success_color: "#b8bb26".to_string(),
305
+ error_color: "#fb4934".to_string(),
306
+ warning_color: "#fabd2f".to_string(),
307
+ info_color: "#83a598".to_string(),
308
+ background_color: "#282828".to_string(),
309
+ foreground_color: "#ebdbb2".to_string(),
310
+ highlight_color: "#d3869b".to_string(),
311
+ dimmed_color: "#928374".to_string(),
312
+ }
313
+ }
314
+
315
+ /// Gruvbox Light theme
316
+ fn gruvbox_light() -> Self {
317
+ Self {
318
+ name: "gruvbox-light".to_string(),
319
+ success_color: "#79740e".to_string(),
320
+ error_color: "#9d0006".to_string(),
321
+ warning_color: "#b57614".to_string(),
322
+ info_color: "#076678".to_string(),
323
+ background_color: "#fbf1c7".to_string(),
324
+ foreground_color: "#3c3836".to_string(),
325
+ highlight_color: "#8f3f71".to_string(),
326
+ dimmed_color: "#928374".to_string(),
327
+ }
328
+ }
329
+
330
+ /// Tokyo Night theme
331
+ fn tokyo_night() -> Self {
332
+ Self {
333
+ name: "tokyo-night".to_string(),
334
+ success_color: "#9ece6a".to_string(),
335
+ error_color: "#f7768e".to_string(),
336
+ warning_color: "#e0af68".to_string(),
337
+ info_color: "#7aa2f7".to_string(),
338
+ background_color: "#1a1b26".to_string(),
339
+ foreground_color: "#c0caf5".to_string(),
340
+ highlight_color: "#bb9af7".to_string(),
341
+ dimmed_color: "#565f89".to_string(),
342
+ }
343
+ }
344
+
345
+ /// One Dark theme
346
+ fn one_dark() -> Self {
347
+ Self {
348
+ name: "one-dark".to_string(),
349
+ success_color: "#98c379".to_string(),
350
+ error_color: "#e06c75".to_string(),
351
+ warning_color: "#e5c07b".to_string(),
352
+ info_color: "#61afef".to_string(),
353
+ background_color: "#282c34".to_string(),
354
+ foreground_color: "#abb2bf".to_string(),
355
+ highlight_color: "#c678dd".to_string(),
356
+ dimmed_color: "#5c6370".to_string(),
357
+ }
358
+ }
359
+ }
360
+
361
+ /// Animation configuration
362
+ #[derive(Debug, Clone, Serialize, Deserialize)]
363
+ pub struct AnimationConfig {
364
+ /// Enable/disable animations
365
+ pub enabled: bool,
366
+ /// Animation style
367
+ pub style: AnimationStyle,
368
+ /// Animation speed (1-100, higher = faster)
369
+ pub speed: u8,
370
+ /// Number of animation cycles
371
+ pub cycles: u8,
372
+ /// Duration in milliseconds
373
+ pub duration_ms: u64,
374
+ }
375
+
376
+ impl Default for AnimationConfig {
377
+ fn default() -> Self {
378
+ Self {
379
+ enabled: true,
380
+ style: AnimationStyle::Pulse,
381
+ speed: 50,
382
+ cycles: 3,
383
+ duration_ms: 2000,
384
+ }
385
+ }
386
+ }
387
+
388
+ /// Animation styles
389
+ #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
390
+ pub enum AnimationStyle {
391
+ /// Pulse animation (gentle fade in/out)
392
+ Pulse,
393
+ /// Flash animation (quick blink)
394
+ Flash,
395
+ /// Fade animation (slow fade out)
396
+ Fade,
397
+ /// Breathe animation (smooth sine wave)
398
+ Breathe,
399
+ /// None (static, no animation)
400
+ None,
401
+ }
402
+
403
+ impl Default for AnimationStyle {
404
+ fn default() -> Self {
405
+ Self::Pulse
406
+ }
407
+ }
408
+
409
+ impl AnimationStyle {
410
+ /// Parse animation style from string
411
+ pub fn from_str(s: &str) -> Self {
412
+ match s.to_lowercase().as_str() {
413
+ "pulse" => Self::Pulse,
414
+ "flash" => Self::Flash,
415
+ "fade" => Self::Fade,
416
+ "breathe" => Self::Breathe,
417
+ "none" | "disabled" => Self::None,
418
+ _ => Self::Pulse,
419
+ }
420
+ }
421
+ }
422
+
423
+ /// Accessibility configuration
424
+ #[derive(Debug, Clone, Serialize, Deserialize)]
425
+ pub struct AccessibilityConfig {
426
+ /// Enable high contrast mode
427
+ pub high_contrast: bool,
428
+ /// Enable reduced motion mode (disables animations)
429
+ pub reduced_motion: bool,
430
+ /// Enable screen reader announcements
431
+ pub screen_reader: bool,
432
+ /// Use patterns in addition to colors
433
+ pub use_patterns: bool,
434
+ }
435
+
436
+ impl Default for AccessibilityConfig {
437
+ fn default() -> Self {
438
+ Self {
439
+ high_contrast: false,
440
+ reduced_motion: false,
441
+ screen_reader: false,
442
+ use_patterns: true,
443
+ }
444
+ }
445
+ }
446
+
447
+ /// Configuration manager for hot-reload
448
+ #[derive(Default)]
449
+ pub struct ConfigManager {
450
+ /// Last known configuration
451
+ last_config: Option<Config>,
452
+ /// Configuration file path
453
+ config_path: Option<String>,
454
+ }
455
+
456
+ impl ConfigManager {
457
+ /// Create a new configuration manager
458
+ pub fn new() -> Self {
459
+ Self {
460
+ last_config: None,
461
+ config_path: None,
462
+ }
463
+ }
464
+
465
+ /// Set the configuration file path
466
+ pub fn set_path(&mut self, path: &str) {
467
+ self.config_path = Some(path.to_string());
468
+ }
469
+
470
+ /// Reload configuration from file
471
+ pub fn reload(&mut self) -> Option<Config> {
472
+ // In WASM environment, we can't directly read files
473
+ // This would need to be triggered by a custom message from the host
474
+ // For now, return None to indicate no change
475
+ None
476
+ }
477
+
478
+ /// Parse KDL configuration string
479
+ pub fn parse_kdl(&self, content: &str) -> Result<Config, String> {
480
+ // Parse KDL content (kdl 4.x uses str::parse)
481
+ let doc: kdl::KdlDocument = content.parse()
482
+ .map_err(|e: kdl::KdlError| format!("KDL parse error: {}", e))?;
483
+
484
+ let mut config = Config::default();
485
+
486
+ // Parse the document
487
+ for node in doc.nodes() {
488
+ match node.name().value() {
489
+ "enabled" => {
490
+ if let Some(val) = node.get(0) {
491
+ config.enabled = val.value().as_bool().unwrap_or(true);
492
+ }
493
+ }
494
+ "theme" => {
495
+ if let Some(val) = node.get(0) {
496
+ if let Some(name) = val.value().as_string() {
497
+ config.theme = ThemeConfig::from_preset(name);
498
+ }
499
+ }
500
+ // Parse nested theme properties
501
+ if let Some(children) = node.children() {
502
+ for child in children.nodes() {
503
+ match child.name().value() {
504
+ "success_color" => {
505
+ if let Some(val) = child.get(0) {
506
+ if let Some(color) = val.value().as_string() {
507
+ config.theme.success_color = color.to_string();
508
+ }
509
+ }
510
+ }
511
+ "error_color" => {
512
+ if let Some(val) = child.get(0) {
513
+ if let Some(color) = val.value().as_string() {
514
+ config.theme.error_color = color.to_string();
515
+ }
516
+ }
517
+ }
518
+ "warning_color" => {
519
+ if let Some(val) = child.get(0) {
520
+ if let Some(color) = val.value().as_string() {
521
+ config.theme.warning_color = color.to_string();
522
+ }
523
+ }
524
+ }
525
+ "info_color" => {
526
+ if let Some(val) = child.get(0) {
527
+ if let Some(color) = val.value().as_string() {
528
+ config.theme.info_color = color.to_string();
529
+ }
530
+ }
531
+ }
532
+ _ => {}
533
+ }
534
+ }
535
+ }
536
+ }
537
+ "animation" => {
538
+ if let Some(children) = node.children() {
539
+ for child in children.nodes() {
540
+ match child.name().value() {
541
+ "enabled" => {
542
+ if let Some(val) = child.get(0) {
543
+ config.animation.enabled = val.value().as_bool().unwrap_or(true);
544
+ }
545
+ }
546
+ "style" => {
547
+ if let Some(val) = child.get(0) {
548
+ if let Some(style) = val.value().as_string() {
549
+ config.animation.style = AnimationStyle::from_str(style);
550
+ }
551
+ }
552
+ }
553
+ "speed" => {
554
+ if let Some(val) = child.get(0) {
555
+ if let Some(speed) = val.value().as_i64() {
556
+ config.animation.speed = speed.clamp(1, 100) as u8;
557
+ }
558
+ }
559
+ }
560
+ "cycles" => {
561
+ if let Some(val) = child.get(0) {
562
+ if let Some(cycles) = val.value().as_i64() {
563
+ config.animation.cycles = cycles.clamp(1, 10) as u8;
564
+ }
565
+ }
566
+ }
567
+ _ => {}
568
+ }
569
+ }
570
+ }
571
+ }
572
+ "accessibility" => {
573
+ if let Some(children) = node.children() {
574
+ for child in children.nodes() {
575
+ match child.name().value() {
576
+ "high_contrast" => {
577
+ if let Some(val) = child.get(0) {
578
+ config.accessibility.high_contrast = val.value().as_bool().unwrap_or(false);
579
+ }
580
+ }
581
+ "reduced_motion" => {
582
+ if let Some(val) = child.get(0) {
583
+ config.accessibility.reduced_motion = val.value().as_bool().unwrap_or(false);
584
+ if config.accessibility.reduced_motion {
585
+ config.animation.enabled = false;
586
+ }
587
+ }
588
+ }
589
+ _ => {}
590
+ }
591
+ }
592
+ }
593
+ }
594
+ "notification_timeout_ms" => {
595
+ if let Some(val) = node.get(0) {
596
+ if let Some(timeout) = val.value().as_i64() {
597
+ config.notification_timeout_ms = timeout.max(1000) as u64;
598
+ }
599
+ }
600
+ }
601
+ "queue_max_size" => {
602
+ if let Some(val) = node.get(0) {
603
+ if let Some(size) = val.value().as_i64() {
604
+ config.queue_max_size = size.max(1) as usize;
605
+ }
606
+ }
607
+ }
608
+ _ => {}
609
+ }
610
+ }
611
+
612
+ config.validate()?;
613
+ Ok(config)
614
+ }
615
+ }
616
+
617
+ #[cfg(test)]
618
+ mod tests {
619
+ use super::*;
620
+
621
+ #[test]
622
+ fn test_default_config() {
623
+ let config = Config::default();
624
+ assert!(config.enabled);
625
+ assert!(config.animation.enabled);
626
+ assert_eq!(config.animation.style, AnimationStyle::Pulse);
627
+ }
628
+
629
+ #[test]
630
+ fn test_theme_presets() {
631
+ let themes = vec![
632
+ "dracula", "nord", "solarized", "catppuccin", "gruvbox", "tokyo-night", "one-dark"
633
+ ];
634
+
635
+ for theme_name in themes {
636
+ let theme = ThemeConfig::from_preset(theme_name);
637
+ assert!(!theme.success_color.is_empty());
638
+ assert!(!theme.error_color.is_empty());
639
+ }
640
+ }
641
+
642
+ #[test]
643
+ fn test_config_validation() {
644
+ let mut config = Config::default();
645
+ assert!(config.validate().is_ok());
646
+
647
+ config.notification_timeout_ms = 100;
648
+ assert!(config.validate().is_err());
649
+
650
+ config.notification_timeout_ms = 5000;
651
+ config.queue_max_size = 0;
652
+ assert!(config.validate().is_err());
653
+ }
654
+
655
+ #[test]
656
+ fn test_animation_style_parsing() {
657
+ assert_eq!(AnimationStyle::from_str("pulse"), AnimationStyle::Pulse);
658
+ assert_eq!(AnimationStyle::from_str("FLASH"), AnimationStyle::Flash);
659
+ assert_eq!(AnimationStyle::from_str("fade"), AnimationStyle::Fade);
660
+ assert_eq!(AnimationStyle::from_str("breathe"), AnimationStyle::Breathe);
661
+ assert_eq!(AnimationStyle::from_str("none"), AnimationStyle::None);
662
+ assert_eq!(AnimationStyle::from_str("invalid"), AnimationStyle::Pulse);
663
+ }
664
+ }