Skip to main content

scarab_plugin_api/object_model/
window.rs

1//! Window object proxy for Fusabi scripts
2
3use super::{ObjectError, ObjectHandle, ObjectRegistry, ObjectType, Result};
4use crate::status_bar::{RenderItem, StatusBarSide};
5
6/// Proxy for a terminal window
7#[derive(Debug, Clone)]
8pub struct WindowProxy {
9    handle: ObjectHandle,
10    /// Pending status bar updates (stored for later IPC transmission)
11    pending_status: std::sync::Arc<std::sync::Mutex<Vec<(StatusBarSide, Vec<RenderItem>)>>>,
12}
13
14impl WindowProxy {
15    pub fn new(handle: ObjectHandle) -> Result<Self> {
16        if handle.object_type() != ObjectType::Window {
17            return Err(ObjectError::type_mismatch(
18                handle,
19                ObjectType::Window,
20                handle.object_type(),
21            ));
22        }
23        Ok(Self {
24            handle,
25            pending_status: std::sync::Arc::new(std::sync::Mutex::new(Vec::new())),
26        })
27    }
28
29    pub fn handle(&self) -> ObjectHandle {
30        self.handle
31    }
32
33    pub fn id(&self) -> u64 {
34        self.handle.id()
35    }
36
37    // Methods that will dispatch to actual implementation
38    // For now, return placeholder errors
39
40    /// Get the active pane in this window
41    ///
42    /// This returns the active pane from the active tab.
43    ///
44    /// # Arguments
45    ///
46    /// * `registry` - The object registry to use for navigation
47    ///
48    /// # Returns
49    ///
50    /// * `Ok(PaneProxy)` - The active pane in the active tab
51    /// * `Err(ObjectError)` - If navigation fails or no active tab/pane exists
52    pub fn active_pane<T>(&self, registry: &impl ObjectRegistry<T>) -> Result<PaneProxy> {
53        let tab = self.active_tab(registry)?;
54        tab.active_pane(registry)
55    }
56
57    /// Get the active tab in this window
58    ///
59    /// This method returns the first tab in the window.
60    /// In a full implementation, this would query which tab is actually active.
61    ///
62    /// # Arguments
63    ///
64    /// * `registry` - The object registry to use for navigation
65    ///
66    /// # Returns
67    ///
68    /// * `Ok(TabProxy)` - The active tab in this window
69    /// * `Err(ObjectError)` - If the window has no tabs or navigation fails
70    pub fn active_tab<T>(&self, registry: &impl ObjectRegistry<T>) -> Result<TabProxy> {
71        let tabs = self.tabs(registry)?;
72        tabs.into_iter()
73            .next()
74            .ok_or_else(|| ObjectError::method_not_found(self.handle, "active_tab: no tabs"))
75    }
76
77    /// Get all tabs in this window
78    ///
79    /// # Arguments
80    ///
81    /// * `registry` - The object registry to use for navigation
82    ///
83    /// # Returns
84    ///
85    /// * `Ok(Vec<TabProxy>)` - All tabs in this window (may be empty)
86    /// * `Err(ObjectError)` - If navigation fails
87    pub fn tabs<T>(&self, registry: &impl ObjectRegistry<T>) -> Result<Vec<TabProxy>> {
88        let child_handles = registry.get_children(&self.handle)?;
89
90        child_handles
91            .into_iter()
92            .map(|handle| {
93                if handle.object_type() != ObjectType::Tab {
94                    return Err(ObjectError::type_mismatch(
95                        handle,
96                        ObjectType::Tab,
97                        handle.object_type(),
98                    ));
99                }
100                TabProxy::new(handle)
101            })
102            .collect()
103    }
104
105    /// Get window dimensions
106    pub fn get_dimensions(&self) -> Result<(u32, u32)> {
107        Err(ObjectError::method_not_found(self.handle, "get_dimensions"))
108    }
109
110    /// Check if window is focused
111    pub fn is_focused(&self) -> Result<bool> {
112        Err(ObjectError::method_not_found(self.handle, "is_focused"))
113    }
114
115    /// Set right status bar content
116    ///
117    /// Stores the render items for later IPC transmission to the client.
118    /// The items will be sent to the UI when the next status update is triggered.
119    ///
120    /// # Arguments
121    ///
122    /// * `items` - Vector of RenderItem elements to display on the right side
123    ///
124    /// # Returns
125    ///
126    /// * `Ok(())` - Items stored successfully
127    /// * `Err(ObjectError)` - If storage fails
128    pub fn set_right_status(&self, items: Vec<RenderItem>) -> Result<()> {
129        let mut pending = self.pending_status.lock().map_err(|_| {
130            ObjectError::method_not_found(self.handle, "set_right_status: lock poisoned")
131        })?;
132        pending.push((StatusBarSide::Right, items));
133        Ok(())
134    }
135
136    /// Set left status bar content
137    ///
138    /// Stores the render items for later IPC transmission to the client.
139    /// The items will be sent to the UI when the next status update is triggered.
140    ///
141    /// # Arguments
142    ///
143    /// * `items` - Vector of RenderItem elements to display on the left side
144    ///
145    /// # Returns
146    ///
147    /// * `Ok(())` - Items stored successfully
148    /// * `Err(ObjectError)` - If storage fails
149    pub fn set_left_status(&self, items: Vec<RenderItem>) -> Result<()> {
150        let mut pending = self.pending_status.lock().map_err(|_| {
151            ObjectError::method_not_found(self.handle, "set_left_status: lock poisoned")
152        })?;
153        pending.push((StatusBarSide::Left, items));
154        Ok(())
155    }
156
157    /// Clear all status bar content
158    ///
159    /// Clears both left and right status bar sections.
160    ///
161    /// # Returns
162    ///
163    /// * `Ok(())` - Status cleared successfully
164    /// * `Err(ObjectError)` - If clearing fails
165    pub fn clear_status(&self) -> Result<()> {
166        let mut pending = self.pending_status.lock().map_err(|_| {
167            ObjectError::method_not_found(self.handle, "clear_status: lock poisoned")
168        })?;
169        pending.push((StatusBarSide::Left, vec![]));
170        pending.push((StatusBarSide::Right, vec![]));
171        Ok(())
172    }
173
174    /// Drain pending status updates for IPC transmission
175    ///
176    /// Returns and clears all pending status updates that need to be sent to the client.
177    /// This is called internally by the daemon's IPC layer.
178    ///
179    /// # Returns
180    ///
181    /// Vector of (side, items) tuples representing pending updates
182    pub fn drain_pending_status(&self) -> Vec<(StatusBarSide, Vec<RenderItem>)> {
183        self.pending_status
184            .lock()
185            .map(|mut pending| pending.drain(..).collect())
186            .unwrap_or_default()
187    }
188
189    /// Show a toast notification
190    pub fn toast_notification(&self, _title: &str, _message: &str) -> Result<()> {
191        Err(ObjectError::method_not_found(
192            self.handle,
193            "toast_notification",
194        ))
195    }
196}
197
198// Forward declarations for cross-references
199// These will be defined in their respective modules
200use super::PaneProxy;
201use super::TabProxy;
202
203#[cfg(test)]
204mod tests {
205    use super::*;
206
207    #[test]
208    fn test_window_proxy_creation() {
209        let handle = ObjectHandle::new(ObjectType::Window, 1, 0);
210        let proxy = WindowProxy::new(handle);
211        assert!(proxy.is_ok());
212
213        let proxy = proxy.unwrap();
214        assert_eq!(proxy.id(), 1);
215        assert_eq!(proxy.handle(), handle);
216    }
217
218    #[test]
219    fn test_window_proxy_type_validation() {
220        let pane_handle = ObjectHandle::new(ObjectType::Pane, 1, 0);
221        let result = WindowProxy::new(pane_handle);
222
223        assert!(result.is_err());
224        match result {
225            Err(ObjectError::TypeMismatch {
226                expected, actual, ..
227            }) => {
228                assert_eq!(expected, ObjectType::Window);
229                assert_eq!(actual, ObjectType::Pane);
230            }
231            _ => panic!("Expected TypeMismatch error"),
232        }
233    }
234
235    #[test]
236    fn test_window_proxy_methods_not_implemented() {
237        let handle = ObjectHandle::new(ObjectType::Window, 1, 0);
238        let proxy = WindowProxy::new(handle).unwrap();
239
240        // Some methods should return MethodNotFound errors
241        assert!(matches!(
242            proxy.get_dimensions(),
243            Err(ObjectError::MethodNotFound { .. })
244        ));
245        assert!(matches!(
246            proxy.is_focused(),
247            Err(ObjectError::MethodNotFound { .. })
248        ));
249        assert!(matches!(
250            proxy.toast_notification("title", "message"),
251            Err(ObjectError::MethodNotFound { .. })
252        ));
253
254        // Status methods should now work
255        assert!(proxy.set_right_status(vec![]).is_ok());
256        assert!(proxy.set_left_status(vec![]).is_ok());
257        assert!(proxy.clear_status().is_ok());
258    }
259
260    #[test]
261    fn test_window_proxy_clone() {
262        let handle = ObjectHandle::new(ObjectType::Window, 1, 0);
263        let proxy1 = WindowProxy::new(handle).unwrap();
264        let proxy2 = proxy1.clone();
265
266        assert_eq!(proxy1.handle(), proxy2.handle());
267        assert_eq!(proxy1.id(), proxy2.id());
268    }
269
270    #[test]
271    fn test_status_bar_methods() {
272        let handle = ObjectHandle::new(ObjectType::Window, 1, 0);
273        let proxy = WindowProxy::new(handle).unwrap();
274
275        // Test setting left status
276        let left_items = vec![RenderItem::Text("Left".to_string())];
277        assert!(proxy.set_left_status(left_items).is_ok());
278
279        // Test setting right status
280        let right_items = vec![RenderItem::Text("Right".to_string())];
281        assert!(proxy.set_right_status(right_items).is_ok());
282
283        // Test draining pending status
284        let pending = proxy.drain_pending_status();
285        assert_eq!(pending.len(), 2);
286
287        // After draining, should be empty
288        let pending2 = proxy.drain_pending_status();
289        assert_eq!(pending2.len(), 0);
290    }
291
292    #[test]
293    fn test_status_bar_clear() {
294        let handle = ObjectHandle::new(ObjectType::Window, 1, 0);
295        let proxy = WindowProxy::new(handle).unwrap();
296
297        // Clear status should create two pending updates (left and right)
298        assert!(proxy.clear_status().is_ok());
299
300        let pending = proxy.drain_pending_status();
301        assert_eq!(pending.len(), 2);
302
303        // Both should have empty item vectors
304        for (_, items) in pending {
305            assert!(items.is_empty());
306        }
307    }
308
309    // Test navigation methods with mock registry
310    use super::super::registry::{ObjectRegistry, RegistryEntry};
311    use std::collections::HashMap;
312
313    #[derive(Clone)]
314    struct TestObject;
315
316    struct TestRegistry {
317        objects: HashMap<u64, RegistryEntry<TestObject>>,
318        parents: HashMap<u64, ObjectHandle>,
319        children: HashMap<u64, Vec<ObjectHandle>>,
320        generations: HashMap<u64, u32>,
321    }
322
323    impl TestRegistry {
324        fn new() -> Self {
325            Self {
326                objects: HashMap::new(),
327                parents: HashMap::new(),
328                children: HashMap::new(),
329                generations: HashMap::new(),
330            }
331        }
332
333        fn set_parent(&mut self, child: ObjectHandle, parent: ObjectHandle) {
334            self.parents.insert(child.id(), parent);
335            self.children
336                .entry(parent.id())
337                .or_insert_with(Vec::new)
338                .push(child);
339        }
340    }
341
342    impl ObjectRegistry<TestObject> for TestRegistry {
343        fn register(&mut self, object: TestObject) -> ObjectHandle {
344            let id = self.objects.len() as u64 + 1;
345            let generation = self.current_generation(id);
346            let handle = ObjectHandle::new(ObjectType::Window, id, generation);
347            self.objects.insert(id, RegistryEntry::new(handle, object));
348            handle
349        }
350
351        fn unregister(&mut self, handle: ObjectHandle) -> Result<TestObject> {
352            self.increment_generation(handle.id());
353            self.objects
354                .remove(&handle.id())
355                .map(|entry| entry.into_object())
356                .ok_or_else(|| ObjectError::not_found(handle))
357        }
358
359        fn get(&self, handle: ObjectHandle) -> Result<&TestObject> {
360            let current_gen = self.current_generation(handle.id());
361            if !handle.is_valid(current_gen) {
362                return Err(ObjectError::stale_handle(handle, current_gen));
363            }
364            self.objects
365                .get(&handle.id())
366                .map(|entry| entry.object())
367                .ok_or_else(|| ObjectError::not_found(handle))
368        }
369
370        fn get_mut(&mut self, handle: ObjectHandle) -> Result<&mut TestObject> {
371            let current_gen = self.current_generation(handle.id());
372            if !handle.is_valid(current_gen) {
373                return Err(ObjectError::stale_handle(handle, current_gen));
374            }
375            self.objects
376                .get_mut(&handle.id())
377                .map(|entry| entry.object_mut())
378                .ok_or_else(|| ObjectError::not_found(handle))
379        }
380
381        fn get_by_id(&self, id: u64) -> Option<&TestObject> {
382            self.objects.get(&id).map(|entry| entry.object())
383        }
384
385        fn next_id(&mut self) -> u64 {
386            self.objects.len() as u64 + 1
387        }
388
389        fn increment_generation(&mut self, id: u64) {
390            let gen = self.generations.entry(id).or_insert(0);
391            *gen = gen.wrapping_add(1);
392        }
393
394        fn current_generation(&self, id: u64) -> u32 {
395            self.generations.get(&id).copied().unwrap_or(0)
396        }
397
398        fn all_ids(&self) -> Vec<u64> {
399            self.objects.keys().copied().collect()
400        }
401
402        fn len(&self) -> usize {
403            self.objects.len()
404        }
405
406        fn get_parent(&self, handle: &ObjectHandle) -> Result<Option<ObjectHandle>> {
407            let current_gen = self.current_generation(handle.id());
408            if !handle.is_valid(current_gen) {
409                return Err(ObjectError::stale_handle(*handle, current_gen));
410            }
411            if !self.objects.contains_key(&handle.id()) {
412                return Err(ObjectError::not_found(*handle));
413            }
414            Ok(self.parents.get(&handle.id()).copied())
415        }
416
417        fn get_children(&self, handle: &ObjectHandle) -> Result<Vec<ObjectHandle>> {
418            let current_gen = self.current_generation(handle.id());
419            if !handle.is_valid(current_gen) {
420                return Err(ObjectError::stale_handle(*handle, current_gen));
421            }
422            if !self.objects.contains_key(&handle.id()) {
423                return Err(ObjectError::not_found(*handle));
424            }
425            Ok(self
426                .children
427                .get(&handle.id())
428                .cloned()
429                .unwrap_or_else(Vec::new))
430        }
431    }
432
433    #[test]
434    fn test_window_navigation_to_tabs() {
435        let mut registry = TestRegistry::new();
436
437        // Create a window and tabs
438        let window_handle = ObjectHandle::new(ObjectType::Window, 1, 0);
439        let tab1_handle = ObjectHandle::new(ObjectType::Tab, 2, 0);
440        let tab2_handle = ObjectHandle::new(ObjectType::Tab, 3, 0);
441
442        registry
443            .objects
444            .insert(1, RegistryEntry::new(window_handle, TestObject));
445        registry
446            .objects
447            .insert(2, RegistryEntry::new(tab1_handle, TestObject));
448        registry
449            .objects
450            .insert(3, RegistryEntry::new(tab2_handle, TestObject));
451
452        // Set window as parent of tabs
453        registry.set_parent(tab1_handle, window_handle);
454        registry.set_parent(tab2_handle, window_handle);
455
456        let window_proxy = WindowProxy::new(window_handle).unwrap();
457        let tabs = window_proxy.tabs(&registry).unwrap();
458
459        assert_eq!(tabs.len(), 2);
460        assert!(tabs.iter().any(|t| t.id() == 2));
461        assert!(tabs.iter().any(|t| t.id() == 3));
462    }
463
464    #[test]
465    fn test_window_navigation_active_tab() {
466        let mut registry = TestRegistry::new();
467
468        // Create a window and tabs
469        let window_handle = ObjectHandle::new(ObjectType::Window, 1, 0);
470        let tab1_handle = ObjectHandle::new(ObjectType::Tab, 2, 0);
471        let tab2_handle = ObjectHandle::new(ObjectType::Tab, 3, 0);
472
473        registry
474            .objects
475            .insert(1, RegistryEntry::new(window_handle, TestObject));
476        registry
477            .objects
478            .insert(2, RegistryEntry::new(tab1_handle, TestObject));
479        registry
480            .objects
481            .insert(3, RegistryEntry::new(tab2_handle, TestObject));
482
483        // Set window as parent of tabs
484        registry.set_parent(tab1_handle, window_handle);
485        registry.set_parent(tab2_handle, window_handle);
486
487        let window_proxy = WindowProxy::new(window_handle).unwrap();
488        let active_tab = window_proxy.active_tab(&registry).unwrap();
489
490        // Should return the first tab
491        assert!(active_tab.id() == 2 || active_tab.id() == 3);
492    }
493
494    #[test]
495    fn test_window_navigation_active_pane() {
496        let mut registry = TestRegistry::new();
497
498        // Create a window, tab, and pane hierarchy
499        let window_handle = ObjectHandle::new(ObjectType::Window, 1, 0);
500        let tab_handle = ObjectHandle::new(ObjectType::Tab, 2, 0);
501        let pane_handle = ObjectHandle::new(ObjectType::Pane, 3, 0);
502
503        registry
504            .objects
505            .insert(1, RegistryEntry::new(window_handle, TestObject));
506        registry
507            .objects
508            .insert(2, RegistryEntry::new(tab_handle, TestObject));
509        registry
510            .objects
511            .insert(3, RegistryEntry::new(pane_handle, TestObject));
512
513        // Set hierarchy: window -> tab -> pane
514        registry.set_parent(tab_handle, window_handle);
515        registry.set_parent(pane_handle, tab_handle);
516
517        let window_proxy = WindowProxy::new(window_handle).unwrap();
518        let active_pane = window_proxy.active_pane(&registry).unwrap();
519
520        assert_eq!(active_pane.id(), 3);
521    }
522
523    #[test]
524    fn test_window_navigation_no_tabs() {
525        let mut registry = TestRegistry::new();
526
527        let window_handle = ObjectHandle::new(ObjectType::Window, 1, 0);
528        registry
529            .objects
530            .insert(1, RegistryEntry::new(window_handle, TestObject));
531
532        let window_proxy = WindowProxy::new(window_handle).unwrap();
533
534        // No tabs should return empty vector
535        let tabs = window_proxy.tabs(&registry).unwrap();
536        assert!(tabs.is_empty());
537
538        // Active tab should fail
539        let result = window_proxy.active_tab(&registry);
540        assert!(result.is_err());
541
542        // Active pane should also fail
543        let result = window_proxy.active_pane(&registry);
544        assert!(result.is_err());
545    }
546
547    #[test]
548    fn test_window_navigation_wrong_child_type() {
549        let mut registry = TestRegistry::new();
550
551        // Create a window with a pane as direct child (wrong!)
552        let window_handle = ObjectHandle::new(ObjectType::Window, 1, 0);
553        let pane_handle = ObjectHandle::new(ObjectType::Pane, 2, 0);
554
555        registry
556            .objects
557            .insert(1, RegistryEntry::new(window_handle, TestObject));
558        registry
559            .objects
560            .insert(2, RegistryEntry::new(pane_handle, TestObject));
561
562        // Set window as parent of pane (should be tab)
563        registry.set_parent(pane_handle, window_handle);
564
565        let window_proxy = WindowProxy::new(window_handle).unwrap();
566        let result = window_proxy.tabs(&registry);
567
568        // Should fail with type mismatch
569        assert!(matches!(result, Err(ObjectError::TypeMismatch { .. })));
570    }
571
572    #[test]
573    fn test_window_navigation_complete_hierarchy() {
574        let mut registry = TestRegistry::new();
575
576        // Create complete hierarchy: window -> tab -> pane
577        let window_handle = ObjectHandle::new(ObjectType::Window, 1, 0);
578        let tab1_handle = ObjectHandle::new(ObjectType::Tab, 2, 0);
579        let tab2_handle = ObjectHandle::new(ObjectType::Tab, 3, 0);
580        let pane1_handle = ObjectHandle::new(ObjectType::Pane, 4, 0);
581        let pane2_handle = ObjectHandle::new(ObjectType::Pane, 5, 0);
582
583        registry
584            .objects
585            .insert(1, RegistryEntry::new(window_handle, TestObject));
586        registry
587            .objects
588            .insert(2, RegistryEntry::new(tab1_handle, TestObject));
589        registry
590            .objects
591            .insert(3, RegistryEntry::new(tab2_handle, TestObject));
592        registry
593            .objects
594            .insert(4, RegistryEntry::new(pane1_handle, TestObject));
595        registry
596            .objects
597            .insert(5, RegistryEntry::new(pane2_handle, TestObject));
598
599        // Set hierarchy
600        registry.set_parent(tab1_handle, window_handle);
601        registry.set_parent(tab2_handle, window_handle);
602        registry.set_parent(pane1_handle, tab1_handle);
603        registry.set_parent(pane2_handle, tab2_handle);
604
605        let window_proxy = WindowProxy::new(window_handle).unwrap();
606
607        // Test tabs navigation
608        let tabs = window_proxy.tabs(&registry).unwrap();
609        assert_eq!(tabs.len(), 2);
610
611        // Test active tab navigation
612        let active_tab = window_proxy.active_tab(&registry).unwrap();
613        assert!(active_tab.id() == 2 || active_tab.id() == 3);
614
615        // Test active pane navigation (through active tab)
616        let active_pane = window_proxy.active_pane(&registry).unwrap();
617        assert!(active_pane.id() == 4 || active_pane.id() == 5);
618    }
619}