From 045e8e7411931a35ad7592668a17a1737f39b001 Mon Sep 17 00:00:00 2001 From: Tassilo Horn Date: Tue, 6 Jul 2021 21:50:49 +0200 Subject: [PATCH] Implement auto_tile feature --- src/bin/swayr.rs | 2 +- src/client.rs | 4 +- src/cmds.rs | 47 +++++++++++++----- src/con.rs | 106 ++++++++++++++++++++++++++++++++++++---- src/config.rs | 48 +++++++++++++++++++ src/demon.rs | 58 ++++++++++++++++------ src/ipc.rs | 122 ----------------------------------------------- src/layout.rs | 104 ++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 2 +- 9 files changed, 333 insertions(+), 160 deletions(-) delete mode 100644 src/ipc.rs create mode 100644 src/layout.rs diff --git a/src/bin/swayr.rs b/src/bin/swayr.rs index c3b5f80..ba5dde3 100644 --- a/src/bin/swayr.rs +++ b/src/bin/swayr.rs @@ -27,7 +27,7 @@ use clap::{crate_version, Clap}; )] struct Opts { #[clap(subcommand)] - command: swayr::ipc::SwayrCommand, + command: swayr::cmds::SwayrCommand, } fn main() { diff --git a/src/client.rs b/src/client.rs index e77ca9d..a3a830f 100644 --- a/src/client.rs +++ b/src/client.rs @@ -13,13 +13,13 @@ // You should have received a copy of the GNU General Public License along with // this program. If not, see . -use crate::ipc; +use crate::cmds; use crate::util; use std::io::Write; use std::os::unix::net::UnixStream; pub fn send_swayr_cmd( - cmd: ipc::SwayrCommand, + cmd: cmds::SwayrCommand, ) -> std::result::Result<(), std::io::Error> { let mut sock = UnixStream::connect(util::get_swayr_socket_path())?; sock.write_all(serde_json::to_string(&cmd).unwrap().as_bytes()) diff --git a/src/cmds.rs b/src/cmds.rs index f56c970..790a9c8 100644 --- a/src/cmds.rs +++ b/src/cmds.rs @@ -17,18 +17,43 @@ use crate::con; use crate::config as cfg; -use crate::ipc; -use crate::ipc::SwayrCommand; use crate::util; use crate::util::DisplayFormat; +use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::sync::Arc; use std::sync::RwLock; use swayipc as s; +use clap::Clap; + +#[derive(Clap, Debug, Deserialize, Serialize)] +pub enum SwayrCommand { + /// Switch to next urgent window (if any) or to last recently used window. + SwitchToUrgentOrLRUWindow, + /// Focus the selected window + SwitchWindow, + /// Focus the next window. + NextWindow, + /// Focus the previous window. + PrevWindow, + /// Quit the selected window + QuitWindow, + /// Switch to the selected workspace + SwitchWorkspace, + /// Switch to the selected workspace or focus the selected window + SwitchWorkspaceOrWindow, + /// Quit all windows of selected workspace or the selected window + QuitWorkspaceOrWindow, + /// Select and execute a swaymsg command + ExecuteSwaymsgCommand, + /// Select and execute a swayr command + ExecuteSwayrCommand, +} + pub struct ExecSwayrCmdArgs<'a> { pub cmd: &'a SwayrCommand, - pub extra_props: Arc>>, + pub extra_props: Arc>>, } impl DisplayFormat for SwayrCommand { @@ -98,7 +123,7 @@ fn quit_window_by_id(id: i64) { run_sway_command(&[format!("[con_id={}]", id).as_str(), "kill"]); } -fn get_tree() -> s::Node { +pub fn get_tree() -> s::Node { match s::Connection::new() { Ok(mut con) => con.get_tree().expect("Got no root node"), Err(err) => panic!("{}", err), @@ -106,7 +131,7 @@ fn get_tree() -> s::Node { } pub fn switch_to_urgent_or_lru_window( - extra_props: Option<&HashMap>, + extra_props: Option<&HashMap>, ) { let root = get_tree(); let windows = con::get_windows(&root, false, extra_props); @@ -122,7 +147,7 @@ pub fn switch_to_urgent_or_lru_window( } } -pub fn switch_window(extra_props: Option<&HashMap>) { +pub fn switch_window(extra_props: Option<&HashMap>) { let root = get_tree(); let windows = con::get_windows(&root, true, extra_props); @@ -138,7 +163,7 @@ pub enum Direction { pub fn focus_next_window_in_direction( dir: Direction, - extra_props: Option<&HashMap>, + extra_props: Option<&HashMap>, ) { let root = get_tree(); let windows = con::get_windows(&root, false, None); @@ -174,7 +199,7 @@ pub fn focus_next_window_in_direction( } } -pub fn switch_workspace(extra_props: Option<&HashMap>) { +pub fn switch_workspace(extra_props: Option<&HashMap>) { let root = get_tree(); let workspaces = con::get_workspaces(&root, false, extra_props); @@ -186,7 +211,7 @@ pub fn switch_workspace(extra_props: Option<&HashMap>) { } pub fn switch_workspace_or_window( - extra_props: Option<&HashMap>, + extra_props: Option<&HashMap>, ) { let root = get_tree(); let workspaces = con::get_workspaces(&root, true, extra_props); @@ -204,7 +229,7 @@ pub fn switch_workspace_or_window( } } -pub fn quit_window(extra_props: Option<&HashMap>) { +pub fn quit_window(extra_props: Option<&HashMap>) { let root = get_tree(); let windows = con::get_windows(&root, true, extra_props); @@ -214,7 +239,7 @@ pub fn quit_window(extra_props: Option<&HashMap>) { } pub fn quit_workspace_or_window( - extra_props: Option<&HashMap>, + extra_props: Option<&HashMap>, ) { let root = get_tree(); let workspaces = con::get_workspaces(&root, true, extra_props); diff --git a/src/con.rs b/src/con.rs index 6f1885a..5651b06 100644 --- a/src/con.rs +++ b/src/con.rs @@ -16,20 +16,110 @@ //! Convenience data structures built from the IPC structs. use crate::config as cfg; -use crate::ipc; -use crate::ipc::NodeMethods; use crate::util; use crate::util::DisplayFormat; use lazy_static::lazy_static; +use serde::{Deserialize, Serialize}; use std::cmp; use std::collections::HashMap; use swayipc as s; +/// Immutable Node Iterator +/// +/// Iterates nodes in depth-first order, tiled nodes before floating nodes. +pub struct NodeIter<'a> { + stack: Vec<&'a s::Node>, +} + +impl<'a> NodeIter<'a> { + pub fn new(node: &'a s::Node) -> NodeIter { + NodeIter { stack: vec![node] } + } +} + +impl<'a> Iterator for NodeIter<'a> { + type Item = &'a s::Node; + + fn next(&mut self) -> Option { + if let Some(node) = self.stack.pop() { + for n in &node.floating_nodes { + self.stack.push(&n); + } + for n in &node.nodes { + self.stack.push(&n); + } + Some(node) + } else { + None + } + } +} + +/// 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 all nodes being application windows. + fn windows(&self) -> Vec<&s::Node>; + + /// Returns all nodes being workspaces. + fn workspaces(&self) -> Vec<&s::Node>; + + fn is_scratchpad(&self) -> bool; +} + +impl NodeMethods for s::Node { + fn iter(&self) -> NodeIter { + 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 is_container(&self) -> bool { + self.node_type == s::NodeType::Workspace + || self.node_type == s::NodeType::Con + && self.name.is_none() + && self.layout != s::NodeLayout::None + } + + fn windows(&self) -> Vec<&s::Node> { + self.iter().filter(|n| n.is_window()).collect() + } + + fn workspaces(&self) -> Vec<&s::Node> { + self.iter() + .filter(|n| n.node_type == s::NodeType::Workspace) + .collect() + } + + fn is_scratchpad(&self) -> bool { + self.name.is_some() && self.name.as_ref().unwrap().eq("__i3_scratch") + } +} + +/// Extra properties gathered by swayrd for windows and workspaces. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct ExtraProps { + /// Milliseconds since UNIX epoch. + pub last_focus_time: u128, +} + #[derive(Debug)] pub struct Window<'a> { node: &'a s::Node, workspace: &'a s::Node, - extra_props: Option, + extra_props: Option, } impl Window<'_> { @@ -183,7 +273,7 @@ impl<'a> DisplayFormat for Window<'a> { fn build_windows<'a>( root: &'a s::Node, include_scratchpad_windows: bool, - extra_props: Option<&HashMap>, + extra_props: Option<&HashMap>, ) -> Vec> { let mut v = vec![]; for workspace in root.workspaces() { @@ -205,7 +295,7 @@ fn build_windows<'a>( fn build_workspaces<'a>( root: &'a s::Node, include_scratchpad: bool, - extra_props: Option<&HashMap>, + extra_props: Option<&HashMap>, ) -> Vec> { let mut v = vec![]; for workspace in root.workspaces() { @@ -240,7 +330,7 @@ fn build_workspaces<'a>( pub fn get_windows<'a>( root: &'a s::Node, include_scratchpad_windows: bool, - extra_props: Option<&HashMap>, + extra_props: Option<&HashMap>, ) -> Vec> { let extra_props_given = extra_props.is_some(); let mut wins = build_windows(root, include_scratchpad_windows, extra_props); @@ -254,7 +344,7 @@ pub fn get_windows<'a>( pub fn get_workspaces<'a>( root: &'a s::Node, include_scratchpad: bool, - extra_props: Option<&HashMap>, + extra_props: Option<&HashMap>, ) -> Vec> { let mut workspaces = build_workspaces(root, include_scratchpad, extra_props); @@ -314,7 +404,7 @@ pub fn select_workspace_or_window<'a>( pub struct Workspace<'a> { node: &'a s::Node, - extra_props: Option, + extra_props: Option, pub windows: Vec>, } diff --git a/src/config.rs b/src/config.rs index 8383775..e8c101a 100644 --- a/src/config.rs +++ b/src/config.rs @@ -17,6 +17,7 @@ use directories::ProjectDirs; use serde::{Deserialize, Serialize}; +use std::collections::HashMap; use std::fs::DirBuilder; use std::fs::OpenOptions; use std::io::{Read, Write}; @@ -26,6 +27,7 @@ use std::path::Path; pub struct Config { pub menu: Option, pub format: Option, + pub layout: Option, } #[derive(Debug, Serialize, Deserialize)] @@ -44,6 +46,28 @@ pub struct Format { pub fallback_icon: Option, } +#[derive(Debug, Serialize, Deserialize)] +pub struct Layout { + pub auto_tile: Option, + pub auto_tile_min_window_width_per_output_width: Option>, +} + +impl Layout { + pub fn auto_tile_min_window_width_per_output_width_as_map( + &self, + ) -> Option> { + if let Some(vec) = &self.auto_tile_min_window_width_per_output_width { + let mut map = HashMap::new(); + for tup in vec { + map.insert(tup[0], tup[1]); + } + Some(map) + } else { + None + } + } +} + impl Default for Menu { fn default() -> Self { Menu { @@ -90,11 +114,35 @@ impl Default for Format { } } +impl Default for Layout { + fn default() -> Layout { + let resolution_min_width_vec = vec![ + [1024, 500], + [1280, 600], + [1400, 680], + [1440, 700], + [1600, 780], + [1920, 920], + [2560, 1000], + [3440, 1000], + [4096, 1200], + ]; + + Layout { + auto_tile: Some(false), + auto_tile_min_window_width_per_output_width: Some( + resolution_min_width_vec, + ), + } + } +} + impl Default for Config { fn default() -> Self { Config { menu: Some(Menu::default()), format: Some(Format::default()), + layout: Some(Layout::default()), } } } diff --git a/src/demon.rs b/src/demon.rs index 47f1af5..3d804f2 100644 --- a/src/demon.rs +++ b/src/demon.rs @@ -16,9 +16,10 @@ //! Functions and data structures of the swayrd demon. use crate::cmds; -use crate::ipc; +use crate::con; +use crate::config; +use crate::layout; use crate::util; - use std::collections::HashMap; use std::io::Read; use std::os::unix::net::{UnixListener, UnixStream}; @@ -26,11 +27,10 @@ use std::sync::Arc; use std::sync::RwLock; use std::thread; use std::time::{SystemTime, UNIX_EPOCH}; - use swayipc as s; pub fn run_demon() { - let extra_props: Arc>> = + let extra_props: Arc>> = Arc::new(RwLock::new(HashMap::new())); let extra_props_for_ev_handler = extra_props.clone(); @@ -47,8 +47,11 @@ fn connect_and_subscribe() -> s::Fallible { } pub fn monitor_sway_events( - extra_props: Arc>>, + extra_props: Arc>>, ) { + let config = config::load_config(); + let layout = config.layout.unwrap_or_else(config::Layout::default); + 'reset: loop { println!("Connecting to sway for subscribing to events..."); match connect_and_subscribe() { @@ -67,6 +70,7 @@ pub fn monitor_sway_events( handled = handle_window_event( win_ev, extra_props_clone, + &layout, ); } s::Event::Workspace(ws_ev) => { @@ -99,31 +103,55 @@ pub fn monitor_sway_events( } } +fn maybe_auto_tile(layout: &config::Layout) { + if layout.auto_tile == Some(true) { + println!("\nauto_tile: start"); + layout::auto_tile(layout); + println!("auto_tile: end\n"); + } +} + fn handle_window_event( ev: Box, - extra_props: Arc>>, + extra_props: Arc>>, + layout: &config::Layout, ) -> bool { let s::WindowEvent { change, container, .. } = *ev; match change { - s::WindowChange::New | s::WindowChange::Focus => { + s::WindowChange::Focus => { + update_last_focus_time(container.id, extra_props); + println!("Handled window event type {:?}", change); + true + } + s::WindowChange::New => { + maybe_auto_tile(layout); update_last_focus_time(container.id, extra_props); println!("Handled window event type {:?}", change); true } s::WindowChange::Close => { remove_extra_props(container.id, extra_props); + maybe_auto_tile(layout); println!("Handled window event type {:?}", change); true } - _ => false, + s::WindowChange::Move | s::WindowChange::Floating => { + maybe_auto_tile(layout); + println!("Handled window event type {:?}", change); + false // We don't affect the extra_props state here. + } + _ => { + println!("Unhandled window event type {:?}", change); + false + } } } fn handle_workspace_event( ev: Box, - extra_props: Arc>>, + extra_props: Arc>>, ) -> bool { let s::WorkspaceEvent { change, @@ -156,7 +184,7 @@ fn handle_workspace_event( fn update_last_focus_time( id: i64, - extra_props: Arc>>, + extra_props: Arc>>, ) { let mut write_lock = extra_props.write().unwrap(); if let Some(wp) = write_lock.get_mut(&id) { @@ -164,7 +192,7 @@ fn update_last_focus_time( } else { write_lock.insert( id, - ipc::ExtraProps { + con::ExtraProps { last_focus_time: get_epoch_time_as_millis(), }, ); @@ -173,7 +201,7 @@ fn update_last_focus_time( fn remove_extra_props( id: i64, - extra_props: Arc>>, + extra_props: Arc>>, ) { extra_props.write().unwrap().remove(&id); } @@ -186,7 +214,7 @@ fn get_epoch_time_as_millis() -> u128 { } pub fn serve_client_requests( - extra_props: Arc>>, + extra_props: Arc>>, ) { match std::fs::remove_file(util::get_swayr_socket_path()) { Ok(()) => println!("Deleted stale socket from previous run."), @@ -215,11 +243,11 @@ pub fn serve_client_requests( fn handle_client_request( mut stream: UnixStream, - extra_props: Arc>>, + extra_props: Arc>>, ) { let mut cmd_str = String::new(); if stream.read_to_string(&mut cmd_str).is_ok() { - if let Ok(cmd) = serde_json::from_str::(&cmd_str) { + if let Ok(cmd) = serde_json::from_str::(&cmd_str) { cmds::exec_swayr_cmd(cmds::ExecSwayrCmdArgs { cmd: &cmd, extra_props, diff --git a/src/ipc.rs b/src/ipc.rs deleted file mode 100644 index 2f7f047..0000000 --- a/src/ipc.rs +++ /dev/null @@ -1,122 +0,0 @@ -// 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 . - -//! Extensions of swayipc types and IPC structs. - -use clap::Clap; -use serde::{Deserialize, Serialize}; -use swayipc as s; - -/// Immutable Node Iterator -/// -/// Iterates nodes in depth-first order, tiled nodes before floating nodes. -pub struct NodeIter<'a> { - stack: Vec<&'a s::Node>, -} - -impl<'a> NodeIter<'a> { - pub fn new(node: &'a s::Node) -> NodeIter { - NodeIter { stack: vec![node] } - } -} - -impl<'a> Iterator for NodeIter<'a> { - type Item = &'a s::Node; - - fn next(&mut self) -> Option { - if let Some(node) = self.stack.pop() { - for n in &node.floating_nodes { - self.stack.push(&n); - } - for n in &node.nodes { - self.stack.push(&n); - } - Some(node) - } else { - None - } - } -} - -/// Extension methods for [`swayipc::reply::Node`]. -pub trait NodeMethods { - /// Returns an iterator for this [`swayipc::reply::Node`] and its childres. - fn iter(&self) -> NodeIter; - - /// Returns all nodes being application windows. - fn windows(&self) -> Vec<&s::Node>; - - /// Returns all nodes being workspaces. - fn workspaces(&self) -> Vec<&s::Node>; - - fn is_scratchpad(&self) -> bool; -} - -impl NodeMethods for s::Node { - fn iter(&self) -> NodeIter { - NodeIter::new(self) - } - - fn windows(&self) -> Vec<&s::Node> { - self.iter() - .filter(|n| { - (n.node_type == s::NodeType::Con - || n.node_type == s::NodeType::FloatingCon) - && n.name.is_some() - }) - .collect() - } - - fn workspaces(&self) -> Vec<&s::Node> { - self.iter() - .filter(|n| n.node_type == s::NodeType::Workspace) - .collect() - } - - fn is_scratchpad(&self) -> bool { - self.name.is_some() && self.name.as_ref().unwrap().eq("__i3_scratch") - } -} - -#[derive(Clap, Debug, Deserialize, Serialize)] -pub enum SwayrCommand { - /// Switch to next urgent window (if any) or to last recently used window. - SwitchToUrgentOrLRUWindow, - /// Focus the selected window - SwitchWindow, - /// Focus the next window. - NextWindow, - /// Focus the previous window. - PrevWindow, - /// Quit the selected window - QuitWindow, - /// Switch to the selected workspace - SwitchWorkspace, - /// Switch to the selected workspace or focus the selected window - SwitchWorkspaceOrWindow, - /// Quit all windows of selected workspace or the selected window - QuitWorkspaceOrWindow, - /// Select and execute a swaymsg command - ExecuteSwaymsgCommand, - /// Select and execute a swayr command - ExecuteSwayrCommand, -} - -/// Extra properties gathered by swayrd for windows and workspaces. -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct ExtraProps { - /// Milliseconds since UNIX epoch. - pub last_focus_time: u128, -} diff --git a/src/layout.rs b/src/layout.rs new file mode 100644 index 0000000..4697790 --- /dev/null +++ b/src/layout.rs @@ -0,0 +1,104 @@ +// 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 . + +//! Functions and data structures of the swayrd demon. + +use crate::con; +use crate::con::NodeMethods; +use crate::config; +use swayipc as s; + +pub fn auto_tile(layout: &config::Layout) { + if let Ok(mut con) = s::Connection::new() { + if let Ok(tree) = con.get_tree() { + let config_map = + layout.auto_tile_min_window_width_per_output_width_as_map(); + let default_map = config::Layout::default() + .auto_tile_min_window_width_per_output_width_as_map() + .unwrap(); + for output in &tree.nodes { + println!("output: {:?}", output.name); + let output_width = output.rect.width; + let min_window_width = &config_map + .as_ref() + .unwrap_or(&default_map) + .get(&output_width); + + if let Some(min_window_width) = min_window_width { + for container in + con::NodeIter::new(output).filter(|n| n.is_container()) + { + println!( + " container: {:?}, {} nodes", + container.node_type, + container.nodes.len() + ); + for child_win in + container.nodes.iter().filter(|n| n.is_window()) + { + println!(" child_win: {:?}", child_win.app_id); + // Width if we'd split once more. + let estimated_width = + child_win.rect.width as f32 / 2.0; + println!("estimated_width = {}", estimated_width); + let split = if container.layout + == s::NodeLayout::SplitH + && estimated_width <= **min_window_width as f32 + { + Some("splitv") + } else if container.layout == s::NodeLayout::SplitV + && estimated_width > **min_window_width as f32 + { + Some("splith") + } else { + None + }; + + if let Some(split) = split { + println!( + "Auto-tiling performing {} on window {} \ + because estimated width after another \ + split is {} and the minimum window width \ + is {} on this output.", + split, + child_win.id, + estimated_width, + min_window_width + ); + match con.run_command(format!( + "[con_id={}] {}", + child_win.id, split + )) { + Ok(_) => (), + Err(e) => eprintln!( + "Couldn't set {} on con {}: {:?}", + split, child_win.id, e + ), + } + } + } + } + } else { + eprintln!("No layout.auto_tile_min_window_width_per_output_width \ + setting for output_width {}", output_width); + } + } + } else { + eprintln!("Couldn't call get_tree during auto_tile."); + } + } else { + eprintln!("Couldn't get connection for auto_tile"); + } +} diff --git a/src/lib.rs b/src/lib.rs index 6103e96..76dab6c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -28,5 +28,5 @@ pub mod cmds; pub mod con; pub mod config; pub mod demon; -pub mod ipc; +pub mod layout; pub mod util;