diff --git a/Cargo.toml b/Cargo.toml index b1e4ed9..d23b7bb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "swayr" version = "0.3.5" -description = "A wofi-based LRU window-switcher (and more) for the sway window manager" +description = "A LRU window-switcher (and more) for the sway window manager" homepage = "https://sr.ht/~tsdh/swayr/" repository = "https://git.sr.ht/~tsdh/swayr" authors = ["Tassilo Horn "] diff --git a/README.md b/README.md index 48480cc..1da8bc2 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,20 @@ bindsym $mod+Shift+c exec env RUST_BACKTRACE=1 \ Of course, configure the keys to your liking. Again, enabling rust backtraces and logging are optional. +## Configuration + +Swayr can be configured using the `~/.config/swayr/config.toml` config file. +If it doesn't exist, a simple default configuration will be created on the +first invocation for use with the [wofi](https://todo.sr.ht/~scoopta/wofi) +launcher. It should be easy to adapt that default config for usage with other +launchers such as [dmenu](https://tools.suckless.org/dmenu/), +[bemenu](https://github.com/Cloudef/bemenu), +[rofi](https://github.com/davatorium/rofi), a script spawning a terminal with +[fzf](https://github.com/junegunn/fzf), or whatever. The only requirement is +that the launcher needs to be able to read the items to choose from from stdin. + +TODO: Show default config and describe it. + ## Questions & Patches For asking questions, sending feedback, or patches, refer to [my public inbox diff --git a/src/cmds.rs b/src/cmds.rs index 5126d64..dcf808c 100644 --- a/src/cmds.rs +++ b/src/cmds.rs @@ -1,6 +1,8 @@ //! Functions and data structures of the swayr client. use crate::con; +use crate::con::DisplayFormat; +use crate::config as cfg; use crate::ipc; use crate::ipc::SwayrCommand; use crate::util; @@ -20,7 +22,14 @@ pub struct ExecSwayrCmdArgs<'a> { impl fmt::Display for SwayrCommand { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { - write!(f, "{:?}", self) + write!(f, "{:?}", self) + } +} + +impl DisplayFormat for SwayrCommand { + fn format_for_display(&self, _: &cfg::Config) -> std::string::String { + // TODO: Add a format to Config + format!("{}", self) } } @@ -134,7 +143,7 @@ pub fn focus_next_window_in_direction( } let pred: Box bool> = - if windows.iter().find(|w| w.is_focused()).is_none() { + if !windows.iter().any(|w| w.is_focused()) { let last_focused_win_id = con::get_windows(&root, false, extra_props) .get(0) @@ -321,7 +330,14 @@ struct SwaymsgCmd<'a> { impl<'a> fmt::Display for SwaymsgCmd<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { - write!(f, "{}", self.cmd.join(" ")) + write!(f, "{}", self.cmd.join(" ")) + } +} + +impl DisplayFormat for SwaymsgCmd<'_> { + fn format_for_display(&self, _: &cfg::Config) -> std::string::String { + // TODO: Add a format to Config + format!("{}", self) } } diff --git a/src/con.rs b/src/con.rs index 7ff611e..5cc5233 100644 --- a/src/con.rs +++ b/src/con.rs @@ -1,13 +1,18 @@ //! Convenience data structures built from the IPC structs. +use crate::config as cfg; use crate::ipc; +use crate::ipc::NodeMethods; use crate::util; -use ipc::NodeMethods; 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, @@ -90,23 +95,71 @@ impl<'a> fmt::Display for Window<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { write!( f, - "“{}” \ - {} \ - on workspace {} \ - id {}", // Almost hide ID! - if self.node.urgent { - " background=\"darkred\" foreground=\"white\"" - } else { - "" - }, + "“{}” — {} on workspace {} (id: {}, urgent: {})", self.get_title(), self.get_app_name(), self.workspace.name.as_ref().unwrap(), - self.get_id() + self.get_id(), + self.node.urgent ) } } +impl<'a> DisplayFormat for Window<'a> { + fn format_for_display(&self, cfg: &cfg::Config) -> String { + let default = cfg::Config::default(); + let fmt = cfg + .format + .as_ref() + .and_then(|f| f.window_format.as_ref()) + .unwrap_or_else(|| { + default + .format + .as_ref() + .unwrap() + .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 + .as_ref() + .unwrap() + .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 + .as_ref() + .unwrap() + .urgency_end + .as_ref() + .unwrap() + }); + + fmt.replace("{id}", format!("{}", self.get_id()).as_str()) + .replace("{urgency_start}", urgency_start.as_str()) + .replace("{urgency_end}", urgency_end.as_str()) + .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, @@ -218,6 +271,17 @@ impl<'a> fmt::Display for WsOrWin<'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 } => { + "\t".to_owned() + &win.format_for_display(cfg) + } + } + } +} + impl WsOrWin<'_> { pub fn from_workspaces<'a>( workspaces: &'a [Workspace], @@ -292,12 +356,28 @@ impl PartialOrd for Workspace<'_> { 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() - ) + 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 = cfg::Config::default(); + let fmt = cfg + .format + .as_ref() + .and_then(|f| f.workspace_format.as_ref()) + .unwrap_or_else(|| { + default + .format + .as_ref() + .unwrap() + .workspace_format + .as_ref() + .unwrap() + }); + + fmt.replace("{id}", format!("{}", self.get_id()).as_str()) + .replace("{name}", self.get_name()) } } diff --git a/src/config.rs b/src/config.rs index 49b3543..4fdc4cc 100644 --- a/src/config.rs +++ b/src/config.rs @@ -7,8 +7,8 @@ use std::path::Path; #[derive(Debug, Serialize, Deserialize)] pub struct Config { - launcher: Option, - format: Option, + pub launcher: Option, + pub format: Option, } impl Default for Config { @@ -32,6 +32,8 @@ impl Default for Config { .to_string(), ), workspace_format: Some("Workspace {name}\t({id})".to_string()), + urgency_start: Some(String::new()), + urgency_end: Some(String::new()) }), } } @@ -39,14 +41,16 @@ impl Default for Config { #[derive(Debug, Serialize, Deserialize)] pub struct Launcher { - executable: Option, - args: Option>, + pub executable: Option, + pub args: Option>, } #[derive(Debug, Serialize, Deserialize)] pub struct Format { - window_format: Option, - workspace_format: Option, + pub window_format: Option, + pub workspace_format: Option, + pub urgency_start: Option, + pub urgency_end: Option, } fn get_config_file_path() -> Box { @@ -78,6 +82,19 @@ pub fn load_config() -> Config { let path = get_config_file_path(); if !path.exists() { save_config(Config::default()); + // Tell the user that a fresh default config has been created. + std::process::Command::new("swaynag") + .arg("--message") + .arg( + "Welcome to swayr. ".to_owned() + + "I've created a fresh (but boring) config for you in " + + &path.to_string_lossy() + + ".", + ) + .arg("--dismiss-button") + .arg("Thanks!") + .spawn() + .ok(); } let mut file = OpenOptions::new() .read(true) diff --git a/src/util.rs b/src/util.rs index 17df8c0..a9452e8 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,5 +1,7 @@ //! Utility functions including wofi-selection. +use crate::con::DisplayFormat; +use crate::config as cfg; use std::collections::HashMap; use std::io::Write; use std::process as proc; @@ -24,29 +26,48 @@ pub fn wofi_select<'a, 'b, TS>( choices: &'b [TS], ) -> Option<&'b TS> where - TS: std::fmt::Display + Sized, + TS: DisplayFormat + Sized, { let mut map: HashMap = HashMap::new(); let mut strs: Vec = vec![]; + let cfg = cfg::load_config(); for c in choices { - let s = format!("{}", c); - strs.push(String::from(s.as_str())); + let s = c.format_for_display(&cfg); + strs.push(s.clone()); map.insert(s, c); } - let mut wofi = proc::Command::new("wofi") - .arg("--show=dmenu") - .arg("--allow-markup") - .arg("--allow-images") - .arg("--insensitive") - .arg("--cache-file=/dev/null") - .arg("--parse-search") - .arg("--prompt") - .arg(prompt) + let default = cfg::Config::default(); + let launcher = cfg + .launcher + .as_ref() + .and_then(|l| l.executable.as_ref()) + .unwrap_or_else(|| { + default + .launcher + .as_ref() + .unwrap() + .executable + .as_ref() + .unwrap() + }); + let args: Vec = cfg + .launcher + .as_ref() + .and_then(|l| l.args.as_ref()) + .unwrap_or_else(|| { + default.launcher.as_ref().unwrap().args.as_ref().unwrap() + }) + .iter() + .map(|a| a.replace("{prompt}", prompt)) + .collect(); + + let mut wofi = proc::Command::new(launcher) + .args(args) .stdin(proc::Stdio::piped()) .stdout(proc::Stdio::piped()) .spawn() - .expect("Error running wofi!"); + .expect(&("Error running ".to_owned() + launcher)); { let stdin = wofi.stdin.as_mut().expect("Failed to open wofi stdin"); @@ -63,12 +84,3 @@ where choice.pop(); // Remove trailing \n from choice. map.get(&choice).copied() } - -#[test] -#[ignore = "interactive test requiring user input"] -fn test_wofi_select() { - let choices = vec!["a", "b", "c"]; - let choice = wofi_select("Choose wisely", &choices); - assert!(choice.is_some()); - assert!(choices.contains(choice.unwrap())); -}