diff --git a/README.md b/README.md index adca80f..02ad828 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,11 @@ `motd` is a command line utility which outputs a (semi) intelligible, randomly generated message to your standard output. +``` + ❯ motd -t "$HOME is where the {noun} is." + /home/admin is where the dragon is. +``` + ## Build and Install ##### Requirements `git`, `rust`/`rustup`/`cargo` @@ -22,7 +27,7 @@ You may remove the cloned directory. `motd` is best when customized with your own word pools and templates. -`motd` will look for a `$HOME/.motdrc`, `$XDG_CONFIG_HOME/.motdrc`, or `$XDG_CONFIG_HOME/motd/motdrc` file and use the settings there as defaults when running `motd` with no flags. If somehow `motd` is configured in a way that would result in an empty word list, it will fallback to the built in defaults. +Run `motd -h` to view all options. `motd` will look for a `$HOME/.motdrc`, `$XDG_CONFIG_HOME/.motdrc`, or `$XDG_CONFIG_HOME/motd/motdrc` file and use the settings there as defaults when running `motd` with no flags. If somehow `motd` is configured in a way that would result in an empty word list, it will fallback to the built in defaults. ``` # Example motdrc @@ -86,7 +91,7 @@ motd -p adverbs > $XDG_CONFIG_HOME/motd/adverbs motd -p locations > $XDG_CONFIG_HOME/motd/locations ``` Words are separated by line, meaning you can have multiple words treated as a single unit.\ -Re-defining a word will not add an additional entry to the word pool. +Re-defining a word will not add an additional entry to the word pool.\ Lines beginning with a `!` are actively removed from their respective word pool.\ Lines beginning with a `#` are ignored as comments. Lines with the format `$()` can be `bash` expressions which are evaluated at runtime.\ diff --git a/src/config.rs b/src/config.rs index 6571d88..e21fdb3 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,6 +1,6 @@ use crate::cli::RcArgs; use crate::words::WordList; -use crate::words::{WordType, WORD_TYPES}; +use crate::words::{WORD_TYPES, WordType}; use regex::Regex; use std::env; use std::fs; @@ -27,7 +27,8 @@ impl Config { let mut templates = Vec::new(); let mut template_exclusions = Vec::new(); for word_type in WORD_TYPES { - let (word_list, exclusion_list) = load_word_list_with_exclusions(&config_dir.join(word_type.name())); + let (word_list, exclusion_list) = + load_word_list_with_exclusions(&config_dir.join(word_type.name())); if !word_list.is_empty() { match word_type { WordType::Adjective => words.adjectives = Some(word_list), @@ -80,27 +81,27 @@ fn load_word_list_with_exclusions(path: &PathBuf) -> (Vec, Vec) .filter(|line| !line.is_empty() && !line.starts_with('#')) .map(|line| expand_line(&line)) .collect(); - + // Separate exclusions (lines starting with '!') from regular words let mut exclusions = Vec::new(); let mut words = Vec::new(); - + for line in lines { - if line.starts_with('!') { - // Remove the '!' prefix and add to exclusions - exclusions.push(line[1..].to_string()); + if let Some(stripped_line) = line.strip_prefix('!') { + exclusions.push(stripped_line.to_string()); } else { words.push(line); } } - + // Filter out any words that are in the exclusion list - let filtered_words = words.into_iter() + let filtered_words = words + .into_iter() .filter(|word| !exclusions.contains(word)) .collect(); - + (filtered_words, exclusions) - }, + } Err(_) => (Vec::new(), Vec::new()), } } @@ -122,7 +123,7 @@ fn expand_env_vars(text: &str) -> String { Some(v) => v.as_str(), None => return "".to_string(), }; - env::var(var_name).unwrap_or_else(|_| format!("${{{}}}", var_name)) + env::var(var_name).unwrap_or_else(|_| format!("${{{var_name}}}")) }) .to_string() } @@ -150,10 +151,10 @@ fn execute_bash_command(command: &str) -> String { if output.status.success() { String::from_utf8_lossy(&output.stdout).trim().to_string() } else { - format!("$({})", command) + format!("$({command})") } } - Err(_) => format!("$({})", command), + Err(_) => format!("$({command})"), } } @@ -175,7 +176,7 @@ fn is_recursive_command(command: &str) -> bool { return true; } - if let Some(command_name) = token.split('/').last() { + if let Some(command_name) = token.split('/').next_back() { if command_name == program_name { return true; } @@ -187,48 +188,59 @@ fn is_recursive_command(command: &str) -> bool { pub fn load_rc_file() -> RcArgs { let rc_locations = get_rc_locations(); - + for path in rc_locations { if path.exists() { match fs::read_to_string(&path) { - Ok(content) => { - match toml::from_str::(&content) { - Ok(rc_args) => return rc_args, - Err(e) => { - eprintln!("Warning: Failed to parse RC file '{}': {}", path.display(), e); - } + Ok(content) => match toml::from_str::(&content) { + Ok(rc_args) => return rc_args, + Err(e) => { + eprintln!( + "Warning: Failed to parse RC file '{}': {}", + path.display(), + e + ); } - } + }, Err(e) => { - eprintln!("Warning: Failed to read RC file '{}': {}", path.display(), e); + eprintln!( + "Warning: Failed to read RC file '{}': {}", + path.display(), + e + ); } } } } - + RcArgs::default() } fn get_rc_locations() -> Vec { let mut locations = Vec::new(); - + // ~/.motdrc if let Ok(home) = env::var("HOME") { locations.push(PathBuf::from(home).join(".motdrc")); } - + // $XDG_CONFIG_HOME/.motdrc if let Ok(xdg_config) = env::var("XDG_CONFIG_HOME") { locations.push(PathBuf::from(xdg_config).join(".motdrc")); } - + // $XDG_CONFIG_HOME/motd/motdrc if let Ok(xdg_config) = env::var("XDG_CONFIG_HOME") { locations.push(PathBuf::from(xdg_config).join("motd").join("motdrc")); } else if let Ok(home) = env::var("HOME") { // Fallback to ~/.config/motd/motdrc if XDG_CONFIG_HOME is not set - locations.push(PathBuf::from(home).join(".config").join("motd").join("motdrc")); + locations.push( + PathBuf::from(home) + .join(".config") + .join("motd") + .join("motdrc"), + ); } - + locations } diff --git a/src/generator.rs b/src/generator.rs index 2fd2f58..9d55342 100644 --- a/src/generator.rs +++ b/src/generator.rs @@ -27,8 +27,7 @@ impl fmt::Display for GeneratorError { ), GeneratorError::CutOffExceeded { attempts, cut_off } => write!( f, - "Could not generate a message within {} characters after {} attempts.", - cut_off, attempts + "Could not generate a message within {cut_off} characters after {attempts} attempts.", ), } } @@ -56,7 +55,7 @@ pub fn generate_message(arg_words: WordList, args: Args) -> Result> = WORD_TYPES + let word_lists: HashMap> = WORD_TYPES .iter() .filter(|&word_type| *word_type != WordType::Template) .map(|&word_type| { @@ -95,7 +94,13 @@ pub fn generate_message(arg_words: WordList, args: Args) -> Result Result= MAX_CUTOFF_ATTEMPTS { - return Err(GeneratorError::CutOffExceeded { - attempts, - cut_off: cut_off_len + return Err(GeneratorError::CutOffExceeded { + attempts, + cut_off: cut_off_len, }); } - + // Continue the loop to try again continue; } else { @@ -203,12 +208,12 @@ fn build_word_list( fn build_template_list(config_templates: &[String], exclusions: &[String]) -> Vec { let mut templates: Vec = TEMPLATES.iter().map(|s| s.to_string()).collect(); templates.extend(config_templates.iter().cloned()); - + // Apply exclusions if they exist if !exclusions.is_empty() { templates.retain(|template| !exclusions.contains(template)); } - + templates } diff --git a/src/main.rs b/src/main.rs index ef11321..4e28430 100644 --- a/src/main.rs +++ b/src/main.rs @@ -36,38 +36,37 @@ fn main() -> Result<(), Box> { match word_type.to_lowercase().as_str() { "adjectives" | "adjective" => { for adj in adjectives::ADJECTIVES { - println!("{}", adj); + println!("{adj}"); } } "nouns" | "noun" => { for noun in nouns::NOUNS { - println!("{}", noun); + println!("{noun}"); } } "verbs" | "verb" => { for verb in verbs::VERBS { - println!("{}", verb); + println!("{verb}"); } } "adverbs" | "adverb" => { for adverb in adverbs::ADVERBS { - println!("{}", adverb); + println!("{adverb}"); } } "locations" | "location" => { for location in locations::LOCATIONS { - println!("{}", location); + println!("{location}"); } } "templates" | "template" => { for template in TEMPLATES { - println!("{}", template); + println!("{template}"); } } _ => { eprintln!( - "Unknown word type: {}. Valid types are: adjectives, nouns, verbs, adverbs, locations, templates", - word_type + "Unknown word type: {word_type}. Valid types are: adjectives, nouns, verbs, adverbs, locations, templates", ); std::process::exit(1); } @@ -104,13 +103,13 @@ fn main() -> Result<(), Box> { let message = generate_message(word_list, args)?; match output { Some(output_path) => { - std::fs::write(&output_path, format!("{}\n", message)).unwrap_or_else(|e| { - eprintln!("Failed to write to file '{}': {}", output_path, e); + std::fs::write(&output_path, format!("{message}\n")).unwrap_or_else(|e| { + eprintln!("Failed to write to file '{output_path}': {e}"); std::process::exit(1); }); } None => { - println!("{}", message); + println!("{message}"); } } diff --git a/src/text_utils.rs b/src/text_utils.rs index 6e46ade..3b10ae6 100644 --- a/src/text_utils.rs +++ b/src/text_utils.rs @@ -49,11 +49,11 @@ pub fn capitalize_sentences(text: &str, uppercase: bool, lowercase: bool) -> Str if capitalize_next && ch.is_alphabetic() { result.push(ch.to_uppercase().next().unwrap_or(ch)); capitalize_next = false; + } else if matches!(ch, ' ') { + result.push(ch); } else { result.push(ch); - if matches!(ch, '.' | '!' | '?') { - capitalize_next = true; - } + capitalize_next = matches!(ch, '.' | '!' | '?'); } } }