1use crate::model::muxbox::*;
2use crate::{model::layout::Layout, Bounds};
3use crate::live_yaml_sync::LiveYamlSync;
4
5use std::fs::File;
6use std::io::Read;
7
8use petgraph::graph::{DiGraph, NodeIndex};
9use petgraph::visit::EdgeRef;
10use serde::{Deserialize, Serialize};
11
12#[derive(Debug, Serialize)]
14struct SerializableApp {
15 app: App,
16}
17
18impl SerializableApp {
20 fn to_yaml_string(&self) -> Result<String, Box<dyn std::error::Error>> {
21 let yaml_content = serde_yaml::to_string(&self)?;
23 Ok(yaml_content)
24 }
25}
26use serde_yaml;
27use std::collections::HashMap;
28use std::sync::Arc;
29
30use crate::validation::SchemaValidator;
31use crate::{calculate_bounds_map, Config, FieldUpdate, Updatable};
32use core::hash::Hash;
33use regex::Regex;
34use std::env;
35use std::hash::{DefaultHasher, Hasher};
36
37#[derive(Debug, Clone)]
40pub struct VariableContext {
41 app_vars: HashMap<String, String>,
42 layout_vars: HashMap<String, String>,
43}
44
45impl VariableContext {
46 pub fn new(
47 app_vars: Option<&HashMap<String, String>>,
48 layout_vars: Option<&HashMap<String, String>>,
49 ) -> Self {
50 Self {
51 app_vars: app_vars.cloned().unwrap_or_default(),
52 layout_vars: layout_vars.cloned().unwrap_or_default(),
53 }
54 }
55
56 pub fn resolve_variable(
60 &self,
61 name: &str,
62 default: &str,
63 muxbox_hierarchy: &[&MuxBox],
64 ) -> String {
65 for muxbox in muxbox_hierarchy.iter() {
67 if let Some(variables) = &muxbox.variables {
68 if let Some(muxbox_val) = variables.get(name) {
69 return muxbox_val.clone();
70 }
71 }
72 }
73
74 if let Some(layout_val) = self.layout_vars.get(name) {
76 return layout_val.clone();
77 }
78
79 if let Some(app_val) = self.app_vars.get(name) {
81 return app_val.clone();
82 }
83
84 if let Ok(env_val) = env::var(name) {
86 return env_val;
87 }
88
89 default.to_string()
91 }
92
93 pub fn substitute_in_string(
95 &self,
96 content: &str,
97 muxbox_hierarchy: &[&MuxBox],
98 ) -> Result<String, Box<dyn std::error::Error>> {
99 let mut result = content.to_string();
100
101 if result.contains("${") {
103 let nested_pattern = Regex::new(r"\$\{[^}]*\$\{[^}]*\}")?;
104 if let Some(nested_match) = nested_pattern.find(&result) {
105 let problematic_text = &result[nested_match.start()..nested_match.end()];
106 return Err(format!(
107 "Nested variable substitution is not supported. Found: '{}' - Use simple variables only.",
108 problematic_text
109 ).into());
110 }
111 }
112
113 let var_pattern = Regex::new(r"\$\{([^}:]+)(?::([^}]*))?\}")?;
116
117 result = var_pattern
118 .replace_all(&result, |caps: ®ex::Captures| {
119 let var_name = &caps[1];
120 let default_value = caps.get(2).map_or("", |m| m.as_str());
121
122 if default_value.contains("${") && !default_value.ends_with("}") {
124 return format!(
125 "error: malformed nested variable in default for '{}'",
126 var_name
127 );
128 }
129
130 self.resolve_variable(var_name, default_value, muxbox_hierarchy)
131 })
132 .to_string();
133
134 let env_pattern = Regex::new(r"\$([A-Z_][A-Z0-9_]*)")?;
136
137 result = env_pattern
138 .replace_all(&result, |caps: ®ex::Captures| {
139 let var_name = &caps[1];
140 self.resolve_variable(var_name, &format!("${}", var_name), muxbox_hierarchy)
141 })
142 .to_string();
143
144 Ok(result)
145 }
146}
147
148#[derive(Debug, Deserialize, Serialize)]
149pub struct TemplateRoot {
150 pub app: App,
151}
152
153#[derive(Debug, Deserialize, Serialize)]
154pub struct App {
155 pub layouts: Vec<Layout>,
156 #[serde(default)]
157 pub libs: Option<Vec<String>>,
158 #[serde(default)]
159 pub on_keypress: Option<HashMap<String, Vec<String>>>,
160 #[serde(default)]
161 pub hot_keys: Option<HashMap<String, String>>,
162 #[serde(default)]
163 pub variables: Option<HashMap<String, String>>,
164 #[serde(skip)]
165 app_graph: Option<AppGraph>,
166 #[serde(skip)]
167 pub adjusted_bounds: Option<HashMap<String, HashMap<String, Bounds>>>,
168}
169
170impl PartialEq for App {
171 fn eq(&self, other: &Self) -> bool {
172 self.layouts == other.layouts
173 && self.on_keypress == other.on_keypress
174 && self.hot_keys == other.hot_keys
175 && self.app_graph == other.app_graph
176 && self.adjusted_bounds == other.adjusted_bounds
177 }
178}
179
180impl Eq for App {}
181
182impl Default for App {
183 fn default() -> Self {
184 Self::new()
185 }
186}
187
188impl App {
189 pub fn new() -> Self {
190 App {
191 layouts: Vec::new(),
192 libs: None,
193 on_keypress: None,
194 hot_keys: None,
195 variables: None,
196 app_graph: None,
197 adjusted_bounds: None,
198 }
199 }
200
201 pub fn get_adjusted_bounds(
202 &mut self,
203 force_readjust: Option<bool>,
204 ) -> &HashMap<String, HashMap<String, Bounds>> {
205 if self.adjusted_bounds.is_none() || force_readjust.unwrap_or(false) {
206 self.adjusted_bounds = Some(self.calculate_bounds());
207 }
208 self.adjusted_bounds
209 .as_ref()
210 .expect("Failed to calculate adjusted bounds!")
211 }
212
213 pub fn get_adjusted_bounds_and_app_graph(
214 &mut self,
215 force_readjust: Option<bool>,
216 ) -> (HashMap<String, HashMap<String, Bounds>>, AppGraph) {
217 let adjusted_bounds = self.get_adjusted_bounds(force_readjust).clone();
219
220 let app_graph = self.generate_graph();
222
223 (adjusted_bounds, app_graph)
224 }
225
226 pub fn get_layout_by_id(&self, id: &str) -> Option<&Layout> {
227 self.layouts.iter().find(|l| l.id == id)
228 }
229
230 pub fn get_layout_by_id_mut(&mut self, id: &str) -> Option<&mut Layout> {
231 self.layouts.iter_mut().find(|l| l.id == id)
232 }
233
234 pub fn get_root_layout(&self) -> Option<&Layout> {
235 let mut roots = self.layouts.iter().filter(|l| l.root.unwrap_or(false));
236 match roots.clone().count() {
237 1 => roots.next(),
238 0 => None,
239 _ => panic!("Multiple root layouts found, which is not allowed."),
240 }
241 }
242
243 pub fn get_root_layout_mut(&mut self) -> Option<&mut Layout> {
244 let mut roots: Vec<&mut Layout> = self
245 .layouts
246 .iter_mut()
247 .filter(|l| l.root.unwrap_or(false))
248 .collect();
249
250 match roots.len() {
251 1 => Some(roots.remove(0)),
252 0 => None,
253 _ => panic!("Multiple root layouts found, which is not allowed."),
254 }
255 }
256
257 pub fn get_active_layout(&self) -> Option<&Layout> {
258 let mut actives = self.layouts.iter().filter(|l| l.active.unwrap_or(false));
259 match actives.clone().count() {
260 1 => actives.next(),
261 0 => None,
262 _ => panic!("Multiple active layouts found, which is not allowed."),
263 }
264 }
265
266 pub fn get_active_layout_mut(&mut self) -> Option<&mut Layout> {
267 let mut actives: Vec<&mut Layout> = self
268 .layouts
269 .iter_mut()
270 .filter(|l| l.active.unwrap_or(false))
271 .collect();
272
273 match actives.len() {
274 1 => Some(actives.remove(0)),
275 0 => None,
276 _ => panic!("Multiple active layouts found, which is not allowed."),
277 }
278 }
279
280 pub fn set_active_layout(&mut self, layout_id: &str) {
281 let mut found_layout = false;
283
284 for layout in &mut self.layouts {
286 if layout.id == layout_id {
287 layout.active = Some(true);
289 found_layout = true;
290 } else {
291 layout.active = Some(false);
293 }
294 }
295
296 if !found_layout {
298 log::error!("Layout with ID '{}' not found.", layout_id);
299 }
300 }
301
302 pub fn set_active_layout_with_yaml_save(&mut self, layout_id: &str, yaml_path: Option<&str>) -> Result<(), Box<dyn std::error::Error>> {
304 self.set_active_layout(layout_id);
305
306 if let Some(path) = yaml_path {
308 save_active_layout_to_yaml(path, layout_id)?;
309 }
310
311 Ok(())
312 }
313
314 pub fn get_muxbox_by_id(&self, id: &str) -> Option<&MuxBox> {
315 for layout in &self.layouts {
316 if let Some(muxbox) = layout.get_muxbox_by_id(id) {
317 return Some(muxbox);
318 }
319 }
320 None
321 }
322
323 pub fn get_muxbox_by_id_mut(&mut self, id: &str) -> Option<&mut MuxBox> {
324 for layout in &mut self.layouts {
325 if let Some(muxbox) = layout.get_muxbox_by_id_mut(id) {
326 return Some(muxbox);
327 }
328 }
329 None
330 }
331
332 pub fn validate(&mut self) {
333 let mut validator = SchemaValidator::new();
334 match validator.validate_app(self) {
335 Ok(_) => {
336 if let Err(e) = apply_post_validation_setup(self) {
338 panic!("Post-validation setup error: {}", e);
339 }
340 }
341 Err(validation_errors) => {
342 let error_messages: Vec<String> = validation_errors
343 .into_iter()
344 .map(|e| format!("{}", e))
345 .collect();
346 let combined_message = error_messages.join("; ");
347 panic!("Validation errors: {}", combined_message);
348 }
349 }
350 }
351
352 pub fn calculate_bounds(&mut self) -> HashMap<String, HashMap<String, Bounds>> {
353 let mut calculated_bounds: HashMap<String, HashMap<String, Bounds>> = HashMap::new();
354
355 let app_graph = self.generate_graph();
356
357 for layout in &mut self.layouts {
358 let calculated_layout_bounds = calculate_bounds_map(&app_graph, layout);
359 calculated_bounds.insert(layout.id.clone(), calculated_layout_bounds);
360 }
361
362 calculated_bounds
363 }
364
365 pub fn generate_graph(&mut self) -> AppGraph {
366 if let Some(app_graph) = self.app_graph.clone() {
367 app_graph
368 } else {
369 let mut app_graph = AppGraph::new();
370
371 for layout in &self.layouts {
372 app_graph.add_layout(layout);
373 }
374 self.app_graph = Some(app_graph.clone());
375 app_graph
376 }
377 }
378
379 pub fn replace_muxbox(&mut self, muxbox: MuxBox) {
380 for layout in &mut self.layouts {
381 if let Some(replaced) = layout.replace_muxbox_recursive(&muxbox) {
382 if replaced {
383 return;
384 }
385 }
386 }
387 }
388}
389
390impl Clone for App {
391 fn clone(&self) -> Self {
392 App {
393 layouts: self.layouts.to_vec(),
394 libs: self.libs.clone(),
395 on_keypress: self.on_keypress.clone(),
396 hot_keys: self.hot_keys.clone(),
397 variables: self.variables.clone(),
398 app_graph: self.app_graph.clone(),
399 adjusted_bounds: self.adjusted_bounds.clone(),
400 }
401 }
402}
403
404impl Updatable for App {
406 fn generate_diff(&self, other: &Self) -> Vec<FieldUpdate> {
407 let mut updates = Vec::new();
408
409 for (self_layout, other_layout) in self.layouts.iter().zip(&other.layouts) {
411 updates.extend(self_layout.generate_diff(other_layout));
412 }
413
414 if self.on_keypress != other.on_keypress {
416 updates.push(FieldUpdate {
417 entity_type: crate::EntityType::App,
418 entity_id: None,
419 field_name: "on_keypress".to_string(),
420 new_value: serde_json::to_value(&other.on_keypress).unwrap(),
421 });
422 }
423
424 if self.adjusted_bounds != other.adjusted_bounds {
426 updates.push(FieldUpdate {
427 entity_type: crate::EntityType::App,
428 entity_id: None,
429 field_name: "adjusted_bounds".to_string(),
430 new_value: serde_json::to_value(&other.adjusted_bounds).unwrap(),
431 });
432 }
433
434 updates
435 }
436
437 fn apply_updates(&mut self, updates: Vec<FieldUpdate>) {
438 let updates_for_layouts = updates.clone();
439 for update in updates {
440 if update.entity_id.is_some() {
441 continue;
443 }
444 match update.field_name.as_str() {
445 "on_keypress" => {
446 if let Ok(new_on_keypress) = serde_json::from_value::<
447 Option<HashMap<String, Vec<String>>>,
448 >(update.new_value.clone())
449 {
450 self.on_keypress = new_on_keypress;
451 }
452 }
453 "adjusted_bounds" => {
454 if let Ok(new_adjusted_bounds) = serde_json::from_value::<
455 Option<HashMap<String, HashMap<String, Bounds>>>,
456 >(update.new_value.clone())
457 {
458 self.adjusted_bounds = new_adjusted_bounds;
459 }
460 }
461 _ => {
462 log::warn!("Unknown field name for App: {}", update.field_name);
463 }
464 }
465 }
466 for layout in &mut self.layouts {
467 layout.apply_updates(updates_for_layouts.clone());
468 }
469 }
470}
471
472#[derive(Debug)]
473pub struct AppGraph {
474 graphs: HashMap<String, DiGraph<MuxBox, ()>>,
475 node_maps: HashMap<String, HashMap<String, NodeIndex>>,
476}
477
478impl Hash for AppGraph {
479 fn hash<H: Hasher>(&self, state: &mut H) {
480 for (key, graph) in &self.graphs {
481 key.hash(state);
482 for node in graph.node_indices() {
483 graph.node_weight(node).unwrap().hash(state);
484 }
485 }
486 for (key, node_map) in &self.node_maps {
487 key.hash(state);
488 for (node_key, node_index) in node_map {
489 node_key.hash(state);
490 node_index.hash(state);
491 }
492 }
493 }
494}
495
496impl PartialEq for AppGraph {
497 fn eq(&self, other: &Self) -> bool {
498 let mut hasher1 = DefaultHasher::new();
500 let mut hasher2 = DefaultHasher::new();
501 self.hash(&mut hasher1);
502 other.hash(&mut hasher2);
503 hasher1.finish() == hasher2.finish()
504 }
505}
506
507impl Eq for AppGraph {}
508
509impl Default for AppGraph {
510 fn default() -> Self {
511 Self::new()
512 }
513}
514
515impl AppGraph {
516 pub fn new() -> Self {
517 AppGraph {
518 graphs: HashMap::new(),
519 node_maps: HashMap::new(),
520 }
521 }
522
523 pub fn add_layout(&mut self, layout: &Layout) {
524 let mut graph = DiGraph::new();
525 let mut node_map = HashMap::new();
526
527 if let Some(children) = &layout.children {
528 for muxbox in children {
529 self.add_muxbox_recursively(
530 &mut graph,
531 &mut node_map,
532 muxbox.clone(),
533 None,
534 &layout.id,
535 );
536 }
537 }
538
539 self.graphs.insert(layout.id.clone(), graph);
540 self.node_maps.insert(layout.id.clone(), node_map);
541 }
542
543 fn add_muxbox_recursively(
544 &self,
545 graph: &mut DiGraph<MuxBox, ()>,
546 node_map: &mut HashMap<String, NodeIndex>,
547 mut muxbox: MuxBox,
548 parent_id: Option<String>,
549 parent_layout_id: &str,
550 ) {
551 muxbox.parent_layout_id = Some(parent_layout_id.to_string());
552 let muxbox_id = muxbox.id.clone();
553 let node_index = graph.add_node(muxbox.clone());
554 node_map.insert(muxbox_id.clone(), node_index);
555
556 if let Some(parent_id) = muxbox.parent_id.clone() {
557 if let Some(&parent_index) = node_map.get(&parent_id) {
558 graph.add_edge(parent_index, node_index, ());
559 }
560 } else if let Some(parent_id) = parent_id {
561 if let Some(&parent_index) = node_map.get(&parent_id) {
562 graph.add_edge(parent_index, node_index, ());
563 }
564 }
565
566 if let Some(children) = muxbox.children {
567 for mut child in children {
568 child.parent_id = Some(muxbox_id.clone());
569 self.add_muxbox_recursively(
570 graph,
571 node_map,
572 child,
573 Some(muxbox_id.clone()),
574 parent_layout_id,
575 );
576 }
577 }
578 }
579
580 pub fn get_layout_muxbox_by_id(&self, layout_id: &str, muxbox_id: &str) -> Option<&MuxBox> {
581 self.node_maps.get(layout_id).and_then(|node_map| {
582 node_map.get(muxbox_id).and_then(|&index| {
583 self.graphs
584 .get(layout_id)
585 .and_then(|graph| graph.node_weight(index))
586 })
587 })
588 }
589
590 pub fn get_muxbox_by_id(&self, muxbox_id: &str) -> Option<&MuxBox> {
591 for (layout_id, node_map) in &self.node_maps {
592 if let Some(&index) = node_map.get(muxbox_id) {
593 return self
594 .graphs
595 .get(layout_id)
596 .and_then(|graph| graph.node_weight(index));
597 }
598 }
599 None
600 }
601
602 pub fn get_children(&self, layout_id: &str, muxbox_id: &str) -> Vec<&MuxBox> {
603 if let Some(node_map) = self.node_maps.get(layout_id) {
604 if let Some(&index) = node_map.get(muxbox_id) {
605 return self.graphs[layout_id]
606 .edges_directed(index, petgraph::Direction::Outgoing)
607 .map(|edge| self.graphs[layout_id].node_weight(edge.target()).unwrap())
608 .collect();
609 }
610 }
611 Vec::new()
612 }
613
614 pub fn get_layout_children(&self, layout_id: &str) -> Vec<&MuxBox> {
615 if let Some(node_map) = self.node_maps.get(layout_id) {
616 let root_node = node_map.get(layout_id).unwrap();
617 return self.graphs[layout_id]
618 .edges_directed(*root_node, petgraph::Direction::Outgoing)
619 .map(|edge| self.graphs[layout_id].node_weight(edge.target()).unwrap())
620 .collect();
621 }
622 Vec::new()
623 }
624
625 pub fn get_parent(&self, layout_id: &str, muxbox_id: &str) -> Option<&MuxBox> {
626 if let Some(node_map) = self.node_maps.get(layout_id) {
627 if let Some(&index) = node_map.get(muxbox_id) {
628 return self.graphs[layout_id]
629 .edges_directed(index, petgraph::Direction::Incoming)
630 .next()
631 .and_then(|edge| self.graphs[layout_id].node_weight(edge.source()));
632 }
633 }
634 None
635 }
636}
637
638#[derive(Debug, Serialize)]
639pub struct AppContext {
640 pub app: App,
641 pub config: Config,
642 #[serde(skip)]
643 pub plugin_registry: std::sync::Arc<std::sync::Mutex<crate::plugin::PluginRegistry>>,
644 #[serde(skip)]
645 pub pty_manager: Option<std::sync::Arc<crate::pty_manager::PtyManager>>,
646 pub yaml_file_path: Option<String>,
647 #[serde(skip)]
648 pub live_yaml_sync: Option<Arc<LiveYamlSync>>,
649}
650
651impl Updatable for AppContext {
652 fn generate_diff(&self, other: &Self) -> Vec<FieldUpdate> {
653 let mut updates = Vec::new();
654
655 updates.extend(self.app.generate_diff(&other.app));
657
658 if self.config != other.config {
660 updates.push(FieldUpdate {
661 entity_type: crate::EntityType::AppContext,
662 entity_id: None,
663 field_name: "config".to_string(),
664 new_value: serde_json::to_value(&other.config).unwrap(),
665 });
666 }
667
668 updates
669 }
670
671 fn apply_updates(&mut self, updates: Vec<FieldUpdate>) {
672 let updates_for_layouts = updates.clone();
673
674 for update in updates {
675 if update.entity_id.is_some() {
676 continue;
678 }
679 match update.field_name.as_str() {
680 "config" => {
681 if let Ok(new_config) =
682 serde_json::from_value::<Config>(update.new_value.clone())
683 {
684 self.config = new_config;
685 }
686 }
687 _ => log::warn!("Unknown field name for AppContext: {}", update.field_name),
688 }
689 }
690
691 self.app.apply_updates(updates_for_layouts);
692 }
693}
694
695impl PartialEq for AppContext {
696 fn eq(&self, other: &Self) -> bool {
697 self.app == other.app && self.config == other.config
698 }
699}
700
701impl AppContext {
702 pub fn new(app: App, config: Config) -> Self {
703 AppContext {
705 app,
706 config,
707 plugin_registry: std::sync::Arc::new(std::sync::Mutex::new(
708 crate::plugin::PluginRegistry::new(),
709 )),
710 pty_manager: None,
711 yaml_file_path: None,
712 live_yaml_sync: None,
713 }
714 }
715
716 pub fn new_with_pty(
717 app: App,
718 config: Config,
719 pty_manager: std::sync::Arc<crate::pty_manager::PtyManager>,
720 ) -> Self {
721 AppContext {
722 app,
723 config,
724 plugin_registry: std::sync::Arc::new(std::sync::Mutex::new(
725 crate::plugin::PluginRegistry::new(),
726 )),
727 pty_manager: Some(pty_manager),
728 yaml_file_path: None,
729 live_yaml_sync: None,
730 }
731 }
732
733 pub fn new_with_yaml_path(app: App, config: Config, yaml_path: String) -> Self {
735 Self::new_with_yaml_path_and_lock(app, config, yaml_path, false)
736 }
737
738 pub fn new_with_yaml_path_and_lock(app: App, config: Config, yaml_path: String, locked: bool) -> Self {
739 let live_yaml_sync = if !locked {
740 match LiveYamlSync::new(yaml_path.clone(), true) {
741 Ok(sync) => {
742 log::info!("Live YAML sync initialized");
743 Some(Arc::new(sync))
744 }
745 Err(e) => {
746 log::error!("Failed to initialize live YAML sync: {}", e);
747 None
748 }
749 }
750 } else {
751 None
752 };
753
754 AppContext {
755 app,
756 config,
757 plugin_registry: std::sync::Arc::new(std::sync::Mutex::new(
758 crate::plugin::PluginRegistry::new(),
759 )),
760 pty_manager: None,
761 yaml_file_path: Some(yaml_path),
762 live_yaml_sync,
763 }
764 }
765
766 pub fn new_with_pty_and_yaml(
767 app: App,
768 config: Config,
769 pty_manager: std::sync::Arc<crate::pty_manager::PtyManager>,
770 yaml_path: String,
771 ) -> Self {
772 Self::new_with_pty_and_yaml_and_lock(app, config, pty_manager, yaml_path, false)
773 }
774
775 pub fn new_with_pty_and_yaml_and_lock(
776 app: App,
777 config: Config,
778 pty_manager: std::sync::Arc<crate::pty_manager::PtyManager>,
779 yaml_path: String,
780 locked: bool,
781 ) -> Self {
782 let live_yaml_sync = if !locked {
783 match LiveYamlSync::new(yaml_path.clone(), true) {
784 Ok(sync) => {
785 log::info!("Live YAML sync initialized with PTY");
786 Some(Arc::new(sync))
787 }
788 Err(e) => {
789 log::error!("Failed to initialize live YAML sync: {}", e);
790 None
791 }
792 }
793 } else {
794 None
795 };
796
797 AppContext {
798 app,
799 config,
800 plugin_registry: std::sync::Arc::new(std::sync::Mutex::new(
801 crate::plugin::PluginRegistry::new(),
802 )),
803 pty_manager: Some(pty_manager),
804 yaml_file_path: Some(yaml_path),
805 live_yaml_sync,
806 }
807 }
808}
809
810impl Clone for AppContext {
811 fn clone(&self) -> Self {
812 AppContext {
813 app: self.app.clone(),
814 config: self.config.clone(),
815 plugin_registry: self.plugin_registry.clone(),
816 pty_manager: self.pty_manager.clone(),
817 yaml_file_path: self.yaml_file_path.clone(),
818 live_yaml_sync: self.live_yaml_sync.clone(),
819 }
820 }
821}
822
823impl Hash for AppContext {
824 fn hash<H: Hasher>(&self, state: &mut H) {
825 self.app.hash(state);
826 self.config.hash(state);
827 }
829}
830
831impl Hash for App {
832 fn hash<H: Hasher>(&self, state: &mut H) {
833 for layout in &self.layouts {
834 layout.hash(state);
835 }
836 }
837}
838
839impl Clone for AppGraph {
840 fn clone(&self) -> Self {
841 let mut new_graphs = HashMap::new();
842 let mut new_node_maps = HashMap::new();
843
844 for (key, graph) in &self.graphs {
845 let new_graph = graph.clone();
846 let new_node_map = self.node_maps.get(key).unwrap().clone();
849 new_graphs.insert(key.clone(), new_graph);
850 new_node_maps.insert(key.clone(), new_node_map);
851 }
852
853 AppGraph {
854 graphs: new_graphs,
855 node_maps: new_node_maps,
856 }
857 }
858}
859
860pub fn load_app_from_yaml(file_path: &str) -> Result<App, Box<dyn std::error::Error>> {
861 load_app_from_yaml_with_lock(file_path, false)
862}
863
864pub fn load_app_from_yaml_with_lock(file_path: &str, _locked: bool) -> Result<App, Box<dyn std::error::Error>> {
865 let mut file = File::open(file_path)?;
866 let mut contents = String::new();
867 file.read_to_string(&mut contents)?;
868
869 let schema_dir = "schemas";
871 if std::path::Path::new(schema_dir).exists() {
872 let mut validator = SchemaValidator::new();
873 if let Err(schema_errors) = validator.validate_with_json_schema(&contents, schema_dir) {
874 let error_messages: Vec<String> = schema_errors
875 .into_iter()
876 .map(|e| format!("{}", e))
877 .collect();
878 let combined_message = error_messages.join("\n");
879 return Err(format!("JSON Schema validation failed:\n{}", combined_message).into());
880 }
881 }
882
883 let root_result: Result<TemplateRoot, _> = serde_yaml::from_str(&contents);
885
886 let mut app = match root_result {
887 Ok(root) => root.app,
888 Err(serde_error) => {
889 if let Some(location) = serde_error.location() {
891 let line_num = location.line();
892 let col_num = location.column();
893 let error_display = create_rust_style_error_display(
894 &contents,
895 file_path,
896 line_num,
897 col_num,
898 &format!("{}", serde_error),
899 );
900 return Err(error_display.into());
901 }
902
903 match serde_yaml::from_str::<App>(&contents) {
905 Ok(app) => app,
906 Err(app_error) => {
907 if let Some(location) = app_error.location() {
908 let line_num = location.line();
909 let col_num = location.column();
910 let error_display = create_rust_style_error_display(
911 &contents,
912 file_path,
913 line_num,
914 col_num,
915 &format!("{}", app_error),
916 );
917 return Err(error_display.into());
918 }
919 return Err(format!("YAML parsing error: {}", app_error).into());
920 }
921 }
922 }
923 };
924
925 apply_variable_substitution(&mut app)?;
927
928 let mut validator = SchemaValidator::new();
930 match validator.validate_app(&app) {
931 Ok(_) => {
932 apply_post_validation_setup(&mut app)?;
934 Ok(app)
935 }
936 Err(validation_errors) => {
937 let error_messages: Vec<String> = validation_errors
938 .into_iter()
939 .map(|e| format!("{}", e))
940 .collect();
941 let combined_message = error_messages.join("; ");
942 Err(format!("Configuration validation errors: {}", combined_message).into())
943 }
944 }
945}
946
947fn create_rust_style_error_display(
948 contents: &str,
949 file_path: &str,
950 line_num: usize,
951 col_num: usize,
952 error_msg: &str,
953) -> String {
954 let lines: Vec<&str> = contents.lines().collect();
955
956 if line_num == 0 || line_num > lines.len() {
958 return format!("YAML parsing error: {}", error_msg);
959 }
960
961 let error_line = lines[line_num - 1];
962 let line_num_width = format!("{}", line_num).len().max(3);
963
964 let mut result = String::new();
966 result.push_str(&format!("error: {}\n", error_msg));
967 result.push_str(&format!(" --> {}:{}:{}\n", file_path, line_num, col_num));
968 result.push_str(&format!(" |\n"));
969 result.push_str(&format!(
970 "{:width$} | {}\n",
971 line_num,
972 error_line,
973 width = line_num_width
974 ));
975
976 if col_num > 0 && col_num <= error_line.len() + 1 {
978 let spaces_before_pipe = " ".repeat(line_num_width);
979 let spaces_before_caret = " ".repeat(col_num.saturating_sub(1));
980 result.push_str(&format!(
981 "{} | {}{}\n",
982 spaces_before_pipe, spaces_before_caret, "^"
983 ));
984 }
985
986 result
987}
988
989fn apply_variable_substitution(app: &mut App) -> Result<(), Box<dyn std::error::Error>> {
991 let context = VariableContext::new(app.variables.as_ref(), None);
993
994 for layout in &mut app.layouts {
996 apply_layout_variable_substitution(layout, &context)?;
997 }
998
999 Ok(())
1000}
1001
1002fn apply_layout_variable_substitution(
1004 layout: &mut Layout,
1005 context: &VariableContext,
1006) -> Result<(), Box<dyn std::error::Error>> {
1007 let layout_context = context;
1009
1010 if let Some(ref mut title) = layout.title {
1012 *title = layout_context
1013 .substitute_in_string(title, &[])
1014 .map_err(|e| format!("Error in layout '{}' title: {}", layout.id, e))?;
1015 }
1016
1017 if let Some(ref mut children) = layout.children {
1019 for child in children {
1020 apply_muxbox_variable_substitution(child, &layout_context, &[])?;
1021 }
1022 }
1023
1024 Ok(())
1025}
1026
1027fn apply_muxbox_variable_substitution(
1029 muxbox: &mut MuxBox,
1030 context: &VariableContext,
1031 parent_hierarchy: &[&MuxBox],
1032) -> Result<(), Box<dyn std::error::Error>> {
1033 let local_context = if let Some(ref muxbox_vars) = muxbox.variables {
1035 let mut combined_app_vars = context.app_vars.clone();
1036 combined_app_vars.extend(muxbox_vars.clone());
1038 VariableContext::new(Some(&combined_app_vars), Some(&context.layout_vars))
1039 } else {
1040 context.clone()
1041 };
1042
1043 let full_hierarchy = parent_hierarchy.to_vec();
1045 if let Some(ref mut title) = muxbox.title {
1050 *title = local_context
1051 .substitute_in_string(title, &full_hierarchy)
1052 .map_err(|e| format!("Error in muxbox '{}' title: {}", muxbox.id, e))?;
1053 }
1054
1055 if let Some(ref mut content) = muxbox.content {
1056 *content = local_context
1057 .substitute_in_string(content, &full_hierarchy)
1058 .map_err(|e| format!("Error in muxbox '{}' content: {}", muxbox.id, e))?;
1059 }
1060
1061 if let Some(ref mut script) = muxbox.script {
1062 for (i, script_line) in script.iter_mut().enumerate() {
1063 *script_line = local_context
1064 .substitute_in_string(script_line, &full_hierarchy)
1065 .map_err(|e| {
1066 format!(
1067 "Error in muxbox '{}' script line {}: {}",
1068 muxbox.id,
1069 i + 1,
1070 e
1071 )
1072 })?;
1073 }
1074 }
1075
1076 if let Some(ref mut redirect) = muxbox.redirect_output {
1077 *redirect = local_context
1078 .substitute_in_string(redirect, &full_hierarchy)
1079 .map_err(|e| format!("Error in muxbox '{}' redirect_output: {}", muxbox.id, e))?;
1080 }
1081
1082 if let Some(ref mut choices) = muxbox.choices {
1084 for choice in choices {
1085 if let Some(ref mut choice_content) = choice.content {
1086 *choice_content = local_context
1087 .substitute_in_string(choice_content, &full_hierarchy)
1088 .map_err(|e| {
1089 format!(
1090 "Error in muxbox '{}' choice '{}' content: {}",
1091 muxbox.id, choice.id, e
1092 )
1093 })?;
1094 }
1095
1096 if let Some(ref mut choice_script) = choice.script {
1097 for (i, script_line) in choice_script.iter_mut().enumerate() {
1098 *script_line = local_context
1099 .substitute_in_string(script_line, &full_hierarchy)
1100 .map_err(|e| {
1101 format!(
1102 "Error in muxbox '{}' choice '{}' script line {}: {}",
1103 muxbox.id,
1104 choice.id,
1105 i + 1,
1106 e
1107 )
1108 })?;
1109 }
1110 }
1111 }
1112 }
1113
1114 if let Some(ref mut children) = muxbox.children {
1116 for child in children {
1117 apply_muxbox_variable_substitution(child, &local_context, &full_hierarchy)?;
1120 }
1121 }
1122
1123 Ok(())
1124}
1125
1126pub fn substitute_variables(content: &str) -> Result<String, Box<dyn std::error::Error>> {
1128 let context = VariableContext::new(None, None);
1131 context.substitute_in_string(content, &[])
1132}
1133
1134fn apply_post_validation_setup(app: &mut App) -> Result<(), String> {
1135 fn set_parent_ids(muxbox: &mut MuxBox, parent_layout_id: &str, parent_id: Option<String>) {
1139 muxbox.parent_layout_id = Some(parent_layout_id.to_string());
1140 muxbox.parent_id = parent_id;
1141
1142 if let Some(ref mut children) = muxbox.children {
1143 for child in children {
1144 set_parent_ids(child, parent_layout_id, Some(muxbox.id.clone()));
1145 }
1146 }
1147 }
1148
1149 let mut root_layout_id: Option<String> = None;
1150
1151 for layout in &mut app.layouts {
1152 let mut layout_clone = layout.clone();
1153 let muxboxes_in_tab_order = layout_clone.get_muxboxes_in_tab_order();
1154
1155 if layout.root.unwrap_or(false) {
1157 root_layout_id = Some(layout.id.clone());
1158 }
1159
1160 if layout.children.is_none() {
1161 continue;
1162 }
1163
1164 for muxbox in layout.children.as_mut().unwrap() {
1166 set_parent_ids(muxbox, &layout.id, None);
1167 if !muxboxes_in_tab_order.is_empty() && muxbox.id == muxboxes_in_tab_order[0].id {
1168 muxbox.selected = Some(true);
1169 }
1170 if let Some(choices) = &mut muxbox.choices {
1171 if !choices.is_empty() {
1172 choices[0].selected = true;
1173 }
1174 }
1175 }
1176 }
1177
1178 if root_layout_id.is_none() {
1180 if let Some(first_layout) = app.layouts.first() {
1181 root_layout_id = Some(first_layout.id.clone());
1182 }
1183 }
1184
1185 if let Some(root_layout_id) = root_layout_id {
1187 if let Some(root_layout) = app.layouts.iter_mut().find(|l| l.id == root_layout_id) {
1188 root_layout.active = Some(true);
1189 root_layout.root = Some(true);
1190
1191 for layout in &mut app.layouts {
1193 if layout.id != root_layout_id {
1194 layout.active = Some(false);
1195 layout.root = Some(false);
1196 }
1197 }
1198 }
1199 }
1200
1201 Ok(())
1202}
1203
1204#[cfg(test)]
1208mod tests {
1209 use super::*;
1210 use crate::model::common::InputBounds;
1211 use crate::model::layout::Layout;
1212 use crate::model::muxbox::MuxBox;
1213 use std::collections::HashMap;
1214
1215 fn create_test_muxbox(id: &str) -> MuxBox {
1220 MuxBox {
1221 id: id.to_string(),
1222 title: Some(format!("Test MuxBox {}", id)),
1223 position: InputBounds {
1224 x1: "0%".to_string(),
1225 y1: "0%".to_string(),
1226 x2: "100%".to_string(),
1227 y2: "100%".to_string(),
1228 },
1229 tab_order: Some("1".to_string()),
1230 selected: Some(false),
1231 ..Default::default()
1232 }
1233 }
1234
1235 fn create_test_layout(id: &str, children: Option<Vec<MuxBox>>) -> Layout {
1238 Layout {
1239 id: id.to_string(),
1240 title: Some(format!("Test Layout {}", id)),
1241 children,
1242 root: Some(false),
1243 active: Some(false),
1244 ..Default::default()
1245 }
1246 }
1247
1248 fn create_test_app() -> App {
1251 let muxbox1 = create_test_muxbox("muxbox1");
1252 let muxbox2 = create_test_muxbox("muxbox2");
1253 let layout1 = create_test_layout("layout1", Some(vec![muxbox1, muxbox2]));
1254
1255 let mut app = App::new();
1256 app.layouts.push(layout1);
1257 app
1258 }
1259
1260 fn create_test_app_context() -> AppContext {
1263 let app = create_test_app();
1264 AppContext::new(app, Config::default())
1265 }
1266
1267 fn load_test_app_context() -> AppContext {
1268 let current_dir = std::env::current_dir().expect("Failed to get current directory");
1269 let dashboard_path = current_dir.join("layouts/tests.yaml");
1270 let app = load_app_from_yaml(dashboard_path.to_str().unwrap()).expect("Failed to load app");
1271 AppContext::new(app, Config::default())
1272 }
1273
1274 fn setup_app_context() -> AppContext {
1275 load_test_app_context()
1276 }
1277
1278 #[test]
1283 fn test_app_new() {
1284 let app = App::new();
1285 assert_eq!(app.layouts.len(), 0);
1286 assert_eq!(app.libs, None);
1287 assert_eq!(app.on_keypress, None);
1288 assert_eq!(app.app_graph, None);
1289 assert_eq!(app.adjusted_bounds, None);
1290 }
1291
1292 #[test]
1295 fn test_app_default() {
1296 let app = App::default();
1297 assert_eq!(app.layouts.len(), 0);
1298 assert_eq!(app.libs, None);
1299 assert_eq!(app.on_keypress, None);
1300 assert_eq!(app.app_graph, None);
1301 assert_eq!(app.adjusted_bounds, None);
1302 }
1303
1304 #[test]
1309 fn test_app_get_layout_by_id() {
1310 let app = create_test_app();
1311
1312 let found_layout = app.get_layout_by_id("layout1");
1313 assert!(found_layout.is_some());
1314 assert_eq!(found_layout.unwrap().id, "layout1");
1315
1316 let not_found = app.get_layout_by_id("nonexistent");
1317 assert!(not_found.is_none());
1318 }
1319
1320 #[test]
1323 fn test_app_get_layout_by_id_mut() {
1324 let mut app = create_test_app();
1325
1326 let found_layout = app.get_layout_by_id_mut("layout1");
1327 assert!(found_layout.is_some());
1328
1329 found_layout.unwrap().title = Some("Modified Layout".to_string());
1331
1332 let verified_layout = app.get_layout_by_id("layout1");
1334 assert_eq!(
1335 verified_layout.unwrap().title,
1336 Some("Modified Layout".to_string())
1337 );
1338 }
1339
1340 #[test]
1343 fn test_app_get_layout_by_id_mut_empty() {
1344 let mut app = App::new();
1345
1346 let found_layout = app.get_layout_by_id_mut("nonexistent");
1347 assert!(found_layout.is_none());
1348 }
1349
1350 #[test]
1355 fn test_app_get_root_layout() {
1356 let mut app = create_test_app();
1357
1358 assert!(app.get_root_layout().is_none());
1360
1361 app.layouts[0].root = Some(true);
1363
1364 let root_layout = app.get_root_layout();
1365 assert!(root_layout.is_some());
1366 assert_eq!(root_layout.unwrap().id, "layout1");
1367 }
1368
1369 #[test]
1372 #[should_panic(expected = "Multiple root layouts found, which is not allowed.")]
1373 fn test_app_get_root_layout_multiple_panics() {
1374 let mut app = create_test_app();
1375
1376 let layout2 = create_test_layout("layout2", None);
1378 app.layouts.push(layout2);
1379 app.layouts[0].root = Some(true);
1380 app.layouts[1].root = Some(true);
1381
1382 app.get_root_layout();
1383 }
1384
1385 #[test]
1388 fn test_app_get_root_layout_mut() {
1389 let mut app = create_test_app();
1390 app.layouts[0].root = Some(true);
1391
1392 let root_layout = app.get_root_layout_mut();
1393 assert!(root_layout.is_some());
1394
1395 root_layout.unwrap().title = Some("Modified Root".to_string());
1397
1398 let verified_layout = app.get_root_layout();
1400 assert_eq!(
1401 verified_layout.unwrap().title,
1402 Some("Modified Root".to_string())
1403 );
1404 }
1405
1406 #[test]
1411 fn test_app_get_active_layout() {
1412 let mut app = create_test_app();
1413
1414 assert!(app.get_active_layout().is_none());
1416
1417 app.layouts[0].active = Some(true);
1419
1420 let active_layout = app.get_active_layout();
1421 assert!(active_layout.is_some());
1422 assert_eq!(active_layout.unwrap().id, "layout1");
1423 }
1424
1425 #[test]
1428 #[should_panic(expected = "Multiple active layouts found, which is not allowed.")]
1429 fn test_app_get_active_layout_multiple_panics() {
1430 let mut app = create_test_app();
1431
1432 let layout2 = create_test_layout("layout2", None);
1434 app.layouts.push(layout2);
1435 app.layouts[0].active = Some(true);
1436 app.layouts[1].active = Some(true);
1437
1438 app.get_active_layout();
1439 }
1440
1441 #[test]
1444 fn test_app_get_active_layout_mut() {
1445 let mut app = create_test_app();
1446 app.layouts[0].active = Some(true);
1447
1448 let active_layout = app.get_active_layout_mut();
1449 assert!(active_layout.is_some());
1450
1451 active_layout.unwrap().title = Some("Modified Active".to_string());
1453
1454 let verified_layout = app.get_active_layout();
1456 assert_eq!(
1457 verified_layout.unwrap().title,
1458 Some("Modified Active".to_string())
1459 );
1460 }
1461
1462 #[test]
1465 fn test_app_set_active_layout() {
1466 let mut app = create_test_app();
1467
1468 let layout2 = create_test_layout("layout2", None);
1470 app.layouts.push(layout2);
1471
1472 app.set_active_layout("layout2");
1474
1475 let active_layout = app.get_active_layout();
1476 assert!(active_layout.is_some());
1477 assert_eq!(active_layout.unwrap().id, "layout2");
1478
1479 let layout1 = app.get_layout_by_id("layout1").unwrap();
1481 assert_eq!(layout1.active, Some(false));
1482 }
1483
1484 #[test]
1487 fn test_app_set_active_layout_nonexistent() {
1488 let mut app = create_test_app();
1489
1490 app.set_active_layout("nonexistent");
1492
1493 assert!(app.get_active_layout().is_none());
1495 }
1496
1497 #[test]
1502 fn test_app_get_muxbox_by_id() {
1503 let app = create_test_app();
1504
1505 let found_muxbox = app.get_muxbox_by_id("muxbox1");
1506 assert!(found_muxbox.is_some());
1507 assert_eq!(found_muxbox.unwrap().id, "muxbox1");
1508
1509 let not_found = app.get_muxbox_by_id("nonexistent");
1510 assert!(not_found.is_none());
1511 }
1512
1513 #[test]
1516 fn test_app_get_muxbox_by_id_mut() {
1517 let mut app = create_test_app();
1518
1519 let found_muxbox = app.get_muxbox_by_id_mut("muxbox1");
1520 assert!(found_muxbox.is_some());
1521
1522 found_muxbox.unwrap().title = Some("Modified MuxBox".to_string());
1524
1525 let verified_muxbox = app.get_muxbox_by_id("muxbox1");
1527 assert_eq!(
1528 verified_muxbox.unwrap().title,
1529 Some("Modified MuxBox".to_string())
1530 );
1531 }
1532
1533 #[test]
1536 fn test_app_get_muxbox_by_id_mut_empty() {
1537 let mut app = App::new();
1538
1539 let found_muxbox = app.get_muxbox_by_id_mut("nonexistent");
1540 assert!(found_muxbox.is_none());
1541 }
1542
1543 #[test]
1548 fn test_app_validate() {
1549 let mut app = create_test_app();
1550
1551 let muxbox = app.get_muxbox_by_id("muxbox1").unwrap();
1553 assert_eq!(muxbox.parent_layout_id, None);
1554 assert_eq!(muxbox.parent_id, None);
1555
1556 app.validate();
1557
1558 let muxbox = app.get_muxbox_by_id("muxbox1").unwrap();
1560 assert_eq!(muxbox.parent_layout_id, Some("layout1".to_string()));
1561 assert_eq!(muxbox.parent_id, None); }
1563
1564 #[test]
1567 fn test_app_validate_root_layout_activation() {
1568 let mut app = create_test_app();
1569 app.layouts[0].root = Some(true);
1570
1571 app.validate();
1572
1573 let layout = app.get_layout_by_id("layout1").unwrap();
1574 assert_eq!(layout.active, Some(true));
1575 }
1576
1577 #[test]
1580 fn test_app_validate_default_root() {
1581 let mut app = create_test_app();
1582
1583 let layout2 = create_test_layout("layout2", None);
1585 app.layouts.push(layout2);
1586
1587 app.validate();
1588
1589 let layout1 = app.get_layout_by_id("layout1").unwrap();
1591 assert_eq!(layout1.root, Some(true));
1592 assert_eq!(layout1.active, Some(true));
1593
1594 let layout2 = app.get_layout_by_id("layout2").unwrap();
1596 assert_eq!(layout2.root, Some(false));
1597 assert_eq!(layout2.active, Some(false));
1598 }
1599
1600 #[test]
1603 #[should_panic(expected = "Required field 'layouts' is missing")]
1604 fn test_app_validate_empty_panics() {
1605 let mut app = App::new();
1606 app.validate();
1607 }
1608
1609 #[test]
1612 #[should_panic(expected = "Duplicate ID 'muxbox1' found in muxboxes")]
1613 fn test_app_validate_duplicate_ids_panics() {
1614 let mut app = App::new();
1615
1616 let muxbox1a = create_test_muxbox("muxbox1");
1618 let muxbox1b = create_test_muxbox("muxbox1"); let layout = create_test_layout("layout1", Some(vec![muxbox1a, muxbox1b]));
1621 app.layouts.push(layout);
1622
1623 app.validate();
1624 }
1625
1626 #[test]
1629 #[should_panic(
1630 expected = "Schema structure error: Multiple root layouts detected. Only one layout can be marked as 'root: true'."
1631 )]
1632 fn test_app_validate_multiple_root_panics() {
1633 let mut app = create_test_app();
1634
1635 let mut layout2 = create_test_layout("layout2", None);
1637 layout2.root = Some(true);
1638 app.layouts.push(layout2);
1639 app.layouts[0].root = Some(true);
1640
1641 app.validate();
1642 }
1643
1644 #[test]
1649 fn test_app_calculate_bounds() {
1650 let mut app = create_test_app();
1651
1652 let bounds = app.calculate_bounds();
1653 assert!(bounds.contains_key("layout1"));
1654
1655 let layout_bounds = bounds.get("layout1").unwrap();
1656 assert!(layout_bounds.contains_key("muxbox1"));
1657 assert!(layout_bounds.contains_key("muxbox2"));
1658 }
1659
1660 #[test]
1663 fn test_app_get_adjusted_bounds() {
1664 let mut app = create_test_app();
1665
1666 let bounds1 = app.get_adjusted_bounds(None).clone();
1668 assert!(bounds1.contains_key("layout1"));
1669
1670 let bounds2 = app.get_adjusted_bounds(None).clone();
1672 assert_eq!(bounds1, bounds2);
1673
1674 let bounds3 = app.get_adjusted_bounds(Some(true));
1676 assert!(bounds3.contains_key("layout1"));
1677 }
1678
1679 #[test]
1682 fn test_app_get_adjusted_bounds_and_app_graph() {
1683 let mut app = create_test_app();
1684
1685 let (bounds, app_graph) = app.get_adjusted_bounds_and_app_graph(None);
1686
1687 assert!(bounds.contains_key("layout1"));
1688 assert!(app_graph.graphs.contains_key("layout1"));
1689 }
1690
1691 #[test]
1696 fn test_app_generate_graph() {
1697 let mut app = create_test_app();
1698
1699 let app_graph = app.generate_graph();
1700 assert!(app_graph.graphs.contains_key("layout1"));
1701
1702 let graph = &app_graph.graphs["layout1"];
1703 assert_eq!(graph.node_count(), 2); }
1705
1706 #[test]
1709 fn test_app_generate_graph_caching() {
1710 let mut app = create_test_app();
1711
1712 let graph1 = app.generate_graph();
1714
1715 let graph2 = app.generate_graph();
1717 assert_eq!(graph1, graph2);
1718 }
1719
1720 #[test]
1725 fn test_app_replace_muxbox() {
1726 let mut app = create_test_app();
1727
1728 let mut replacement_muxbox = create_test_muxbox("muxbox1");
1730 replacement_muxbox.title = Some("Replaced MuxBox".to_string());
1731
1732 app.replace_muxbox(replacement_muxbox);
1733
1734 let replaced_muxbox = app.get_muxbox_by_id("muxbox1").unwrap();
1736 assert_eq!(replaced_muxbox.title, Some("Replaced MuxBox".to_string()));
1737 }
1738
1739 #[test]
1742 fn test_app_replace_muxbox_nonexistent() {
1743 let mut app = create_test_app();
1744
1745 let replacement_muxbox = create_test_muxbox("nonexistent");
1747
1748 app.replace_muxbox(replacement_muxbox);
1750
1751 let original_muxbox = app.get_muxbox_by_id("muxbox1").unwrap();
1753 assert_eq!(
1754 original_muxbox.title,
1755 Some("Test MuxBox muxbox1".to_string())
1756 );
1757 }
1758
1759 #[test]
1764 fn test_app_clone() {
1765 let app1 = create_test_app();
1766 let app2 = app1.clone();
1767
1768 assert_eq!(app1.layouts.len(), app2.layouts.len());
1769 assert_eq!(app1.layouts[0].id, app2.layouts[0].id);
1770 assert_eq!(app1.libs, app2.libs);
1771 assert_eq!(app1.on_keypress, app2.on_keypress);
1772 }
1773
1774 #[test]
1777 fn test_app_clone_comprehensive() {
1778 let mut app1 = create_test_app();
1779 app1.libs = Some(vec!["lib1.sh".to_string(), "lib2.sh".to_string()]);
1780
1781 let mut keypress_map = HashMap::new();
1782 keypress_map.insert("ctrl+c".to_string(), vec!["exit".to_string()]);
1783 app1.on_keypress = Some(keypress_map);
1784
1785 let app2 = app1.clone();
1786
1787 assert_eq!(app1.libs, app2.libs);
1788 assert_eq!(app1.on_keypress, app2.on_keypress);
1789 assert_eq!(
1790 app1.layouts[0].children.as_ref().unwrap().len(),
1791 app2.layouts[0].children.as_ref().unwrap().len()
1792 );
1793 }
1794
1795 #[test]
1800 fn test_app_hash() {
1801 let app1 = create_test_app();
1802 let app2 = create_test_app();
1803 let mut app3 = create_test_app();
1804 app3.layouts[0].id = "different".to_string();
1805
1806 use std::collections::hash_map::DefaultHasher;
1807 use std::hash::{Hash, Hasher};
1808
1809 let mut hasher1 = DefaultHasher::new();
1810 let mut hasher2 = DefaultHasher::new();
1811 let mut hasher3 = DefaultHasher::new();
1812
1813 app1.hash(&mut hasher1);
1814 app2.hash(&mut hasher2);
1815 app3.hash(&mut hasher3);
1816
1817 assert_eq!(hasher1.finish(), hasher2.finish());
1818 assert_ne!(hasher1.finish(), hasher3.finish());
1819 }
1820
1821 #[test]
1826 fn test_app_equality() {
1827 let app1 = create_test_app();
1828 let app2 = create_test_app();
1829 let mut app3 = create_test_app();
1830 app3.layouts[0].id = "different".to_string();
1831
1832 assert_eq!(app1, app2);
1833 assert_ne!(app1, app3);
1834 }
1835
1836 #[test]
1841 fn test_app_context_new() {
1842 let app = create_test_app();
1843 let config = Config::new(60);
1844 let app_context = AppContext::new(app, config);
1845
1846 assert_eq!(app_context.config.frame_delay, 60);
1847 assert_eq!(app_context.app.layouts.len(), 1);
1848 }
1849
1850 #[test]
1853 fn test_app_context_clone() {
1854 let app_context1 = create_test_app_context();
1855 let app_context2 = app_context1.clone();
1856
1857 assert_eq!(app_context1.config, app_context2.config);
1858 assert_eq!(app_context1.app, app_context2.app);
1859 }
1860
1861 #[test]
1864 fn test_app_context_hash() {
1865 let app_context1 = create_test_app_context();
1866 let app_context2 = create_test_app_context();
1867
1868 use std::collections::hash_map::DefaultHasher;
1869 use std::hash::{Hash, Hasher};
1870
1871 let mut hasher1 = DefaultHasher::new();
1872 let mut hasher2 = DefaultHasher::new();
1873
1874 app_context1.hash(&mut hasher1);
1875 app_context2.hash(&mut hasher2);
1876
1877 assert_eq!(hasher1.finish(), hasher2.finish());
1878 }
1879
1880 #[test]
1883 fn test_app_context_equality() {
1884 let app_context1 = create_test_app_context();
1885 let app_context2 = create_test_app_context();
1886
1887 assert_eq!(app_context1, app_context2);
1888 }
1889
1890 #[test]
1895 fn test_app_graph_new() {
1896 let app_graph = AppGraph::new();
1897 assert_eq!(app_graph.graphs.len(), 0);
1898 assert_eq!(app_graph.node_maps.len(), 0);
1899 }
1900
1901 #[test]
1904 fn test_app_graph_default() {
1905 let app_graph = AppGraph::default();
1906 assert_eq!(app_graph.graphs.len(), 0);
1907 assert_eq!(app_graph.node_maps.len(), 0);
1908 }
1909
1910 #[test]
1913 fn test_app_graph_add_layout() {
1914 let layout = create_test_layout("test", Some(vec![create_test_muxbox("muxbox1")]));
1915 let mut app_graph = AppGraph::new();
1916
1917 app_graph.add_layout(&layout);
1918
1919 assert!(app_graph.graphs.contains_key("test"));
1920 assert!(app_graph.node_maps.contains_key("test"));
1921 assert_eq!(app_graph.graphs["test"].node_count(), 1);
1922 }
1923
1924 #[test]
1927 fn test_app_graph_get_layout_muxbox_by_id() {
1928 let layout = create_test_layout("test", Some(vec![create_test_muxbox("muxbox1")]));
1929 let mut app_graph = AppGraph::new();
1930 app_graph.add_layout(&layout);
1931
1932 let muxbox = app_graph.get_layout_muxbox_by_id("test", "muxbox1");
1933 assert!(muxbox.is_some());
1934 assert_eq!(muxbox.unwrap().id, "muxbox1");
1935
1936 let not_found = app_graph.get_layout_muxbox_by_id("test", "nonexistent");
1937 assert!(not_found.is_none());
1938 }
1939
1940 #[test]
1943 fn test_app_graph_get_muxbox_by_id() {
1944 let layout1 = create_test_layout("layout1", Some(vec![create_test_muxbox("muxbox1")]));
1945 let layout2 = create_test_layout("layout2", Some(vec![create_test_muxbox("muxbox2")]));
1946 let mut app_graph = AppGraph::new();
1947 app_graph.add_layout(&layout1);
1948 app_graph.add_layout(&layout2);
1949
1950 let muxbox1 = app_graph.get_muxbox_by_id("muxbox1");
1951 assert!(muxbox1.is_some());
1952 assert_eq!(muxbox1.unwrap().id, "muxbox1");
1953
1954 let muxbox2 = app_graph.get_muxbox_by_id("muxbox2");
1955 assert!(muxbox2.is_some());
1956 assert_eq!(muxbox2.unwrap().id, "muxbox2");
1957 }
1958
1959 #[test]
1962 fn test_app_graph_get_children() {
1963 let child_muxbox = create_test_muxbox("child");
1964 let mut parent_muxbox = create_test_muxbox("parent");
1965 parent_muxbox.children = Some(vec![child_muxbox]);
1966
1967 let layout = create_test_layout("test", Some(vec![parent_muxbox]));
1968 let mut app_graph = AppGraph::new();
1969 app_graph.add_layout(&layout);
1970
1971 let children = app_graph.get_children("test", "parent");
1972 assert_eq!(children.len(), 1);
1973 assert_eq!(children[0].id, "child");
1974 }
1975
1976 #[test]
1979 fn test_app_graph_get_parent() {
1980 let child_muxbox = create_test_muxbox("child");
1981 let mut parent_muxbox = create_test_muxbox("parent");
1982 parent_muxbox.children = Some(vec![child_muxbox]);
1983
1984 let layout = create_test_layout("test", Some(vec![parent_muxbox]));
1985 let mut app_graph = AppGraph::new();
1986 app_graph.add_layout(&layout);
1987
1988 let parent = app_graph.get_parent("test", "child");
1989 assert!(parent.is_some());
1990 assert_eq!(parent.unwrap().id, "parent");
1991 }
1992
1993 #[test]
1996 fn test_app_graph_hash() {
1997 let layout = create_test_layout("test", Some(vec![create_test_muxbox("muxbox1")]));
1998 let mut app_graph1 = AppGraph::new();
1999 let mut app_graph2 = AppGraph::new();
2000 app_graph1.add_layout(&layout);
2001 app_graph2.add_layout(&layout);
2002
2003 use std::collections::hash_map::DefaultHasher;
2004 use std::hash::{Hash, Hasher};
2005
2006 let mut hasher1 = DefaultHasher::new();
2007 let mut hasher2 = DefaultHasher::new();
2008
2009 app_graph1.hash(&mut hasher1);
2010 app_graph2.hash(&mut hasher2);
2011
2012 assert_eq!(hasher1.finish(), hasher2.finish());
2013 }
2014
2015 #[test]
2018 fn test_app_graph_equality() {
2019 let layout = create_test_layout("test", Some(vec![create_test_muxbox("muxbox1")]));
2020 let mut app_graph1 = AppGraph::new();
2021 let mut app_graph2 = AppGraph::new();
2022 app_graph1.add_layout(&layout);
2023 app_graph2.add_layout(&layout);
2024
2025 assert_eq!(app_graph1, app_graph2);
2026 }
2027
2028 #[test]
2031 fn test_layout_and_muxboxes_addition() {
2032 let mut app_context = setup_app_context();
2033 let app_graph = app_context.app.generate_graph();
2034 assert!(app_graph.graphs.contains_key("dashboard"));
2035 let graph = &app_graph.graphs["dashboard"];
2036 assert_eq!(
2037 graph.node_count(),
2038 9,
2039 "Should include all muxboxes and sub-muxboxes"
2040 );
2041 }
2042
2043 #[test]
2044 fn test_get_muxbox_by_id() {
2045 let mut app_context = setup_app_context();
2046 let app_graph = app_context.app.generate_graph();
2047 let muxboxes = [
2048 "header",
2049 "title",
2050 "time",
2051 "cpu",
2052 "memory",
2053 "log",
2054 "log_input",
2055 "log_output",
2056 "footer",
2057 ];
2058 for &muxbox_id in muxboxes.iter() {
2059 let muxbox = app_graph.get_muxbox_by_id(muxbox_id);
2060 assert!(
2061 muxbox.is_some(),
2062 "MuxBox with ID {} should exist",
2063 muxbox_id
2064 );
2065 }
2066 }
2067
2068 #[test]
2069 fn test_get_children() {
2070 let mut app_context = setup_app_context();
2071 let app_graph = app_context.app.generate_graph();
2072 let children = app_graph.get_children("dashboard", "header");
2073 assert_eq!(children.len(), 2, "Header should have exactly 2 children");
2074 assert!(
2075 children.iter().any(|&p| p.id == "title"),
2076 "Title should be a child of header"
2077 );
2078 assert!(
2079 children.iter().any(|&p| p.id == "time"),
2080 "Time should be a child of header"
2081 );
2082 }
2083
2084 #[test]
2085 fn test_get_parent() {
2086 let mut app_context = setup_app_context();
2087 let app_graph = app_context.app.generate_graph();
2088 let parent = app_graph.get_parent("dashboard", "title");
2089 assert!(parent.is_some(), "Parent should exist for 'title'");
2090 assert_eq!(
2091 parent.unwrap().id,
2092 "header",
2093 "Parent of 'title' should be 'header'"
2094 );
2095 }
2096
2097 #[test]
2098 fn test_app_graph_clone() {
2099 let mut app_context = setup_app_context();
2100 let app_graph = app_context.app.generate_graph();
2101 let cloned_graph = app_graph.clone();
2102 assert_eq!(app_graph, cloned_graph);
2103 }
2104
2105 #[test]
2110 fn test_load_app_from_yaml() {
2111 let current_dir = std::env::current_dir().expect("Failed to get current directory");
2112 let dashboard_path = current_dir.join("layouts/tests.yaml");
2113
2114 let result = load_app_from_yaml(dashboard_path.to_str().unwrap());
2115 assert!(result.is_ok());
2116
2117 let app = result.unwrap();
2118 assert_eq!(app.layouts.len(), 1);
2119 assert_eq!(app.layouts[0].id, "dashboard");
2120 }
2121
2122 #[test]
2125 fn test_load_app_from_yaml_invalid_file() {
2126 let result = load_app_from_yaml("nonexistent.yaml");
2127 assert!(result.is_err());
2128 }
2129
2130 #[test]
2133 fn test_load_app_from_yaml_with_schema_validation() {
2134 use std::fs;
2135 use std::io::Write;
2136
2137 let temp_file = "/tmp/boxmux_test_invalid.yaml";
2139 let invalid_yaml_content = r#"
2140app:
2141 layouts:
2142 - id: 'layout1'
2143 root: true
2144 children:
2145 - id: 'muxbox1'
2146 position:
2147 x1: 0%
2148 y1: 0%
2149 x2: 50%
2150 y2: 50%
2151 - id: 'muxbox1' # Duplicate ID - should fail validation
2152 position:
2153 x1: 50%
2154 y1: 0%
2155 x2: 100%
2156 y2: 50%
2157"#;
2158
2159 let mut file = fs::File::create(temp_file).expect("Failed to create temp file");
2161 file.write_all(invalid_yaml_content.as_bytes())
2162 .expect("Failed to write temp file");
2163
2164 let result = load_app_from_yaml(temp_file);
2166 assert!(
2167 result.is_err(),
2168 "SchemaValidator should catch duplicate IDs"
2169 );
2170
2171 let error_msg = result.unwrap_err().to_string();
2172 assert!(
2173 error_msg.contains("Duplicate ID 'muxbox1' found in muxboxes"),
2174 "Error message should mention duplicate ID: {}",
2175 error_msg
2176 );
2177
2178 let _ = fs::remove_file(temp_file);
2180
2181 let current_dir = std::env::current_dir().expect("Failed to get current directory");
2183 let valid_dashboard_path = current_dir.join("layouts/tests.yaml");
2184
2185 let valid_result = load_app_from_yaml(valid_dashboard_path.to_str().unwrap());
2186 assert!(
2187 valid_result.is_ok(),
2188 "Valid YAML should pass SchemaValidator"
2189 );
2190
2191 let app = valid_result.unwrap();
2192 assert_eq!(app.layouts.len(), 1);
2193 assert_eq!(app.layouts[0].id, "dashboard");
2194 }
2195
2196 #[test]
2199 fn test_load_app_from_yaml_multiple_root_layouts_error() {
2200 use std::fs;
2201 use std::io::Write;
2202
2203 let temp_file = "/tmp/boxmux_test_multiple_roots.yaml";
2204 let multiple_roots_yaml = r#"
2205app:
2206 layouts:
2207 - id: 'layout1'
2208 root: true
2209 children:
2210 - id: 'muxbox1'
2211 position:
2212 x1: 0%
2213 y1: 0%
2214 x2: 100%
2215 y2: 100%
2216 - id: 'layout2'
2217 root: true # Second root - should fail validation
2218 children:
2219 - id: 'muxbox2'
2220 position:
2221 x1: 0%
2222 y1: 0%
2223 x2: 100%
2224 y2: 100%
2225"#;
2226
2227 let mut file = fs::File::create(temp_file).expect("Failed to create temp file");
2228 file.write_all(multiple_roots_yaml.as_bytes())
2229 .expect("Failed to write temp file");
2230
2231 let result = load_app_from_yaml(temp_file);
2232 assert!(
2233 result.is_err(),
2234 "SchemaValidator should catch multiple root layouts"
2235 );
2236
2237 let error_msg = result.unwrap_err().to_string();
2238 assert!(
2239 error_msg.contains("Multiple root layouts detected"),
2240 "Error message should mention multiple root layouts: {}",
2241 error_msg
2242 );
2243
2244 let _ = fs::remove_file(temp_file);
2246 }
2247}
2248
2249pub fn save_complete_state_to_yaml(
2253 yaml_path: &str,
2254 app_context: &AppContext,
2255) -> Result<(), Box<dyn std::error::Error>> {
2256 use std::fs;
2257
2258 let serializable_app = SerializableApp {
2260 app: app_context.app.clone(),
2261 };
2262
2263 let yaml_content = serializable_app.to_yaml_string()?;
2265
2266 let temp_path = format!("{}.tmp", yaml_path);
2268 fs::write(&temp_path, yaml_content)?;
2269 fs::rename(&temp_path, yaml_path)?;
2270
2271 log::debug!("Saved complete application state to YAML: {}", yaml_path);
2272 Ok(())
2273}
2274
2275pub fn save_active_layout_to_yaml(
2277 yaml_path: &str,
2278 active_layout_id: &str,
2279) -> Result<(), Box<dyn std::error::Error>> {
2280 use serde_yaml::Value;
2281 use std::fs;
2282
2283 let yaml_content = fs::read_to_string(yaml_path)?;
2285 let mut yaml_value: Value = serde_yaml::from_str(&yaml_content)?;
2286
2287 if let Some(root_map) = yaml_value.as_mapping_mut() {
2289 if let Some(app_map) = root_map.get_mut(&Value::String("app".to_string())) {
2290 if let Value::Mapping(app_map) = app_map {
2291 if let Some(layouts_seq) = app_map.get_mut(&Value::String("layouts".to_string())) {
2292 if let Value::Sequence(layouts_seq) = layouts_seq {
2293 for layout_value in layouts_seq.iter_mut() {
2294 if let Value::Mapping(layout_map) = layout_value {
2295 if let Some(Value::String(layout_id)) = layout_map.get(&Value::String("id".to_string())) {
2296 let is_active = layout_id == active_layout_id;
2297 layout_map.insert(
2298 Value::String("active".to_string()),
2299 Value::Bool(is_active)
2300 );
2301 }
2302 }
2303 }
2304 }
2305 }
2306 }
2307 }
2308 }
2309
2310 let temp_path = format!("{}.tmp", yaml_path);
2312 let updated_yaml = serde_yaml::to_string(&yaml_value)?;
2313 fs::write(&temp_path, updated_yaml)?;
2314 fs::rename(&temp_path, yaml_path)?;
2315
2316 log::info!("Updated active layout to '{}' in YAML: {}", active_layout_id, yaml_path);
2317 Ok(())
2318}
2319
2320pub fn save_muxbox_bounds_to_yaml(
2322 yaml_path: &str,
2323 muxbox_id: &str,
2324 new_bounds: &crate::InputBounds,
2325) -> Result<(), Box<dyn std::error::Error>> {
2326 use serde_yaml::{self, Value};
2327 use std::fs;
2328
2329 let yaml_content = fs::read_to_string(yaml_path)?;
2331 let mut yaml_value: Value = serde_yaml::from_str(&yaml_content)?;
2332
2333 update_muxbox_bounds_recursive(&mut yaml_value, muxbox_id, new_bounds)?;
2335
2336 let temp_path = format!("{}.tmp", yaml_path);
2338 let updated_yaml = serde_yaml::to_string(&yaml_value)?;
2339 fs::write(&temp_path, updated_yaml)?;
2340 fs::rename(&temp_path, yaml_path)?;
2341
2342 log::debug!("Updated muxbox {} bounds in YAML: {}", muxbox_id, yaml_path);
2343 Ok(())
2344}
2345
2346pub fn save_muxbox_content_to_yaml(
2348 yaml_path: &str,
2349 muxbox_id: &str,
2350 new_content: &str,
2351) -> Result<(), Box<dyn std::error::Error>> {
2352 use serde_yaml::Value;
2353 use std::fs;
2354
2355 let yaml_content = fs::read_to_string(yaml_path)?;
2356 let mut yaml_value: Value = serde_yaml::from_str(&yaml_content)?;
2357
2358 update_muxbox_field_recursive(&mut yaml_value, muxbox_id, "content", &Value::String(new_content.to_string()))?;
2359
2360 let temp_path = format!("{}.tmp", yaml_path);
2361 let updated_yaml = serde_yaml::to_string(&yaml_value)?;
2362 fs::write(&temp_path, updated_yaml)?;
2363 fs::rename(&temp_path, yaml_path)?;
2364
2365 log::debug!("Updated muxbox {} content in YAML: {}", muxbox_id, yaml_path);
2366 Ok(())
2367}
2368
2369pub fn save_muxbox_scroll_to_yaml(
2371 yaml_path: &str,
2372 muxbox_id: &str,
2373 scroll_x: usize,
2374 scroll_y: usize,
2375) -> Result<(), Box<dyn std::error::Error>> {
2376 use serde_yaml::Value;
2377 use std::fs;
2378
2379 let yaml_content = fs::read_to_string(yaml_path)?;
2380 let mut yaml_value: Value = serde_yaml::from_str(&yaml_content)?;
2381
2382 update_muxbox_field_recursive(&mut yaml_value, muxbox_id, "scroll_x", &Value::Number(serde_yaml::Number::from(scroll_x)))?;
2383 update_muxbox_field_recursive(&mut yaml_value, muxbox_id, "scroll_y", &Value::Number(serde_yaml::Number::from(scroll_y)))?;
2384
2385 let temp_path = format!("{}.tmp", yaml_path);
2386 let updated_yaml = serde_yaml::to_string(&yaml_value)?;
2387 fs::write(&temp_path, updated_yaml)?;
2388 fs::rename(&temp_path, yaml_path)?;
2389
2390 log::debug!("Updated muxbox {} scroll position in YAML: {}", muxbox_id, yaml_path);
2391 Ok(())
2392}
2393
2394fn update_muxbox_field_recursive(
2396 value: &mut serde_yaml::Value,
2397 target_muxbox_id: &str,
2398 field_name: &str,
2399 new_value: &serde_yaml::Value,
2400) -> Result<bool, Box<dyn std::error::Error>> {
2401 use serde_yaml::Value;
2402 match value {
2403 Value::Mapping(map) => {
2404 if let Some(Value::String(id)) = map.get(&Value::String("id".to_string())) {
2406 if id == target_muxbox_id {
2407 map.insert(Value::String(field_name.to_string()), new_value.clone());
2408 return Ok(true);
2409 }
2410 }
2411
2412 for (_, child_value) in map.iter_mut() {
2414 if update_muxbox_field_recursive(child_value, target_muxbox_id, field_name, new_value)? {
2415 return Ok(true);
2416 }
2417 }
2418 }
2419 Value::Sequence(seq) => {
2420 for child_value in seq.iter_mut() {
2421 if update_muxbox_field_recursive(child_value, target_muxbox_id, field_name, new_value)? {
2422 return Ok(true);
2423 }
2424 }
2425 }
2426 _ => {}
2427 }
2428 Ok(false)
2429}
2430
2431pub fn update_muxbox_bounds_recursive(
2432 value: &mut serde_yaml::Value,
2433 target_muxbox_id: &str,
2434 new_bounds: &crate::InputBounds,
2435) -> Result<bool, Box<dyn std::error::Error>> {
2436 use serde_yaml::Value;
2437 match value {
2438 Value::Mapping(map) => {
2439 if let Some(Value::String(id)) = map.get(&Value::String("id".to_string())) {
2441 if id == target_muxbox_id {
2442 if let Some(position_value) =
2444 map.get_mut(&Value::String("position".to_string()))
2445 {
2446 if let Value::Mapping(position_map) = position_value {
2447 position_map.insert(
2448 Value::String("x1".to_string()),
2449 Value::String(new_bounds.x1.clone()),
2450 );
2451 position_map.insert(
2452 Value::String("y1".to_string()),
2453 Value::String(new_bounds.y1.clone()),
2454 );
2455 position_map.insert(
2456 Value::String("x2".to_string()),
2457 Value::String(new_bounds.x2.clone()),
2458 );
2459 position_map.insert(
2460 Value::String("y2".to_string()),
2461 Value::String(new_bounds.y2.clone()),
2462 );
2463 return Ok(true);
2464 }
2465 }
2466 let mut position_map = serde_yaml::Mapping::new();
2468 position_map.insert(
2469 Value::String("x1".to_string()),
2470 Value::String(new_bounds.x1.clone()),
2471 );
2472 position_map.insert(
2473 Value::String("y1".to_string()),
2474 Value::String(new_bounds.y1.clone()),
2475 );
2476 position_map.insert(
2477 Value::String("x2".to_string()),
2478 Value::String(new_bounds.x2.clone()),
2479 );
2480 position_map.insert(
2481 Value::String("y2".to_string()),
2482 Value::String(new_bounds.y2.clone()),
2483 );
2484 map.insert(
2485 Value::String("position".to_string()),
2486 Value::Mapping(position_map),
2487 );
2488 return Ok(true);
2489 }
2490 }
2491
2492 for (_, child_value) in map.iter_mut() {
2494 if update_muxbox_bounds_recursive(child_value, target_muxbox_id, new_bounds)? {
2495 return Ok(true);
2496 }
2497 }
2498 }
2499 Value::Sequence(seq) => {
2500 for item in seq.iter_mut() {
2502 if update_muxbox_bounds_recursive(item, target_muxbox_id, new_bounds)? {
2503 return Ok(true);
2504 }
2505 }
2506 }
2507 _ => {
2508 }
2510 }
2511
2512 Ok(false)
2513}