From eca1414390bd9c67f84dd9d5ca16c51a0fa42189 Mon Sep 17 00:00:00 2001 From: zegonix Date: Tue, 24 Jun 2025 21:53:56 +0200 Subject: [PATCH] implemented displaying invalid path and according styles --- README.md | 4 +- .../config-parser-common/src/format.rs | 76 ++++++++++++++++++- config-parser/config-parser-common/src/lib.rs | 1 + .../src/generator_functions.rs | 2 +- src/arguments.rs | 5 +- src/bookmarks.rs | 52 ++++++++----- src/config.rs | 54 ++++++++++--- src/main.rs | 19 ++--- src/stack.rs | 49 +++++++----- 9 files changed, 193 insertions(+), 69 deletions(-) diff --git a/README.md b/README.md index c8c8b7e..ceb01ca 100644 --- a/README.md +++ b/README.md @@ -62,8 +62,8 @@ Style settings accept styles and one color in the following formats: - [x] color option for punctuation (mostly '/') - [x] bookmarks - [x] do not resolve links in bookmarks - - [ ] option to show invalid paths - - [ ] style option for invalid paths + - [x] option to show invalid paths + - [x] style option for invalid paths - [ ] option & subcommand to remove invalid paths - [x] push to push path in stack - [x] write documentation diff --git a/config-parser/config-parser-common/src/format.rs b/config-parser/config-parser-common/src/format.rs index 3510e2b..3676d64 100644 --- a/config-parser/config-parser-common/src/format.rs +++ b/config-parser/config-parser-common/src/format.rs @@ -105,8 +105,16 @@ pub enum ColorContext { } /// prepends input with style string and appends the reset sequence at the end -pub fn apply_format(input: &str, style: &str) -> String { - format!("{}{}{}", style, input, RESET_SEQ) +pub fn apply_format(input: &String, style: &String) -> Result { + let style_set: String = match parse_ansi_set(style) { + Ok(value) => value, + Err(error) => return Err(error), + }; + let style_reset: String = match parse_ansi_reset(style) { + Ok(value) => value, + Err(error) => return Err(error), + }; + Ok(format!("{}{}{}", style_set, input, style_reset)) } /// generates a common style sequence of format @@ -185,11 +193,11 @@ pub fn make_padding_string(len: usize) -> String { String::from_utf8(vec![b' '; len]).unwrap() } -/// convert color setting to ansi escape sequence +/// convert color setting to ansi escape sequence to set style /// input format is a quoted string (either double or single) /// the style can be a combination of **one** color and /// one or more style options (bold, italic, underlined, strikethrough) -pub fn parse_style(arg: &String) -> Result { +pub fn parse_ansi_set(arg: &String) -> Result { let mut colors: Vec = Vec::::new(); let mut styles: Vec = Vec::::new(); @@ -246,6 +254,66 @@ pub fn parse_style(arg: &String) -> Result { Ok(styles.join("")) } +/// convert color setting to ansi escape sequence to reset style +/// input format is a quoted string (either double or single) +/// the style can be a combination of **one** color and +/// one or more style options (bold, italic, underlined, strikethrough) +pub fn parse_ansi_reset(arg: &String) -> Result { + let mut colors: Vec = Vec::::new(); + let mut styles: Vec = Vec::::new(); + + // separate style options + let mut tokens: Vec = arg.split([' ', ',', '\"', '\'']).map(|entry| entry.trim().to_lowercase()).collect(); + tokens.retain(|entry| !entry.is_empty()); + + // parse options + for option in tokens { + // parse numbered colors + if let Ok(_) = parse_numbered_color(&option) { + colors.push(generate_style_sequence(None, Some(COLORS.fg.default), None)); + continue; + } + + // parse rgb colors + if let Ok(_) = parse_rgb_color(&option) { + colors.push(generate_style_sequence(None, Some(COLORS.fg.default), None)); + continue; + } + + // parse styles and named colors + match option.as_str() { + // styles + "bold" => styles.push(generate_style_sequence(Some(STYLES.reset.bold), None, None)), + "dim" => styles.push(generate_style_sequence(Some(STYLES.reset.dim), None, None)), + "italic" => styles.push(generate_style_sequence(Some(STYLES.reset.italic), None, None)), + "underlined" => styles.push(generate_style_sequence(Some(STYLES.reset.underlined), None, None)), + "blinking" => styles.push(generate_style_sequence(Some(STYLES.reset.blinking), None, None)), + "reversed" => styles.push(generate_style_sequence(Some(STYLES.reset.reversed), None, None)), + "invisible" => styles.push(generate_style_sequence(Some(STYLES.reset.invisible), None, None)), + "strikethrough" => styles.push(generate_style_sequence(Some(STYLES.reset.strikethrough), None, None)), + // named colors + "black" => colors.push(generate_style_sequence(None, Some(COLORS.fg.default), None)), + "red" => colors.push(generate_style_sequence(None, Some(COLORS.fg.default), None)), + "green" => colors.push(generate_style_sequence(None, Some(COLORS.fg.default), None)), + "yellow" => colors.push(generate_style_sequence(None, Some(COLORS.fg.default), None)), + "blue" => colors.push(generate_style_sequence(None, Some(COLORS.fg.default), None)), + "magenta" => colors.push(generate_style_sequence(None, Some(COLORS.fg.default), None)), + "cyan" => colors.push(generate_style_sequence(None, Some(COLORS.fg.default), None)), + "white" => colors.push(generate_style_sequence(None, Some(COLORS.fg.default), None)), + "default" => colors.push(generate_style_sequence(None, Some(COLORS.fg.default), None)), + _ => return Err(Error::other(format!("-- could not parse style token `{}` in config file", option))), + }; + } + + if colors.len() > 1 { + return Err(Error::other(format!("-- too many colors found in setting <{}>", arg))); + } + if !colors.is_empty() { + styles.push(colors.pop().unwrap()); + } + Ok(styles.join("")) +} + fn parse_numbered_color(string: &String) -> Result { // check for numbered color if let Ok(number) = string.parse::() { diff --git a/config-parser/config-parser-common/src/lib.rs b/config-parser/config-parser-common/src/lib.rs index ea9dba3..7681613 100644 --- a/config-parser/config-parser-common/src/lib.rs +++ b/config-parser/config-parser-common/src/lib.rs @@ -8,3 +8,4 @@ pub use common::{ parse_config_file, remove_inline_comment, }; + diff --git a/config-parser/config-parser-macro/src/generator_functions.rs b/config-parser/config-parser-macro/src/generator_functions.rs index 52a3dd0..30f6155 100644 --- a/config-parser/config-parser-macro/src/generator_functions.rs +++ b/config-parser/config-parser-macro/src/generator_functions.rs @@ -62,7 +62,7 @@ pub fn gen_to_ansi_sequences(fields: &Punctuated) -> TokenStream { match attr_name.first() { Some(value) => if value.ident == "style_config" { conversions.extend(quote! { - self.#name = match config_parser::parse_style(&self.#name) { + self.#name = match config_parser::parse_ansi_set(&self.#name) { Ok(value) => value, Err(_) => return Err(std::io::Error::other(format!("failed to convert '{}' to ansi escape sequence", self.#name))), }; diff --git a/src/arguments.rs b/src/arguments.rs index f3c95ca..bcd9b91 100644 --- a/src/arguments.rs +++ b/src/arguments.rs @@ -2,7 +2,6 @@ #![allow(non_camel_case_types)] use clap::{Args, Parser, Subcommand}; -use std::path::PathBuf; /// implements stack for cd wrapper script #[derive(Parser, Debug)] @@ -32,7 +31,7 @@ pub enum Action { bookmark(BookmarkArgs), /// display current configuartion (mostly for debugging) - configuration(ConfigArgs), + configuration, } #[derive(Debug, Clone, Args)] @@ -105,7 +104,7 @@ pub enum BookmarkAction { remove(BookmarkSubArgs), /// get bookmarknames for shell completions - names(EmptyArgs), + completions(EmptyArgs), } #[derive(Debug, Clone, Args)] diff --git a/src/bookmarks.rs b/src/bookmarks.rs index 499b981..15c17c1 100644 --- a/src/bookmarks.rs +++ b/src/bookmarks.rs @@ -9,7 +9,7 @@ use std::str::FromStr; use dirs::{config_dir, home_dir}; use super::{config::*, util::to_rooted}; -use config_parser::{apply_format, generate_style_sequence, make_padding_string, RESET_SEQ, STYLES}; +use config_parser::{apply_format, make_padding_string}; #[derive(Debug, Clone)] pub struct Bookmarks { @@ -49,9 +49,11 @@ impl Bookmarks { Err(err) => return Err(Error::other(err.to_string())), }; to_rooted(&mut path)?; - if !path.is_dir() { - continue; - } + + // if !path.is_dir() { + // continue; + // } + bookmarks.bookmarks.insert(key, path); } Ok(bookmarks) @@ -110,17 +112,14 @@ impl Bookmarks { Some(value) => value, None => return Err(Error::other("-- failed to determine maximum bookmark name length")), }; - for (mark, path) in &self.bookmarks { - let padding = make_padding_string(max_name_len - mark.len()); - let name = apply_format(mark, &config.styles.bookmarks_name_style); - let separator = apply_format( - &config.format.bookmarks_separator, - &config.styles.bookmarks_seperator_style, - ); + for (raw_name, raw_path) in &self.bookmarks { + let padding: String = make_padding_string(max_name_len - raw_name.len()); + let mut name: String = raw_name.clone(); + let mut separator: String = config.format.bookmarks_separator.clone(); + let mut path: String = raw_path.clone().into_os_string().into_string().unwrap(); - let mut path = apply_format(path.to_str().unwrap(), &config.styles.bookmarks_path_style); if config.format.show_home_as_tilde { - let home = match home_dir() { + let home: String = match home_dir() { Some(value) => match value.into_os_string().into_string() { Ok(value) => value, Err(error) => return Err(Error::other(format!("-- failed to conver home directory to string: {}", error.to_str().unwrap()))), @@ -129,13 +128,30 @@ impl Bookmarks { }; path = path.replace(&home, "~"); } - path = path.replace('/', &format!("{}{}/{}{}", RESET_SEQ, config.styles.bookmarks_punct_style, RESET_SEQ, &config.styles.bookmarks_path_style)); - if config.format.align_separators { - buffer.push_str(&format!("{}{}{}{}\n", name, padding, separator, path)); - } else { - buffer.push_str(&format!("{}{}{}{}\n", name, separator, padding, path)); + if raw_path.is_dir() { + let slash: String = apply_format(&"/".to_owned(), &config.styles.bookmarks_punct_style)?; + let mut segments: Vec = path.split('/').map(|element| element.to_owned()).collect(); + for element in segments.iter_mut() { + *element = apply_format(&element, &config.styles.bookmarks_path_style)?; + } + path = segments.join(&slash); + + name = apply_format(&name, &config.styles.bookmarks_name_style)?; + separator = apply_format(&separator, &config.styles.stack_separator_style)?; } + + let mut line: String; + if config.format.align_separators { + line = format!("{}{}{}{}\n", name, padding, separator, path); + } else { + line = format!("{}{}{}{}\n", name, separator, padding, path); + } + if !raw_path.is_dir() { + line = apply_format(&line, &config.styles.bookmarks_invalid_style)?; + } + + buffer.push_str(&line); } } Ok(buffer) diff --git a/src/config.rs b/src/config.rs index 77fdffb..fecb9ac 100644 --- a/src/config.rs +++ b/src/config.rs @@ -5,7 +5,12 @@ use dirs::config_dir; use std::fs; -use std::io::{Error, Result}; +use std::{ + io::{ + Error, Result + }, + path::PathBuf, +}; use config_parser::*; #[derive(Debug, Clone, Default, ConfigParser)] @@ -28,9 +33,21 @@ pub struct GeneralSettings { #[default_value(false)] pub show_stack_on_pop: bool, - /// (bool) show book marks when adding, removing or changing to a bookmark + /// (bool) show invalid stack entries #[default_value(false)] - pub show_books_on_bookmark: bool, + pub show_invalid_stack_entries: bool, + + /// (bool) show bookmarks when adding, removing or changing to a bookmark + #[default_value(false)] + pub show_entries_on_bookmark: bool, + + /// (bool) show invalid bookmarks when displaying bookmarks + #[default_value(true)] + pub show_invalid_bookmarks: bool, + + /// (bool) remove invalid bookmarks on call + #[default_value(false)] + pub cleanup_bookmarks: bool, } #[derive(Debug, Clone, Default, ConfigParser)] @@ -88,6 +105,11 @@ pub struct StyleSettings { #[default_value("'magenta'")] pub stack_punct_style: String, + /// (string) style applied to punctuation (i.e. '/') when displaying the stack + #[style_config] + #[default_value("'default, strikethrough'")] + pub stack_invalid_style: String, + /// (string) style applied to bookmark names when displaying the bookmarks #[style_config] #[default_value("'default'")] @@ -107,6 +129,11 @@ pub struct StyleSettings { #[style_config] #[default_value("'magenta'")] pub bookmarks_punct_style: String, + + /// (string) style applied to punctuation (i.e. '/') when displaying the bookmarks + #[style_config] + #[default_value("'strikethrough'")] + pub bookmarks_invalid_style: String, } impl Config { @@ -123,23 +150,30 @@ impl Config { "; /// generates and populates a new instance of Config - pub fn new(styles_as_ansi_sequences: bool) -> Result { + pub fn new() -> Result { let mut config: Config = Self::default(); // get configuration directory - let mut config_file = match config_dir() { + let mut config_file: PathBuf = match config_dir() { Some(value) => value, None => { return Err(Error::other("-- failed to retrieve configuration directory")) } }; + config_file.push(Self::CONFIG_DIRECTORY_NAME); + if !config_file.is_dir() { + if fs::create_dir(&config_file).is_err() { + return Err(Error::other("-- failed to create a directory for the configuration files")); + } + } + // expand path to configuration file and default configuration file let mut default_file = config_file.clone(); - default_file.push(format!("{}/{}", Self::CONFIG_DIRECTORY_NAME, Self::DEFAULT_CONFIG_NAME)); - config_file.push(format!("{}/{}", Self::CONFIG_DIRECTORY_NAME, Self::CONFIG_FILE_NAME)); + default_file.push(Self::DEFAULT_CONFIG_NAME); + config_file.push(Self::CONFIG_FILE_NAME); // write default configuration file if it does not exist if !default_file.is_file() { - let mut default_string = Self::DEFAULT_FILE_HEADER.to_string(); + let mut default_string = Self::DEFAULT_FILE_HEADER.to_owned(); default_string.push_str(&config.to_string()); _ = fs::write(&default_file, default_string); } @@ -161,10 +195,6 @@ impl Config { _ = config.parse_from_string(&default_config); } - if styles_as_ansi_sequences { - config.to_ansi_sequences()?; - } - Ok(config) } diff --git a/src/main.rs b/src/main.rs index 513f33b..6a4ed69 100644 --- a/src/main.rs +++ b/src/main.rs @@ -22,7 +22,7 @@ use std::str::FromStr; fn main() -> Result<()> { let mut output = Output::new(); - let config = match Config::new(true) { + let config = match Config::new() { Ok(value) => value, Err(error) => { // config object is not ready at this point so the style @@ -70,7 +70,7 @@ fn main() -> Result<()> { Action::pop(pop_args) => handle_pop(&pop_args, &config, &mut stack, &mut output), Action::stack(stack_args) => handle_stack(&stack_args, &config, &mut stack, &mut output), Action::bookmark(bookmark_args) => handle_bookmark(&bookmark_args, &config, &mut bookmarks, &mut stack, &mut output), - Action::configuration(config_args) => handle_config(&config_args, &mut output), + Action::configuration => handle_config(&mut output), }; if res.is_err() { @@ -159,13 +159,12 @@ fn handle_stack(args: &StackArgs, config: &Config, stack: &mut Stack, output: &m } fn handle_bookmark(args: &BookmarkArgs, config: &Config, bookmarks: &mut Bookmarks, stack: &mut Stack, output: &mut Output) -> Result<()> { - // if args.bookmark_action.is_some() { if let Some(action) = &args.bookmark_action { match action { BookmarkAction::list(_) => list_bookmarks(config, bookmarks, output)?, BookmarkAction::add(args) => add_bookmarks(args, config, bookmarks, output)?, BookmarkAction::remove(args) => remove_bookmarks(args, config, bookmarks, output)?, - BookmarkAction::names(_) => println!("echo '{}'", bookmarks.get_bookmarknames()), + BookmarkAction::completions(_) => println!("echo '{}'", bookmarks.get_bookmarknames()), }; } else if args.name.is_some() { // handle `change to bookmark` let path = bookmarks.get_path_by_name(args.name.as_ref().unwrap())?; @@ -176,12 +175,8 @@ fn handle_bookmark(args: &BookmarkArgs, config: &Config, bookmarks: &mut Bookmar Ok(()) } -fn handle_config(args: &ConfigArgs, output: &mut Output) -> Result<()> { - let convert: bool = match args.convert { - Some(value) => value, - None => false, - }; - let config = Config::new(convert); +fn handle_config(output: &mut Output) -> Result<()> { + let config = Config::new(); output.push_info(&format!("config = {:#?}", config)); Ok(()) } @@ -201,7 +196,7 @@ fn add_bookmarks(args: &BookmarkSubArgs, config: &Config, bookmarks: &mut Bookma } bookmarks.add_bookmark(&args.name, &path)?; - if config.general.show_books_on_bookmark { + if config.general.show_entries_on_bookmark { output.push_info(&bookmarks.to_formatted_string(config)?); } else { _ = to_rooted(&mut path); @@ -216,7 +211,7 @@ fn add_bookmarks(args: &BookmarkSubArgs, config: &Config, bookmarks: &mut Bookma fn remove_bookmarks(args: &BookmarkSubArgs, config: &Config, bookmarks: &mut Bookmarks, output: &mut Output) -> Result<()> { bookmarks.remove_bookmark(&args.name)?; - if config.general.show_books_on_bookmark { + if config.general.show_entries_on_bookmark { output.push_info(&bookmarks.to_formatted_string(config)?); } else { output.push_info(&format!("remove bookmark `{}{}{}`.", generate_style_sequence(Some(STYLES.set.bold), None, None), args.name, RESET_SEQ)); diff --git a/src/stack.rs b/src/stack.rs index 7a53eb6..e02529d 100644 --- a/src/stack.rs +++ b/src/stack.rs @@ -5,7 +5,6 @@ use std::fs::File; use std::io::{Error, Result}; use std::path::{Path, PathBuf}; use std::str::FromStr; -use config_parser::RESET_SEQ; use sysinfo::{Pid, System}; use dirs::home_dir; @@ -41,17 +40,15 @@ impl Stack { buffer.push_str("-- the stack is empty"); } else { // print stack to string - let max_num_len = self.stack.len().to_string().len(); + let max_num_len: usize = self.stack.len().to_string().len(); for (n, item) in self.stack.iter().rev().enumerate() { - let padding = make_padding_string(max_num_len - n.to_string().len()); - let number = apply_format(&n.to_string(), &config.styles.stack_number_style); - let separator = apply_format( - &config.format.stack_separator, - &config.styles.stack_separator_style, - ); - let mut path = apply_format(item.to_str().unwrap(), &config.styles.stack_path_style); + let padding: String = make_padding_string(max_num_len - n.to_string().len()); + let mut number: String = n.to_string(); + let mut separator: String = config.format.stack_separator.clone(); + let mut path: String = item.clone().into_os_string().into_string().unwrap(); + if config.format.show_home_as_tilde { - let home = match home_dir() { + let home: String = match home_dir() { Some(value) => match value.into_os_string().into_string() { Ok(value) => value, Err(error) => return Err(Error::other(format!("-- failed to conver home directory to string: {}", error.to_str().unwrap()))), @@ -60,14 +57,32 @@ impl Stack { }; path = path.replace(&home, "~"); } - path = path.replace('/', &format!("{}{}/{}{}", RESET_SEQ, config.styles.stack_punct_style, RESET_SEQ, config.styles.stack_path_style)); - if config.format.stack_hide_numbers { - buffer.push_str(&format!("{}\n", path)); - } else if config.format.align_separators { - buffer.push_str(&format!("{}{}{}{}\n", number, padding, separator, path)); - } else { - buffer.push_str(&format!("{}{}{}{}\n", number, separator, padding, path)); + + if item.is_dir() { + let slash: String = apply_format(&"/".to_owned(), &config.styles.stack_punct_style)?; + let mut segments: Vec = path.split('/').map(|element| element.to_owned()).collect(); + for element in segments.iter_mut() { + *element = apply_format(&element, &config.styles.stack_path_style)?; + } + path = segments.join(&slash); + + number = apply_format(&number, &config.styles.stack_number_style)?; + separator = apply_format(&separator, &config.styles.stack_separator_style)?; } + + let mut line: String; + if config.format.stack_hide_numbers { + line = format!("{}\n", path); + } else if config.format.align_separators { + line = format!("{}{}{}{}\n", number, padding, separator, path); + } else { + line = format!("{}{}{}{}\n", number, separator, padding, path); + } + if !item.is_dir() { + line = apply_format(&line, &config.styles.stack_invalid_style)?; + } + + buffer.push_str(&line); } } Ok(buffer)