Implement the actual usage of the config

timeout_old
Tassilo Horn 3 years ago
parent 2db7700388
commit c3764926c5
  1. 2
      Cargo.toml
  2. 14
      README.md
  3. 22
      src/cmds.rs
  4. 116
      src/con.rs
  5. 29
      src/config.rs
  6. 56
      src/util.rs

@ -1,7 +1,7 @@
[package] [package]
name = "swayr" name = "swayr"
version = "0.3.5" 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/" homepage = "https://sr.ht/~tsdh/swayr/"
repository = "https://git.sr.ht/~tsdh/swayr" repository = "https://git.sr.ht/~tsdh/swayr"
authors = ["Tassilo Horn <tsdh@gnu.org>"] authors = ["Tassilo Horn <tsdh@gnu.org>"]

@ -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 Of course, configure the keys to your liking. Again, enabling rust backtraces
and logging are optional. 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 ## Questions & Patches
For asking questions, sending feedback, or patches, refer to [my public inbox For asking questions, sending feedback, or patches, refer to [my public inbox

@ -1,6 +1,8 @@
//! Functions and data structures of the swayr client. //! Functions and data structures of the swayr client.
use crate::con; use crate::con;
use crate::con::DisplayFormat;
use crate::config as cfg;
use crate::ipc; use crate::ipc;
use crate::ipc::SwayrCommand; use crate::ipc::SwayrCommand;
use crate::util; use crate::util;
@ -20,7 +22,14 @@ pub struct ExecSwayrCmdArgs<'a> {
impl fmt::Display for SwayrCommand { impl fmt::Display for SwayrCommand {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
write!(f, "<b>{:?}</b>", 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<dyn Fn(&con::Window) -> bool> = let pred: Box<dyn Fn(&con::Window) -> bool> =
if windows.iter().find(|w| w.is_focused()).is_none() { if !windows.iter().any(|w| w.is_focused()) {
let last_focused_win_id = let last_focused_win_id =
con::get_windows(&root, false, extra_props) con::get_windows(&root, false, extra_props)
.get(0) .get(0)
@ -321,7 +330,14 @@ struct SwaymsgCmd<'a> {
impl<'a> fmt::Display for SwaymsgCmd<'a> { impl<'a> fmt::Display for SwaymsgCmd<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
write!(f, "<b>{}</b>", 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)
} }
} }

@ -1,13 +1,18 @@
//! Convenience data structures built from the IPC structs. //! Convenience data structures built from the IPC structs.
use crate::config as cfg;
use crate::ipc; use crate::ipc;
use crate::ipc::NodeMethods;
use crate::util; use crate::util;
use ipc::NodeMethods;
use std::cmp; use std::cmp;
use std::collections::HashMap; use std::collections::HashMap;
use std::fmt; use std::fmt;
use swayipc::reply as r; use swayipc::reply as r;
pub trait DisplayFormat {
fn format_for_display(&self, config: &cfg::Config) -> String;
}
#[derive(Debug)] #[derive(Debug)]
pub struct Window<'a> { pub struct Window<'a> {
node: &'a r::Node, 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> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
write!( write!(
f, f,
"<span font_weight=\"bold\" {}>{}</span> \ "“{}” — {} on workspace {} (id: {}, urgent: {})",
<i>{}</i> \
on workspace <b>{}</b> \
<span alpha=\"20000\">id {}</span>", // Almost hide ID!
if self.node.urgent {
" background=\"darkred\" foreground=\"white\""
} else {
""
},
self.get_title(), self.get_title(),
self.get_app_name(), self.get_app_name(),
self.workspace.name.as_ref().unwrap(), 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>( fn build_windows<'a>(
root: &'a r::Node, root: &'a r::Node,
include_scratchpad_windows: bool, 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<'_> { impl WsOrWin<'_> {
pub fn from_workspaces<'a>( pub fn from_workspaces<'a>(
workspaces: &'a [Workspace], workspaces: &'a [Workspace],
@ -292,12 +356,28 @@ impl PartialOrd for Workspace<'_> {
impl<'a> fmt::Display for Workspace<'a> { impl<'a> fmt::Display for Workspace<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
write!( write!(f, "“Workspace {}” (id: {})", self.get_name(), self.get_id())
f, }
"<span font_weight=\"bold\">Workspace {}</span> \ }
<span alpha=\"20000\">id {}</span>", // Almost hide ID!
self.get_name(), impl<'a> DisplayFormat for Workspace<'a> {
self.get_id() 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())
} }
} }

@ -7,8 +7,8 @@ use std::path::Path;
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct Config { pub struct Config {
launcher: Option<Launcher>, pub launcher: Option<Launcher>,
format: Option<Format>, pub format: Option<Format>,
} }
impl Default for Config { impl Default for Config {
@ -32,6 +32,8 @@ impl Default for Config {
.to_string(), .to_string(),
), ),
workspace_format: Some("Workspace {name}\t({id})".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)] #[derive(Debug, Serialize, Deserialize)]
pub struct Launcher { pub struct Launcher {
executable: Option<String>, pub executable: Option<String>,
args: Option<Vec<String>>, pub args: Option<Vec<String>>,
} }
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct Format { pub struct Format {
window_format: Option<String>, pub window_format: Option<String>,
workspace_format: Option<String>, pub workspace_format: Option<String>,
pub urgency_start: Option<String>,
pub urgency_end: Option<String>,
} }
fn get_config_file_path() -> Box<Path> { fn get_config_file_path() -> Box<Path> {
@ -78,6 +82,19 @@ pub fn load_config() -> Config {
let path = get_config_file_path(); let path = get_config_file_path();
if !path.exists() { if !path.exists() {
save_config(Config::default()); 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() let mut file = OpenOptions::new()
.read(true) .read(true)

@ -1,5 +1,7 @@
//! Utility functions including wofi-selection. //! Utility functions including wofi-selection.
use crate::con::DisplayFormat;
use crate::config as cfg;
use std::collections::HashMap; use std::collections::HashMap;
use std::io::Write; use std::io::Write;
use std::process as proc; use std::process as proc;
@ -24,29 +26,48 @@ pub fn wofi_select<'a, 'b, TS>(
choices: &'b [TS], choices: &'b [TS],
) -> Option<&'b TS> ) -> Option<&'b TS>
where where
TS: std::fmt::Display + Sized, TS: DisplayFormat + Sized,
{ {
let mut map: HashMap<String, &TS> = HashMap::new(); let mut map: HashMap<String, &TS> = HashMap::new();
let mut strs: Vec<String> = vec![]; let mut strs: Vec<String> = vec![];
let cfg = cfg::load_config();
for c in choices { for c in choices {
let s = format!("{}", c); let s = c.format_for_display(&cfg);
strs.push(String::from(s.as_str())); strs.push(s.clone());
map.insert(s, c); map.insert(s, c);
} }
let mut wofi = proc::Command::new("wofi") let default = cfg::Config::default();
.arg("--show=dmenu") let launcher = cfg
.arg("--allow-markup") .launcher
.arg("--allow-images") .as_ref()
.arg("--insensitive") .and_then(|l| l.executable.as_ref())
.arg("--cache-file=/dev/null") .unwrap_or_else(|| {
.arg("--parse-search") default
.arg("--prompt") .launcher
.arg(prompt) .as_ref()
.unwrap()
.executable
.as_ref()
.unwrap()
});
let args: Vec<String> = 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()) .stdin(proc::Stdio::piped())
.stdout(proc::Stdio::piped()) .stdout(proc::Stdio::piped())
.spawn() .spawn()
.expect("Error running wofi!"); .expect(&("Error running ".to_owned() + launcher));
{ {
let stdin = wofi.stdin.as_mut().expect("Failed to open wofi stdin"); let stdin = wofi.stdin.as_mut().expect("Failed to open wofi stdin");
@ -63,12 +84,3 @@ where
choice.pop(); // Remove trailing \n from choice. choice.pop(); // Remove trailing \n from choice.
map.get(&choice).copied() 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()));
}

Loading…
Cancel
Save