diff --git a/README.md b/README.md index e15e09c..23c525f 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,11 @@ creations, deletions, and focus changes using sway's JSON IPC interface. The client `swayr` offers subcommands, see `swayr --help`. Right now, there are these subcommands: -* `switch-window`: a wofi-based window switcher sorting the windows in the - order urgent first, then LRU, focused last. -* `quit-window`: displays all windows using wofi and quits the selected one. +* `switch-window` displays all windows in the order urgent first, then LRU, + focused last and focuses the selected. +* `quit-window` displays all windows and quits the selected one. +* `switch-workspace` displays all workspaces in LRU order and switches to the + selected. Swayr is licensed under the [GPLv3](https://www.gnu.org/licenses/gpl-3.0.en.html) (or later). diff --git a/src/bin/swayr.rs b/src/bin/swayr.rs index d44ac0a..ed106a3 100644 --- a/src/bin/swayr.rs +++ b/src/bin/swayr.rs @@ -22,6 +22,8 @@ enum SwayrCommand { SwitchWindow, /// Quit a window with display order focused first, then reverse-LRU order, urgent last QuitWindow, + /// Switch workspace with LRU display order + SwitchWorkspace, } fn main() { @@ -29,5 +31,6 @@ fn main() { match opts.command { SwayrCommand::SwitchWindow => client::switch_window(), SwayrCommand::QuitWindow => client::quit_window(), + SwayrCommand::SwitchWorkspace => client::switch_workspace(), } } diff --git a/src/bin/swayrd.rs b/src/bin/swayrd.rs index 7a7d0e0..2bbe262 100644 --- a/src/bin/swayrd.rs +++ b/src/bin/swayrd.rs @@ -9,15 +9,15 @@ use swayr::demon; use swayr::ipc; fn main() { - let win_props: Arc>> = + let con_props: Arc>> = Arc::new(RwLock::new(HashMap::new())); - let win_props_for_ev_handler = win_props.clone(); + let con_props_for_ev_handler = con_props.clone(); let subscriber_handle = thread::spawn(move || { - demon::monitor_window_events(win_props_for_ev_handler) + demon::monitor_window_events(con_props_for_ev_handler) }); - match demon::serve_client_requests(win_props) { + match demon::serve_client_requests(con_props) { Ok(()) => { let subscriber_result = subscriber_handle.join(); match subscriber_result { diff --git a/src/client.rs b/src/client.rs index 5101087..d34bacc 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,10 +1,9 @@ use crate::con; -use crate::ipc; use crate::util; pub fn switch_window() { - let root_node = get_tree(); - let mut windows = con::get_windows(&root_node); + let root = con::get_tree(); + let mut windows = con::get_windows(&root); windows.sort(); if let Some(window) = con::select_window("Switch to window", &windows) { @@ -15,9 +14,21 @@ pub fn switch_window() { } } +pub fn switch_workspace() { + let root = con::get_tree(); + let mut workspaces = con::get_workspaces(&root, false); + workspaces.sort(); + + if let Some(workspace) = + con::select_workspace("Switch to workspace", &workspaces) + { + util::swaymsg(vec!["workspace", "number", workspace.get_name()]); + } +} + pub fn quit_window() { - let root_node = get_tree(); - let mut windows = con::get_windows(&root_node); + let root = con::get_tree(); + let mut windows = con::get_windows(&root); windows.sort_by(|a, b| a.cmp(b).reverse()); if let Some(window) = con::select_window("Quit window", &windows) { @@ -27,38 +38,3 @@ pub fn quit_window() { ]); } } - -fn get_tree() -> ipc::Node { - let output = util::swaymsg(vec!["-t", "get_tree"]); - let result = serde_json::from_str(output.as_str()); - - match result { - Ok(node) => node, - Err(e) => { - eprintln!("Error: {}", e); - panic!() - } - } -} - -#[test] -fn test_get_tree() { - let tree = get_tree(); - - println!("Those IDs are in get_tree():"); - for n in tree.iter() { - println!(" id: {}, type: {:?}", n.id, n.r#type); - } -} - -#[test] -fn test_get_windows() { - let tree = get_tree(); - let cons = con::get_windows(&tree); - - println!("There are {} cons.", cons.len()); - - for c in cons { - println!(" {}", c); - } -} diff --git a/src/con.rs b/src/con.rs index fb340dd..7b55643 100644 --- a/src/con.rs +++ b/src/con.rs @@ -5,16 +5,39 @@ use std::collections::HashMap; use std::fmt; use std::os::unix::net::UnixStream; +pub fn get_tree() -> ipc::Node { + let output = util::swaymsg(vec!["-t", "get_tree"]); + let result = serde_json::from_str(output.as_str()); + + match result { + Ok(node) => node, + Err(e) => { + eprintln!("Error: {}", e); + panic!() + } + } +} + +#[test] +fn test_get_tree() { + let tree = get_tree(); + + println!("Those IDs are in get_tree():"); + for n in tree.iter() { + println!(" id: {}, type: {:?}", n.id, n.r#type); + } +} + #[derive(Debug)] pub struct Window<'a> { node: &'a ipc::Node, workspace: &'a ipc::Node, - win_props: Option, + con_props: Option, } impl Window<'_> { - pub fn get_id(&self) -> ipc::Id { - self.node.id + pub fn get_id(&self) -> &ipc::Id { + &self.node.id } pub fn get_app_name(&self) -> &str { @@ -38,7 +61,7 @@ impl Window<'_> { } impl PartialEq for Window<'_> { - fn eq(&self, other: &Window) -> bool { + fn eq(&self, other: &Self) -> bool { self.get_id() == other.get_id() } } @@ -59,9 +82,9 @@ impl Ord for Window<'_> { std::cmp::Ordering::Greater } else { let lru_a = - self.win_props.as_ref().map_or(0, |wp| wp.last_focus_time); + self.con_props.as_ref().map_or(0, |wp| wp.last_focus_time); let lru_b = - other.win_props.as_ref().map_or(0, |wp| wp.last_focus_time); + other.con_props.as_ref().map_or(0, |wp| wp.last_focus_time); lru_a.cmp(&lru_b).reverse() } } @@ -95,15 +118,15 @@ impl<'a> fmt::Display for Window<'a> { } fn build_windows( - tree: &ipc::Node, - mut win_props: HashMap, + root: &ipc::Node, + mut con_props: HashMap, ) -> Vec { let mut v = vec![]; - for workspace in tree.workspaces() { + for workspace in root.workspaces() { for n in workspace.windows() { v.push(Window { node: &n, - win_props: win_props.remove(&n.id), + con_props: con_props.remove(&n.id), workspace: &workspace, }) } @@ -111,8 +134,37 @@ fn build_windows( v } -fn get_window_props( -) -> Result, serde_json::Error> { +fn build_workspaces( + root: &ipc::Node, + mut con_props: HashMap, + include_scratchpad: bool, +) -> Vec { + let mut v = vec![]; + for workspace in root.workspaces() { + if !include_scratchpad + && workspace.name.as_ref().unwrap().eq("__i3_scratch") + { + continue; + } + v.push(Workspace { + node: &workspace, + con_props: con_props.remove(&workspace.id), + windows: workspace + .windows() + .iter() + .map(|w| Window { + node: &w, + con_props: con_props.remove(&w.id), + workspace: &workspace, + }) + .collect(), + }) + } + v +} + +fn get_con_props() -> Result, serde_json::Error> +{ if let Ok(sock) = UnixStream::connect(util::get_swayr_socket_path()) { serde_json::from_reader(sock) } else { @@ -121,16 +173,44 @@ fn get_window_props( } /// Gets all application windows of the tree. -pub fn get_windows(root_node: &ipc::Node) -> Vec { - let win_props = match get_window_props() { - Ok(win_props) => Some(win_props), +pub fn get_windows(root: &ipc::Node) -> Vec { + let con_props = match get_con_props() { + Ok(con_props) => Some(con_props), + Err(e) => { + eprintln!("Got no con_props: {:?}", e); + None + } + }; + + build_windows(root, con_props.unwrap_or_default()) +} + +/// Gets all application windows of the tree. +pub fn get_workspaces( + root: &ipc::Node, + include_scratchpad: bool, +) -> Vec { + let con_props = match get_con_props() { + Ok(con_props) => Some(con_props), Err(e) => { - eprintln!("Got no win_props: {:?}", e); + eprintln!("Got no con_props: {:?}", e); None } }; - build_windows(&root_node, win_props.unwrap_or_default()) + build_workspaces(root, con_props.unwrap_or_default(), include_scratchpad) +} + +#[test] +fn test_get_windows() { + let root = get_tree(); + let cons = get_windows(&root); + + println!("There are {} cons.", cons.len()); + + for c in cons { + println!(" {}", c); + } } pub fn select_window<'a>( @@ -139,3 +219,66 @@ pub fn select_window<'a>( ) -> Option<&'a Window<'a>> { util::wofi_select(prompt, windows) } + +pub fn select_workspace<'a>( + prompt: &'a str, + workspaces: &'a [Workspace], +) -> Option<&'a Workspace<'a>> { + util::wofi_select(prompt, workspaces) +} + +pub struct Workspace<'a> { + node: &'a ipc::Node, + con_props: Option, + windows: Vec>, +} + +impl Workspace<'_> { + pub fn get_name(&self) -> &str { + self.node.name.as_ref().unwrap() + } + + pub fn get_id(&self) -> &ipc::Id { + &self.node.id + } +} + +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.con_props.as_ref().map_or(0, |wp| wp.last_focus_time); + let lru_b = + other.con_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 {}", // Almost hide ID! + self.get_name(), + self.get_id() + ) + } +} diff --git a/src/demon.rs b/src/demon.rs index 0fc56c4..b556cb4 100644 --- a/src/demon.rs +++ b/src/demon.rs @@ -11,50 +11,86 @@ use std::thread; use std::time::{SystemTime, UNIX_EPOCH}; pub fn monitor_window_events( - win_props: Arc>>, + con_props: Arc>>, ) { let child = proc::Command::new("swaymsg") .arg("--monitor") .arg("--raw") .arg("-t") .arg("subscribe") - .arg("[\"window\"]") + .arg("[\"window\", \"workspace\"]") .stdout(proc::Stdio::piped()) .spawn() .expect("Failed to subscribe to window events"); let stdout: std::process::ChildStdout = child.stdout.unwrap(); - let stream = - Deserializer::from_reader(stdout).into_iter::(); + let stream = Deserializer::from_reader(stdout).into_iter::(); for res in stream { match res { - Ok(win_ev) => handle_window_event(win_ev, win_props.clone()), + Ok(win_ev) => handle_con_event(win_ev, con_props.clone()), Err(err) => eprintln!("Error handling window event:\n{:?}", err), } } } -fn handle_window_event( - ev: ipc::WindowEvent, - win_props: Arc>>, +fn update_last_focus_time( + id: ipc::Id, + con_props: Arc>>, ) { - match ev.change { - ipc::WindowEventType::Focus => { - let mut write_lock = win_props.write().unwrap(); - if let Some(mut wp) = write_lock.get_mut(&ev.container.id) { - wp.last_focus_time = get_epoch_time_as_millis(); - } else { - write_lock.insert( - ev.container.id, - ipc::WindowProps { - last_focus_time: get_epoch_time_as_millis(), - }, - ); + let mut write_lock = con_props.write().unwrap(); + if let Some(mut wp) = write_lock.get_mut(&id) { + wp.last_focus_time = get_epoch_time_as_millis(); + } else { + write_lock.insert( + id, + ipc::ConProps { + last_focus_time: get_epoch_time_as_millis(), + }, + ); + } +} + +fn remove_winprops( + id: &ipc::Id, + con_props: Arc>>, +) { + con_props.write().unwrap().remove(id); +} + +fn handle_con_event( + ev: ipc::ConEvent, + con_props: Arc>>, +) { + let mut handled = true; + let con_props2 = con_props.clone(); + + match ev { + ipc::ConEvent::WindowEvent { change, container } => match change { + ipc::WindowEventType::New | ipc::WindowEventType::Focus => { + update_last_focus_time(container.id, con_props) } - } - ipc::WindowEventType::Close => { - win_props.write().unwrap().remove(&ev.container.id); - } - _ => (), + ipc::WindowEventType::Close => { + remove_winprops(&container.id, con_props) + } + _ => handled = false, + }, + ipc::ConEvent::WorkspaceEvent { + change, + current, + old: _, + } => match change { + ipc::WorkspaceEventType::Init | ipc::WorkspaceEventType::Focus => { + println!("WsEv"); + update_last_focus_time(current.id, con_props) + } + ipc::WorkspaceEventType::Empty => { + remove_winprops(¤t.id, con_props) + } + _ => handled = false, + }, + } + + if handled { + println!("New con_props state:\n{:#?}", *con_props2.read().unwrap()); } } @@ -66,7 +102,7 @@ fn get_epoch_time_as_millis() -> u128 { } pub fn serve_client_requests( - win_props: Arc>>, + con_props: Arc>>, ) -> std::io::Result<()> { match std::fs::remove_file(util::get_swayr_socket_path()) { Ok(()) => println!("Deleted stale socket from previous run."), @@ -77,7 +113,7 @@ pub fn serve_client_requests( for stream in listener.incoming() { match stream { Ok(stream) => { - let wp_clone = win_props.clone(); + let wp_clone = con_props.clone(); thread::spawn(move || handle_client_request(stream, wp_clone)); } Err(err) => return Err(err), @@ -88,9 +124,9 @@ pub fn serve_client_requests( fn handle_client_request( mut stream: UnixStream, - win_props: Arc>>, + con_props: Arc>>, ) { - let json = serde_json::to_string(&*win_props.read().unwrap()).unwrap(); + let json = serde_json::to_string(&*con_props.read().unwrap()).unwrap(); if let Err(err) = stream.write_all(json.as_bytes()) { eprintln!("Error writing to client: {:?}", err); } diff --git a/src/ipc.rs b/src/ipc.rs index 8df3dab..0a2a247 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -202,13 +202,40 @@ pub enum WindowEventType { #[derive(Deserialize, Debug)] #[allow(dead_code)] -pub struct WindowEvent { - pub change: WindowEventType, - pub container: Node, +pub enum WorkspaceEventType { + #[serde(rename = "init")] + Init, + #[serde(rename = "empty")] + Empty, + #[serde(rename = "focus")] + Focus, + #[serde(rename = "move")] + Move, + #[serde(rename = "rename")] + Rename, + #[serde(rename = "urgent")] + Urgent, + #[serde(rename = "reload")] + Reload, +} + +#[derive(Deserialize, Debug)] +#[serde(untagged)] +#[allow(dead_code)] +pub enum ConEvent { + WindowEvent { + change: WindowEventType, + container: Node, + }, + WorkspaceEvent { + change: WorkspaceEventType, + current: Node, + old: Node, + }, } #[derive(Debug, Deserialize, Serialize)] -pub struct WindowProps { +pub struct ConProps { /// Milliseconds since UNIX epoch. pub last_focus_time: u128, }