use std::path::PathBuf; use std::str::FromStr; use std::{env, fmt, fs}; use anyhow::{Error, anyhow}; use clap::{Parser, ValueEnum}; use serde::{Deserialize, Serialize}; use serde_json::Value; use thiserror::Error; #[derive(Debug)] pub enum ConfigurationError { Open(String), Parse(String), } impl fmt::Display for ConfigurationError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { ConfigurationError::Open(e) => write!(f, "{e}"), ConfigurationError::Parse(e) => write!(f, "{e}"), } } } #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Debug, Serialize, Deserialize)] pub enum MatchMethod { Fuzzy, Contains, MultiContains, } #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Debug, Serialize, Deserialize)] pub enum Orientation { Vertical, Horizontal, } #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Debug, Serialize, Deserialize)] pub enum Align { Fill, Start, Center, } #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Debug, Serialize, Deserialize)] pub enum Animation { None, Expand, ExpandVertical, ExpandHorizontal, } #[derive(Clone, Debug, Serialize, Deserialize)] pub enum Mode { /// searches `$PATH` for executables and allows them to be run by selecting them. Run, /// searches `$XDG_DATA_HOME/applications` and `$XDG_DATA_DIRS/applications` /// for desktop files and allows them to be run by selecting them. Drun, /// reads from stdin and displays options which when selected will be output to stdout. Dmenu, /// tries to determine automatically what to do Auto, } #[derive(Debug, Error)] pub enum ArgsError { #[error("input is not valid {0}")] InvalidParameter(String), } impl FromStr for Mode { type Err = ArgsError; fn from_str(s: &str) -> Result { match s { "run" => Ok(Mode::Run), "drun" => Ok(Mode::Drun), "dmenu" => Ok(Mode::Dmenu), "auto" => Ok(Mode::Auto), _ => Err(ArgsError::InvalidParameter( format!("{s} is not a valid argument show this, see help for details").to_owned(), )), } } } #[derive(Debug, Deserialize, Serialize, Clone, Parser)] #[clap(about = "Worf is a wofi clone written in rust, it aims to be a drop-in replacement")] pub struct Config { /// Forks the menu so you can close the terminal #[clap(short = 'f', long = "fork")] pub fork: Option, /// Selects a config file to use #[clap(short = 'c', long = "conf")] pub config: Option, /// Runs in dmenu mode #[clap(short = 'd', long = "dmenu")] pub dmenu: Option, /// Prints the version and then exits #[clap(short = 'v', long = "version")] pub version: Option, /// Defines the style sheet to be loaded. /// Defaults to `$XDG_CONF_DIR/worf/style.css` /// or `$HOME/.config/worf/style.css` if `$XDG_CONF_DIR` is not set. #[serde(default = "default_style")] #[clap(long = "style")] pub style: Option, /// Defines the mode worf is running in #[clap(long = "show")] pub show: Option, /// Default width of the window, defaults to 50% of the screen #[serde(default = "default_width")] #[clap(long = "width")] pub width: Option, /// Default height of the window, defaults to 40% of the screen #[serde(default = "default_height")] #[clap(long = "height")] pub height: Option, #[clap(short = 'p', long = "prompt")] pub prompt: Option, #[clap(short = 'x', long = "xoffset")] pub xoffset: Option, #[clap(long = "x")] pub x: Option, #[clap(short = 'y', long = "yoffset")] pub yoffset: Option, #[clap(long = "y")] pub y: Option, /// If true a normal window instead of a layer shell will be used #[serde(default = "default_normal_window")] #[clap(short = 'n', long = "normal-window")] pub normal_window: bool, #[clap(short = 'I', long = "allow-images")] pub allow_images: Option, #[clap(short = 'm', long = "allow-markup")] pub allow_markup: Option, #[clap(short = 'k', long = "cache-file")] pub cache_file: Option, #[clap(short = 't', long = "term")] pub term: Option, #[serde(default = "default_password_char")] #[clap(short = 'P', long = "password")] pub password: Option, #[clap(short = 'e', long = "exec-search")] pub exec_search: Option, #[clap(short = 'b', long = "hide-scroll")] pub hide_scroll: Option, #[serde(default = "default_match_method")] #[clap(short = 'M', long = "matching")] pub matching: Option, #[clap(short = 'i', long = "insensitive")] pub insensitive: Option, #[clap(short = 'q', long = "parse-search")] pub parse_search: Option, #[clap(short = 'l', long = "location")] pub location: Option, #[clap(short = 'a', long = "no-actions")] pub no_actions: Option, #[clap(short = 'L', long = "lines")] pub lines: Option, #[serde(default = "default_columns")] #[clap(short = 'w', long = "columns")] pub columns: Option, #[clap(short = 'O', long = "sort-order")] pub sort_order: Option, #[clap(short = 'G', long = "gtk-dark")] pub gtk_dark: Option, #[clap(short = 'Q', long = "search")] pub search: Option, #[clap(short = 'o', long = "monitor")] pub monitor: Option, #[clap(short = 'r', long = "pre-display-cmd")] pub pre_display_cmd: Option, #[serde(default = "default_orientation")] #[clap(long = "orientation")] pub orientation: Option, #[serde(default = "default_halign")] #[clap(long = "halign")] pub halign: Option, #[serde(default = "default_content_halign")] #[clap(long = "content-halign")] pub content_halign: Option, #[clap(long = "valign")] pub valign: Option, pub filter_rate: Option, #[serde(default = "default_image_size")] #[clap(long = "image-size")] pub image_size: Option, pub key_up: Option, pub key_down: Option, pub key_left: Option, pub key_right: Option, pub key_forward: Option, pub key_backward: Option, pub key_submit: Option, pub key_exit: Option, pub key_pgup: Option, pub key_pgdn: Option, pub key_expand: Option, pub key_hide_search: Option, pub key_copy: Option, // todo re-add this // #[serde(flatten)] // pub key_custom: Option>, pub line_wrap: Option, pub global_coords: Option, pub hide_search: Option, pub dynamic_lines: Option, pub layer: Option, pub copy_exec: Option, pub single_click: Option, pub pre_display_exec: Option, /// Minimum score for a fuzzy search to be shown #[serde(default = "default_fuzzy_min_score")] #[clap(long = "fuzzy-min-score")] pub fuzzy_min_score: Option, /// Orientation of items in the row box where items are displayed #[serde(default = "default_row_box_orientation")] #[clap(long = "row-box-orientation")] pub row_bow_orientation: Option, /// Set to to true to wrap text after a given amount of chars #[serde(default = "default_text_wrap")] #[clap(long = "text-wrap")] pub text_wrap: Option, /// Defines after how many chars a line is broken over. /// Only cuts at spaces. #[serde(default = "default_text_wrap_length")] #[clap(long = "text-wrap-length")] pub text_wrap_length: Option, /// Defines the animation when the window is show. /// Defaults to Expand #[serde(default = "default_show_animation")] #[clap(long = "show-animation")] pub show_animation: Option, /// Defines how long it takes for the show animation to finish /// Defaults to 70ms #[serde(default = "default_show_animation_time")] #[clap(long = "show-animation-time")] pub show_animation_time: Option, /// Defines the animation when the window is hidden. /// Defaults to None, because it is a bit buggy with /// gtk layer shell. works fine with normal window though #[serde(default = "default_hide_animation")] #[clap(long = "hide-animation")] pub hide_animation: Option, /// Defines how long it takes for the hide animation to finish /// Defaults to 100ms #[serde(default = "default_hide_animation_time")] #[clap(long = "hide-animation-time")] pub hide_animation_time: Option, } impl Default for Config { fn default() -> Self { Config { fork: None, config: None, dmenu: None, version: None, style: default_style(), show: None, width: default_width(), height: default_height(), prompt: None, xoffset: None, x: None, yoffset: None, y: None, normal_window: default_normal_window(), allow_images: None, allow_markup: None, cache_file: None, term: None, password: None, exec_search: None, hide_scroll: None, matching: None, insensitive: None, parse_search: None, location: None, no_actions: None, lines: None, columns: default_columns(), sort_order: None, gtk_dark: None, search: None, monitor: None, pre_display_cmd: None, orientation: default_row_box_orientation(), halign: default_halign(), content_halign: default_content_halign(), valign: None, filter_rate: None, image_size: default_image_size(), key_up: None, key_down: None, key_left: None, key_right: None, key_forward: None, key_backward: None, key_submit: None, key_exit: None, key_pgup: None, key_pgdn: None, key_expand: None, key_hide_search: None, key_copy: None, //key_custom: None, line_wrap: None, global_coords: None, hide_search: None, dynamic_lines: None, layer: None, copy_exec: None, single_click: None, pre_display_exec: None, fuzzy_min_score: default_fuzzy_min_score(), row_bow_orientation: default_row_box_orientation(), text_wrap: default_text_wrap(), text_wrap_length: default_text_wrap_length(), show_animation: default_show_animation(), show_animation_time: default_show_animation_time(), hide_animation: default_hide_animation(), hide_animation_time: default_hide_animation_time(), } } } // allowed because option is needed for serde macro #[allow(clippy::unnecessary_wraps)] #[must_use] pub fn default_show_animation_time() -> Option { Some(70) } // allowed because option is needed for serde macro #[allow(clippy::unnecessary_wraps)] #[must_use] pub fn default_show_animation() -> Option { Some(Animation::Expand) } // allowed because option is needed for serde macro #[allow(clippy::unnecessary_wraps)] #[must_use] pub fn default_hide_animation_time() -> Option { Some(100) } // allowed because option is needed for serde macro #[allow(clippy::unnecessary_wraps)] #[must_use] pub fn default_hide_animation() -> Option { Some(Animation::None) } // allowed because option is needed for serde macro #[allow(clippy::unnecessary_wraps)] #[must_use] pub fn default_row_box_orientation() -> Option { Some(Orientation::Horizontal) } // allowed because option is needed for serde macro #[allow(clippy::unnecessary_wraps)] #[must_use] pub fn default_orientation() -> Option { Some(Orientation::Vertical) } // allowed because option is needed for serde macro #[allow(clippy::unnecessary_wraps)] #[must_use] pub fn default_halign() -> Option { Some(Align::Fill) } // allowed because option is needed for serde macro #[allow(clippy::unnecessary_wraps)] #[must_use] pub fn default_content_halign() -> Option { Some(Align::Fill) } // allowed because option is needed for serde macro #[allow(clippy::unnecessary_wraps)] #[must_use] pub fn default_columns() -> Option { Some(1) } // allowed because option is needed for serde macro #[allow(clippy::unnecessary_wraps)] #[must_use] pub fn default_normal_window() -> bool { false } // TODO // GtkOrientation orientation = config_get_mnemonic(config, "orientation", "vertical", 2, "vertical", "horizontal"); // outer_orientation = config_get_mnemonic(cstoonfig, "orientation", "vertical", 2, "horizontal", "vertical"); // GtkAlign halign = config_get_mnemonic(config, "halign", "fill", 4, "fill", "start", "end", "center"); // content_halign = config_get_mnemonic(config, "content_halign", "fill", 4, "fill", "start", "end", "center"); // char* default_valign = "start"; // if(outer_orientation == GTK_ORIENTATION_HORIZONTAL) { // default_valign = "center"; // } // GtkAlign valign = config_get_mnemonic(config, "valign", default_valign, 4, "fill", "start", "end", "center"); // char* prompt = config_get(config, "prompt", mode); // uint64_t filter_rate = strtol(config_get(config, "filter_rate", "100"), NULL, 10); // allow_images = strcmp(config_get(config, "allow_images", "false"), "true") == 0; // allow_markup = strcmp(config_get(config, "allow_markup", "false"), "true") == 0; // image_size = strtol(config_get(config, "image_size", "32"), NULL, 10); // cache_file = map_get(config, "cache_file"); // config_dir = map_get(config, "config_dir"); // terminal = map_get(config, "term"); // exec_search = strcmp(config_get(config, "exec_search", "false"), "true") == 0; // bool hide_scroll = strcmp(config_get(config, "hide_scroll", "false"), "true") == 0; // matching = config_get_mnemonic(config, "matching", "contains", 3, "contains", "multi-contains", "fuzzy"); // insensitive = strcmp(config_get(config, "insensitive", "false"), "true") == 0; // parse_search = strcmp(config_get(config, "parse_search", "false"), "true") == 0; // location = config_get_mnemonic(config, "location", "center", 18, // "center", "top_left", "top", "top_right", "right", "bottom_right", "bottom", "bottom_left", "left", // "0", "1", "2", "3", "4", "5", "6", "7", "8"); // no_actions = strcmp(config_get(config, "no_actions", "false"), "true") == 0; // lines = strtol(config_get(config, "lines", "0"), NULL, 10); // max_lines = lines; // columns = strtol(config_get(config, "columns", "1"), NULL, 10); // sort_order = config_get_mnemonic(config, "sort_order", "default", 2, "default", "alphabetical"); // line_wrap = config_get_mnemonic(config, "line_wrap", "off", 4, "off", "word", "char", "word_char") - 1; // bool global_coords = strcmp(config_get(config, "global_coords", "false"), "true") == 0; // hide_search = strcmp(config_get(config, "hide_search", "false"), "true") == 0; // char* search = map_get(config, "search"); // dynamic_lines = strcmp(config_get(config, "dynamic_lines", "false"), "true") == 0; // char* monitor = map_get(config, "monitor"); // char* layer = config_get(config, "layer", "top"); // copy_exec = config_get(config, "copy_exec", "wl-copy"); // pre_display_cmd = map_get(config, "pre_display_cmd"); // pre_display_exec = strcmp(config_get(config, "pre_display_exec", "false"), "true") == 0; // single_click = strcmp(config_get(config, "single_click", "false"), "true") == 0; // // keys = map_init_void(); // mods = map_init_void(); // // map_put_void(mods, "Shift", &shift_mask); // map_put_void(mods, "Ctrl", &ctrl_mask); // map_put_void(mods, "Alt", &alt_mask); // // key_default = "Up"; // char* key_up = (i == 0) ? "Up" : config_get(config, "key_up", key_default); // key_default = "Down"; // char* key_down = (i == 0) ? key_default : config_get(config, "key_down", key_default); // key_default = "Left"; // char* key_left = (i == 0) ? key_default : config_get(config, "key_left", key_default); // key_default = "Right"; // char* key_right = (i == 0) ? key_default : config_get(config, "key_right", key_default); // key_default = "Tab"; // char* key_forward = (i == 0) ? key_default : config_get(config, "key_forward", key_default); // key_default = "Shift-ISO_Left_Tab"; // char* key_backward = (i == 0) ? key_default : config_get(config, "key_backward", key_default); // key_default = "Return"; // char* key_submit = (i == 0) ? key_default : config_get(config, "key_submit", key_default); // key_default = "Escape"; // char* key_exit = (i == 0) ? key_default : config_get(config, "key_exit", key_default); // key_default = "Page_Up"; // char* key_pgup = (i == 0) ? key_default : config_get(config, "key_pgup", key_default); // key_default = "Page_Down"; // char* key_pgdn = (i == 0) ? key_default : config_get(config, "key_pgdn", key_default); // key_default = ""; // char* key_expand = (i == 0) ? key_default: config_get(config, "key_expand", key_default); // key_default = ""; // char* key_hide_search = (i == 0) ? key_default: config_get(config, "key_hide_search", key_default); // key_default = "Ctrl-c"; // char* key_copy = (i == 0) ? key_default : config_get(config, "key_copy", key_default); // allowed because option is needed for serde macro #[allow(clippy::unnecessary_wraps)] #[must_use] pub fn default_style() -> Option { style_path(None) .ok() .map(|pb| pb.display().to_string()) .or_else(|| { log::error!("no stylesheet found, using system styles"); None }) } // allowed because option is needed for serde macro #[allow(clippy::unnecessary_wraps)] #[must_use] pub fn default_height() -> Option { Some("40%".to_owned()) } // allowed because option is needed for serde macro #[allow(clippy::unnecessary_wraps)] #[must_use] pub fn default_width() -> Option { Some("50%".to_owned()) } // allowed because option is needed for serde macro #[allow(clippy::unnecessary_wraps)] #[must_use] pub fn default_password_char() -> Option { Some("*".to_owned()) } // allowed because option is needed for serde macro #[allow(clippy::unnecessary_wraps)] #[must_use] pub fn default_fuzzy_min_length() -> Option { Some(10) } // allowed because option is needed for serde macro #[allow(clippy::unnecessary_wraps)] #[must_use] pub fn default_fuzzy_min_score() -> Option { Some(0.1) } // allowed because option is needed for serde macro #[allow(clippy::unnecessary_wraps)] #[must_use] pub fn default_match_method() -> Option { Some(MatchMethod::Contains) } // allowed because option is needed for serde macro #[allow(clippy::unnecessary_wraps)] #[must_use] pub fn default_image_size() -> Option { Some(32) } // allowed because option is needed for serde macro #[allow(clippy::unnecessary_wraps)] #[must_use] pub fn default_text_wrap_length() -> Option { Some(15) } // allowed because option is needed for serde macro #[allow(clippy::unnecessary_wraps)] #[must_use] pub fn default_text_wrap() -> Option { Some(false) } #[must_use] pub fn parse_args() -> Config { Config::parse() } /// # Errors /// /// Will return Err when it cannot resolve any path or no style is found pub fn style_path(full_path: Option) -> Result { let alternative_paths = path_alternatives( vec![dirs::config_dir()], &PathBuf::from("worf").join("style.css"), ); resolve_path(full_path, alternative_paths.into_iter().collect()) } /// # Errors /// /// Will return Err when it cannot resolve any path or no style is found pub fn conf_path(full_path: Option) -> Result { let alternative_paths = path_alternatives( vec![dirs::config_dir()], &PathBuf::from("worf").join("config"), ); resolve_path(full_path, alternative_paths.into_iter().collect()) } #[must_use] pub fn path_alternatives(base_paths: Vec>, sub_path: &PathBuf) -> Vec { base_paths .into_iter() .flatten() .map(|pb| pb.join(sub_path)) .filter_map(|pb| pb.canonicalize().ok()) .filter(|c| c.exists()) .collect() } /// # Errors /// /// Will return `Err` if it is not able to find any valid path pub fn resolve_path( full_path: Option, alternatives: Vec, ) -> Result { full_path .map(PathBuf::from) .and_then(|p| p.canonicalize().ok().filter(|c| c.exists())) .or_else(|| { alternatives .into_iter() .filter(|p| p.exists()) .find_map(|pb| pb.canonicalize().ok().filter(|c| c.exists())) }) .ok_or_else(|| anyhow!("Could not find a valid file.")) } /// # Errors /// /// Will return Err when it /// * cannot read the config file /// * cannot parse the config file /// * no config file exists /// * config file and args cannot be merged pub fn load_config(args_opt: Option) -> Result { let config_path = conf_path(args_opt.as_ref().map(|c| c.config.clone()).flatten()); match config_path { Ok(path) => { let toml_content = fs::read_to_string(path).map_err(|e| ConfigurationError::Open(format!("{e}")))?; let mut config: Config = toml::from_str(&toml_content) .map_err(|e| ConfigurationError::Parse(format!("{e}")))?; if let Some(args) = args_opt { let mut merge_result = merge_config_with_args(&mut config, &args) .map_err(|e| ConfigurationError::Parse(format!("{e}")))?; if merge_result.prompt.is_none() { match &merge_result.show { None => {} Some(mode) => match mode { Mode::Run => merge_result.prompt = Some("run".to_owned()), Mode::Drun => merge_result.prompt = Some("drun".to_owned()), Mode::Dmenu => merge_result.prompt = Some("dmenu".to_owned()), _ => {} }, } } Ok(merge_result) } else { Ok(config) } } Err(e) => Err(ConfigurationError::Open(format!("{e}"))), } } pub fn expand_path(input: &str) -> PathBuf { let mut path = input.to_string(); // Expand ~ to home directory if path.starts_with("~") { if let Some(home_dir) = dirs::home_dir() { path = path.replacen("~", home_dir.to_str().unwrap_or(""), 1); } } // Expand $VAR style environment variables if path.contains('$') { for (key, value) in env::vars() { let var_pattern = format!("${}", key); if path.contains(&var_pattern) { path = path.replace(&var_pattern, &value); } } } PathBuf::from(path) } /// # Errors /// /// Will return Err when it fails to merge the config with the arguments. pub fn merge_config_with_args(config: &mut Config, args: &Config) -> anyhow::Result { let args_json = serde_json::to_value(args)?; let mut config_json = serde_json::to_value(config)?; merge_json(&mut config_json, &args_json); Ok(serde_json::from_value(config_json).unwrap_or_default()) } fn merge_json(a: &mut Value, b: &Value) { match (a, b) { (Value::Object(a_map), Value::Object(b_map)) => { for (k, v) in b_map { merge_json(a_map.entry(k.clone()).or_insert(Value::Null), v); } } (a_val, b_val) => { if *b_val != Value::Null { *a_val = b_val.clone(); } } } }