Skip to main content

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