diff --git a/Cargo.toml b/Cargo.toml index aa4ecd4..a80feb3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,3 +9,4 @@ dirs = "5.0.1" serde = { version = "1.0.216", features = [ "std", "derive" ] } sysinfo = "0.32.0" toml = "0.8.19" +derive_config_parser = { path = "derive_config_parser" } diff --git a/derive_config_parser/Cargo.toml b/derive_config_parser/Cargo.toml new file mode 100644 index 0000000..3a57365 --- /dev/null +++ b/derive_config_parser/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "derive_config_parser" +version = "0.1.0" +autotests = false +edition = "2021" +publish = false + +[lib] +proc-macro = true + +[dependencies] +quote = "1.0.38" +syn = { version = "2.0.92", features = ["extra-traits"] } + diff --git a/derive_config_parser/src/lib.rs b/derive_config_parser/src/lib.rs new file mode 100644 index 0000000..6cfb546 --- /dev/null +++ b/derive_config_parser/src/lib.rs @@ -0,0 +1,45 @@ + +use core::panic; +use proc_macro::TokenStream; + +/// **for structs only** +/// - implements `parse_config(&mut self, input: &String) -> Result<()>` +/// which parses a string and fills the fills recognised values into the struct +/// - implements `write_default_config() -> Result` +/// which write a default configuration, in case the documentation is lacking +#[proc_macro_derive(ConfigParser)] +pub fn derive(input: TokenStream) -> TokenStream { + let ast = syn::parse_macro_input!(input as syn::DeriveInput); + let name = &ast.ident; + let fields = if let syn::Data::Struct(syn::DataStruct{ fields: syn::Fields::Named(syn::FieldsNamed{ ref named, .. }), .. }) = ast.data { + named + } else { + panic!("failed to parse struct fields"); + }; + + println!("{:#?}", ast); +// let string = format!("{:#?}", ast); +// _ = std::fs::write("test.txt", string); + + let expanded_stream: TokenStream = quote::quote! { + impl #name { + pub fn parse_config(&mut self, input: &String) -> std::io::Result<()> { + let mut setting_names = std::collections::HashMap::new(); + let lines = input.lines(); + for line in lines { + let mut tokens: Vec<&str> = line.split('=').map(|entry| entry.trim()).collect(); + tokens.retain(|entry| !entry.is_empty()); + if tokens.len() != 2 { + // println!("error in line'", line); + continue; + } + setting_names.insert(tokens[0], tokens[1]); + } + //println!("{:#?}", setting_names); + Ok(()) + } + } + }.into(); + expanded_stream +// TokenStream::new() +} diff --git a/src/config.rs b/src/config.rs index 083db9e..34fd60f 100644 --- a/src/config.rs +++ b/src/config.rs @@ -5,25 +5,10 @@ use crate::format::*; use dirs::config_dir; -use serde::{Deserialize, Serialize}; use std::fs; use std::io::{Error, Result}; use std::path::PathBuf; - -// macro_rules! generate_field_names { -// ($(#[$attr:meta])* struct $name:ident { $($fname:ident : $ftype:ty),* }) => { -// $(#[$attr])* struct $name { -// $($fname : $ftype),* -// } - -// impl $name { -// fn field_names() -> &'static [&'static str] { -// static NAMES: &'static [&'static str] = &[$(stringify!($fname)),*]; -// NAMES -// } -// } -// } -// } +use derive_config_parser::ConfigParser; #[derive(Debug, Clone)] pub struct Config { @@ -31,32 +16,28 @@ pub struct Config { pub settings: Settings, } -#[derive(Debug, Clone, Deserialize, Serialize, Default)] -#[serde(default)] +#[derive(Debug, Clone, Default)] pub struct Settings { pub general: GeneralSettings, pub format: FormatSettings, pub styles: StyleSettings, } -#[derive(Debug, Clone, Deserialize, Serialize, Default)] -#[serde(default)] +#[derive(Debug, Clone, Default)] pub struct GeneralSettings { pub show_stack_on_push: bool, pub show_stack_on_pop: bool, pub show_stack_on_bookmark: bool, } -#[derive(Debug, Clone, Deserialize, Serialize, Default)] -#[serde(default)] +#[derive(Debug, Clone, Default)] pub struct FormatSettings { pub stack_separator: String, pub bookmarks_separator: String, pub align_separators: bool, } -#[derive(Debug, Clone, Deserialize, Serialize, Default)] -#[serde(default)] +#[derive(Debug, Clone, Default, ConfigParser)] pub struct StyleSettings { pub stack_number: String, pub stack_separator: String, @@ -121,9 +102,7 @@ impl Config { /// formats and prints config to string pub fn to_formatted_string(&self) -> Result { - let mut buffer = String::new(); - buffer = format!("{:#?}", self.settings); - Ok(buffer) + Ok(format!("{:#?}", self.settings)) } /// reads and parses the configuration file @@ -191,16 +170,16 @@ impl Config { /// convert color settings to ansi escape sequences pub fn parse_color_settings(&mut self) -> Result<()> { - self.settings.styles.stack_number = parse_color(self.settings.styles.stack_number.clone())?; + self.settings.styles.stack_number = parse_style(self.settings.styles.stack_number.clone())?; self.settings.styles.stack_separator = - parse_color(self.settings.styles.stack_separator.clone())?; - self.settings.styles.stack_path = parse_color(self.settings.styles.stack_path.clone())?; + parse_style(self.settings.styles.stack_separator.clone())?; + self.settings.styles.stack_path = parse_style(self.settings.styles.stack_path.clone())?; self.settings.styles.bookmarks_name = - parse_color(self.settings.styles.bookmarks_name.clone())?; + parse_style(self.settings.styles.bookmarks_name.clone())?; self.settings.styles.bookmarks_seperator = - parse_color(self.settings.styles.bookmarks_seperator.clone())?; + parse_style(self.settings.styles.bookmarks_seperator.clone())?; self.settings.styles.bookmarks_path = - parse_color(self.settings.styles.bookmarks_path.clone())?; + parse_style(self.settings.styles.bookmarks_path.clone())?; Ok(()) } } diff --git a/src/format.rs b/src/format.rs index f2345aa..dfa9c1e 100644 --- a/src/format.rs +++ b/src/format.rs @@ -191,53 +191,17 @@ pub fn make_padding_string(len: usize) -> String { } /// convert color setting to ansi escape sequence -pub fn parse_color(color: String) -> Result { +pub fn parse_style(string: String) -> Result { // check for numbered color - if let Ok(numbered) = color.parse::() { // TODO: only accept numbers between 16 and 256 - return Ok(generate_256color_sequence( - ColorContext::Foreground, - numbered, - )); + if let Ok(sequence) = parse_numbered_color(&string) { + return Ok(sequence); } // check for rgb color - if color.as_bytes()[0] == b'#' && color.len() == 7 { - // match u8::from_str_radix(&color, 16) { - let red = match u8::from_str_radix(&color[1..=2], 16) { - Ok(value) => value, - Err(_) => { - return Err(Error::other(format!( - "-- failed to parse rgb color `{}` in config file", - color - ))) - } - }; - let green = match u8::from_str_radix(&color[3..=4], 16) { - Ok(value) => value, - Err(_) => { - return Err(Error::other(format!( - "-- failed to parse rgb color `{}` in config file", - color - ))) - } - }; - let blue = match u8::from_str_radix(&color[5..=6], 16) { - Ok(value) => value, - Err(_) => { - return Err(Error::other(format!( - "-- failed to parse rgb color `{}` in config file", - color - ))) - } - }; - return Ok(generate_rgb_sequence( - ColorContext::Foreground, - red, - green, - blue, - )); + if let Ok(sequence) = parse_rgb_color(&string) { + return Ok(sequence); } // check for named color - match color.to_ascii_lowercase().as_str() { + match string.to_ascii_lowercase().as_str() { "black" => Ok(generate_style_sequence(None, Some(COLORS.fg.black), None)), "red" => Ok(generate_style_sequence(None, Some(COLORS.fg.red), None)), "green" => Ok(generate_style_sequence(None, Some(COLORS.fg.green), None)), @@ -249,7 +213,60 @@ pub fn parse_color(color: String) -> Result { "default" => Ok(generate_style_sequence(None, Some(COLORS.fg.default), None)), _ => Err(Error::other(format!( "-- could not parse color `{}` in config file", - color + string ))), } } + +fn parse_numbered_color(string: &String) -> Result { + // check for numbered color + if let Ok(number) = string.parse::() { + if number >= 16 { + return Ok(generate_256color_sequence( + ColorContext::Foreground, + number, + ));} + } + Err(Error::other(format!("no numbered color found in '{}'", string))) +} + +fn parse_rgb_color(string: &String) -> Result { + // check for rgb color + if string.as_bytes()[0] == b'#' && string.len() == 7 { + // match u8::from_str_radix(&color, 16) { + let red = match u8::from_str_radix(&string[1..=2], 16) { + Ok(value) => value, + Err(_) => { + return Err(Error::other(format!( + "-- failed to parse rgb color `{}` in config file", + string + ))) + } + }; + let green = match u8::from_str_radix(&string[3..=4], 16) { + Ok(value) => value, + Err(_) => { + return Err(Error::other(format!( + "-- failed to parse rgb color `{}` in config file", + string + ))) + } + }; + let blue = match u8::from_str_radix(&string[5..=6], 16) { + Ok(value) => value, + Err(_) => { + return Err(Error::other(format!( + "-- failed to parse rgb color `{}` in config file", + string + ))) + } + }; + return Ok(generate_rgb_sequence( + ColorContext::Foreground, + red, + green, + blue, + )); + } + Err(Error::other(format!("no rgb color found in '{}'", string))) +} diff --git a/src/test_config.rs b/src/test_config.rs new file mode 100644 index 0000000..356e153 --- /dev/null +++ b/src/test_config.rs @@ -0,0 +1,137 @@ +#![allow(dead_code)] + +use dirs::config_dir; +use std::collections::HashMap; +use std::fs; +use std::io::{Error, Result}; +use std::path::PathBuf; + +const DEFAULT_SETTINGS: &[&str] = &[ + "show_stack_on_push=false", + "show_stack_on_pop=false", + "show_stack_on_bookmark=false", + "stack_separator= - ", + "bookmark_separator= - ", + "stack_number=default, bold", + "stack_separator=cyan", + "stack_path=default", + "bookmark_name=default", + "bookmark_separator=cyan", + "bookmark_path=default", +]; + +#[derive(Debug, Clone)] +pub struct Config { + conf_file_path: PathBuf, + pub settings: HashMap<&'static str, &'static str>, +} + +impl Config { + const CONFIG_DIR_NAME: &str = "navigate/"; + const CONFIG_FILE_NAME: &str = "navigate.conf"; + + pub fn new() -> Result { + let mut settings = HashMap::<&str, &str>::new(); + + // fill the hashmaps with the defined settings and their default values + for item in DEFAULT_SETTINGS { + let tokens = item.split(['=']).collect::>(); + if tokens.len() != 2 { + panic!("-- fix default format settings") + } + settings.insert(tokens.first().unwrap(), tokens.last().unwrap()); + } + + let mut config = Config { + conf_file_path: PathBuf::new(), + settings, + }; + + // get configuration directory + config.conf_file_path = match config_dir() { + Some(value) => value, + None => { + return Err(Error::other( + "-- failed to retrieve configuration directory", + )) + } + }; + // expand path to configuration file + config.conf_file_path.push(format!( + "{}{}", + Self::CONFIG_DIR_NAME, + Self::CONFIG_FILE_NAME, + )); + + // parse configuration file and populate config struct + if config.parse_config().is_err() { + config.write_config()?; + } + + // config.parse_color_settings()?; + + Ok(config) + } + + /// formats and prints config to string + pub fn to_formatted_string(&self) -> Result { + // TODO implement + Ok("hi".to_owned()) + } + + /// parse config file + fn parse_config(&mut self) -> Result<()> { + if !self.conf_file_path.is_file() { + return Err(Error::other("-- config file does not exist")); + } + + let config = match fs::read_to_string(&self.conf_file_path.clone()) { + Ok(value) => value, + Err(error) => return Err(error), + }; + + for line in config.lines() { + // ignore comments + if line.starts_with("#") { + continue; + } + + let token = line.split(['=']).collect::>(); + let key = match token.first() { + Some(value) => value, + None => return Err(Error::other(format!("-- failed to parse '{}'", line))), + }; + let value = match token.last() { + Some(value) => value, + None => return Err(Error::other(format!("-- failed to parse '{}'", line))), + }; + if self.settings.contains_key(key) { + if let Some(entry) = self.settings.get_mut(key) { + *entry = value.clone(); + } + } else { + println!("-- ignored unknown setting : {}", line); + } + } + + // self.settings.entry(key).and_modify(|entry| *entry = value); + Ok(()) + } + + /// write configuration file + fn write_config(&self) -> Result<()> { + let mut buffer = String::new(); + + buffer.push_str("# `navigate` settings\n"); + for (name, value) in self.settings.clone() { + buffer.push_str(&format!("{}={}\n", name, value)); + } + buffer.push('\n'); + + if fs::write(self.conf_file_path.clone(), buffer).is_err() { + return Err(Error::other("-- failed to write configuration file")); + } + + Ok(()) + } +}