leftwm_core/handlers/command_handler/
scratchpad_handler.rs

1/// # Module for handling the scratchpad related commands
2/// Global facing structures are `ReleaseScratchPadOption` and `Direction` which are re-exported at
3/// the upper levels to make it easier to use.
4///
5/// All the other public methods are only ment for the use as command handlers
6use std::collections::VecDeque;
7
8use serde::{Deserialize, Serialize};
9
10use crate::{
11    child_process::{exec_shell_with_args, ChildID},
12    models::{Handle, ScratchPadName, TagId, WindowHandle},
13    Command, Config, DisplayAction, DisplayServer, Manager, Window,
14};
15
16/// Describes the options for the release scratchpad command
17#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
18pub enum ReleaseScratchPadOption<H: Handle> {
19    /// Release a window from a scratchpad given a window handle
20    #[serde(bound = "")]
21    Handle(WindowHandle<H>),
22    /// Release a window from a scratchpad given a scratchpad name, the most upper window in the
23    /// scratchpad queue will be released
24    ScratchpadName(ScratchPadName),
25    /// Release the currently focused window from its scratchpad
26    None,
27}
28
29/// Hide scratchpad window:
30/// Expects that the window handle is a valid handle to a visible scratchpad window
31fn hide_scratchpad<H: Handle, C: Config, SERVER: DisplayServer<H>>(
32    manager: &mut Manager<H, C, SERVER>,
33    scratchpad_window: &WindowHandle<H>,
34) -> Result<(), &'static str> {
35    tracing::trace!("Hide scratchpad window {:?}", scratchpad_window);
36    let nsp_tag = manager
37        .state
38        .tags
39        .get_hidden_by_label("NSP")
40        .ok_or("Could not find NSP tag")?;
41    let window = manager
42        .state
43        .windows
44        .iter_mut()
45        .find(|w| w.handle == *scratchpad_window)
46        .ok_or("Could not find window from scratchpad_window")?;
47
48    window.untag();
49    // Hide the scratchpad.
50    window.tag(&nsp_tag.id);
51    window.set_visible(false);
52
53    // Send tag changement to X
54    let act = DisplayAction::SetWindowTag(*scratchpad_window, window.tag);
55    manager.state.actions.push_back(act);
56    manager.state.sort_windows();
57    manager
58        .state
59        .handle_single_border(manager.config.border_width());
60
61    // Will ignore current window handler because we just set it invisible
62    let last_focused_still_visible = manager
63        .state
64        .focus_manager
65        .window_history
66        .iter()
67        .find(|handle| {
68            manager
69                .state
70                .windows
71                .iter()
72                .find(|window| Some(window.handle) == **handle)
73                .is_some_and(Window::visible)
74        })
75        .copied();
76
77    // Make sure when changing focus the lastly focused window is focused
78    let handle = if let Some(prev) = last_focused_still_visible {
79        prev
80    } else if let Some(ws) = manager
81        .state
82        .focus_manager
83        .workspace(&manager.state.workspaces)
84    {
85        manager
86            .state
87            .windows
88            .iter()
89            .find(|w| ws.is_managed(w))
90            .map(|w| w.handle)
91    } else {
92        None
93    };
94    if let Some(handle) = handle {
95        manager.state.handle_window_focus(&handle);
96    }
97
98    Ok(())
99}
100
101/// Makes a scratchpad window visible:
102/// Expects that the window handle is a valid handle to an invisible scratchpad window
103fn show_scratchpad<H: Handle, C: Config, SERVER: DisplayServer<H>>(
104    manager: &mut Manager<H, C, SERVER>,
105    scratchpad_window: &WindowHandle<H>,
106) -> Result<(), &'static str> {
107    tracing::trace!("Show scratchpad window {:?}", scratchpad_window);
108    let current_tag = &manager
109        .state
110        .focus_manager
111        .tag(0)
112        .ok_or("Could not retrieve the current tag")?;
113    let window = manager
114        .state
115        .windows
116        .iter_mut()
117        .find(|w| w.handle == *scratchpad_window)
118        .ok_or("Could not find window from scratchpad_window")?;
119    let previous_tag = window.tag;
120    window.untag();
121
122    // Remove the entry for the previous tag to prevent the scratchpad being
123    // refocused.
124    if let Some(previous_tag) = previous_tag {
125        manager
126            .state
127            .focus_manager
128            .tags_last_window
129            .remove(&previous_tag);
130    }
131    // Show the scratchpad.
132    window.tag(current_tag);
133    window.set_visible(true);
134
135    // Send tag changement to X
136    let act = DisplayAction::SetWindowTag(*scratchpad_window, window.tag);
137    manager.state.actions.push_back(act);
138    manager.state.sort_windows();
139    manager
140        .state
141        .handle_single_border(manager.config.border_width());
142    manager.state.handle_window_focus(scratchpad_window);
143    manager.state.move_to_top(scratchpad_window);
144
145    Ok(())
146}
147
148/// With the introduction of `VecDeque` for scratchpads, it is possible that a window gets destroyed
149/// in the middle of the `VecDeque`. This is an abstraction to retrieve the next valid pid from a
150/// scratchpad. While walking the scratchpad windows, invalid pids will get removed.
151fn next_valid_scratchpad_pid<H: Handle>(
152    scratchpad_windows: &mut VecDeque<u32>,
153    managed_windows: &[Window<H>],
154    direction: Direction,
155) -> Option<u32> {
156    while let Some(window) = if direction == Direction::Forward {
157        scratchpad_windows.pop_front()
158    } else {
159        scratchpad_windows.pop_back()
160    } {
161        if managed_windows.iter().any(|w| w.pid == Some(window)) {
162            if direction == Direction::Forward {
163                scratchpad_windows.push_front(window);
164            } else {
165                scratchpad_windows.push_back(window);
166            }
167            return Some(window);
168        }
169
170        tracing::info!(
171            "Dead window in scratchpad found, discard: window PID: {}",
172            window
173        );
174    }
175
176    None
177}
178
179/// Check if the scratchpad is visible on the current tag.
180/// Returns `false` immediately if the scratchpad name isn't defined in the config
181fn is_scratchpad_visible<H: Handle, C: Config, SERVER: DisplayServer<H>>(
182    manager: &Manager<H, C, SERVER>,
183    scratchpad_name: &ScratchPadName,
184) -> bool {
185    // Like Try operator but returns false and only works on `Option`s
186    macro_rules! try_bool {
187        ($cond:expr) => {
188            if let Some(value) = $cond {
189                value
190            } else {
191                return false;
192            }
193        };
194    }
195    let current_tag = try_bool!(manager.state.focus_manager.tag(0));
196    let scratchpad = try_bool!(manager.state.active_scratchpads.get(scratchpad_name));
197
198    // Filter out all the non existing windows (invalid pid) and map to window
199    // Check if any of them is in the current tag
200    scratchpad
201        .iter()
202        .filter_map(|pid| manager.state.windows.iter().find(|w| w.pid == Some(*pid)))
203        .any(|window| window.has_tag(&current_tag))
204}
205
206/// Handle the command to toggle the scratchpad
207pub fn toggle_scratchpad<H: Handle, C: Config, SERVER: DisplayServer<H>>(
208    manager: &mut Manager<H, C, SERVER>,
209    name: &ScratchPadName,
210) -> Option<bool> {
211    let current_tag = &manager.state.focus_manager.tag(0)?;
212
213    // Check if there is a valid scratchpad, if so handle it and return immediately
214    if let Some(id) = manager.state.active_scratchpads.get_mut(name) {
215        if let Some(first_in_scratchpad) =
216            next_valid_scratchpad_pid(id, &manager.state.windows, Direction::Forward)
217        {
218            if let Some((is_visible, window_handle)) = manager
219                .state
220                .windows
221                .iter()
222                .find(|w| w.pid == Some(first_in_scratchpad))
223                .map(|w| (w.has_tag(current_tag), w.handle))
224            {
225                let action_result = if is_visible {
226                    // Window is visible => Hide the scratchpad.
227                    hide_scratchpad(manager, &window_handle)
228                } else {
229                    // Window is hidden => show the scratchpad
230                    show_scratchpad(manager, &window_handle)
231                };
232
233                // Report the result of hiding/showing the scratchpad
234                return match action_result {
235                    Ok(()) => Some(true),
236                    Err(msg) => {
237                        tracing::error!("{}", msg);
238                        return Some(false);
239                    }
240                };
241            }
242        }
243    }
244
245    let scratchpad = manager
246        .state
247        .scratchpads
248        .iter()
249        .find(|s| name == &s.name)?
250        .clone();
251
252    tracing::debug!(
253        "No active scratchpad found for name {:?}. Creating a new one",
254        name
255    );
256    tracing::debug!("Args for scratchpad: {:?}", &scratchpad.args);
257
258    let pid: ChildID = exec_shell_with_args(
259        &scratchpad.value,
260        scratchpad.args.unwrap_or_else(Vec::new),
261        &mut manager.children,
262    )?;
263
264    match manager.state.active_scratchpads.get_mut(name) {
265        Some(windows) => {
266            windows.push_front(pid);
267        }
268        None => {
269            manager
270                .state
271                .active_scratchpads
272                .insert(scratchpad.name, VecDeque::from([pid]));
273        }
274    }
275
276    None
277}
278
279/// Attaches the `WindowHandle` or the currently selected window to the selected `scratchpad`
280pub fn attach_scratchpad<H: Handle, C: Config, SERVER: DisplayServer<H>>(
281    window: Option<WindowHandle<H>>,
282    scratchpad: &ScratchPadName,
283    manager: &mut Manager<H, C, SERVER>,
284) -> Option<bool> {
285    // If `None`, replace with current window
286    let window_handle = {
287        let current_window = manager
288            .state
289            .focus_manager
290            .window_history
291            .front()?
292            .as_ref()
293            .copied();
294
295        window.or(current_window)?
296    };
297
298    // Retrieve and prepare window information
299    let window_pid = {
300        let ws = manager
301            .state
302            .focus_manager
303            .workspace(&manager.state.workspaces)?;
304        let to_scratchpad = manager
305            .state
306            .scratchpads
307            .iter()
308            .find(|s| &s.name == scratchpad)?;
309        let new_float_exact = to_scratchpad.xyhw(&ws.xyhw);
310
311        let window = manager
312            .state
313            .windows
314            .iter_mut()
315            .find(|w| w.handle == window_handle)?;
316
317        // Put window in correct position
318        window.set_floating(true);
319        window.normal = ws.xyhw;
320        window.set_floating_exact(new_float_exact);
321        tracing::debug!("Set window to floating: {:?}", window);
322
323        window.pid?
324    };
325
326    if let Some(windows) = manager.state.active_scratchpads.get_mut(scratchpad) {
327        tracing::debug!(
328            "Scratchpad {:?} already active, push scratchpad",
329            &scratchpad
330        );
331        let previous_scratchpad_handle = manager
332            .state
333            .windows
334            .iter()
335            .find(|w| w.pid.as_ref() == windows.front())
336            .map(|w| w.handle);
337
338        // Check if window already in scratchpad
339        if windows.iter().any(|pid| *pid == window_pid) {
340            return Some(false);
341        }
342
343        windows.push_front(window_pid);
344        if let Some(previous_scratchpad_handle) = previous_scratchpad_handle {
345            hide_scratchpad(manager, &previous_scratchpad_handle).ok()?; // first hide current scratchpad window
346        }
347    } else {
348        tracing::debug!(
349            "Scratchpad {:?} not active yet, open scratchpad",
350            &scratchpad
351        );
352        manager
353            .state
354            .active_scratchpads
355            .insert(scratchpad.clone(), VecDeque::from([window_pid]));
356    }
357    manager.state.sort_windows();
358
359    Some(true)
360}
361
362/// Release a scratchpad to become a normal window. When tag is None, use current active tag as the
363/// destination. Window can be a handle to select a specific window, the name of a scratchpad or
364/// none to select the current window.
365pub fn release_scratchpad<H: Handle, C: Config, SERVER: DisplayServer<H>>(
366    window: ReleaseScratchPadOption<H>,
367    tag: Option<TagId>,
368    manager: &mut Manager<H, C, SERVER>,
369) -> Option<bool> {
370    let destination_tag =
371        tag.or_else(|| manager.state.focus_manager.tag_history.front().copied())?;
372
373    // If `None`, replace with current window
374    let window = if window == ReleaseScratchPadOption::None {
375        ReleaseScratchPadOption::Handle(
376            manager
377                .state
378                .focus_manager
379                .window_history
380                .front()?
381                .as_ref()
382                .copied()?,
383        )
384    } else {
385        window
386    };
387
388    match window {
389        ReleaseScratchPadOption::Handle(window_handle) => {
390            // Check if window is in active scratchpad
391            let window = manager
392                .state
393                .windows
394                .iter_mut()
395                .find(|w| w.handle == window_handle)?;
396
397            let scratchpad_name: ScratchPadName = manager
398                .state
399                .active_scratchpads
400                .iter_mut()
401                .find(|(_, id)| window.pid.as_ref() == id.front())
402                .map(|(name, _)| name.clone())?;
403
404            tracing::debug!(
405                "Releasing scratchpad {:?} to tag {}",
406                scratchpad_name,
407                destination_tag
408            );
409
410            // If we found window in scratchpad, remove it from active_scratchpads
411            if let Some(windows) = manager.state.active_scratchpads.get_mut(&scratchpad_name) {
412                if windows.len() > 1 {
413                    // If more than 1, pop of the stack
414                    tracing::debug!("Removed 1 window from scratchpad {:?}", &scratchpad_name);
415                    windows.remove(
416                        windows
417                            .iter()
418                            .position(|w| Some(w) == window.pid.as_ref())?,
419                    );
420                } else {
421                    // If only 1, remove entire vec, not needed anymore
422                    tracing::debug!(
423                        "Empty scratchpad {:?}, removing from active_scratchpads",
424                        &scratchpad_name
425                    );
426                    manager.state.active_scratchpads.remove(&scratchpad_name);
427                }
428            }
429
430            Some(manager.command_handler(&Command::SendWindowToTag {
431                window: Some(window_handle),
432                tag: destination_tag,
433            }))
434        }
435        ReleaseScratchPadOption::ScratchpadName(scratchpad_name) => {
436            // Remove and get value from active_scratchpad
437            let window_pid = manager
438                .state
439                .active_scratchpads
440                .get_mut(&scratchpad_name)
441                .and_then(|pids| {
442                    next_valid_scratchpad_pid(pids, &manager.state.windows, Direction::Forward)
443                })?;
444            manager // We found already a working pid, discard from scratchpad
445                .state
446                .active_scratchpads
447                .get_mut(&scratchpad_name)?
448                .pop_front();
449
450            let window_handle = manager
451                .state
452                .windows
453                .iter()
454                .find(|w| w.pid == Some(window_pid))
455                .map(|w| w.handle);
456
457            tracing::debug!(
458                "Releasing scratchpad {:?} to tag {}",
459                scratchpad_name,
460                destination_tag
461            );
462
463            Some(manager.command_handler(&Command::SendWindowToTag {
464                window: window_handle,
465                tag: destination_tag,
466            }))
467        }
468        ReleaseScratchPadOption::None => unreachable!(), // Should not be possible
469    }
470}
471
472#[derive(Debug, Clone, Copy, Eq, PartialEq)]
473pub enum Direction {
474    Forward,
475    Backward,
476}
477
478/// Cycles the currently visible scratchpad window given the scratchpads name. Only visible
479/// scratchpads will be handled, otherwise ignored
480pub fn cycle_scratchpad_window<H: Handle, C: Config, SERVER: DisplayServer<H>>(
481    manager: &mut Manager<H, C, SERVER>,
482    scratchpad_name: &ScratchPadName,
483    direction: Direction,
484) -> Option<bool> {
485    // Prevent cycles when scratchpad is not visible
486    if !is_scratchpad_visible(manager, scratchpad_name) {
487        return Some(false);
488    }
489
490    let scratchpad = manager.state.active_scratchpads.get_mut(scratchpad_name)?;
491    // Get a handle to the currently visible window, so we can hide it later
492    let visible_window_handle = manager
493        .state
494        .windows
495        .iter()
496        .find(|w| w.pid.as_ref() == scratchpad.front()) // scratchpad.front() ok because checked in is_scratchpad_visible
497        .map(|w| w.handle);
498
499    // Reorder the scratchpads
500    // Clean scratchpad and exit if no next exists
501    next_valid_scratchpad_pid(scratchpad, &manager.state.windows, direction)?;
502    // Perform cycle
503    match direction {
504        Direction::Forward => scratchpad.rotate_left(1),
505        Direction::Backward => scratchpad.rotate_right(1),
506    };
507    let new_window_pid = *scratchpad.front()?;
508
509    // Hide the previous visible window
510    if let Err(msg) = hide_scratchpad(manager, &visible_window_handle?) {
511        tracing::error!("{}", msg);
512        return Some(false);
513    }
514
515    // Show the new front window
516    let new_window_handle = manager
517        .state
518        .windows
519        .iter()
520        .find(|w| w.pid == Some(new_window_pid))
521        .map(|w| w.handle)?;
522    if let Err(msg) = show_scratchpad(manager, &new_window_handle) {
523        tracing::error!("{}", msg);
524        return Some(false);
525    }
526
527    // Communicate changes to the rest of manager
528    manager.state.sort_windows();
529
530    Some(true)
531}
532
533#[cfg(test)]
534mod tests {
535    use crate::{
536        config::ScratchPad,
537        models::{MockHandle, ScratchPadName},
538    };
539
540    use super::*;
541
542    #[test]
543    fn show_scratchpad_test() {
544        let mut manager = Manager::new_test(vec!["AO".to_string(), "EU".to_string()]);
545        manager.screen_create_handler(Default::default());
546        let nsp_tag = manager.state.tags.get_hidden_by_label("NSP").unwrap().id;
547        let first_tag = manager.state.tags.get(1).unwrap().id;
548
549        let mock_window = 1_u32;
550        let window_handle = WindowHandle::<MockHandle>(mock_window as i32);
551        manager.window_created_handler(Window::new(window_handle, None, Some(mock_window)), -1, -1);
552        // Make sure the window is on the first tag
553        manager.command_handler(&Command::SendWindowToTag {
554            window: None,
555            tag: first_tag,
556        });
557
558        show_scratchpad(&mut manager, &window_handle).unwrap();
559
560        let window = manager
561            .state
562            .windows
563            .iter_mut()
564            .find(|w| w.pid == Some(mock_window))
565            .unwrap();
566
567        assert!(
568            !window.has_tag(&nsp_tag),
569            "Scratchpad window is still in hidden NSP tag"
570        );
571        assert!(
572            window.visible(),
573            "Scratchpad window still is marked as invisible"
574        );
575    }
576
577    #[test]
578    fn hide_scratchpad_test() {
579        let mut manager = Manager::new_test(vec!["AO".to_string(), "EU".to_string()]);
580        manager.screen_create_handler(Default::default());
581        let nsp_tag = manager.state.tags.get_hidden_by_label("NSP").unwrap().id;
582        let first_tag = manager.state.tags.get(1).unwrap().id;
583
584        let mock_window = 1_u32;
585        let window_handle = WindowHandle::<MockHandle>(mock_window as i32);
586        manager.window_created_handler(Window::new(window_handle, None, Some(mock_window)), -1, -1);
587        // Make sure the window is on the first tag
588        manager.command_handler(&Command::SendWindowToTag {
589            window: None,
590            tag: first_tag,
591        });
592
593        hide_scratchpad(&mut manager, &window_handle).unwrap();
594
595        let window = manager
596            .state
597            .windows
598            .iter_mut()
599            .find(|w| w.pid == Some(mock_window))
600            .unwrap();
601
602        assert!(
603            window.has_tag(&nsp_tag),
604            "Scratchpad window is not in hidden NSP tag"
605        );
606        assert!(
607            !window.visible(),
608            "Scratchpad window is not marked as invisible"
609        );
610    }
611
612    #[test]
613    fn toggle_scratchpad_test() {
614        let mut manager = Manager::new_test(vec!["AO".to_string(), "EU".to_string()]);
615        manager.screen_create_handler(Default::default());
616        let nsp_tag = manager.state.tags.get_hidden_by_label("NSP").unwrap().id;
617
618        let mock_window = 1_u32;
619        let window_handle = WindowHandle::<MockHandle>(mock_window as i32);
620        let scratchpad_name: ScratchPadName = "Alacritty".into();
621        manager.window_created_handler(Window::new(window_handle, None, Some(mock_window)), -1, -1);
622        manager.state.scratchpads.push(ScratchPad {
623            name: scratchpad_name.clone(),
624            value: String::new(),
625            args: None,
626            x: None,
627            y: None,
628            height: None,
629            width: None,
630        });
631        manager
632            .state
633            .active_scratchpads
634            .insert(scratchpad_name.clone(), VecDeque::from([mock_window]));
635
636        manager.command_handler(&Command::ToggleScratchPad(scratchpad_name.clone()));
637
638        // Assert window is hidden
639        {
640            let window = manager
641                .state
642                .windows
643                .iter_mut()
644                .find(|w| w.pid == Some(mock_window))
645                .unwrap();
646
647            assert!(
648                window.has_tag(&nsp_tag),
649                "Scratchpad window is not in hidden NSP tag"
650            );
651            assert!(!window.visible(), "Scratchpad is still marked as visible");
652        }
653
654        manager.command_handler(&Command::ToggleScratchPad(scratchpad_name));
655
656        // Assert window is revealed
657        {
658            let window = manager
659                .state
660                .windows
661                .iter_mut()
662                .find(|w| w.pid == Some(mock_window))
663                .unwrap();
664
665            assert!(
666                !window.has_tag(&nsp_tag),
667                "Scratchpad window should not be in the hidden NSP tag"
668            );
669            assert!(
670                window.visible(),
671                "Scratchpad window is still marked as invisible"
672            );
673        }
674    }
675
676    #[test]
677    /// Test release scratchpad command for 1 window in the scratchpad
678    /// After releasing, the scratchpad should not be active anymore (no more windows)
679    fn release_scratchpad_test() {
680        let mut manager = Manager::new_test(vec!["AO".to_string(), "EU".to_string()]);
681        manager.screen_create_handler(Default::default());
682
683        // Setup
684        let mock_window1 = 10_u32;
685        let scratchpad_name: ScratchPadName = "Alacritty".into();
686        manager
687            .state
688            .active_scratchpads
689            .insert(scratchpad_name.clone(), VecDeque::from([mock_window1]));
690        manager.window_created_handler(
691            Window::new(
692                WindowHandle::<MockHandle>(mock_window1 as i32),
693                None,
694                Some(mock_window1),
695            ),
696            -1,
697            -1,
698        );
699
700        let expected_tag = manager.state.tags.get(1).unwrap().id;
701
702        // Release Scratchpad
703        manager.command_handler(&Command::ReleaseScratchPad {
704            window: ReleaseScratchPadOption::Handle(WindowHandle::<MockHandle>(
705                mock_window1 as i32,
706            )),
707            tag: Some(expected_tag),
708        });
709
710        // Assert
711        assert!(!manager
712            .state
713            .active_scratchpads
714            .contains_key(&scratchpad_name));
715        assert_eq!(
716            *manager.state.focus_manager.tag_history.front().unwrap(),
717            expected_tag
718        );
719    }
720
721    #[test]
722    /// Testing release scratchpad command with more than 1 window in a scratchpad
723    /// After releasing 1 window, the rest should still be in the scratchpad
724    fn release_scratchpad_multiple_windows_test() {
725        let mut manager = Manager::new_test(vec!["AO".to_string(), "EU".to_string()]);
726        manager.screen_create_handler(Default::default());
727        let nsp_tag = manager.state.tags.get_hidden_by_label("NSP").unwrap().id;
728
729        // Setup
730        let mock_window1 = 1_u32;
731        let mock_window2 = 2_u32;
732        let mock_window3 = 3_u32;
733        let scratchpad_name: ScratchPadName = "Alacritty".into();
734        manager.state.active_scratchpads.insert(
735            scratchpad_name.clone(),
736            VecDeque::from([mock_window1, mock_window2, mock_window3]),
737        );
738        for window in [mock_window1, mock_window2, mock_window3] {
739            manager.window_created_handler(
740                Window::new(
741                    WindowHandle::<MockHandle>(window as i32),
742                    None,
743                    Some(window),
744                ),
745                -1,
746                -1,
747            );
748        }
749
750        let expected_tag = manager.state.tags.get(1).unwrap().id;
751
752        // Release Scratchpad
753        manager.command_handler(&Command::ReleaseScratchPad {
754            window: ReleaseScratchPadOption::Handle(WindowHandle::<MockHandle>(
755                mock_window1 as i32,
756            )),
757            tag: Some(expected_tag),
758        });
759
760        // Assert
761        let scratchpad = manager
762            .state
763            .active_scratchpads
764            .get_mut(&scratchpad_name)
765            .unwrap();
766
767        assert!(manager
768            .state
769            .windows
770            .iter()
771            .find(|w| w.pid == Some(mock_window1))
772            .map(|w| !w.has_tag(&nsp_tag))
773            .unwrap());
774        for mock_window_pid in [mock_window2, mock_window3] {
775            let window_pid = scratchpad.pop_front();
776            assert_eq!(window_pid, Some(mock_window_pid));
777            assert!(!manager
778                .state
779                .windows
780                .iter()
781                .find(|w| w.pid == window_pid)
782                .map(|w| w.has_tag(&nsp_tag))
783                .unwrap());
784        }
785        assert_eq!(scratchpad.pop_front(), None);
786
787        assert_eq!(
788            *manager.state.focus_manager.tag_history.front().unwrap(),
789            expected_tag
790        );
791    }
792
793    #[test]
794    fn attach_scratchpad_test() {
795        let mut manager = Manager::new_test(vec!["AO".to_string(), "EU".to_string()]);
796        manager.screen_create_handler(Default::default());
797        let nsp_tag = manager.state.tags.get_hidden_by_label("NSP").unwrap().id;
798
799        // Setup
800        let mock_window1 = 1_u32;
801        let mock_window2 = 2_u32;
802        let mock_window3 = 3_u32;
803        let scratchpad_name: ScratchPadName = "Alacritty".into();
804        manager.state.scratchpads.push(ScratchPad {
805            name: scratchpad_name.clone(),
806            value: "scratchpad".to_string(),
807            args: None,
808            x: None,
809            y: None,
810            height: None,
811            width: None,
812        });
813        manager.state.active_scratchpads.insert(
814            scratchpad_name.clone(),
815            VecDeque::from([mock_window2, mock_window3]),
816        );
817        for mock_window in [mock_window1, mock_window2, mock_window3] {
818            let mut window = Window::new(
819                WindowHandle::<MockHandle>(mock_window as i32),
820                None,
821                Some(mock_window),
822            );
823            if mock_window != mock_window1 {
824                window.tag(&nsp_tag);
825            }
826
827            manager.window_created_handler(window, -1, -1);
828        }
829
830        // Attach Scratchpad
831        manager.command_handler(&Command::AttachScratchPad {
832            window: Some(WindowHandle::<MockHandle>(mock_window1 as i32)),
833            scratchpad: scratchpad_name.clone(),
834        });
835
836        // Assert
837        let scratchpad = manager
838            .state
839            .active_scratchpads
840            .get_mut(&scratchpad_name)
841            .unwrap();
842
843        assert_eq!(scratchpad.pop_front(), Some(mock_window1));
844        assert!(manager
845            .state
846            .windows
847            .iter()
848            .find(|w| w.pid == Some(mock_window1))
849            .map(|w| !w.has_tag(&nsp_tag))
850            .unwrap());
851        for mock_window_pid in [mock_window2, mock_window3] {
852            let window_pid = scratchpad.pop_front();
853            assert_eq!(window_pid, Some(mock_window_pid));
854            assert!(manager
855                .state
856                .windows
857                .iter()
858                .find(|w| w.pid == window_pid)
859                .map(|w| w.has_tag(&nsp_tag))
860                .unwrap());
861        }
862        assert_eq!(scratchpad.pop_front(), None);
863    }
864
865    #[test]
866    fn next_valid_pid_forward_test() {
867        // Setup
868        let mock_window1 = 1_u32;
869        let mock_window2 = 2_u32;
870        let mock_window3 = 3_u32;
871        let mock_window4 = 4_u32;
872
873        let mut managed_windows = [mock_window1, mock_window2, mock_window3, mock_window4]
874            .iter()
875            .map(|pid| Window::new(WindowHandle::<MockHandle>(*pid as i32), None, Some(*pid)))
876            .collect::<Vec<Window<MockHandle>>>();
877        let mut scratchpad =
878            VecDeque::from([mock_window1, mock_window2, mock_window3, mock_window4]);
879
880        assert_eq!(
881            next_valid_scratchpad_pid(&mut scratchpad, &managed_windows, Direction::Forward),
882            Some(1)
883        );
884
885        managed_windows.remove(1);
886        assert_eq!(
887            next_valid_scratchpad_pid(&mut scratchpad, &managed_windows, Direction::Forward),
888            Some(1)
889        );
890
891        scratchpad.pop_front();
892        assert_eq!(
893            next_valid_scratchpad_pid(&mut scratchpad, &managed_windows, Direction::Forward),
894            Some(3)
895        );
896        assert_eq!(scratchpad.len(), 2);
897    }
898
899    #[test]
900    fn next_valid_pid_backward_test() {
901        // setup
902        let mock_window1 = 1_u32;
903        let mock_window2 = 2_u32;
904        let mock_window3 = 3_u32;
905        let mock_window4 = 4_u32;
906
907        let mut managed_windows = [mock_window1, mock_window2, mock_window3, mock_window4]
908            .iter()
909            .map(|pid| Window::new(WindowHandle::<MockHandle>(*pid as i32), None, Some(*pid)))
910            .collect::<Vec<Window<MockHandle>>>();
911        let mut scratchpad =
912            VecDeque::from([mock_window1, mock_window2, mock_window3, mock_window4]);
913
914        assert_eq!(
915            next_valid_scratchpad_pid(&mut scratchpad, &managed_windows, Direction::Backward),
916            Some(4)
917        );
918
919        managed_windows.remove(2);
920        assert_eq!(
921            next_valid_scratchpad_pid(&mut scratchpad, &managed_windows, Direction::Backward),
922            Some(4)
923        );
924
925        scratchpad.pop_back();
926        assert_eq!(
927            next_valid_scratchpad_pid(&mut scratchpad, &managed_windows, Direction::Backward),
928            Some(2)
929        );
930        assert_eq!(scratchpad.len(), 2);
931    }
932
933    #[test]
934    #[allow(clippy::too_many_lines)]
935    fn cycle_scratchpad_window_test() {
936        fn is_visible<H: Handle, C: Config, SERVER: DisplayServer<H>>(
937            manager: &Manager<H, C, SERVER>,
938            pid: u32,
939            nsp_tag: TagId,
940        ) -> bool {
941            manager
942                .state
943                .windows
944                .iter()
945                .find(|w| w.pid == Some(pid))
946                .map(|w| w.visible() && !w.has_tag(&nsp_tag))
947                .unwrap()
948        }
949        fn is_only_first_visible<H: Handle, C: Config, SERVER: DisplayServer<H>>(
950            manager: &Manager<H, C, SERVER>,
951            mut pids: impl Iterator<Item = u32>,
952            nsp_tag: TagId,
953        ) -> bool {
954            if !is_visible(manager, pids.next().unwrap(), nsp_tag) {
955                return false;
956            }
957            for pid in pids {
958                if is_visible(manager, pid, nsp_tag) {
959                    return false;
960                }
961            }
962
963            true
964        }
965
966        let mut manager = Manager::new_test(vec!["AO".to_string(), "EU".to_string()]);
967        manager.screen_create_handler(Default::default());
968        let nsp_tag = manager.state.tags.get_hidden_by_label("NSP").unwrap().id;
969
970        // Setup
971        let mock_window1 = 1_u32;
972        let mock_window2 = 2_u32;
973        let mock_window3 = 3_u32;
974        let scratchpad_name: ScratchPadName = "Alacritty".into();
975
976        for mock_window in [mock_window1, mock_window2, mock_window3] {
977            let mut window = Window::new(
978                WindowHandle::<MockHandle>(mock_window as i32),
979                None,
980                Some(mock_window),
981            );
982            if mock_window != mock_window1 {
983                window.tag(&nsp_tag);
984            }
985
986            manager.window_created_handler(window, -1, -1);
987        }
988        manager.state.scratchpads.push(ScratchPad {
989            name: scratchpad_name.clone(),
990            value: "scratchpad".to_string(),
991            args: None,
992            x: None,
993            y: None,
994            height: None,
995            width: None,
996        });
997        manager.state.active_scratchpads.insert(
998            scratchpad_name.clone(),
999            VecDeque::from([mock_window1, mock_window2, mock_window3]),
1000        );
1001
1002        cycle_scratchpad_window(&mut manager, &scratchpad_name, Direction::Forward);
1003        let mut scratchpad_iterator = manager
1004            .state
1005            .active_scratchpads
1006            .get(&scratchpad_name)
1007            .unwrap()
1008            .iter();
1009        assert!(
1010            is_only_first_visible(&manager, scratchpad_iterator.clone().copied(), nsp_tag),
1011            "On the first forward cycle, the first window is not visible or the other windows are visible"
1012        );
1013        assert_eq!(scratchpad_iterator.next(), Some(&mock_window2));
1014        assert_eq!(scratchpad_iterator.next(), Some(&mock_window3));
1015        assert_eq!(scratchpad_iterator.next(), Some(&mock_window1));
1016        assert_eq!(scratchpad_iterator.next(), None);
1017
1018        cycle_scratchpad_window(&mut manager, &scratchpad_name, Direction::Forward);
1019        let mut scratchpad_iterator = manager
1020            .state
1021            .active_scratchpads
1022            .get(&scratchpad_name)
1023            .unwrap()
1024            .iter();
1025        assert!(is_only_first_visible(
1026            &manager,
1027            scratchpad_iterator.clone().copied(),
1028            nsp_tag
1029        ),
1030            "On the second forward cycle, the first window is not visible or the other windows are visible"
1031        );
1032        assert_eq!(scratchpad_iterator.next(), Some(&mock_window3));
1033        assert_eq!(scratchpad_iterator.next(), Some(&mock_window1));
1034        assert_eq!(scratchpad_iterator.next(), Some(&mock_window2));
1035        assert_eq!(scratchpad_iterator.next(), None);
1036
1037        cycle_scratchpad_window(&mut manager, &scratchpad_name, Direction::Backward);
1038        let mut scratchpad_iterator = manager
1039            .state
1040            .active_scratchpads
1041            .get(&scratchpad_name)
1042            .unwrap()
1043            .iter();
1044        assert!(is_only_first_visible(
1045            &manager,
1046            scratchpad_iterator.clone().copied(),
1047            nsp_tag
1048        ),
1049            "After 2 forward and 1 backward cycles, the first window is not visible or the other windows are visible"
1050        );
1051        assert_eq!(scratchpad_iterator.next(), Some(&mock_window2));
1052        assert_eq!(scratchpad_iterator.next(), Some(&mock_window3));
1053        assert_eq!(scratchpad_iterator.next(), Some(&mock_window1));
1054        assert_eq!(scratchpad_iterator.next(), None);
1055
1056        cycle_scratchpad_window(&mut manager, &scratchpad_name, Direction::Backward);
1057        let mut scratchpad_iterator = manager
1058            .state
1059            .active_scratchpads
1060            .get(&scratchpad_name)
1061            .unwrap()
1062            .iter();
1063        assert!(is_only_first_visible(
1064            &manager,
1065            scratchpad_iterator.clone().copied(),
1066            nsp_tag
1067        ),
1068            "After 2 forward and 2 backward cycles, the first window is not visible or the other windows are visible"
1069        );
1070        assert_eq!(scratchpad_iterator.next(), Some(&mock_window1));
1071        assert_eq!(scratchpad_iterator.next(), Some(&mock_window2));
1072        assert_eq!(scratchpad_iterator.next(), Some(&mock_window3));
1073        assert_eq!(scratchpad_iterator.next(), None);
1074    }
1075
1076    #[test]
1077    fn change_focus_with_open_scratchpad_test() {
1078        let mut manager = Manager::new_test(vec!["AO".to_string(), "EU".to_string()]);
1079        manager.screen_create_handler(Default::default());
1080
1081        // Setup
1082        let mock_window1 = 1_u32;
1083        let mock_window2 = 2_u32;
1084        let mock_window3 = 3_u32;
1085        let scratchpad_name: ScratchPadName = "Alacritty".into();
1086
1087        for mock_window in [mock_window1, mock_window2, mock_window3] {
1088            let mut window = Window::new(
1089                WindowHandle::<MockHandle>(mock_window as i32),
1090                None,
1091                Some(mock_window),
1092            );
1093            window.set_visible(true);
1094            window.tag(&1);
1095
1096            manager.window_created_handler(window, -1, -1);
1097        }
1098        manager.state.scratchpads.push(ScratchPad {
1099            name: scratchpad_name.clone(),
1100            value: "scratchpad".to_string(),
1101            args: None,
1102            x: None,
1103            y: None,
1104            height: None,
1105            width: None,
1106        });
1107        manager
1108            .state
1109            .active_scratchpads
1110            .insert(scratchpad_name, VecDeque::from([mock_window3]));
1111
1112        // Focus first window
1113        let focus_window_handler = manager.state.windows[0].handle;
1114        manager.state.handle_window_focus(&focus_window_handler);
1115        assert_eq!(
1116            manager
1117                .state
1118                .focus_manager
1119                .window(&manager.state.windows)
1120                .unwrap()
1121                .handle,
1122            WindowHandle::<MockHandle>(1),
1123            "Initially the first window (1) should be focused"
1124        );
1125
1126        manager.command_handler(&Command::FocusWindowDown);
1127        assert_eq!(
1128            manager
1129                .state
1130                .focus_manager
1131                .window(&manager.state.windows)
1132                .unwrap()
1133                .handle,
1134            WindowHandle::<MockHandle>(2),
1135            "After 1 down window (2) should be focused"
1136        );
1137
1138        manager.command_handler(&Command::FocusWindowDown);
1139        assert_eq!(
1140            manager
1141                .state
1142                .focus_manager
1143                .window(&manager.state.windows)
1144                .unwrap()
1145                .handle,
1146            WindowHandle::<MockHandle>(3),
1147            "After 2 down window (3) should be focused"
1148        );
1149
1150        manager.command_handler(&Command::FocusWindowDown);
1151        assert_eq!(
1152            manager
1153                .state
1154                .focus_manager
1155                .window(&manager.state.windows)
1156                .unwrap()
1157                .handle,
1158            WindowHandle::<MockHandle>(1),
1159            "After 3 down window (1) should be focused (cycle back)"
1160        );
1161
1162        manager.command_handler(&Command::FocusWindowUp);
1163        assert_eq!(
1164            manager
1165                .state
1166                .focus_manager
1167                .window(&manager.state.windows)
1168                .unwrap()
1169                .handle,
1170            WindowHandle::<MockHandle>(3),
1171            "After 3 down and 1 up window (3) should be focused (cycle back)"
1172        );
1173
1174        manager.command_handler(&Command::FocusWindowUp);
1175        assert_eq!(
1176            manager
1177                .state
1178                .focus_manager
1179                .window(&manager.state.windows)
1180                .unwrap()
1181                .handle,
1182            WindowHandle::<MockHandle>(2),
1183            "After 3 down and 2 up window (2) should be focused"
1184        );
1185    }
1186
1187    #[test]
1188    fn focus_top_from_scratchpad_test() {
1189        let mut manager = Manager::new_test(vec!["AO".to_string(), "EU".to_string()]);
1190        manager.screen_create_handler(Default::default());
1191
1192        // Setup
1193        let mock_window1 = 1_u32;
1194        let mock_window2 = 2_u32;
1195        let mock_window3 = 3_u32;
1196        let scratchpad_name: ScratchPadName = "Alacritty".into();
1197
1198        for mock_window in [mock_window1, mock_window2, mock_window3] {
1199            let mut window = Window::new(
1200                WindowHandle::<MockHandle>(mock_window as i32),
1201                None,
1202                Some(mock_window),
1203            );
1204            window.set_visible(true);
1205            window.tag(&1);
1206
1207            manager.window_created_handler(window, -1, -1);
1208        }
1209        manager.state.scratchpads.push(ScratchPad {
1210            name: scratchpad_name.clone(),
1211            value: "scratchpad".to_string(),
1212            args: None,
1213            x: None,
1214            y: None,
1215            height: None,
1216            width: None,
1217        });
1218        manager
1219            .state
1220            .active_scratchpads
1221            .insert(scratchpad_name, VecDeque::from([mock_window3]));
1222
1223        // Focus first window
1224        let focus_window_handler = manager.state.windows[0].handle;
1225        manager.state.handle_window_focus(&focus_window_handler);
1226        assert_eq!(
1227            manager
1228                .state
1229                .focus_manager
1230                .window(&manager.state.windows)
1231                .unwrap()
1232                .handle,
1233            WindowHandle::<MockHandle>(1),
1234            "Initially the first window (1) should be focused"
1235        );
1236
1237        manager.command_handler(&Command::FocusWindowUp);
1238        assert_eq!(
1239            manager
1240                .state
1241                .focus_manager
1242                .window(&manager.state.windows)
1243                .unwrap()
1244                .handle,
1245            WindowHandle::<MockHandle>(3),
1246            "After 1 up window (3) should be focused (scratchpad window)"
1247        );
1248
1249        manager.command_handler(&Command::FocusWindowTop { swap: false });
1250        assert_eq!(
1251            manager
1252                .state
1253                .focus_manager
1254                .window(&manager.state.windows)
1255                .unwrap()
1256                .handle,
1257            WindowHandle::<MockHandle>(1),
1258            "After focusing the scratchpad and then focusing the top, window (1) should be focused"
1259        );
1260    }
1261
1262    #[test]
1263    fn toggle_scratchpad_also_toggles_single_window_borders() {
1264        let mut manager = Manager::new_test_with_border(vec!["1".to_string(), "2".to_string()], 1);
1265        manager.screen_create_handler(Default::default());
1266        let second_tag = manager.state.tags.get(2).unwrap().id;
1267
1268        let scratchpad_pid = 1_u32;
1269        let scratchpad_handle = WindowHandle::<MockHandle>(scratchpad_pid as i32);
1270        let scratchpad_name: ScratchPadName = "Alacritty".into();
1271        let mut scratchpad = Window::new(scratchpad_handle, None, Some(scratchpad_pid));
1272        scratchpad.tag = Some(second_tag);
1273        manager.window_created_handler(scratchpad, -1, -1);
1274        manager.state.scratchpads.push(ScratchPad {
1275            name: scratchpad_name.clone(),
1276            value: String::new(),
1277            args: None,
1278            x: None,
1279            y: None,
1280            height: None,
1281            width: None,
1282        });
1283        manager
1284            .state
1285            .active_scratchpads
1286            .insert(scratchpad_name.clone(), VecDeque::from([scratchpad_pid]));
1287
1288        let window_pid = 2_u32;
1289        let window_handle = WindowHandle::<MockHandle>(window_pid as i32);
1290        manager.window_created_handler(Window::new(window_handle, None, Some(window_pid)), -1, -1);
1291
1292        manager.command_handler(&Command::ToggleScratchPad(scratchpad_name.clone()));
1293
1294        {
1295            let scratchpad = manager
1296                .state
1297                .windows
1298                .iter_mut()
1299                .find(|w| w.pid == Some(scratchpad_pid))
1300                .unwrap();
1301            assert_eq!(scratchpad.border(), 1);
1302
1303            let window = manager
1304                .state
1305                .windows
1306                .iter_mut()
1307                .find(|w| w.pid == Some(window_pid))
1308                .unwrap();
1309            assert_eq!(window.border(), 1);
1310        }
1311
1312        manager.command_handler(&Command::ToggleScratchPad(scratchpad_name));
1313
1314        {
1315            let window = manager
1316                .state
1317                .windows
1318                .iter_mut()
1319                .find(|w| w.pid == Some(window_pid))
1320                .unwrap();
1321
1322            assert_eq!(window.border(), 0);
1323        }
1324    }
1325}