diff --git a/src/cmds.rs b/src/cmds.rs index 71496b9..8569f88 100644 --- a/src/cmds.rs +++ b/src/cmds.rs @@ -172,7 +172,7 @@ impl DisplayFormat for SwayrCommand { } } -fn always_true(_x: &con::Window) -> bool { +fn always_true(_x: &con::DisplayNode) -> bool { true } @@ -216,16 +216,18 @@ pub fn exec_swayr_cmd(args: ExecSwayrCmdArgs) { Direction::Forward, windows, &*props.read().unwrap(), - Box::new(|w: &con::Window| { - !w.is_floating() && w.is_child_of_tiled_container() + Box::new(|dn: &con::DisplayNode| { + !dn.node.is_floating() + && dn.tree.is_child_of_tiled_container(dn.node.id) }), ), SwayrCommand::PrevTiledWindow { windows } => focus_window_in_direction( Direction::Backward, windows, &*props.read().unwrap(), - Box::new(|w: &con::Window| { - !w.is_floating() && w.is_child_of_tiled_container() + Box::new(|dn: &con::DisplayNode| { + !dn.node.is_floating() + && dn.tree.is_child_of_tiled_container(dn.node.id) }), ), SwayrCommand::NextTabbedOrStackedWindow { windows } => { @@ -233,9 +235,11 @@ pub fn exec_swayr_cmd(args: ExecSwayrCmdArgs) { Direction::Forward, windows, &*props.read().unwrap(), - Box::new(|w: &con::Window| { - !w.is_floating() - && w.is_child_of_tabbed_or_stacked_container() + Box::new(|dn: &con::DisplayNode| { + !dn.node.is_floating() + && dn + .tree + .is_child_of_tabbed_or_stacked_container(dn.node.id) }), ) } @@ -244,9 +248,11 @@ pub fn exec_swayr_cmd(args: ExecSwayrCmdArgs) { Direction::Backward, windows, &*props.read().unwrap(), - Box::new(|w: &con::Window| { - !w.is_floating() - && w.is_child_of_tabbed_or_stacked_container() + Box::new(|dn: &con::DisplayNode| { + !dn.node.is_floating() + && dn + .tree + .is_child_of_tabbed_or_stacked_container(dn.node.id) }), ) } @@ -255,7 +261,7 @@ pub fn exec_swayr_cmd(args: ExecSwayrCmdArgs) { Direction::Forward, windows, &*props.read().unwrap(), - Box::new(|w: &con::Window| w.is_floating()), + Box::new(|dn: &con::DisplayNode| dn.node.is_floating()), ) } SwayrCommand::PrevFloatingWindow { windows } => { @@ -263,7 +269,7 @@ pub fn exec_swayr_cmd(args: ExecSwayrCmdArgs) { Direction::Backward, windows, &*props.read().unwrap(), - Box::new(|w: &con::Window| w.is_floating()), + Box::new(|dn: &con::DisplayNode| dn.node.is_floating()), ) } SwayrCommand::NextWindowOfSameLayout { windows } => { @@ -375,25 +381,39 @@ fn quit_window_by_id(id: i64) { run_sway_command(&[format!("[con_id={}]", id).as_str(), "kill"]); } -pub fn get_tree() -> s::Node { +#[deprecated] +pub fn get_tree_old() -> s::Node { match s::Connection::new() { Ok(mut con) => con.get_tree().expect("Got no root node"), Err(err) => panic!("{}", err), } } +pub fn get_tree(include_scratchpad: bool) -> s::Node { + match s::Connection::new() { + Ok(mut con) => { + let mut root = con.get_tree().expect("Got no root node"); + if !include_scratchpad { + root.nodes.retain(|o| !o.is_scratchpad()); + } + root + } + Err(err) => panic!("{}", err), + } +} + pub fn switch_to_urgent_or_lru_window( extra_props: &HashMap, ) { - let root = get_tree(); - let windows = con::get_windows(&root, false, extra_props); - if let Some(win) = windows - .iter() - .find(|w| w.is_urgent()) - .or_else(|| windows.get(0)) - { - println!("Switching to {}, id: {}", win.get_app_name(), win.get_id()); - focus_window_by_id(win.get_id()) + let root = get_tree(false); + let tree = con::get_tree(&root, extra_props); + if let Some(win) = tree.get_windows().get(0) { + println!( + "Switching to {}, id: {}", + win.node.get_app_name(), + win.node.id + ); + focus_window_by_id(win.node.id) } else { println!("No window to switch to.") } @@ -447,11 +467,11 @@ fn handle_non_matching_input(input: &str) { } pub fn switch_window(extra_props: &HashMap) { - let root = get_tree(); - let windows = con::get_windows(&root, true, extra_props); + let root = get_tree(true); + let tree = con::get_tree(&root, extra_props); - match util::select_from_menu("Switch to window", &windows) { - Ok(window) => focus_window_by_id(window.get_id()), + match util::select_from_menu("Switch to window", &tree.get_windows()) { + Ok(window) => focus_window_by_id(window.node.id), Err(non_matching_input) => { handle_non_matching_input(&non_matching_input) } @@ -467,54 +487,49 @@ pub fn focus_window_in_direction( dir: Direction, consider_wins: &ConsiderWindows, extra_props: &HashMap, - pred: Box bool>, + pred: Box bool>, ) { - let root = get_tree(); - let root = match consider_wins { - ConsiderWindows::AllWorkspaces => &root, - ConsiderWindows::CurrentWorkspace => { - con::get_workspaces(&root, false, extra_props) - .iter() - .find(|w| w.is_current()) - .expect("No current workspace!") - .node - } - }; + let root = get_tree(false); + let tree = con::get_tree(&root, extra_props); + let mut wins = tree.get_windows(); + + if consider_wins == &ConsiderWindows::CurrentWorkspace { + let cur_ws = tree.get_current_workspace(); + wins.retain(|w| { + tree.get_workspace_node(w.node.id).unwrap().id == cur_ws.id + }); + } - let mut windows: Vec = - con::get_windows(root, false, extra_props) - .into_iter() - .filter(|w| pred(w)) - .collect(); + wins.retain(pred); - if windows.len() < 2 { + if wins.len() < 2 { return; } - windows.sort_by(|a, b| { - let lru_a = a.last_focus_time_for_next_prev_seq(); - let lru_b = b.last_focus_time_for_next_prev_seq(); + wins.sort_by(|a, b| { + let lru_a = tree.last_focus_time_for_next_prev_seq(a.node.id); + let lru_b = tree.last_focus_time_for_next_prev_seq(b.node.id); lru_a.cmp(&lru_b).reverse() }); - let is_focused_window: Box bool> = - if !windows.iter().any(|w| w.is_focused()) { - let last_focused_win_id = windows.get(0).unwrap().get_id(); - Box::new(move |w| w.get_id() == last_focused_win_id) + let is_focused_window: Box bool> = + if !wins.iter().any(|w| w.node.focused) { + let last_focused_win_id = wins.get(0).unwrap().node.id; + Box::new(move |dn| dn.node.id == last_focused_win_id) } else { - Box::new(|w: &con::Window| w.is_focused()) + Box::new(|dn| dn.node.focused) }; - let mut iter: Box> = match dir { - Direction::Forward => Box::new(windows.iter().rev().cycle()), - Direction::Backward => Box::new(windows.iter().cycle()), + let mut iter: Box> = match dir { + Direction::Forward => Box::new(wins.iter().rev().cycle()), + Direction::Backward => Box::new(wins.iter().cycle()), }; loop { let win = iter.next().unwrap(); if is_focused_window(win) { let win = iter.next().unwrap(); - focus_window_by_id(win.get_id()); + focus_window_by_id(win.node.id); return; } } @@ -525,32 +540,35 @@ pub fn focus_window_of_same_layout_in_direction( consider_wins: &ConsiderWindows, extra_props: &HashMap, ) { - let root = get_tree(); - let windows = con::get_windows(&root, false, extra_props); - let current_window = windows - .iter() - .find(|w| w.is_focused()) - .or_else(|| windows.get(0)); - - if let Some(current_window) = current_window { + let root = get_tree(false); + let tree = con::get_tree(&root, extra_props); + let wins = tree.get_windows(); + let cur_win = wins.get(0); + + if let Some(cur_win) = cur_win { focus_window_in_direction( dir, consider_wins, extra_props, - if current_window.is_floating() { - Box::new(|w| w.is_floating()) - } else if !current_window.is_floating() - && current_window.is_child_of_tabbed_or_stacked_container() + if cur_win.node.is_floating() { + Box::new(|dn| dn.node.is_floating()) + } else if !cur_win.node.is_floating() + && cur_win + .tree + .is_child_of_tabbed_or_stacked_container(cur_win.node.id) { - Box::new(|w| { - !w.is_floating() - && w.is_child_of_tabbed_or_stacked_container() + Box::new(|dn| { + !dn.node.is_floating() + && dn + .tree + .is_child_of_tabbed_or_stacked_container(dn.node.id) }) - } else if !current_window.is_floating() - && current_window.is_child_of_tiled_container() + } else if !cur_win.node.is_floating() + && cur_win.tree.is_child_of_tiled_container(cur_win.node.id) { - Box::new(|w| { - !w.is_floating() && w.is_child_of_tiled_container() + Box::new(|dn| { + !dn.node.is_floating() + && dn.tree.is_child_of_tiled_container(dn.node.id) }) } else { Box::new(always_true) @@ -560,11 +578,14 @@ pub fn focus_window_of_same_layout_in_direction( } pub fn switch_workspace(extra_props: &HashMap) { - let root = get_tree(); - let workspaces = con::get_workspaces(&root, false, extra_props); + let root = get_tree(false); + let tree = con::get_tree(&root, extra_props); - match util::select_from_menu("Switch to workspace", &workspaces) { - Ok(workspace) => run_sway_command(&["workspace", workspace.get_name()]), + match util::select_from_menu("Switch to workspace", &tree.get_workspaces()) + { + Ok(workspace) => { + run_sway_command(&["workspace", workspace.node.get_name()]) + } Err(non_matching_input) => { handle_non_matching_input(&non_matching_input) } @@ -574,15 +595,16 @@ pub fn switch_workspace(extra_props: &HashMap) { pub fn move_focused_container_to_workspace( extra_props: &HashMap, ) { - let root = get_tree(); - let workspaces = con::get_workspaces(&root, false, extra_props); + let root = get_tree(true); + let tree = con::get_tree(&root, extra_props); + let workspaces = tree.get_workspaces(); let val = util::select_from_menu( "Move focused container to workspace", &workspaces, ); let ws_name = &match val { - Ok(workspace) => String::from(workspace.get_name()), + Ok(workspace) => String::from(workspace.node.get_name()), Err(input) => String::from(chop_workspace_shortcut(&input)), }; @@ -601,15 +623,20 @@ pub fn move_focused_container_to_workspace( } pub fn switch_workspace_or_window(extra_props: &HashMap) { - let root = get_tree(); - let workspaces = con::get_workspaces(&root, true, extra_props); - let ws_or_wins = con::WsOrWin::from_workspaces(&workspaces); + let root = get_tree(true); + let tree = con::get_tree(&root, extra_props); + let ws_or_wins = tree.get_workspaces_and_windows(); match util::select_from_menu("Select workspace or window", &ws_or_wins) { - Ok(ws_or_win) => match ws_or_win { - con::WsOrWin::Ws { ws } => { - run_sway_command(&["workspace", ws.get_name()]); + Ok(tn) => match tn.node.get_type() { + con::Type::Workspace => { + if !tn.node.is_scratchpad() { + run_sway_command(&["workspace", tn.node.get_name()]); + } + } + con::Type::Window => focus_window_by_id(tn.node.id), + t => { + eprintln!("Cannot handle {:?} in switch_workspace_or_window", t) } - con::WsOrWin::Win { win } => focus_window_by_id(win.get_id()), }, Err(non_matching_input) => { handle_non_matching_input(&non_matching_input) @@ -618,28 +645,35 @@ pub fn switch_workspace_or_window(extra_props: &HashMap) { } pub fn quit_window(extra_props: &HashMap) { - let root = get_tree(); - let windows = con::get_windows(&root, true, extra_props); + let root = get_tree(true); + let tree = con::get_tree(&root, extra_props); - if let Ok(window) = util::select_from_menu("Quit window", &windows) { - quit_window_by_id(window.get_id()) + if let Ok(window) = + util::select_from_menu("Quit window", &tree.get_windows()) + { + quit_window_by_id(window.node.id) } } pub fn quit_workspace_or_window(extra_props: &HashMap) { - let root = get_tree(); - let workspaces = con::get_workspaces(&root, true, extra_props); - let ws_or_wins = con::WsOrWin::from_workspaces(&workspaces); - if let Ok(ws_or_win) = + let root = get_tree(true); + let tree = con::get_tree(&root, extra_props); + let ws_or_wins = tree.get_workspaces_and_windows(); + if let Ok(tn) = util::select_from_menu("Quit workspace or window", &ws_or_wins) { - match ws_or_win { - con::WsOrWin::Ws { ws } => { - for win in ws.get_windows() { - quit_window_by_id(win.get_id()) + match tn.node.get_type() { + con::Type::Workspace => { + for win in + tn.node.iter().filter(|n| n.get_type() == con::Type::Window) + { + quit_window_by_id(win.id) } } - con::WsOrWin::Win { win } => quit_window_by_id(win.get_id()), + con::Type::Window => quit_window_by_id(tn.node.id), + t => { + eprintln!("Cannot handle {:?} in quit_workspace_or_window", t) + } } } } @@ -662,22 +696,19 @@ fn tile_current_workspace(floating: &ConsiderFloating, shuffle: bool) { if win.is_floating() { con.run_command(format!( "[con_id={}] floating disable", - win.get_id() + win.id ))?; } std::thread::sleep(std::time::Duration::from_millis(25)); con.run_command(format!( "[con_id={}] move to workspace current", - win.get_id() + win.id ))?; placed_wins.push(win); if shuffle { std::thread::sleep(std::time::Duration::from_millis(25)); if let Some(win) = placed_wins.choose(&mut rng) { - con.run_command(format!( - "[con_id={}] focus", - win.get_id() - ))?; + con.run_command(format!("[con_id={}] focus", win.id))?; } } } @@ -702,14 +733,14 @@ fn tab_current_workspace(floating: &ConsiderFloating) { if win.is_floating() { con.run_command(format!( "[con_id={}] floating disable", - win.get_id() + win.id ))?; } std::thread::sleep(std::time::Duration::from_millis(25)); con.run_command(format!( "[con_id={}] move to workspace current", - win.get_id() + win.id ))?; placed_wins.push(win); } @@ -722,12 +753,9 @@ fn tab_current_workspace(floating: &ConsiderFloating) { } fn toggle_tab_tile_current_workspace(floating: &ConsiderFloating) { - let tree = get_tree(); - let workspaces = tree.workspaces(); - let cur_ws = workspaces - .iter() - .find(|w| con::is_current_container(w)) - .unwrap(); + let tree = get_tree(false); + let workspaces = tree.nodes_of_type(con::Type::Workspace); + let cur_ws = workspaces.iter().find(|w| w.is_current()).unwrap(); if cur_ws.layout == s::NodeLayout::Tabbed { tile_current_workspace(floating, true); } else { diff --git a/src/con.rs b/src/con.rs index 465696e..5004197 100644 --- a/src/con.rs +++ b/src/con.rs @@ -15,7 +15,7 @@ //! Convenience data structures built from the IPC structs. -use crate::config as cfg; +use crate::config; use crate::util; use crate::util::DisplayFormat; use lazy_static::lazy_static; @@ -55,28 +55,37 @@ impl<'a> Iterator for NodeIter<'a> { } } +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum Type { + Root, + Output, + Workspace, + Container, + Window, +} + /// Extension methods for [`swayipc::Node`]. pub trait NodeMethods { /// Returns an iterator for this [`swayipc::Node`] and its childres. fn iter(&self) -> NodeIter; - fn is_window(&self) -> bool; - - /// Either a workspace or a con holding windows, e.g. a vertical split side - /// in a horizontally split workspace. - fn is_container(&self) -> bool; + /// Returns true if this node is an output. + fn get_type(&self) -> Type; - /// Returns all nodes being application windows. - fn windows(&self) -> Vec<&s::Node>; + /// Returns the app_id if present, otherwise the window-properties class if + /// present, otherwise "". + fn get_app_name(&self) -> &str; - /// Returns all nodes being containers. - fn containers(&self) -> Vec<&s::Node>; + fn nodes_of_type(&self, t: Type) -> Vec<&s::Node>; + fn get_name(&self) -> &str; - /// Returns all nodes being workspaces. - fn workspaces(&self) -> Vec<&s::Node>; - - // Returns true if this node is the scratchpad workspace. + // Returns true if this node is the scratchpad output or workspace. fn is_scratchpad(&self) -> bool; + fn is_floating(&self) -> bool; + + fn is_current(&self) -> bool { + self.iter().any(|n| n.focused) + } } impl NodeMethods for s::Node { @@ -84,34 +93,66 @@ impl NodeMethods for s::Node { NodeIter::new(self) } - fn is_window(&self) -> bool { - (self.node_type == s::NodeType::Con - || self.node_type == s::NodeType::FloatingCon) - && self.name.is_some() + fn get_type(&self) -> Type { + match self.node_type { + s::NodeType::Root => Type::Root, + s::NodeType::Output => Type::Output, + s::NodeType::Workspace => Type::Workspace, + s::NodeType::FloatingCon => Type::Window, + _ => { + if self.node_type == s::NodeType::Con + && self.name.is_none() + && self.layout != s::NodeLayout::None + { + Type::Container + } else if (self.node_type == s::NodeType::Con + || self.node_type == s::NodeType::FloatingCon) + && self.name.is_some() + { + Type::Window + } else { + panic!( + "Don't know type of node with id {} and node_type {:?}", + self.id, self.node_type + ) + } + } + } } - fn is_container(&self) -> bool { - self.node_type == s::NodeType::Con - && self.name.is_none() - && self.layout != s::NodeLayout::None + fn get_name(&self) -> &str { + if let Some(name) = &self.name { + name.as_ref() + } else { + "" + } } - fn windows(&self) -> Vec<&s::Node> { - self.iter().filter(|n| n.is_window()).collect() + fn get_app_name(&self) -> &str { + if let Some(app_id) = &self.app_id { + app_id + } else if let Some(wp_class) = self + .window_properties + .as_ref() + .and_then(|wp| wp.class.as_ref()) + { + wp_class + } else { + "" + } } - fn containers(&self) -> Vec<&s::Node> { - self.iter().filter(|n| n.is_container()).collect() + fn is_scratchpad(&self) -> bool { + let name = self.get_name(); + name.eq("__i3") || name.eq("__i3_scratch") } - fn workspaces(&self) -> Vec<&s::Node> { - self.iter() - .filter(|n| n.node_type == s::NodeType::Workspace) - .collect() + fn nodes_of_type(&self, t: Type) -> Vec<&s::Node> { + self.iter().filter(|n| n.get_type() == t).collect() } - fn is_scratchpad(&self) -> bool { - self.name.is_some() && self.name.as_ref().unwrap().eq("__i3_scratch") + fn is_floating(&self) -> bool { + self.node_type == s::NodeType::FloatingCon } } @@ -123,386 +164,295 @@ pub struct ExtraProps { pub last_focus_time_for_next_prev_seq: u128, } -#[derive(Debug)] -pub struct Window<'a> { - node: &'a s::Node, - workspace: &'a s::Node, - extra_props: Option, +pub struct Tree<'a> { + root: &'a s::Node, + id_node: HashMap, + id_parent: HashMap, + extra_props: &'a HashMap, } -impl Window<'_> { - pub fn get_id(&self) -> i64 { - self.node.id - } - - pub fn get_app_name(&self) -> &str { - if let Some(app_id) = &self.node.app_id { - app_id - } else if let Some(wp_class) = self - .node - .window_properties - .as_ref() - .and_then(|wp| wp.class.as_ref()) - { - wp_class - } else { - "" - } - } - - pub fn get_title(&self) -> &str { - self.node.name.as_ref().unwrap() - } - - pub fn is_urgent(&self) -> bool { - self.node.urgent - } - - pub fn is_focused(&self) -> bool { - self.node.focused - } +pub struct DisplayNode<'a> { + pub node: &'a s::Node, + pub tree: &'a Tree<'a>, +} - pub fn is_floating(&self) -> bool { - self.node.node_type == s::NodeType::FloatingCon +impl<'a> Tree<'a> { + fn get_node_by_id(&self, id: i64) -> &&s::Node { + self.id_node + .get(&id) + .unwrap_or_else(|| panic!("No node with id {}", id)) } - pub fn get_parent(&self) -> &s::Node { - NodeIter::new(self.workspace) - .find(|n| { - n.nodes.contains(self.node) - || n.floating_nodes.contains(self.node) - }) - .unwrap_or_else(|| panic!("Window {:?} has no parent node!", self)) + fn get_parent_node(&self, id: i64) -> Option<&&s::Node> { + self.id_parent.get(&id).map(|pid| self.get_node_by_id(*pid)) } - pub fn is_child_of_tiled_container(&self) -> bool { - let layout = &self.get_parent().layout; - layout == &s::NodeLayout::SplitH || layout == &s::NodeLayout::SplitV + pub fn get_workspace_node(&self, id: i64) -> Option<&&s::Node> { + let n = self.get_node_by_id(id); + if n.get_type() == Type::Workspace { + Some(n) + } else if let Some(pid) = self.id_parent.get(&id) { + self.get_workspace_node(*pid) + } else { + None + } } - pub fn is_child_of_tabbed_or_stacked_container(&self) -> bool { - let layout = &self.get_parent().layout; - layout == &s::NodeLayout::Tabbed || layout == &s::NodeLayout::Stacked + pub fn last_focus_time(&self, id: i64) -> u128 { + self.extra_props.get(&id).map_or(0, |wp| wp.last_focus_time) } - pub fn last_focus_time_for_next_prev_seq(&self) -> u128 { + pub fn last_focus_time_for_next_prev_seq(&self, id: i64) -> u128 { self.extra_props - .as_ref() + .get(&id) .map_or(0, |wp| wp.last_focus_time_for_next_prev_seq) } -} - -impl PartialEq for Window<'_> { - fn eq(&self, other: &Self) -> bool { - self.get_id() == other.get_id() - } -} -impl Eq for Window<'_> {} - -impl Ord for Window<'_> { - fn cmp(&self, other: &Self) -> cmp::Ordering { - if self == other { - cmp::Ordering::Equal - } else if self.is_urgent() && !other.is_urgent() - || !self.is_focused() && other.is_focused() - { - cmp::Ordering::Less - } else if !self.is_urgent() && other.is_urgent() - || self.is_focused() && !other.is_focused() - { - std::cmp::Ordering::Greater - } else { - let lru_a = - self.extra_props.as_ref().map_or(0, |wp| wp.last_focus_time); - let lru_b = other - .extra_props - .as_ref() - .map_or(0, |wp| wp.last_focus_time); - lru_a.cmp(&lru_b).reverse() - } - } -} - -impl PartialOrd for Window<'_> { - fn partial_cmp(&self, other: &Window) -> Option { - Some(self.cmp(other)) - } -} - -lazy_static! { - static ref APP_NAME_AND_VERSION_RX: regex::Regex = - regex::Regex::new("(.+)(-[0-9.]+)").unwrap(); -} - -fn maybe_html_escape(do_it: bool, text: &str) -> String { - if do_it { - text.replace("<", "<") - .replace(">", ">") - .replace("&", "&") - } else { - text.to_string() + fn sorted_nodes_of_type_1( + &self, + node: &'a s::Node, + t: Type, + ) -> Vec<&s::Node> { + let mut v: Vec<&s::Node> = node.nodes_of_type(t); + v.sort_by(|a, b| { + if a.urgent && !b.urgent { + cmp::Ordering::Less + } else if !a.urgent && b.urgent { + cmp::Ordering::Greater + } else { + let lru_a = self.last_focus_time(a.id); + let lru_b = self.last_focus_time(b.id); + lru_a.cmp(&lru_b).reverse() + } + }); + v } -} -impl<'a> DisplayFormat for Window<'a> { - fn format_for_display(&self, cfg: &cfg::Config) -> String { - let window_format = cfg.get_format_window_format(); - let urgency_start = cfg.get_format_urgency_start(); - let urgency_end = cfg.get_format_urgency_end(); - let html_escape = cfg.get_format_html_escape(); - let icon_dirs = cfg.get_format_icon_dirs(); - // fallback_icon has no default value. - let fallback_icon: Option> = cfg - .get_format_fallback_icon() - .as_ref() - .map(|i| std::path::Path::new(i).to_owned().into_boxed_path()); - - // Some apps report, e.g., Gimp-2.10 but the icon is still named - // gimp.png. - let app_name_no_version = - APP_NAME_AND_VERSION_RX.replace(self.get_app_name(), "$1"); - - window_format - .replace("{id}", format!("{}", self.get_id()).as_str()) - .replace( - "{urgency_start}", - if self.is_urgent() { - urgency_start.as_str() - } else { - "" - }, - ) - .replace( - "{urgency_end}", - if self.is_urgent() { - urgency_end.as_str() - } else { - "" - }, - ) - .replace( - "{app_name}", - &maybe_html_escape(html_escape, self.get_app_name()), - ) - .replace( - "{workspace_name}", - &maybe_html_escape( - html_escape, - self.workspace.name.as_ref().unwrap().as_str(), - ), - ) - .replace( - "{marks}", - &maybe_html_escape(html_escape, &self.node.marks.join(", ")), - ) - .replace( - "{app_icon}", - util::get_icon(self.get_app_name(), &icon_dirs) - .or_else(|| { - util::get_icon(&app_name_no_version, &icon_dirs) - }) - .or_else(|| { - util::get_icon( - &app_name_no_version.to_lowercase(), - &icon_dirs, - ) - }) - .or(fallback_icon) - .map(|i| i.to_string_lossy().into_owned()) - .unwrap_or_else(String::new) - .as_str(), - ) - .replace( - "{title}", - &maybe_html_escape(html_escape, self.get_title()), - ) + fn sorted_nodes_of_type(&self, t: Type) -> Vec<&s::Node> { + self.sorted_nodes_of_type_1(self.root, t) } -} -fn build_windows<'a>( - root: &'a s::Node, - include_scratchpad_windows: bool, - extra_props: &HashMap, -) -> Vec> { - let mut v = vec![]; - for workspace in root.workspaces() { - if !include_scratchpad_windows && workspace.is_scratchpad() { - continue; - } - - for n in workspace.windows() { - v.push(Window { + fn as_display_nodes(&self, v: Vec<&'a s::Node>) -> Vec { + v.iter() + .map(|n| DisplayNode { node: n, - extra_props: extra_props.get(&n.id).cloned(), - workspace, + tree: self, }) - } + .collect() } - v -} -fn build_workspaces<'a>( - root: &'a s::Node, - include_scratchpad: bool, - extra_props: &HashMap, -) -> Vec> { - let mut v = vec![]; - for workspace in root.workspaces() { - if workspace.is_scratchpad() && !include_scratchpad { - continue; - } - - let mut wins: Vec = workspace - .windows() + pub fn get_current_workspace(&self) -> &s::Node { + self.root .iter() - .map(|w| Window { - node: w, - extra_props: extra_props.get(&w.id).cloned(), - workspace, - }) - .collect(); - if !extra_props.is_empty() { - wins.sort(); - } - v.push(Workspace { - node: workspace, - extra_props: extra_props.get(&workspace.id).cloned(), - windows: wins, - }) - } - if !extra_props.is_empty() { - v.sort(); + .find(|n| n.get_type() == Type::Workspace && n.is_current()) + .expect("No current Workspace") } - v -} -/// Gets all application windows of the tree. -pub fn get_windows<'a>( - root: &'a s::Node, - include_scratchpad_windows: bool, - extra_props: &HashMap, -) -> Vec> { - let mut wins = build_windows(root, include_scratchpad_windows, extra_props); - if !extra_props.is_empty() { - wins.sort(); + pub fn get_workspaces(&self) -> Vec { + let mut v = self.sorted_nodes_of_type(Type::Workspace); + v.rotate_left(1); + self.as_display_nodes(v) } - wins -} - -/// Gets all workspaces of the tree. -pub fn get_workspaces<'a>( - root: &'a s::Node, - include_scratchpad: bool, - extra_props: &HashMap, -) -> Vec> { - let mut workspaces = - build_workspaces(root, include_scratchpad, extra_props); - workspaces.rotate_left(1); - workspaces -} -pub enum WsOrWin<'a> { - Ws { ws: &'a Workspace<'a> }, - Win { win: &'a Window<'a> }, -} - -impl DisplayFormat for WsOrWin<'_> { - fn format_for_display(&self, cfg: &cfg::Config) -> String { - match self { - WsOrWin::Ws { ws } => ws.format_for_display(cfg), - WsOrWin::Win { win } => win.format_for_display(cfg), - } + pub fn get_windows(&self) -> Vec { + let mut v = self.sorted_nodes_of_type(Type::Window); + v.rotate_left(1); + self.as_display_nodes(v) } -} -impl WsOrWin<'_> { - pub fn from_workspaces<'a>( - workspaces: &'a [Workspace], - ) -> Vec> { + pub fn get_workspaces_and_windows(&self) -> Vec { + let workspaces = self.sorted_nodes_of_type(Type::Workspace); + let mut first = true; let mut v = vec![]; for ws in workspaces { - v.push(WsOrWin::Ws { ws }); - for win in &ws.windows { - v.push(WsOrWin::Win { win }); + v.push(ws); + let mut wins = self.sorted_nodes_of_type_1(ws, Type::Window); + if first { + wins.rotate_left(1); + first = false; } + v.append(&mut wins); } - v - } -} -pub struct Workspace<'a> { - pub node: &'a s::Node, - extra_props: Option, - pub windows: Vec>, -} + // Rotate until we have the second recently used workspace in front. + v.rotate_left(1); + while v[0].get_type() != Type::Workspace { + v.rotate_left(1); + } -impl Workspace<'_> { - pub fn get_name(&self) -> &str { - self.node.name.as_ref().unwrap() + self.as_display_nodes(v) } - pub fn get_id(&self) -> i64 { - self.node.id + pub fn is_child_of_tiled_container(&self, id: i64) -> bool { + match self.get_parent_node(id) { + Some(n) => { + n.layout == s::NodeLayout::SplitH + || n.layout == s::NodeLayout::SplitV + } + None => false, + } } - pub fn get_windows(&self) -> &Vec { - &self.windows + pub fn is_child_of_tabbed_or_stacked_container(&self, id: i64) -> bool { + match self.get_parent_node(id) { + Some(n) => { + n.layout == s::NodeLayout::Tabbed + || n.layout == s::NodeLayout::Stacked + } + None => false, + } } +} - pub fn is_scratchpad(&self) -> bool { - self.node.is_scratchpad() - } +fn init_id_parent<'a>( + n: &'a s::Node, + parent: Option<&'a s::Node>, + id_node: &mut HashMap, + id_parent: &mut HashMap, +) { + id_node.insert(n.id, n); - pub fn is_focused(&self) -> bool { - self.node.focused + if let Some(p) = parent { + id_parent.insert(n.id, p.id); } - pub fn is_current(&self) -> bool { - is_current_container(self.node) + for c in &n.nodes { + init_id_parent(c, Some(n), id_node, id_parent); + } + for c in &n.floating_nodes { + init_id_parent(c, Some(n), id_node, id_parent); } } -pub fn is_current_container(node: &s::Node) -> bool { - node.focused || NodeIter::new(node).any(|c| c.focused) -} - -impl PartialEq for Workspace<'_> { - fn eq(&self, other: &Self) -> bool { - self.get_id() == other.get_id() +pub fn get_tree<'a>( + root: &'a s::Node, + extra_props: &'a HashMap, +) -> Tree<'a> { + let mut id_node: HashMap = HashMap::new(); + let mut id_parent: HashMap = HashMap::new(); + init_id_parent(root, None, &mut id_node, &mut id_parent); + + Tree { + root, + id_node, + id_parent, + extra_props, } } -impl Eq for Workspace<'_> {} - -impl Ord for Workspace<'_> { - fn cmp(&self, other: &Self) -> cmp::Ordering { - if self == other { - cmp::Ordering::Equal - } else { - let lru_a = - self.extra_props.as_ref().map_or(0, |wp| wp.last_focus_time); - let lru_b = other - .extra_props - .as_ref() - .map_or(0, |wp| wp.last_focus_time); - lru_a.cmp(&lru_b).reverse() - } - } +lazy_static! { + static ref APP_NAME_AND_VERSION_RX: regex::Regex = + regex::Regex::new("(.+)(-[0-9.]+)").unwrap(); } -impl PartialOrd for Workspace<'_> { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) +fn maybe_html_escape(do_it: bool, text: &str) -> String { + if do_it { + text.replace("<", "<") + .replace(">", ">") + .replace("&", "&") + } else { + text.to_string() } } -impl<'a> DisplayFormat for Workspace<'a> { - fn format_for_display(&self, cfg: &cfg::Config) -> String { - let fmt = cfg.get_format_workspace_format(); - let html_escape = cfg.get_format_html_escape(); - - fmt.replace("{id}", format!("{}", self.get_id()).as_str()) - .replace("{name}", &maybe_html_escape(html_escape, self.get_name())) +impl DisplayFormat for DisplayNode<'_> { + fn format_for_display(&self, cfg: &config::Config) -> String { + match self.node.get_type() { + Type::Root => String::from("Cannot format Root"), + Type::Output => String::from("Cannot format Output"), + Type::Workspace => { + let fmt = cfg.get_format_workspace_format(); + let html_escape = cfg.get_format_html_escape(); + + fmt.replace("{id}", format!("{}", self.node.id).as_str()) + .replace( + "{name}", + &maybe_html_escape(html_escape, self.node.get_name()), + ) + } + Type::Container => { + todo!("DisplayFormat for Container not yet implemented") + } + Type::Window => { + let window_format = cfg.get_format_window_format(); + let urgency_start = cfg.get_format_urgency_start(); + let urgency_end = cfg.get_format_urgency_end(); + let html_escape = cfg.get_format_html_escape(); + let icon_dirs = cfg.get_format_icon_dirs(); + // fallback_icon has no default value. + let fallback_icon: Option> = + cfg.get_format_fallback_icon().as_ref().map(|i| { + std::path::Path::new(i).to_owned().into_boxed_path() + }); + + // Some apps report, e.g., Gimp-2.10 but the icon is still named + // gimp.png. + let app_name_no_version = APP_NAME_AND_VERSION_RX + .replace(self.node.get_app_name(), "$1"); + + window_format + .replace("{id}", format!("{}", self.node.id).as_str()) + .replace( + "{urgency_start}", + if self.node.urgent { + urgency_start.as_str() + } else { + "" + }, + ) + .replace( + "{urgency_end}", + if self.node.urgent { + urgency_end.as_str() + } else { + "" + }, + ) + .replace( + "{app_name}", + &maybe_html_escape( + html_escape, + self.node.get_app_name(), + ), + ) + .replace( + "{workspace_name}", + &maybe_html_escape( + html_escape, + self.tree + .get_workspace_node(self.node.id) + .map_or("", |w| w.get_name()), + ), + ) + .replace( + "{marks}", + &maybe_html_escape( + html_escape, + &self.node.marks.join(", "), + ), + ) + .replace( + "{app_icon}", + util::get_icon(self.node.get_app_name(), &icon_dirs) + .or_else(|| { + util::get_icon(&app_name_no_version, &icon_dirs) + }) + .or_else(|| { + util::get_icon( + &app_name_no_version.to_lowercase(), + &icon_dirs, + ) + }) + .or(fallback_icon) + .map(|i| i.to_string_lossy().into_owned()) + .unwrap_or_else(String::new) + .as_str(), + ) + .replace( + "{title}", + &maybe_html_escape(html_escape, self.node.get_name()), + ) + } + } } } diff --git a/src/layout.rs b/src/layout.rs index 3fd5bda..ff4ab73 100644 --- a/src/layout.rs +++ b/src/layout.rs @@ -42,8 +42,8 @@ pub fn auto_tile(res_to_min_width: &HashMap) { if let Some(min_window_width) = min_window_width { for container in con::NodeIter::new(output).filter(|n| { - n.node_type == s::NodeType::Workspace - || n.is_container() + let t = n.get_type(); + t == con::Type::Workspace || t == con::Type::Container }) { if container.is_scratchpad() { println!(" Skipping scratchpad"); @@ -55,8 +55,10 @@ pub fn auto_tile(res_to_min_width: &HashMap) { container.layout, container.nodes.len(), ); - for child_win in - container.nodes.iter().filter(|n| n.is_window()) + for child_win in container + .nodes + .iter() + .filter(|n| n.get_type() == con::Type::Window) { // Width if we'd split once more. let estimated_width = @@ -132,17 +134,22 @@ const SWAYR_TMP_WORKSPACE: &str = "✨"; pub fn relayout_current_workspace( include_floating: bool, insert_win_fn: Box< - dyn Fn(&mut [&con::Window], &mut s::Connection) -> s::Fallible<()>, + dyn Fn(&mut [&s::Node], &mut s::Connection) -> s::Fallible<()>, >, ) -> s::Fallible<()> { - let root = cmds::get_tree(); - let workspaces = con::get_workspaces(&root, false, &HashMap::new()); + let root = cmds::get_tree(false); + let workspaces: Vec<&s::Node> = root + .iter() + .filter(|n| n.get_type() == con::Type::Workspace) + .collect(); if let Some(cur_ws) = workspaces.iter().find(|ws| ws.is_current()) { if let Ok(mut con) = s::Connection::new() { - let mut moved_wins: Vec<&con::Window> = vec![]; + let mut moved_wins: Vec<&s::Node> = vec![]; let mut focused_win = None; - for win in cur_ws.get_windows() { - if win.is_focused() { + for win in + cur_ws.iter().filter(|n| n.get_type() == con::Type::Window) + { + if win.focused { focused_win = Some(win); } if !include_floating && win.is_floating() { @@ -151,8 +158,7 @@ pub fn relayout_current_workspace( moved_wins.push(win); con.run_command(format!( "[con_id={}] move to workspace {}", - win.get_id(), - SWAYR_TMP_WORKSPACE + win.id, SWAYR_TMP_WORKSPACE ))?; } @@ -160,7 +166,7 @@ pub fn relayout_current_workspace( std::thread::sleep(std::time::Duration::from_millis(25)); if let Some(win) = focused_win { - con.run_command(format!("[con_id={}] focus", win.get_id()))?; + con.run_command(format!("[con_id={}] focus", win.id))?; } Ok(()) } else {