// Copyright (C) 2021 Tassilo Horn // // This program is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by the Free // Software Foundation, either version 3 of the License, or (at your option) // any later version. // // This program is distributed in the hope that it will be useful, but WITHOUT // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or // FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for // more details. // // You should have received a copy of the GNU General Public License along with // this program. If not, see . //! Convenience data structures built from the IPC structs. use crate::config as cfg; use crate::ipc; use crate::ipc::NodeMethods; use crate::util; use std::cmp; use std::collections::HashMap; use std::fmt; use swayipc::reply as r; pub trait DisplayFormat { fn format_for_display(&self, config: &cfg::Config) -> String; } #[derive(Debug)] pub struct Window<'a> { node: &'a r::Node, workspace: &'a r::Node, extra_props: Option, } 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 } } 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)) } } impl<'a> fmt::Display for Window<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { write!( f, "“{}” — {} on workspace {} (id: {}, urgent: {})", self.get_title(), self.get_app_name(), self.workspace.name.as_ref().unwrap(), self.get_id(), self.node.urgent ) } } impl<'a> DisplayFormat for Window<'a> { fn format_for_display(&self, cfg: &cfg::Config) -> String { let default_format = cfg::Format::default(); let fmt = cfg .format .as_ref() .and_then(|f| f.window_format.as_ref()) .unwrap_or_else(|| default_format.window_format.as_ref().unwrap()); let urgency_start = cfg .format .as_ref() .and_then(|f| f.urgency_start.as_ref()) .unwrap_or_else(|| default_format.urgency_start.as_ref().unwrap()); let urgency_end = cfg .format .as_ref() .and_then(|f| f.urgency_end.as_ref()) .unwrap_or_else(|| default_format.urgency_end.as_ref().unwrap()); fmt.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}", self.get_app_name()) .replace( "{workspace_name}", self.workspace.name.as_ref().unwrap().as_str(), ) .replace("{title}", self.get_title()) } } fn build_windows<'a>( root: &'a r::Node, include_scratchpad_windows: bool, extra_props: Option<&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 { node: &n, extra_props: extra_props.and_then(|m| m.get(&n.id).cloned()), workspace: &workspace, }) } } v } fn build_workspaces<'a>( root: &'a r::Node, include_scratchpad: bool, extra_props: Option<&HashMap>, ) -> Vec> { let mut v = vec![]; for workspace in root.workspaces() { if !include_scratchpad && workspace.is_scratchpad() { continue; } let mut wins: Vec = workspace .windows() .iter() .map(|w| Window { node: &w, extra_props: extra_props.and_then(|m| m.get(&w.id).cloned()), workspace: &workspace, }) .collect(); wins.sort(); v.push(Workspace { node: &workspace, extra_props: extra_props .and_then(|m| m.get(&workspace.id).cloned()), windows: wins, }) } v.sort(); v } /// Gets all application windows of the tree. pub fn get_windows<'a>( root: &'a r::Node, include_scratchpad_windows: bool, extra_props: Option<&HashMap>, ) -> Vec> { let extra_props_given = extra_props.is_some(); let mut wins = build_windows(root, include_scratchpad_windows, extra_props); if extra_props_given { wins.sort(); } wins } /// Gets all workspaces of the tree. pub fn get_workspaces<'a>( root: &'a r::Node, include_scratchpad: bool, extra_props: Option<&HashMap>, ) -> Vec> { let mut workspaces = build_workspaces(root, include_scratchpad, extra_props); workspaces.rotate_left(1); workspaces } pub fn select_window<'a>( prompt: &str, windows: &'a [Window], ) -> Option<&'a Window<'a>> { util::select_from_choices(prompt, windows) } pub fn select_workspace<'a>( prompt: &str, workspaces: &'a [Workspace], ) -> Option<&'a Workspace<'a>> { util::select_from_choices(prompt, workspaces) } pub enum WsOrWin<'a> { Ws { ws: &'a Workspace<'a> }, Win { win: &'a Window<'a> }, } impl<'a> fmt::Display for WsOrWin<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { match self { WsOrWin::Ws { ws } => ws.fmt(f), WsOrWin::Win { win } => match f.write_str("\t") { Ok(()) => win.fmt(f), Err(e) => Err(e), }, } } } 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 } => { "\t".to_owned() + &win.format_for_display(cfg) } } } } impl WsOrWin<'_> { pub fn from_workspaces<'a>( workspaces: &'a [Workspace], ) -> Vec> { let mut v = vec![]; for ws in workspaces { v.push(WsOrWin::Ws { ws }); for win in &ws.windows { v.push(WsOrWin::Win { win: &win }); } } v } } pub fn select_workspace_or_window<'a>( prompt: &'a str, ws_or_wins: &'a [WsOrWin<'a>], ) -> Option<&'a WsOrWin<'a>> { util::select_from_choices(prompt, ws_or_wins) } pub struct Workspace<'a> { node: &'a r::Node, extra_props: Option, pub windows: Vec>, } impl Workspace<'_> { pub fn get_name(&self) -> &str { self.node.name.as_ref().unwrap() } pub fn get_id(&self) -> i64 { self.node.id } pub fn is_scratchpad(&self) -> bool { self.node.is_scratchpad() } } impl PartialEq for Workspace<'_> { fn eq(&self, other: &Self) -> bool { self.get_id() == other.get_id() } } 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() } } } impl PartialOrd for Workspace<'_> { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } impl<'a> fmt::Display for Workspace<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { write!(f, "“Workspace {}” (id: {})", self.get_name(), self.get_id()) } } impl<'a> DisplayFormat for Workspace<'a> { fn format_for_display(&self, cfg: &cfg::Config) -> String { let default_format = cfg::Format::default(); let fmt = cfg .format .as_ref() .and_then(|f| f.workspace_format.as_ref()) .unwrap_or_else(|| { default_format.workspace_format.as_ref().unwrap() }); fmt.replace("{id}", format!("{}", self.get_id()).as_str()) .replace("{name}", self.get_name()) } }