From 2952625e8debeb773f9953204b765aa81c978a21 Mon Sep 17 00:00:00 2001 From: Mohamed Achaq Date: Fri, 27 Dec 2024 13:20:03 +0100 Subject: [PATCH 01/43] fix: the issue of config overriding the listing flags --- lla/src/commands/args.rs | 33 +++++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/lla/src/commands/args.rs b/lla/src/commands/args.rs index fe64d22..fa97e18 100644 --- a/lla/src/commands/args.rs +++ b/lla/src/commands/args.rs @@ -485,22 +485,39 @@ impl Args { }) }; + let has_format_flag = matches.is_present("long") + || matches.is_present("tree") + || matches.is_present("table") + || matches.is_present("grid") + || matches.is_present("sizemap") + || matches.is_present("timeline") + || matches.is_present("git") + || matches.is_present("fuzzy") + || matches.is_present("recursive"); + Args { directory: matches.value_of("directory").unwrap_or(".").to_string(), depth: matches .value_of("depth") .and_then(|s| s.parse().ok()) .or(config.default_depth), - long_format: matches.is_present("long") || config.default_format == "long", - tree_format: matches.is_present("tree") || config.default_format == "tree", - table_format: matches.is_present("table") || config.default_format == "table", - grid_format: matches.is_present("grid") || config.default_format == "grid", - sizemap_format: matches.is_present("sizemap") || config.default_format == "sizemap", - timeline_format: matches.is_present("timeline") || config.default_format == "timeline", - git_format: matches.is_present("git") || config.default_format == "git", + long_format: matches.is_present("long") + || (!has_format_flag && config.default_format == "long"), + tree_format: matches.is_present("tree") + || (!has_format_flag && config.default_format == "tree"), + table_format: matches.is_present("table") + || (!has_format_flag && config.default_format == "table"), + grid_format: matches.is_present("grid") + || (!has_format_flag && config.default_format == "grid"), + sizemap_format: matches.is_present("sizemap") + || (!has_format_flag && config.default_format == "sizemap"), + timeline_format: matches.is_present("timeline") + || (!has_format_flag && config.default_format == "timeline"), + git_format: matches.is_present("git") + || (!has_format_flag && config.default_format == "git"), fuzzy_format: matches.is_present("fuzzy"), recursive_format: matches.is_present("recursive") - || config.default_format == "recursive", + || (!has_format_flag && config.default_format == "recursive"), show_icons: matches.is_present("icons") || (!matches.is_present("no-icons") && config.show_icons), no_color: matches.is_present("no-color"), From 152b5f2f0bb4fa23f277f79187b3d18f38f1ad3a Mon Sep 17 00:00:00 2001 From: Mohamed Achaq Date: Fri, 27 Dec 2024 13:20:29 +0100 Subject: [PATCH 02/43] feat: add new filtering options for file listing - Introduced flags to filter output by directories, files, and symlinks. - Added options to exclude directories, files, and symlinks from the listing. - Updated the Args struct and command-line argument parsing to accommodate these new features. --dirs-only: Show only directories --files-only: Show only regular files --symlinks-only: Show only symbolic links --no-dirs: Hide directories --no-files: Hide regular files --no-symlinks: Hide symbolic links --- lla/src/commands/args.rs | 48 ++++++++++++++++++++++++++++++++++ lla/src/commands/file_utils.rs | 20 ++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/lla/src/commands/args.rs b/lla/src/commands/args.rs index fa97e18..3d4f9ab 100644 --- a/lla/src/commands/args.rs +++ b/lla/src/commands/args.rs @@ -28,6 +28,12 @@ pub struct Args { pub disable_plugin: Vec, pub plugins_dir: PathBuf, pub include_dirs: bool, + pub dirs_only: bool, + pub files_only: bool, + pub symlinks_only: bool, + pub no_dirs: bool, + pub no_files: bool, + pub no_symlinks: bool, pub command: Option, } @@ -218,6 +224,36 @@ impl Args { .long("include-dirs") .help("Include directory sizes in the metadata"), ) + .arg( + Arg::with_name("dirs-only") + .long("dirs-only") + .help("Show only directories"), + ) + .arg( + Arg::with_name("files-only") + .long("files-only") + .help("Show only regular files"), + ) + .arg( + Arg::with_name("symlinks-only") + .long("symlinks-only") + .help("Show only symbolic links"), + ) + .arg( + Arg::with_name("no-dirs") + .long("no-dirs") + .help("Hide directories"), + ) + .arg( + Arg::with_name("no-files") + .long("no-files") + .help("Hide regular files"), + ) + .arg( + Arg::with_name("no-symlinks") + .long("no-symlinks") + .help("Hide symbolic links"), + ) .subcommand( SubCommand::with_name("install") .about("Install a plugin") @@ -391,6 +427,12 @@ impl Args { disable_plugin: Vec::new(), plugins_dir: config.plugins_dir.clone(), include_dirs: false, + dirs_only: false, + files_only: false, + symlinks_only: false, + no_dirs: false, + no_files: false, + no_symlinks: false, command: Some(Command::Shortcut(ShortcutAction::Run( potential_shortcut.clone(), args[2..].to_vec(), @@ -545,6 +587,12 @@ impl Args { .map(PathBuf::from) .unwrap_or_else(|| config.plugins_dir.clone()), include_dirs: matches.is_present("include-dirs") || config.include_dirs, + dirs_only: matches.is_present("dirs-only"), + files_only: matches.is_present("files-only"), + symlinks_only: matches.is_present("symlinks-only"), + no_dirs: matches.is_present("no-dirs"), + no_files: matches.is_present("no-files"), + no_symlinks: matches.is_present("no-symlinks"), command, } } diff --git a/lla/src/commands/file_utils.rs b/lla/src/commands/file_utils.rs index 1aca3b1..4ec005d 100644 --- a/lla/src/commands/file_utils.rs +++ b/lla/src/commands/file_utils.rs @@ -151,6 +151,26 @@ pub fn list_and_decorate_files( let fs_metadata = path.metadata().ok()?; let mut metadata = convert_metadata(&fs_metadata); + let should_include = if args.dirs_only { + metadata.is_dir + } else if args.files_only { + metadata.is_file + } else if args.symlinks_only { + metadata.is_symlink + } else { + let include_dirs = !args.no_dirs; + let include_files = !args.no_files; + let include_symlinks = !args.no_symlinks; + + (metadata.is_dir && include_dirs) + || (metadata.is_file && include_files) + || (metadata.is_symlink && include_symlinks) + }; + + if !should_include { + return None; + } + if args.include_dirs && metadata.is_dir { if let Ok(dir_size) = calculate_dir_size(&path) { metadata.size = dir_size; From 97f628490dc67def170b27e35d00095c0dd60453 Mon Sep 17 00:00:00 2001 From: Mohamed Achaq Date: Fri, 27 Dec 2024 13:30:19 +0100 Subject: [PATCH 03/43] feat: enhance file listing with dotfile filtering options - Added new flags to the Args struct for filtering dot files: --no-dotfiles and --dotfiles-only. - Updated command-line argument parsing to include these options. - Implemented logic in file listing to handle dot files based on the new flags. --- lla/src/commands/args.rs | 16 ++++++++++++++++ lla/src/commands/file_utils.rs | 14 ++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/lla/src/commands/args.rs b/lla/src/commands/args.rs index 3d4f9ab..de3a368 100644 --- a/lla/src/commands/args.rs +++ b/lla/src/commands/args.rs @@ -34,6 +34,8 @@ pub struct Args { pub no_dirs: bool, pub no_files: bool, pub no_symlinks: bool, + pub no_dotfiles: bool, + pub dotfiles_only: bool, pub command: Option, } @@ -254,6 +256,16 @@ impl Args { .long("no-symlinks") .help("Hide symbolic links"), ) + .arg( + Arg::with_name("no-dotfiles") + .long("no-dotfiles") + .help("Hide dot files and directories (those starting with a dot)"), + ) + .arg( + Arg::with_name("dotfiles-only") + .long("dotfiles-only") + .help("Show only dot files and directories (those starting with a dot)"), + ) .subcommand( SubCommand::with_name("install") .about("Install a plugin") @@ -433,6 +445,8 @@ impl Args { no_dirs: false, no_files: false, no_symlinks: false, + no_dotfiles: false, + dotfiles_only: false, command: Some(Command::Shortcut(ShortcutAction::Run( potential_shortcut.clone(), args[2..].to_vec(), @@ -593,6 +607,8 @@ impl Args { no_dirs: matches.is_present("no-dirs"), no_files: matches.is_present("no-files"), no_symlinks: matches.is_present("no-symlinks"), + no_dotfiles: matches.is_present("no-dotfiles"), + dotfiles_only: matches.is_present("dotfiles-only"), command, } } diff --git a/lla/src/commands/file_utils.rs b/lla/src/commands/file_utils.rs index 4ec005d..7069424 100644 --- a/lla/src/commands/file_utils.rs +++ b/lla/src/commands/file_utils.rs @@ -151,6 +151,20 @@ pub fn list_and_decorate_files( let fs_metadata = path.metadata().ok()?; let mut metadata = convert_metadata(&fs_metadata); + // Handle dot files filtering + let is_dotfile = path + .file_name() + .and_then(|n| n.to_str()) + .map(|n| n.starts_with('.')) + .unwrap_or(false); + + if args.dotfiles_only && !is_dotfile { + return None; + } else if args.no_dotfiles && is_dotfile { + return None; + } + + // Apply type filtering based on flags let should_include = if args.dirs_only { metadata.is_dir } else if args.files_only { From 1d9f23bd7b646fdba33215e0cd2f1b2d8fab0222 Mon Sep 17 00:00:00 2001 From: Mohamed Achaq Date: Fri, 27 Dec 2024 13:34:26 +0100 Subject: [PATCH 04/43] feat: update dotfile filtering logic in Args and configuration - Refactored the Args struct to utilize the new no_dotfiles configuration option for filtering dot files. - Enhanced command-line argument parsing to respect the no_dotfiles setting from the configuration. - Added no_dotfiles field to FilterConfig for better control over dot file visibility. --- lla/src/commands/args.rs | 4 ++-- lla/src/config/mod.rs | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/lla/src/commands/args.rs b/lla/src/commands/args.rs index de3a368..0d7330a 100644 --- a/lla/src/commands/args.rs +++ b/lla/src/commands/args.rs @@ -445,7 +445,7 @@ impl Args { no_dirs: false, no_files: false, no_symlinks: false, - no_dotfiles: false, + no_dotfiles: config.filter.no_dotfiles, dotfiles_only: false, command: Some(Command::Shortcut(ShortcutAction::Run( potential_shortcut.clone(), @@ -607,7 +607,7 @@ impl Args { no_dirs: matches.is_present("no-dirs"), no_files: matches.is_present("no-files"), no_symlinks: matches.is_present("no-symlinks"), - no_dotfiles: matches.is_present("no-dotfiles"), + no_dotfiles: matches.is_present("no-dotfiles") || config.filter.no_dotfiles, dotfiles_only: matches.is_present("dotfiles-only"), command, } diff --git a/lla/src/config/mod.rs b/lla/src/config/mod.rs index f16ded1..2bb520a 100644 --- a/lla/src/config/mod.rs +++ b/lla/src/config/mod.rs @@ -101,6 +101,8 @@ impl Default for SortConfig { pub struct FilterConfig { #[serde(default)] pub case_sensitive: bool, + #[serde(default)] + pub no_dotfiles: bool, } #[derive(Debug, Serialize, Deserialize, Clone)] @@ -242,6 +244,10 @@ natural = {} # Default: false case_sensitive = {} +# Hide dot files and directories by default +# Default: false +no_dotfiles = {} + # Formatter-specific configurations [formatters.tree] # Maximum number of entries to display in tree view @@ -282,6 +288,7 @@ ignore_patterns = {}"#, self.sort.case_sensitive, self.sort.natural, self.filter.case_sensitive, + self.filter.no_dotfiles, self.formatters.tree.max_lines.unwrap_or(0), self.listers.recursive.max_entries.unwrap_or(0), serde_json::to_string(&self.listers.fuzzy.ignore_patterns).unwrap(), @@ -589,6 +596,14 @@ ignore_patterns = {}"#, )) })?; } + ["filter", "no_dotfiles"] => { + self.filter.no_dotfiles = value.parse().map_err(|_| { + LlaError::Config(ConfigErrorKind::InvalidValue( + key.to_string(), + "must be true or false".to_string(), + )) + })?; + } ["formatters", "tree", "max_lines"] => { let max_lines = value.parse().map_err(|_| { LlaError::Config(ConfigErrorKind::InvalidValue( From 3dbcf3761b2796a494651c17afa05e2f81985658 Mon Sep 17 00:00:00 2001 From: Mohamed Achaq Date: Fri, 27 Dec 2024 13:42:19 +0100 Subject: [PATCH 05/43] feat: expand filtering options in README and configuration - Added detailed documentation for new filtering options: --dirs-only, --files-only, --symlinks-only, --dotfiles-only, and their corresponding hide flags. - Included examples in the README for using the new filtering commands. - Updated configuration to include a default setting for hiding dot files. - Enhanced clarity on combining filters for more complex queries. --- README.md | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/README.md b/README.md index 461ce40..c846ede 100644 --- a/README.md +++ b/README.md @@ -213,11 +213,23 @@ lla -R # use -d to control the depth - Filter by pattern (`-f`, `--filter`) - Case-sensitive filtering (`-c`, `--case-sensitive`) - Support for complex filter patterns: + - Simple text matching - Regular expressions - Glob patterns - Logical operators (AND, OR, NOT, XOR) +- Show only specific types: + - `--dirs-only`: Show only directories + - `--files-only`: Show only regular files + - `--symlinks-only`: Show only symbolic links + - `--dotfiles-only`: Show only dot files and directories (those starting with a dot) +- Hide specific types: + - `--no-dirs`: Hide directories + - `--no-files`: Hide regular files + - `--no-symlinks`: Hide symbolic links + - `--no-dotfiles`: Hide dot files and directories (those starting with a dot) + **Plugin System** - Enable/disable plugins (`--enable-plugin`, `--disable-plugin`) @@ -272,6 +284,22 @@ lla -t -d 3 # Tree view with max depth 3 lla -f "test" # Find files containing "test" lla -f "test" -c # Case-sensitive search lla -f ".rs" # Find files with .rs extension + +# Show only specific types +lla --dirs-only # Show only directories +lla --files-only # Show only regular files +lla --symlinks-only # Show only symbolic links +lla --dotfiles-only # Show only dot files and directories + +# Hide specific types +lla --no-dirs # Hide directories +lla --no-files # Hide regular files +lla --no-symlinks # Hide symbolic links +lla --no-dotfiles # Hide dot files and directories + +# Combine filters +lla --dirs-only --dotfiles-only # Show only dot directories +lla --files-only --no-dotfiles # Show only regular files, excluding dot files ``` #### Advanced Filters @@ -447,6 +475,10 @@ natural = true # Default: false case_sensitive = false +# Hide dot files and directories by default +# Default: false +no_dotfiles = false + # Formatter-specific configurations [formatters.tree] # Maximum number of entries to display in tree view @@ -490,6 +522,7 @@ lla config --set show_icons true lla config --set sort.dirs_first true lla config --set sort.case_sensitive true lla config --set filter.case_sensitive true +lla config --set filter.no_dotfiles true # Hide dot files by default # Manage shortcuts lla shortcut add NAME PLUGIN ACTION [-d DESCRIPTION] # Add shortcut From 12c645f2a36fef5cf7038525e92f0624477011bb Mon Sep 17 00:00:00 2001 From: Mohamed Achaq Date: Fri, 27 Dec 2024 20:45:01 +0100 Subject: [PATCH 06/43] refactor: remove commented-out code for dot file filtering - Eliminated outdated comments regarding dot file filtering in the list_and_decorate_files function. - Streamlined the code for better readability and maintainability. --- lla/src/commands/file_utils.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/lla/src/commands/file_utils.rs b/lla/src/commands/file_utils.rs index 7069424..af45418 100644 --- a/lla/src/commands/file_utils.rs +++ b/lla/src/commands/file_utils.rs @@ -151,7 +151,6 @@ pub fn list_and_decorate_files( let fs_metadata = path.metadata().ok()?; let mut metadata = convert_metadata(&fs_metadata); - // Handle dot files filtering let is_dotfile = path .file_name() .and_then(|n| n.to_str()) @@ -164,7 +163,6 @@ pub fn list_and_decorate_files( return None; } - // Apply type filtering based on flags let should_include = if args.dirs_only { metadata.is_dir } else if args.files_only { From 9e91c05424e22e68559c9b6a70151b14a4e8f79e Mon Sep 17 00:00:00 2001 From: Mohamed Achaq Date: Fri, 27 Dec 2024 20:45:21 +0100 Subject: [PATCH 07/43] feat: introduce lla_plugin_utils module with core functionality - Added a new Cargo.toml file for the lla_plugin_utils package, defining dependencies and features. - Implemented ActionRegistry for managing actions with handlers and help documentation. - Created ConfigManager for handling plugin configuration with validation and persistence. - Developed formatting utilities for entries, including permission and ownership formatting. - Established UI components for displaying help and progress, enhancing user interaction. - Introduced macros for defining actions and creating plugins, streamlining plugin development. --- lla_plugin_utils/Cargo.toml | 26 +++ lla_plugin_utils/src/actions.rs | 72 +++++++ lla_plugin_utils/src/config.rs | 140 ++++++++++++++ lla_plugin_utils/src/format.rs | 102 ++++++++++ lla_plugin_utils/src/lib.rs | 246 ++++++++++++++++++++++++ lla_plugin_utils/src/ui/components.rs | 264 ++++++++++++++++++++++++++ lla_plugin_utils/src/ui/mod.rs | 98 ++++++++++ 7 files changed, 948 insertions(+) create mode 100644 lla_plugin_utils/Cargo.toml create mode 100644 lla_plugin_utils/src/actions.rs create mode 100644 lla_plugin_utils/src/config.rs create mode 100644 lla_plugin_utils/src/format.rs create mode 100644 lla_plugin_utils/src/lib.rs create mode 100644 lla_plugin_utils/src/ui/components.rs create mode 100644 lla_plugin_utils/src/ui/mod.rs diff --git a/lla_plugin_utils/Cargo.toml b/lla_plugin_utils/Cargo.toml new file mode 100644 index 0000000..4168bd4 --- /dev/null +++ b/lla_plugin_utils/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "lla_plugin_utils" +version.workspace = true +edition.workspace = true +description.workspace = true +authors.workspace = true +license.workspace = true + +[dependencies] +lla_plugin_interface = { path = "../lla_plugin_interface" } +serde = { workspace = true } +colored = { workspace = true } +toml = { workspace = true } +dirs = { workspace = true } +prost = { workspace = true } +bytes = "1.5.0" +chrono = { workspace = true } +users = { workspace = true } +indicatif = { workspace = true } +console = "0.15.8" + +[features] +default = ["config", "ui", "format"] +config = [] +ui = [] +format = [] diff --git a/lla_plugin_utils/src/actions.rs b/lla_plugin_utils/src/actions.rs new file mode 100644 index 0000000..9e112e7 --- /dev/null +++ b/lla_plugin_utils/src/actions.rs @@ -0,0 +1,72 @@ +use std::collections::HashMap; + +pub struct Action { + pub handler: Box Result<(), String> + Send + Sync>, + pub help: ActionHelp, +} + +pub struct ActionHelp { + pub usage: String, + pub description: String, + pub examples: Vec, +} + +pub struct ActionRegistry { + actions: HashMap, +} + +impl ActionRegistry { + pub fn new() -> Self { + Self { + actions: HashMap::new(), + } + } + + pub fn register(&mut self, name: &str, help: ActionHelp, handler: F) + where + F: Fn(&[String]) -> Result<(), String> + Send + Sync + 'static, + { + self.actions.insert( + name.to_string(), + Action { + handler: Box::new(handler), + help, + }, + ); + } + + pub fn handle(&self, action: &str, args: &[String]) -> Result<(), String> { + match self.actions.get(action) { + Some(action) => (action.handler)(args), + None => Err(format!("Unknown action: {}", action)), + } + } + + pub fn get_help(&self) -> Vec<(&str, &ActionHelp)> { + self.actions + .iter() + .map(|(name, action)| (name.as_str(), &action.help)) + .collect() + } +} + +impl Default for ActionRegistry { + fn default() -> Self { + Self::new() + } +} + +#[macro_export] +macro_rules! define_action { + ($registry:expr, $name:expr, $usage:expr, $description:expr, $examples:expr, $handler:expr) => { + $registry.register( + $name, + $crate::actions::ActionHelp { + usage: $usage.to_string(), + description: $description.to_string(), + examples: $examples.iter().map(|s| s.to_string()).collect(), + }, + $handler, + ); + }; +} diff --git a/lla_plugin_utils/src/config.rs b/lla_plugin_utils/src/config.rs new file mode 100644 index 0000000..773047a --- /dev/null +++ b/lla_plugin_utils/src/config.rs @@ -0,0 +1,140 @@ +use serde::{de::DeserializeOwned, Serialize}; +use std::path::PathBuf; + +pub trait PluginConfig: Default + Serialize + DeserializeOwned { + fn validate(&self) -> Result<(), String> { + Ok(()) + } +} + +pub struct ConfigManager { + config: T, + config_path: PathBuf, +} + +impl ConfigManager { + pub fn new(plugin_name: &str) -> Self { + let config_path = dirs::config_dir() + .unwrap_or_else(|| PathBuf::from(".")) + .join("lla") + .join(plugin_name) + .with_extension("toml"); + + let config = Self::load_config(&config_path).unwrap_or_default(); + + Self { + config, + config_path, + } + } + + pub fn get(&self) -> &T { + &self.config + } + + pub fn get_mut(&mut self) -> &mut T { + &mut self.config + } + + pub fn save(&self) -> Result<(), String> { + self.config.validate()?; + + if let Some(parent) = self.config_path.parent() { + std::fs::create_dir_all(parent) + .map_err(|e| format!("Failed to create config directory: {}", e))?; + } + + let content = toml::to_string_pretty(&self.config) + .map_err(|e| format!("Failed to serialize config: {}", e))?; + + std::fs::write(&self.config_path, content) + .map_err(|e| format!("Failed to write config file: {}", e))?; + + Ok(()) + } + + pub fn reload(&mut self) -> Result<(), String> { + if let Ok(new_config) = Self::load_config(&self.config_path) { + self.config = new_config; + Ok(()) + } else { + Err("Failed to reload configuration".to_string()) + } + } + + fn load_config(path: &PathBuf) -> Result { + let content = std::fs::read_to_string(path) + .map_err(|e| format!("Failed to read config file: {}", e))?; + + let config: T = + toml::from_str(&content).map_err(|e| format!("Failed to parse config file: {}", e))?; + + config.validate()?; + + Ok(config) + } +} + +pub struct ConfigBuilder { + config: T, +} + +impl ConfigBuilder { + pub fn new() -> Self { + Self { + config: T::default(), + } + } + + pub fn build(self) -> T { + self.config + } +} + +impl Default for ConfigBuilder { + fn default() -> Self { + Self::new() + } +} + +#[macro_export] +macro_rules! plugin_config { + ( + $(#[$meta:meta])* + pub struct $name:ident { + $( + $(#[$field_meta:meta])* + pub $field:ident: $type:ty $(= $default:expr)? + ),* $(,)? + } + ) => { + $(#[$meta])* + #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] + pub struct $name { + $( + $(#[$field_meta])* + pub $field: $type, + )* + } + + impl Default for $name { + fn default() -> Self { + Self { + $( + $field: plugin_config!(@default $($default)?), + )* + } + } + } + + impl $crate::config::PluginConfig for $name {} + }; + + (@default) => { + Default::default() + }; + + (@default $expr:expr) => { + $expr + }; +} diff --git a/lla_plugin_utils/src/format.rs b/lla_plugin_utils/src/format.rs new file mode 100644 index 0000000..e116653 --- /dev/null +++ b/lla_plugin_utils/src/format.rs @@ -0,0 +1,102 @@ +use lla_plugin_interface::DecoratedEntry; +use std::collections::HashMap; + +pub trait EntryFormatter { + fn format_field(&self, entry: &DecoratedEntry, format: &str) -> Option; +} + +pub struct FieldFormatterBuilder { + formatters: HashMap Option>>, +} + +impl FieldFormatterBuilder { + pub fn new() -> Self { + Self { + formatters: HashMap::new(), + } + } + + pub fn add_formatter(mut self, format: &str, formatter: F) -> Self + where + F: Fn(&DecoratedEntry) -> Option + 'static, + { + self.formatters + .insert(format.to_string(), Box::new(formatter)); + self + } + + pub fn build(self) -> CustomFieldFormatter { + CustomFieldFormatter { + formatters: self.formatters, + } + } +} + +pub struct CustomFieldFormatter { + formatters: HashMap Option>>, +} + +impl EntryFormatter for CustomFieldFormatter { + fn format_field(&self, entry: &DecoratedEntry, format: &str) -> Option { + self.formatters.get(format).and_then(|f| f(entry)) + } +} + +impl Default for FieldFormatterBuilder { + fn default() -> Self { + Self::new() + } +} + +pub fn format_permissions(permissions: u32) -> String { + let mut result = String::with_capacity(10); + + result.push(if permissions & 0o040000 != 0 { + 'd' + } else { + '-' + }); + + result.push(if permissions & 0o400 != 0 { 'r' } else { '-' }); + result.push(if permissions & 0o200 != 0 { 'w' } else { '-' }); + result.push(if permissions & 0o100 != 0 { 'x' } else { '-' }); + + result.push(if permissions & 0o040 != 0 { 'r' } else { '-' }); + result.push(if permissions & 0o020 != 0 { 'w' } else { '-' }); + result.push(if permissions & 0o010 != 0 { 'x' } else { '-' }); + + result.push(if permissions & 0o004 != 0 { 'r' } else { '-' }); + result.push(if permissions & 0o002 != 0 { 'w' } else { '-' }); + result.push(if permissions & 0o001 != 0 { 'x' } else { '-' }); + + result +} + +pub fn format_file_type(entry: &DecoratedEntry) -> String { + if entry.metadata.is_dir { + "Directory".to_string() + } else if entry.metadata.is_symlink { + "Symlink".to_string() + } else if entry.metadata.is_file { + match entry.path.extension() { + Some(ext) => ext.to_string_lossy().to_uppercase(), + None => "File".to_string(), + } + } else { + "Unknown".to_string() + } +} + +pub fn format_ownership(uid: u32, gid: u32) -> String { + use users::{get_group_by_gid, get_user_by_uid}; + + let user = get_user_by_uid(uid) + .map(|u| u.name().to_string_lossy().into_owned()) + .unwrap_or_else(|| uid.to_string()); + + let group = get_group_by_gid(gid) + .map(|g| g.name().to_string_lossy().into_owned()) + .unwrap_or_else(|| gid.to_string()); + + format!("{}:{}", user, group) +} diff --git a/lla_plugin_utils/src/lib.rs b/lla_plugin_utils/src/lib.rs new file mode 100644 index 0000000..3ea0b49 --- /dev/null +++ b/lla_plugin_utils/src/lib.rs @@ -0,0 +1,246 @@ +pub mod actions; +pub mod config; +pub mod format; +pub mod ui; + +pub use actions::{Action, ActionHelp, ActionRegistry}; +pub use config::PluginConfig; +pub use ui::{ + components::{BoxComponent, BoxStyle, HelpFormatter, KeyValue, List, Spinner}, + TextBlock, TextStyle, +}; + +use lla_plugin_interface::{proto, PluginRequest, PluginResponse}; +use std::path::PathBuf; + +pub struct BasePlugin { + config: C, + config_file: PathBuf, +} + +impl BasePlugin { + pub fn new() -> Self { + let config_file = dirs::config_dir() + .unwrap_or_else(|| PathBuf::from(".")) + .join("lla") + .join("plugins") + .join(env!("CARGO_PKG_NAME")) + .join("config.toml"); + + let config = if let Ok(content) = std::fs::read_to_string(&config_file) { + toml::from_str(&content).unwrap_or_default() + } else { + C::default() + }; + + Self { + config, + config_file, + } + } + + pub fn config(&self) -> &C { + &self.config + } + + pub fn config_mut(&mut self) -> &mut C { + &mut self.config + } + + pub fn save_config(&self) -> Result<(), String> { + if let Some(parent) = self.config_file.parent() { + std::fs::create_dir_all(parent).map_err(|e| e.to_string())?; + } + let content = toml::to_string_pretty(&self.config) + .map_err(|e| format!("Failed to serialize config: {}", e))?; + std::fs::write(&self.config_file, content).map_err(|e| e.to_string())?; + Ok(()) + } +} + +pub trait ConfigurablePlugin { + type Config: PluginConfig; + + fn config(&self) -> &Self::Config; + fn config_mut(&mut self) -> &mut Self::Config; +} + +pub trait ProtobufHandler { + fn decode_request(&self, request: &[u8]) -> Result { + use prost::Message; + let proto_msg = proto::PluginMessage::decode(request) + .map_err(|e| format!("Failed to decode request: {}", e))?; + + match proto_msg.message { + Some(proto::plugin_message::Message::GetName(_)) => Ok(PluginRequest::GetName), + Some(proto::plugin_message::Message::GetVersion(_)) => Ok(PluginRequest::GetVersion), + Some(proto::plugin_message::Message::GetDescription(_)) => { + Ok(PluginRequest::GetDescription) + } + Some(proto::plugin_message::Message::GetSupportedFormats(_)) => { + Ok(PluginRequest::GetSupportedFormats) + } + Some(proto::plugin_message::Message::Decorate(entry)) => { + let metadata = entry + .metadata + .map(|m| lla_plugin_interface::EntryMetadata { + size: m.size, + modified: m.modified, + accessed: m.accessed, + created: m.created, + is_dir: m.is_dir, + is_file: m.is_file, + is_symlink: m.is_symlink, + permissions: m.permissions, + uid: m.uid, + gid: m.gid, + }) + .ok_or("Missing metadata in decorated entry")?; + + let decorated = lla_plugin_interface::DecoratedEntry { + path: std::path::PathBuf::from(entry.path), + metadata, + custom_fields: entry.custom_fields, + }; + Ok(PluginRequest::Decorate(decorated)) + } + Some(proto::plugin_message::Message::FormatField(req)) => { + let entry = req.entry.ok_or("Missing entry in format field request")?; + let metadata = entry + .metadata + .map(|m| lla_plugin_interface::EntryMetadata { + size: m.size, + modified: m.modified, + accessed: m.accessed, + created: m.created, + is_dir: m.is_dir, + is_file: m.is_file, + is_symlink: m.is_symlink, + permissions: m.permissions, + uid: m.uid, + gid: m.gid, + }) + .ok_or("Missing metadata in decorated entry")?; + + let decorated = lla_plugin_interface::DecoratedEntry { + path: std::path::PathBuf::from(entry.path), + metadata, + custom_fields: entry.custom_fields, + }; + Ok(PluginRequest::FormatField(decorated, req.format)) + } + Some(proto::plugin_message::Message::Action(req)) => { + Ok(PluginRequest::PerformAction(req.action, req.args)) + } + _ => Err("Invalid request type".to_string()), + } + } + + fn encode_response(&self, response: PluginResponse) -> Vec { + use prost::Message; + let response_msg = match response { + PluginResponse::Name(name) => proto::plugin_message::Message::NameResponse(name), + PluginResponse::Version(version) => { + proto::plugin_message::Message::VersionResponse(version) + } + PluginResponse::Description(desc) => { + proto::plugin_message::Message::DescriptionResponse(desc) + } + PluginResponse::SupportedFormats(formats) => { + proto::plugin_message::Message::FormatsResponse(proto::SupportedFormatsResponse { + formats, + }) + } + PluginResponse::Decorated(entry) => { + let proto_metadata = proto::EntryMetadata { + size: entry.metadata.size, + modified: entry.metadata.modified, + accessed: entry.metadata.accessed, + created: entry.metadata.created, + is_dir: entry.metadata.is_dir, + is_file: entry.metadata.is_file, + is_symlink: entry.metadata.is_symlink, + permissions: entry.metadata.permissions, + uid: entry.metadata.uid, + gid: entry.metadata.gid, + }; + + let proto_entry = proto::DecoratedEntry { + path: entry.path.to_string_lossy().to_string(), + metadata: Some(proto_metadata), + custom_fields: entry.custom_fields, + }; + proto::plugin_message::Message::DecoratedResponse(proto_entry) + } + PluginResponse::FormattedField(field) => { + proto::plugin_message::Message::FieldResponse(proto::FormattedFieldResponse { + field, + }) + } + PluginResponse::ActionResult(result) => match result { + Ok(()) => proto::plugin_message::Message::ActionResponse(proto::ActionResponse { + success: true, + error: None, + }), + Err(e) => proto::plugin_message::Message::ActionResponse(proto::ActionResponse { + success: false, + error: Some(e), + }), + }, + PluginResponse::Error(e) => proto::plugin_message::Message::ErrorResponse(e), + }; + + let proto_msg = proto::PluginMessage { + message: Some(response_msg), + }; + let mut buf = bytes::BytesMut::with_capacity(proto_msg.encoded_len()); + proto_msg.encode(&mut buf).unwrap(); + buf.to_vec() + } + + fn encode_error(&self, error: &str) -> Vec { + use prost::Message; + let error_msg = proto::PluginMessage { + message: Some(proto::plugin_message::Message::ErrorResponse( + error.to_string(), + )), + }; + let mut buf = bytes::BytesMut::with_capacity(error_msg.encoded_len()); + error_msg.encode(&mut buf).unwrap(); + buf.to_vec() + } +} + +#[macro_export] +macro_rules! plugin_action { + ($registry:expr, $name:expr, $usage:expr, $description:expr, $examples:expr, $handler:expr) => { + $crate::define_action!($registry, $name, $usage, $description, $examples, $handler); + }; +} + +#[macro_export] +macro_rules! create_plugin { + ($plugin:ty) => { + impl Default for $plugin { + fn default() -> Self { + Self::new() + } + } + + impl $crate::ConfigurablePlugin for $plugin { + type Config = <$plugin as std::ops::Deref>::Target; + + fn config(&self) -> &Self::Config { + self.base.config() + } + + fn config_mut(&mut self) -> &mut Self::Config { + self.base.config_mut() + } + } + + impl $crate::ProtobufHandler for $plugin {} + + lla_plugin_interface::declare_plugin!($plugin); + }; +} diff --git a/lla_plugin_utils/src/ui/components.rs b/lla_plugin_utils/src/ui/components.rs new file mode 100644 index 0000000..6655b24 --- /dev/null +++ b/lla_plugin_utils/src/ui/components.rs @@ -0,0 +1,264 @@ +use super::{TextBlock, TextStyle}; +use indicatif::{ProgressBar, ProgressStyle}; +use std::time::Duration; + +pub struct Spinner { + progress_bar: ProgressBar, +} + +impl Spinner { + pub fn new() -> Self { + let pb = ProgressBar::new_spinner(); + pb.set_style( + ProgressStyle::default_spinner() + .template("{spinner:.green} {msg}") + .unwrap() + .tick_chars("⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"), + ); + pb.enable_steady_tick(Duration::from_millis(80)); + Self { progress_bar: pb } + } + + pub fn set_status(&self, status: impl Into) { + self.progress_bar.set_message(status.into()); + } + + pub fn finish(&self) { + self.progress_bar.finish_and_clear(); + } + + pub fn finish_with_message(&self, msg: impl Into) { + self.progress_bar.finish_with_message(msg.into()); + } +} + +impl Drop for Spinner { + fn drop(&mut self) { + self.finish(); + } +} + +pub struct HelpFormatter { + title: String, + sections: Vec<(String, Vec<(String, String, Vec)>)>, +} + +impl HelpFormatter { + pub fn new(title: impl Into) -> Self { + Self { + title: title.into(), + sections: Vec::new(), + } + } + + pub fn add_section(&mut self, title: impl Into) -> &mut Self { + self.sections.push((title.into(), Vec::new())); + self + } + + pub fn add_command( + &mut self, + command: impl Into, + description: impl Into, + examples: Vec, + ) -> &mut Self { + if let Some((_, commands)) = self.sections.last_mut() { + commands.push((command.into(), description.into(), examples)); + } + self + } + + pub fn render(&self, colors: &std::collections::HashMap) -> String { + let mut output = String::new(); + let default_color = "white".to_string(); + let title_color = colors.get("success").unwrap_or(&default_color); + let section_color = colors.get("info").unwrap_or(&default_color); + let cmd_color = colors.get("name").unwrap_or(&default_color); + + output.push_str( + &TextBlock::new(&self.title) + .color(title_color) + .style(TextStyle::Bold) + .build(), + ); + output.push_str("\n\n"); + + for (section_title, commands) in &self.sections { + output.push_str(&TextBlock::new(section_title).color(section_color).build()); + output.push_str("\n\n"); + + for (command, description, examples) in commands { + output.push_str(" "); + output.push_str( + &TextBlock::new(command) + .color(cmd_color) + .style(TextStyle::Bold) + .build(), + ); + output.push_str("\n "); + output.push_str(description); + output.push_str("\n"); + + if !examples.is_empty() { + output.push_str("\n Examples:\n"); + for example in examples { + output.push_str(" • "); + output.push_str(example); + output.push_str("\n"); + } + } + output.push_str("\n"); + } + } + + output + } +} + +pub struct KeyValue { + key: String, + value: String, + key_color: Option, + value_color: Option, + key_width: Option, +} + +impl KeyValue { + pub fn new(key: impl Into, value: impl Into) -> Self { + Self { + key: key.into(), + value: value.into(), + key_color: None, + value_color: None, + key_width: None, + } + } + + pub fn key_color(mut self, color: impl Into) -> Self { + self.key_color = Some(color.into()); + self + } + + pub fn value_color(mut self, color: impl Into) -> Self { + self.value_color = Some(color.into()); + self + } + + pub fn key_width(mut self, width: usize) -> Self { + self.key_width = Some(width); + self + } + + pub fn render(&self) -> String { + let key = if let Some(color) = &self.key_color { + TextBlock::new(&self.key).color(color).build() + } else { + self.key.clone() + }; + + let value = if let Some(color) = &self.value_color { + TextBlock::new(&self.value).color(color).build() + } else { + self.value.clone() + }; + + if let Some(width) = self.key_width { + format!("{:width$} {}", key, value, width = width) + } else { + format!("{} {}", key, value) + } + } +} + +pub struct List { + items: Vec, +} + +impl List { + pub fn new() -> Self { + Self { items: Vec::new() } + } + + pub fn add_item(&mut self, item: impl Into) -> &mut Self { + self.items.push(item.into()); + self + } + + pub fn style(self, _style: BoxStyle) -> Self { + self + } + + pub fn key_width(self, _width: usize) -> Self { + self + } + + pub fn render(&self) -> String { + let mut output = String::new(); + output.push('┌'); + output.push('─'); + output.push('\n'); + + for item in &self.items { + output.push('│'); + output.push(' '); + output.push_str(item); + output.push('\n'); + } + + output.push('└'); + output.push('─'); + output.push('\n'); + output + } +} + +pub enum BoxStyle { + Minimal, + Rounded, + Double, + Heavy, + Dashed, +} + +pub struct BoxComponent { + content: String, +} + +impl BoxComponent { + pub fn new(content: impl Into) -> Self { + Self { + content: content.into(), + } + } + + pub fn style(self, _style: BoxStyle) -> Self { + self + } + + pub fn width(self, _width: usize) -> Self { + self + } + + pub fn padding(self, _padding: usize) -> Self { + self + } + + pub fn render(&self) -> String { + let mut output = String::new(); + output.push('┌'); + output.push('─'); + output.push('\n'); + + for line in self.content.lines() { + output.push('│'); + output.push(' '); + output.push_str(line); + output.push('\n'); + } + + output.push('└'); + output.push('─'); + output.push('\n'); + output + } +} diff --git a/lla_plugin_utils/src/ui/mod.rs b/lla_plugin_utils/src/ui/mod.rs new file mode 100644 index 0000000..b0e619a --- /dev/null +++ b/lla_plugin_utils/src/ui/mod.rs @@ -0,0 +1,98 @@ +pub mod components; + +use std::fmt::Display; + +#[derive(Clone, Copy)] +pub enum TextStyle { + Normal, + Bold, + Italic, + Underline, +} + +pub struct TextBlock { + content: String, + color: Option, + style: TextStyle, +} + +impl TextBlock { + pub fn new(content: impl Into) -> Self { + Self { + content: content.into(), + color: None, + style: TextStyle::Normal, + } + } + + pub fn color(mut self, color: impl Into) -> Self { + self.color = Some(color.into()); + self + } + + pub fn style(mut self, style: TextStyle) -> Self { + self.style = style; + self + } + + pub fn build(&self) -> String { + let mut text = self.content.clone(); + + if let Some(color) = &self.color { + text = match color.as_str() { + "black" => text.black().to_string(), + "red" => text.red().to_string(), + "green" => text.green().to_string(), + "yellow" => text.yellow().to_string(), + "blue" => text.blue().to_string(), + "magenta" => text.magenta().to_string(), + "cyan" => text.cyan().to_string(), + "white" => text.white().to_string(), + "bright_black" => text.bright_black().to_string(), + "bright_red" => text.bright_red().to_string(), + "bright_green" => text.bright_green().to_string(), + "bright_yellow" => text.bright_yellow().to_string(), + "bright_blue" => text.bright_blue().to_string(), + "bright_magenta" => text.bright_magenta().to_string(), + "bright_cyan" => text.bright_cyan().to_string(), + "bright_white" => text.bright_white().to_string(), + "dimmed" => text.dimmed().to_string(), + _ => text, + }; + } + + match self.style { + TextStyle::Normal => text, + TextStyle::Bold => text.bold().to_string(), + TextStyle::Italic => text.italic().to_string(), + TextStyle::Underline => text.underline().to_string(), + } + } +} + +impl Display for TextBlock { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.build()) + } +} + +use colored::Colorize; + +pub fn format_size(size: u64) -> String { + const KB: u64 = 1024; + const MB: u64 = KB * 1024; + const GB: u64 = MB * 1024; + const TB: u64 = GB * 1024; + + if size >= TB { + format!("{:.2} TB", size as f64 / TB as f64) + } else if size >= GB { + format!("{:.2} GB", size as f64 / GB as f64) + } else if size >= MB { + format!("{:.2} MB", size as f64 / MB as f64) + } else if size >= KB { + format!("{:.2} KB", size as f64 / KB as f64) + } else { + format!("{} B", size) + } +} From 0ac82e3301629c9223dfa1b132d6ee118878e0ac Mon Sep 17 00:00:00 2001 From: Mohamed Achaq Date: Fri, 27 Dec 2024 20:45:49 +0100 Subject: [PATCH 08/43] feat: add dirs_meta plugin for directory metadata analysis - Introduced a new plugin, `dirs_meta`, that analyzes directories and provides metadata such as file counts, sizes, and modification times. - Implemented intelligent caching for improved performance and parallel processing for faster analysis. - Updated workspace configuration to include the new plugin and removed the deprecated `dirs` plugin. - Added comprehensive README documentation for usage and display formats. - Updated Cargo.lock and Cargo.toml files to reflect new dependencies and plugin structure. --- Cargo.lock | 59 ++-- Cargo.toml | 2 +- plugins/dirs/Cargo.toml | 18 -- plugins/dirs/src/lib.rs | 255 --------------- plugins/{dirs => dirs_meta}/.gitignore | 0 plugins/{dirs => dirs_meta}/Cargo.lock | 0 plugins/dirs_meta/Cargo.toml | 20 ++ plugins/{dirs => dirs_meta}/README.md | 0 plugins/dirs_meta/src/lib.rs | 432 +++++++++++++++++++++++++ 9 files changed, 492 insertions(+), 294 deletions(-) delete mode 100644 plugins/dirs/Cargo.toml delete mode 100644 plugins/dirs/src/lib.rs rename plugins/{dirs => dirs_meta}/.gitignore (100%) rename plugins/{dirs => dirs_meta}/Cargo.lock (100%) create mode 100644 plugins/dirs_meta/Cargo.toml rename plugins/{dirs => dirs_meta}/README.md (100%) create mode 100644 plugins/dirs_meta/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 77aac35..a22904e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -110,7 +110,7 @@ version = "0.3.0" dependencies = [ "bytes", "colored", - "dirs 5.0.1", + "dirs", "lla_plugin_interface", "prost", "serde", @@ -185,7 +185,7 @@ version = "0.3.0" dependencies = [ "bytes", "colored", - "dirs 5.0.1", + "dirs", "lla_plugin_interface", "prost", "serde", @@ -199,7 +199,7 @@ dependencies = [ "base64", "bytes", "colored", - "dirs 5.0.1", + "dirs", "lla_plugin_interface", "prost", "ring", @@ -350,20 +350,6 @@ dependencies = [ "crypto-common", ] -[[package]] -name = "dirs" -version = "0.3.0" -dependencies = [ - "bytes", - "colored", - "lazy_static", - "lla_plugin_interface", - "parking_lot", - "prost", - "rayon", - "walkdir", -] - [[package]] name = "dirs" version = "5.0.1" @@ -385,6 +371,22 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "dirs_meta" +version = "0.3.1" +dependencies = [ + "bytes", + "colored", + "lazy_static", + "lla_plugin_interface", + "lla_plugin_utils", + "parking_lot", + "prost", + "rayon", + "serde", + "walkdir", +] + [[package]] name = "duplicate_file_detector" version = "0.3.0" @@ -461,7 +463,7 @@ version = "0.3.0" dependencies = [ "bytes", "colored", - "dirs 5.0.1", + "dirs", "lla_plugin_interface", "prost", ] @@ -663,7 +665,7 @@ version = "0.3.0" dependencies = [ "bytes", "colored", - "dirs 5.0.1", + "dirs", "lla_plugin_interface", "prost", "regex", @@ -733,7 +735,7 @@ dependencies = [ "crossterm", "dashmap", "dialoguer", - "dirs 5.0.1", + "dirs", "glob", "ignore", "indicatif", @@ -766,6 +768,23 @@ dependencies = [ "serde", ] +[[package]] +name = "lla_plugin_utils" +version = "0.3.7" +dependencies = [ + "bytes", + "chrono", + "colored", + "console", + "dirs", + "indicatif", + "lla_plugin_interface", + "prost", + "serde", + "toml", + "users", +] + [[package]] name = "lock_api" version = "0.4.12" diff --git a/Cargo.toml b/Cargo.toml index 1900710..b7f3271 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] resolver = "2" -members = ["lla", "lla_plugin_interface", "plugins/*"] +members = ["lla", "lla_plugin_interface", "lla_plugin_utils", "plugins/*"] [workspace.package] description = "Blazing Fast and highly customizable ls Replacement with Superpowers" diff --git a/plugins/dirs/Cargo.toml b/plugins/dirs/Cargo.toml deleted file mode 100644 index d0ba7b2..0000000 --- a/plugins/dirs/Cargo.toml +++ /dev/null @@ -1,18 +0,0 @@ -[package] -name = "dirs" -description = "Shows directories metadata" -version = "0.3.0" -edition = "2021" - -[dependencies] -colored = "2.0.0" -walkdir = "2.3.2" -lla_plugin_interface = { path = "../../lla_plugin_interface" } -rayon = "1.8" -lazy_static = "1.4" -parking_lot = "0.12" -prost = "0.12" -bytes = "1.5" - -[lib] -crate-type = ["cdylib"] diff --git a/plugins/dirs/src/lib.rs b/plugins/dirs/src/lib.rs deleted file mode 100644 index 92966be..0000000 --- a/plugins/dirs/src/lib.rs +++ /dev/null @@ -1,255 +0,0 @@ -use colored::Colorize; -use lazy_static::lazy_static; -use lla_plugin_interface::{ - proto::{self, plugin_message::Message}, - Plugin, -}; -use parking_lot::RwLock; -use prost::Message as _; -use rayon::prelude::*; -use std::collections::HashMap; -use std::path::Path; -use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering}; -use std::time::SystemTime; -use walkdir::WalkDir; - -type DirStats = (usize, usize, u64); -type CacheEntry = (SystemTime, DirStats); -type DirCache = HashMap; - -lazy_static! { - static ref CACHE: RwLock = RwLock::new(HashMap::new()); -} - -pub struct DirsPlugin; - -impl DirsPlugin { - pub fn new() -> Self { - DirsPlugin - } - - fn encode_error(&self, error: &str) -> Vec { - use prost::Message; - let error_msg = lla_plugin_interface::proto::PluginMessage { - message: Some( - lla_plugin_interface::proto::plugin_message::Message::ErrorResponse( - error.to_string(), - ), - ), - }; - let mut buf = bytes::BytesMut::with_capacity(error_msg.encoded_len()); - error_msg.encode(&mut buf).unwrap(); - buf.to_vec() - } - - fn analyze_directory(path: &Path) -> Option<(usize, usize, u64)> { - let path_str = path.to_string_lossy().to_string(); - if let Ok(metadata) = path.metadata() { - if let Ok(modified_time) = metadata.modified() { - let cache = CACHE.read(); - if let Some((cached_time, stats)) = cache.get(&path_str) { - if *cached_time >= modified_time { - return Some(*stats); - } - } - } - } - - let file_count = AtomicUsize::new(0); - let dir_count = AtomicUsize::new(0); - let total_size = AtomicU64::new(0); - - WalkDir::new(path) - .into_iter() - .par_bridge() - .filter_map(|e| e.ok()) - .for_each(|entry| { - if let Ok(metadata) = entry.metadata() { - if metadata.is_file() { - file_count.fetch_add(1, Ordering::Relaxed); - total_size.fetch_add(metadata.len(), Ordering::Relaxed); - } else if metadata.is_dir() { - dir_count.fetch_add(1, Ordering::Relaxed); - } - } - }); - - let result = ( - file_count.load(Ordering::Relaxed), - dir_count.load(Ordering::Relaxed), - total_size.load(Ordering::Relaxed), - ); - - if let Ok(metadata) = path.metadata() { - if let Ok(modified_time) = metadata.modified() { - let mut cache = CACHE.write(); - cache.insert(path_str, (modified_time, result)); - } - } - - Some(result) - } - - fn format_size(size: u64) -> String { - const KB: u64 = 1024; - const MB: u64 = KB * 1024; - const GB: u64 = MB * 1024; - - if size >= GB { - format!("{:.2} GB", size as f64 / GB as f64) - } else if size >= MB { - format!("{:.2} MB", size as f64 / MB as f64) - } else if size >= KB { - format!("{:.2} KB", size as f64 / KB as f64) - } else { - format!("{} B", size) - } - } -} - -impl Default for DirsPlugin { - fn default() -> Self { - Self::new() - } -} - -impl Plugin for DirsPlugin { - fn handle_raw_request(&mut self, request: &[u8]) -> Vec { - let proto_msg = match proto::PluginMessage::decode(request) { - Ok(msg) => msg, - Err(e) => { - let error_msg = proto::PluginMessage { - message: Some(Message::ErrorResponse(format!( - "Failed to decode request: {}", - e - ))), - }; - let mut buf = bytes::BytesMut::with_capacity(error_msg.encoded_len()); - error_msg.encode(&mut buf).unwrap(); - return buf.to_vec(); - } - }; - - let response_msg = match proto_msg.message { - Some(Message::GetName(_)) => Message::NameResponse(env!("CARGO_PKG_NAME").to_string()), - Some(Message::GetVersion(_)) => { - Message::VersionResponse(env!("CARGO_PKG_VERSION").to_string()) - } - Some(Message::GetDescription(_)) => { - Message::DescriptionResponse(env!("CARGO_PKG_DESCRIPTION").to_string()) - } - Some(Message::GetSupportedFormats(_)) => { - Message::FormatsResponse(proto::SupportedFormatsResponse { - formats: vec!["default".to_string(), "long".to_string()], - }) - } - Some(Message::Decorate(entry)) => { - let mut entry = match lla_plugin_interface::DecoratedEntry::try_from(entry.clone()) - { - Ok(e) => e, - Err(e) => { - return self.encode_error(&format!("Failed to convert entry: {}", e)); - } - }; - - if entry.metadata.is_dir { - if let Some((file_count, dir_count, total_size)) = - Self::analyze_directory(&entry.path) - { - entry - .custom_fields - .insert("dir_file_count".to_string(), file_count.to_string()); - entry - .custom_fields - .insert("dir_subdir_count".to_string(), dir_count.to_string()); - entry - .custom_fields - .insert("dir_total_size".to_string(), Self::format_size(total_size)); - } - } - Message::DecoratedResponse(entry.into()) - } - Some(Message::FormatField(req)) => { - let entry = match req.entry { - Some(e) => match lla_plugin_interface::DecoratedEntry::try_from(e) { - Ok(entry) => entry, - Err(e) => { - return self.encode_error(&format!("Failed to convert entry: {}", e)); - } - }, - None => return self.encode_error("Missing entry in format field request"), - }; - - if !entry.metadata.is_dir { - Message::FieldResponse(proto::FormattedFieldResponse { field: None }) - } else { - let formatted = match req.format.as_str() { - "long" => { - if let (Some(file_count), Some(dir_count), Some(total_size)) = ( - entry.custom_fields.get("dir_file_count"), - entry.custom_fields.get("dir_subdir_count"), - entry.custom_fields.get("dir_total_size"), - ) { - let modified = entry - .path - .metadata() - .ok() - .and_then(|m| m.modified().ok()) - .and_then(|t| t.elapsed().ok()) - .map(|e| { - let secs = e.as_secs(); - if secs < 60 { - format!("{} secs ago", secs) - } else if secs < 3600 { - format!("{} mins ago", secs / 60) - } else if secs < 86400 { - format!("{} hours ago", secs / 3600) - } else { - format!("{} days ago", secs / 86400) - } - }) - .unwrap_or_else(|| "unknown time".to_string()); - - Some(format!( - "{} files, {} dirs, {} (modified {})", - file_count.bright_cyan(), - dir_count.bright_green(), - total_size.bright_yellow(), - modified.bright_magenta() - )) - } else { - None - } - } - "default" => { - if let (Some(file_count), Some(total_size)) = ( - entry.custom_fields.get("dir_file_count"), - entry.custom_fields.get("dir_total_size"), - ) { - Some(format!("{} files, {}", file_count, total_size)) - } else { - None - } - } - _ => None, - }; - Message::FieldResponse(proto::FormattedFieldResponse { field: formatted }) - } - } - Some(Message::Action(_req)) => Message::ActionResponse(proto::ActionResponse { - success: true, - error: None, - }), - _ => Message::ErrorResponse("Invalid request type".to_string()), - }; - - let response = proto::PluginMessage { - message: Some(response_msg), - }; - let mut buf = bytes::BytesMut::with_capacity(response.encoded_len()); - response.encode(&mut buf).unwrap(); - buf.to_vec() - } -} - -lla_plugin_interface::declare_plugin!(DirsPlugin); diff --git a/plugins/dirs/.gitignore b/plugins/dirs_meta/.gitignore similarity index 100% rename from plugins/dirs/.gitignore rename to plugins/dirs_meta/.gitignore diff --git a/plugins/dirs/Cargo.lock b/plugins/dirs_meta/Cargo.lock similarity index 100% rename from plugins/dirs/Cargo.lock rename to plugins/dirs_meta/Cargo.lock diff --git a/plugins/dirs_meta/Cargo.toml b/plugins/dirs_meta/Cargo.toml new file mode 100644 index 0000000..d0d9c76 --- /dev/null +++ b/plugins/dirs_meta/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "dirs_meta" +description = "Analyzes directories and shows metadata" +version = "0.3.1" +edition = "2021" + +[dependencies] +colored = { workspace = true } +walkdir = { workspace = true } +lla_plugin_interface = { path = "../../lla_plugin_interface" } +lla_plugin_utils = { path = "../../lla_plugin_utils" } +rayon = { workspace = true } +lazy_static = "1.4" +parking_lot = { workspace = true } +prost = { workspace = true } +bytes = "1.5" +serde = { workspace = true, features = ["derive"] } + +[lib] +crate-type = ["cdylib"] diff --git a/plugins/dirs/README.md b/plugins/dirs_meta/README.md similarity index 100% rename from plugins/dirs/README.md rename to plugins/dirs_meta/README.md diff --git a/plugins/dirs_meta/src/lib.rs b/plugins/dirs_meta/src/lib.rs new file mode 100644 index 0000000..15ab783 --- /dev/null +++ b/plugins/dirs_meta/src/lib.rs @@ -0,0 +1,432 @@ +use lazy_static::lazy_static; +use lla_plugin_interface::{DecoratedEntry, Plugin, PluginRequest, PluginResponse}; +use lla_plugin_utils::{ + config::PluginConfig, + ui::{ + components::{BoxComponent, BoxStyle, HelpFormatter, KeyValue, List, Spinner}, + format_size, TextBlock, + }, + ActionRegistry, BasePlugin, ConfigurablePlugin, ProtobufHandler, +}; +use parking_lot::RwLock; +use rayon::prelude::*; +use serde::{Deserialize, Serialize}; +use std::{ + collections::HashMap, + path::Path, + sync::atomic::{AtomicU64, AtomicUsize, Ordering}, + time::SystemTime, +}; +use walkdir::WalkDir; + +type DirStats = (usize, usize, u64); +type CacheEntry = (SystemTime, DirStats); +type DirCache = HashMap; + +lazy_static! { + static ref CACHE: RwLock = RwLock::new(HashMap::new()); + static ref SPINNER: RwLock = RwLock::new(Spinner::new()); + static ref ACTION_REGISTRY: RwLock = RwLock::new({ + let mut registry = ActionRegistry::new(); + + lla_plugin_utils::define_action!( + registry, + "clear-cache", + "clear-cache", + "Clear the directory analysis cache", + vec!["lla plugin --name dirs_meta --action clear-cache"], + |_| { + let spinner = SPINNER.write(); + spinner.set_status("Clearing cache...".to_string()); + CACHE.write().clear(); + spinner.finish(); + drop(spinner); + println!( + "{}", + BoxComponent::new( + TextBlock::new("Cache cleared successfully") + .color("bright_green") + .build() + ) + .style(BoxStyle::Minimal) + .padding(1) + .render() + ); + Ok(()) + } + ); + lla_plugin_utils::define_action!( + registry, + "stats", + "stats ", + "Show detailed statistics for a directory", + vec!["lla plugin --name dirs_meta --action stats --args \"/path/to/dir\""], + |args| DirsPlugin::stats_action(args) + ); + + lla_plugin_utils::define_action!( + registry, + "help", + "help", + "Show help information", + vec!["lla plugin --name dirs_meta --action help"], + |_| { + let mut help = HelpFormatter::new("Directory Metadata Plugin".to_string()); + help.add_section("Description".to_string()) + .add_command( + "".to_string(), + "Analyzes directories to provide information about their contents, including file count, subdirectory count, and total size.".to_string(), + vec![], + ); + + help.add_section("Actions".to_string()) + .add_command( + "clear-cache".to_string(), + "Clear the directory analysis cache".to_string(), + vec!["lla plugin --name dirs_meta --action clear-cache".to_string()], + ) + .add_command( + "stats".to_string(), + "Show detailed statistics for a directory".to_string(), + vec![ + "lla plugin --name dirs_meta --action stats --args \"/path/to/dir\"" + .to_string(), + ], + ) + .add_command( + "help".to_string(), + "Show this help information".to_string(), + vec!["lla plugin --name dirs_meta --action help".to_string()], + ); + + help.add_section("Formats".to_string()) + .add_command( + "default".to_string(), + "Show basic directory information (file count and total size)".to_string(), + vec![], + ) + .add_command( + "long".to_string(), + "Show detailed directory information including subdirectories and modification time".to_string(), + vec![], + ); + + println!( + "{}", + BoxComponent::new(help.render(&DirsConfig::default().colors)) + .style(BoxStyle::Minimal) + .padding(2) + .render() + ); + Ok(()) + } + ); + + registry + }); +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DirsConfig { + #[serde(default = "default_cache_size")] + cache_size: usize, + #[serde(default = "default_colors")] + colors: HashMap, +} + +fn default_cache_size() -> usize { + 1000 +} + +fn default_colors() -> HashMap { + let mut colors = HashMap::new(); + colors.insert("files".to_string(), "bright_cyan".to_string()); + colors.insert("dirs".to_string(), "bright_green".to_string()); + colors.insert("size".to_string(), "bright_yellow".to_string()); + colors.insert("time".to_string(), "bright_magenta".to_string()); + colors.insert("success".to_string(), "bright_green".to_string()); + colors.insert("info".to_string(), "bright_blue".to_string()); + colors.insert("name".to_string(), "bright_yellow".to_string()); + colors +} + +impl Default for DirsConfig { + fn default() -> Self { + Self { + cache_size: default_cache_size(), + colors: default_colors(), + } + } +} + +impl PluginConfig for DirsConfig {} + +pub struct DirsPlugin { + base: BasePlugin, +} + +impl DirsPlugin { + fn analyze_directory(path: &Path) -> Option<(usize, usize, u64)> { + let path_str = path.to_string_lossy().to_string(); + + if let Ok(metadata) = path.metadata() { + if let Ok(modified_time) = metadata.modified() { + let cache = CACHE.read(); + if let Some((cached_time, stats)) = cache.get(&path_str) { + if *cached_time >= modified_time { + return Some(*stats); + } + } + } + } + + let file_count = AtomicUsize::new(0); + let dir_count = AtomicUsize::new(0); + let total_size = AtomicU64::new(0); + + let entries: Vec<_> = WalkDir::new(path) + .into_iter() + .filter_map(|e| e.ok()) + .collect(); + + entries.into_par_iter().for_each(|entry| { + if let Ok(metadata) = entry.metadata() { + if metadata.is_file() { + file_count.fetch_add(1, Ordering::Relaxed); + total_size.fetch_add(metadata.len(), Ordering::Relaxed); + } else if metadata.is_dir() { + dir_count.fetch_add(1, Ordering::Relaxed); + } + } + }); + + let result = ( + file_count.load(Ordering::Relaxed), + dir_count.load(Ordering::Relaxed), + total_size.load(Ordering::Relaxed), + ); + + if let Ok(metadata) = path.metadata() { + if let Ok(modified_time) = metadata.modified() { + let mut cache = CACHE.write(); + if cache.len() >= DirsConfig::default().cache_size { + cache.clear(); + } + cache.insert(path_str.clone(), (modified_time, result)); + } + } + + Some(result) + } + + fn stats_action(args: &[String]) -> Result<(), String> { + if args.is_empty() { + return Err("Path argument is required".to_string()); + } + let path = Path::new(&args[0]); + if !path.is_dir() { + return Err("Path must be a directory".to_string()); + } + + let spinner = SPINNER.write(); + spinner.set_status("Analyzing directory...".to_string()); + + let result = Self::analyze_directory(path); + + spinner.finish(); + drop(spinner); + + if let Some((files, dirs, size)) = result { + let colors = DirsConfig::default().colors; + let mut list = List::new().style(BoxStyle::Minimal).key_width(12); + + list.add_item( + KeyValue::new("Files", files.to_string()) + .key_color(colors.get("files").unwrap_or(&"white".to_string())) + .value_color(colors.get("files").unwrap_or(&"white".to_string())) + .key_width(12) + .render(), + ); + + list.add_item( + KeyValue::new("Directories", dirs.to_string()) + .key_color(colors.get("dirs").unwrap_or(&"white".to_string())) + .value_color(colors.get("dirs").unwrap_or(&"white".to_string())) + .key_width(12) + .render(), + ); + + list.add_item( + KeyValue::new("Total Size", format_size(size)) + .key_color(colors.get("size").unwrap_or(&"white".to_string())) + .value_color(colors.get("size").unwrap_or(&"white".to_string())) + .key_width(12) + .render(), + ); + + println!("{}", list.render()); + Ok(()) + } else { + Err("Failed to analyze directory".to_string()) + } + } + + fn format_directory_info(&self, entry: &DecoratedEntry, format: &str) -> Option { + if !entry.metadata.is_dir { + return None; + } + + let (file_count, dir_count, total_size) = match ( + entry.custom_fields.get("dir_file_count"), + entry.custom_fields.get("dir_subdir_count"), + entry.custom_fields.get("dir_total_size"), + ) { + (Some(f), Some(d), Some(s)) => (f, d, s), + _ => return None, + }; + + let colors = &self.base.config().colors; + match format { + "long" => { + let modified = entry + .path + .metadata() + .ok() + .and_then(|m| m.modified().ok()) + .and_then(|t| t.elapsed().ok()) + .map(|e| { + let secs = e.as_secs(); + if secs < 60 { + format!("{} secs ago", secs) + } else if secs < 3600 { + format!("{} mins ago", secs / 60) + } else if secs < 86400 { + format!("{} hours ago", secs / 3600) + } else { + format!("{} days ago", secs / 86400) + } + }) + .unwrap_or_else(|| "unknown time".to_string()); + + let mut list = List::new().style(BoxStyle::Minimal).key_width(12); + + list.add_item( + KeyValue::new("Files", file_count) + .key_color(colors.get("files").unwrap_or(&"white".to_string())) + .value_color(colors.get("files").unwrap_or(&"white".to_string())) + .key_width(12) + .render(), + ); + + list.add_item( + KeyValue::new("Directories", dir_count) + .key_color(colors.get("dirs").unwrap_or(&"white".to_string())) + .value_color(colors.get("dirs").unwrap_or(&"white".to_string())) + .key_width(12) + .render(), + ); + + list.add_item( + KeyValue::new("Total Size", total_size) + .key_color(colors.get("size").unwrap_or(&"white".to_string())) + .value_color(colors.get("size").unwrap_or(&"white".to_string())) + .key_width(12) + .render(), + ); + + list.add_item( + KeyValue::new("Modified", modified) + .key_color(colors.get("time").unwrap_or(&"white".to_string())) + .value_color(colors.get("time").unwrap_or(&"white".to_string())) + .key_width(12) + .render(), + ); + + Some(format!("\n{}", list.render())) + } + "default" => Some(format!( + "\n{}\n", + TextBlock::new(format!("{} files, {}", file_count, total_size)) + .color(colors.get("info").unwrap_or(&"white".to_string())) + .build() + )), + _ => None, + } + } +} + +impl Plugin for DirsPlugin { + fn handle_raw_request(&mut self, request: &[u8]) -> Vec { + match self.decode_request(request) { + Ok(request) => { + let response = match request { + PluginRequest::GetName => { + PluginResponse::Name(env!("CARGO_PKG_NAME").to_string()) + } + PluginRequest::GetVersion => { + PluginResponse::Version(env!("CARGO_PKG_VERSION").to_string()) + } + PluginRequest::GetDescription => { + PluginResponse::Description(env!("CARGO_PKG_DESCRIPTION").to_string()) + } + PluginRequest::GetSupportedFormats => PluginResponse::SupportedFormats(vec![ + "default".to_string(), + "long".to_string(), + ]), + PluginRequest::Decorate(mut entry) => { + if entry.metadata.is_dir { + let result = Self::analyze_directory(&entry.path); + + if let Some((file_count, dir_count, total_size)) = result { + entry + .custom_fields + .insert("dir_file_count".to_string(), file_count.to_string()); + entry + .custom_fields + .insert("dir_subdir_count".to_string(), dir_count.to_string()); + entry + .custom_fields + .insert("dir_total_size".to_string(), format_size(total_size)); + } + } + PluginResponse::Decorated(entry) + } + PluginRequest::FormatField(entry, format) => { + let field = self.format_directory_info(&entry, &format); + PluginResponse::FormattedField(field) + } + PluginRequest::PerformAction(action, args) => { + let result = ACTION_REGISTRY.read().handle(&action, &args); + PluginResponse::ActionResult(result) + } + }; + self.encode_response(response) + } + Err(e) => self.encode_error(&e), + } + } +} + +impl Default for DirsPlugin { + fn default() -> Self { + Self { + base: BasePlugin::new(), + } + } +} + +impl ConfigurablePlugin for DirsPlugin { + type Config = DirsConfig; + + fn config(&self) -> &Self::Config { + self.base.config() + } + + fn config_mut(&mut self) -> &mut Self::Config { + self.base.config_mut() + } +} + +impl ProtobufHandler for DirsPlugin {} + +lla_plugin_interface::declare_plugin!(DirsPlugin); From 9e8e1daa13c47d46ddf0bb53996b0d94c33376ae Mon Sep 17 00:00:00 2001 From: Mohamed Achaq Date: Fri, 27 Dec 2024 20:56:13 +0100 Subject: [PATCH 09/43] feat: add publishing logic for lla_plugin_utils in release workflow - Implemented checks to publish the lla_plugin_utils package if the specified version is not already published. - Added echo statements to inform about the publishing status of lla_plugin_utils. - Included a sleep command to allow for crates.io indexing after publishing. --- .github/workflows/release.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 53fb2ca..903ce3f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -169,6 +169,17 @@ jobs: echo "lla_plugin_interface v$VERSION already published, skipping..." fi + # Check and publish lla_plugin_utils + PUBLISHED_VERSION=$(cargo search lla_plugin_utils --limit 1 | awk -F '"' '{print $2}') + if [ "$PUBLISHED_VERSION" != "$VERSION" ]; then + echo "Publishing lla_plugin_utils v$VERSION" + cargo publish -p lla_plugin_utils + # Wait for crates.io indexing + sleep 30 + else + echo "lla_plugin_utils v$VERSION already published, skipping..." + fi + echo "Publishing lla v$VERSION" cargo publish -p lla From 6cb9675dc7f88e513f65f2ba37177495cca82751 Mon Sep 17 00:00:00 2001 From: Mohamed Achaq Date: Fri, 27 Dec 2024 21:00:42 +0100 Subject: [PATCH 10/43] feat: enhance file metadata plugin with new dependencies and configuration - Added new dependencies: `lazy_static`, `parking_lot`, and `serde` to improve plugin functionality and configuration management. - Updated `Cargo.toml` for the `file_meta` plugin to include `lla_plugin_utils` and `serde` with derive features. - Refactored the `FileMetadataPlugin` to utilize `lazy_static` for action registry and `parking_lot` for improved concurrency. - Introduced `FileMetaConfig` struct for managing plugin configuration with default color settings for metadata display. - Enhanced the `handle_raw_request` method to support new plugin actions and improved response formatting. --- Cargo.lock | 4 + plugins/file_meta/Cargo.toml | 7 +- plugins/file_meta/src/lib.rs | 413 ++++++++++++++++++++++------------- 3 files changed, 273 insertions(+), 151 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a22904e..6eceb31 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -453,8 +453,12 @@ dependencies = [ "bytes", "chrono", "colored", + "lazy_static", "lla_plugin_interface", + "lla_plugin_utils", + "parking_lot", "prost", + "serde", ] [[package]] diff --git a/plugins/file_meta/Cargo.toml b/plugins/file_meta/Cargo.toml index 43c6e3f..dabac29 100644 --- a/plugins/file_meta/Cargo.toml +++ b/plugins/file_meta/Cargo.toml @@ -6,9 +6,14 @@ edition = "2021" [dependencies] lla_plugin_interface = { path = "../../lla_plugin_interface" } -chrono = '0.4.38' +lla_plugin_utils = { path = "../../lla_plugin_utils" } +chrono = "0.4.38" colored = "2.0" prost = "0.12" bytes = "1.5" +serde = { version = "1.0", features = ["derive"] } +lazy_static = "1.4" +parking_lot = "0.12" + [lib] crate-type = ["cdylib"] diff --git a/plugins/file_meta/src/lib.rs b/plugins/file_meta/src/lib.rs index 7916201..e7954e5 100644 --- a/plugins/file_meta/src/lib.rs +++ b/plugins/file_meta/src/lib.rs @@ -1,173 +1,272 @@ -use colored::*; -use lla_plugin_interface::{ - proto::{self, plugin_message::Message}, - Plugin, +use lazy_static::lazy_static; +use lla_plugin_interface::{DecoratedEntry, Plugin, PluginRequest, PluginResponse}; +use lla_plugin_utils::{ + config::PluginConfig, + ui::{ + components::{BoxComponent, BoxStyle, HelpFormatter, KeyValue, List}, + format_size, + }, + ActionRegistry, BasePlugin, ConfigurablePlugin, ProtobufHandler, }; -use prost::Message as _; -use std::time::SystemTime; +use parking_lot::RwLock; +use serde::{Deserialize, Serialize}; +use std::{collections::HashMap, time::SystemTime}; -pub struct FileMetadataPlugin; +lazy_static! { + static ref ACTION_REGISTRY: RwLock = RwLock::new({ + let mut registry = ActionRegistry::new(); -impl FileMetadataPlugin { - pub fn new() -> Self { - FileMetadataPlugin + lla_plugin_utils::define_action!( + registry, + "help", + "help", + "Show help information", + vec!["lla plugin --name file_meta --action help"], + |_| { + let mut help = HelpFormatter::new("File Metadata Plugin".to_string()); + help.add_section("Description".to_string()) + .add_command( + "".to_string(), + "Displays detailed file metadata including timestamps, ownership, size, and permissions.".to_string(), + vec![], + ); + + help.add_section("Actions".to_string()).add_command( + "help".to_string(), + "Show this help information".to_string(), + vec!["lla plugin --name file_meta --action help".to_string()], + ); + + help.add_section("Formats".to_string()) + .add_command( + "default".to_string(), + "Show basic file metadata".to_string(), + vec![], + ) + .add_command( + "long".to_string(), + "Show detailed file metadata including timestamps".to_string(), + vec![], + ); + + println!( + "{}", + BoxComponent::new(help.render(&FileMetaConfig::default().colors)) + .style(BoxStyle::Minimal) + .padding(2) + .render() + ); + Ok(()) + } + ); + + registry + }); +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FileMetaConfig { + #[serde(default = "default_colors")] + colors: HashMap, +} + +fn default_colors() -> HashMap { + let mut colors = HashMap::new(); + colors.insert("accessed".to_string(), "bright_blue".to_string()); + colors.insert("modified".to_string(), "bright_green".to_string()); + colors.insert("created".to_string(), "bright_yellow".to_string()); + colors.insert("ownership".to_string(), "bright_magenta".to_string()); + colors.insert("size".to_string(), "bright_cyan".to_string()); + colors.insert("permissions".to_string(), "bright_red".to_string()); + colors.insert("success".to_string(), "bright_green".to_string()); + colors.insert("info".to_string(), "bright_blue".to_string()); + colors.insert("name".to_string(), "bright_yellow".to_string()); + colors +} + +impl Default for FileMetaConfig { + fn default() -> Self { + Self { + colors: default_colors(), + } } +} + +impl PluginConfig for FileMetaConfig {} + +pub struct FileMetadataPlugin { + base: BasePlugin, +} - fn encode_error(&self, error: &str) -> Vec { - use prost::Message; - let error_msg = lla_plugin_interface::proto::PluginMessage { - message: Some( - lla_plugin_interface::proto::plugin_message::Message::ErrorResponse( - error.to_string(), - ), - ), - }; - let mut buf = bytes::BytesMut::with_capacity(error_msg.encoded_len()); - error_msg.encode(&mut buf).unwrap(); - buf.to_vec() +impl FileMetadataPlugin { + pub fn new() -> Self { + Self { + base: BasePlugin::new(), + } } fn format_timestamp(timestamp: SystemTime) -> String { let datetime: chrono::DateTime = timestamp.into(); datetime.format("%Y-%m-%d %H:%M:%S").to_string() } -} -impl Plugin for FileMetadataPlugin { - fn handle_raw_request(&mut self, request: &[u8]) -> Vec { - let proto_msg = match proto::PluginMessage::decode(request) { - Ok(msg) => msg, - Err(e) => { - let error_msg = proto::PluginMessage { - message: Some(Message::ErrorResponse(format!( - "Failed to decode request: {}", - e - ))), - }; - let mut buf = bytes::BytesMut::with_capacity(error_msg.encoded_len()); - error_msg.encode(&mut buf).unwrap(); - return buf.to_vec(); - } - }; + fn format_file_info(&self, entry: &DecoratedEntry, format: &str) -> Option { + let colors = &self.base.config().colors; - let response_msg = match proto_msg.message { - Some(Message::GetName(_)) => Message::NameResponse(env!("CARGO_PKG_NAME").to_string()), - Some(Message::GetVersion(_)) => { - Message::VersionResponse(env!("CARGO_PKG_VERSION").to_string()) - } - Some(Message::GetDescription(_)) => { - Message::DescriptionResponse(env!("CARGO_PKG_DESCRIPTION").to_string()) - } - Some(Message::GetSupportedFormats(_)) => { - Message::FormatsResponse(proto::SupportedFormatsResponse { - formats: vec!["default".to_string(), "long".to_string()], - }) - } - Some(Message::Decorate(entry)) => { - let mut entry = match lla_plugin_interface::DecoratedEntry::try_from(entry.clone()) - { - Ok(e) => e, - Err(e) => { - return self.encode_error(&format!("Failed to convert entry: {}", e)); - } - }; + match format { + "long" | "default" => { + match ( + entry.custom_fields.get("accessed"), + entry.custom_fields.get("modified"), + entry.custom_fields.get("created"), + entry.custom_fields.get("uid"), + entry.custom_fields.get("gid"), + entry.custom_fields.get("size"), + entry.custom_fields.get("permissions"), + ) { + ( + Some(accessed), + Some(modified), + Some(created), + Some(uid), + Some(gid), + Some(size), + Some(permissions), + ) => { + let mut list = List::new().style(BoxStyle::Minimal).key_width(12); - entry.custom_fields.insert( - "accessed".to_string(), - Self::format_timestamp( - SystemTime::UNIX_EPOCH - + std::time::Duration::from_secs(entry.metadata.accessed), - ), - ); - entry.custom_fields.insert( - "modified".to_string(), - Self::format_timestamp( - SystemTime::UNIX_EPOCH - + std::time::Duration::from_secs(entry.metadata.modified), - ), - ); - entry.custom_fields.insert( - "created".to_string(), - Self::format_timestamp( - SystemTime::UNIX_EPOCH - + std::time::Duration::from_secs(entry.metadata.created), - ), - ); - entry - .custom_fields - .insert("uid".to_string(), entry.metadata.uid.to_string()); - entry - .custom_fields - .insert("gid".to_string(), entry.metadata.gid.to_string()); - entry - .custom_fields - .insert("size".to_string(), entry.metadata.size.to_string()); - entry.custom_fields.insert( - "permissions".to_string(), - format!("{:o}", entry.metadata.permissions), - ); + list.add_item( + KeyValue::new("Accessed", accessed) + .key_color(colors.get("accessed").unwrap_or(&"white".to_string())) + .value_color(colors.get("accessed").unwrap_or(&"white".to_string())) + .key_width(12) + .render(), + ); - Message::DecoratedResponse(entry.into()) - } - Some(Message::FormatField(req)) => { - let entry = match req.entry { - Some(e) => match lla_plugin_interface::DecoratedEntry::try_from(e) { - Ok(entry) => entry, - Err(e) => { - return self.encode_error(&format!("Failed to convert entry: {}", e)); - } - }, - None => return self.encode_error("Missing entry in format field request"), - }; + list.add_item( + KeyValue::new("Modified", modified) + .key_color(colors.get("modified").unwrap_or(&"white".to_string())) + .value_color(colors.get("modified").unwrap_or(&"white".to_string())) + .key_width(12) + .render(), + ); + + list.add_item( + KeyValue::new("Created", created) + .key_color(colors.get("created").unwrap_or(&"white".to_string())) + .value_color(colors.get("created").unwrap_or(&"white".to_string())) + .key_width(12) + .render(), + ); - let formatted = match req.format.as_str() { - "long" | "default" => { - match ( - entry.custom_fields.get("accessed"), - entry.custom_fields.get("modified"), - entry.custom_fields.get("created"), - entry.custom_fields.get("uid"), - entry.custom_fields.get("gid"), - entry.custom_fields.get("size"), - entry.custom_fields.get("permissions"), - ) { - ( - Some(accessed), - Some(modified), - Some(created), - Some(uid), - Some(gid), - Some(size), - Some(permissions), - ) => Some(format!( - "\n{}\n{}\n{}\n{}\n{}\n{}", - format!("Accessed: {}", accessed.blue()), - format!("Modified: {}", modified.green()), - format!("Created: {}", created.yellow()), - format!("UID/GID: {}/{}", uid.magenta(), gid.magenta()), - format!("Size: {}", size.cyan()), - format!("Perms: {}", permissions.red()) - )), - _ => None, - } + list.add_item( + KeyValue::new("UID/GID", format!("{}/{}", uid, gid)) + .key_color(colors.get("ownership").unwrap_or(&"white".to_string())) + .value_color( + colors.get("ownership").unwrap_or(&"white".to_string()), + ) + .key_width(12) + .render(), + ); + + list.add_item( + KeyValue::new("Size", format_size(size.parse().unwrap_or(0))) + .key_color(colors.get("size").unwrap_or(&"white".to_string())) + .value_color(colors.get("size").unwrap_or(&"white".to_string())) + .key_width(12) + .render(), + ); + + list.add_item( + KeyValue::new( + "Permissions", + format!("{:o}", permissions.parse::().unwrap_or(0)), + ) + .key_color(colors.get("permissions").unwrap_or(&"white".to_string())) + .value_color(colors.get("permissions").unwrap_or(&"white".to_string())) + .key_width(12) + .render(), + ); + + Some(format!("\n{}", list.render())) } _ => None, + } + } + _ => None, + } + } +} + +impl Plugin for FileMetadataPlugin { + fn handle_raw_request(&mut self, request: &[u8]) -> Vec { + match self.decode_request(request) { + Ok(request) => { + let response = match request { + PluginRequest::GetName => { + PluginResponse::Name(env!("CARGO_PKG_NAME").to_string()) + } + PluginRequest::GetVersion => { + PluginResponse::Version(env!("CARGO_PKG_VERSION").to_string()) + } + PluginRequest::GetDescription => { + PluginResponse::Description(env!("CARGO_PKG_DESCRIPTION").to_string()) + } + PluginRequest::GetSupportedFormats => PluginResponse::SupportedFormats(vec![ + "default".to_string(), + "long".to_string(), + ]), + PluginRequest::Decorate(mut entry) => { + entry.custom_fields.insert( + "accessed".to_string(), + Self::format_timestamp( + SystemTime::UNIX_EPOCH + + std::time::Duration::from_secs(entry.metadata.accessed), + ), + ); + entry.custom_fields.insert( + "modified".to_string(), + Self::format_timestamp( + SystemTime::UNIX_EPOCH + + std::time::Duration::from_secs(entry.metadata.modified), + ), + ); + entry.custom_fields.insert( + "created".to_string(), + Self::format_timestamp( + SystemTime::UNIX_EPOCH + + std::time::Duration::from_secs(entry.metadata.created), + ), + ); + entry + .custom_fields + .insert("uid".to_string(), entry.metadata.uid.to_string()); + entry + .custom_fields + .insert("gid".to_string(), entry.metadata.gid.to_string()); + entry + .custom_fields + .insert("size".to_string(), entry.metadata.size.to_string()); + entry.custom_fields.insert( + "permissions".to_string(), + entry.metadata.permissions.to_string(), + ); + + PluginResponse::Decorated(entry) + } + PluginRequest::FormatField(entry, format) => { + let field = self.format_file_info(&entry, &format); + PluginResponse::FormattedField(field) + } + PluginRequest::PerformAction(action, args) => { + let result = ACTION_REGISTRY.read().handle(&action, &args); + PluginResponse::ActionResult(result) + } }; - Message::FieldResponse(proto::FormattedFieldResponse { field: formatted }) + self.encode_response(response) } - Some(Message::Action(_)) => Message::ActionResponse(proto::ActionResponse { - success: true, - error: None, - }), - _ => Message::ErrorResponse("Invalid request type".to_string()), - }; - - let response = proto::PluginMessage { - message: Some(response_msg), - }; - let mut buf = bytes::BytesMut::with_capacity(response.encoded_len()); - response.encode(&mut buf).unwrap(); - buf.to_vec() + Err(e) => self.encode_error(&e), + } } } @@ -177,4 +276,18 @@ impl Default for FileMetadataPlugin { } } +impl ConfigurablePlugin for FileMetadataPlugin { + type Config = FileMetaConfig; + + fn config(&self) -> &Self::Config { + self.base.config() + } + + fn config_mut(&mut self) -> &mut Self::Config { + self.base.config_mut() + } +} + +impl ProtobufHandler for FileMetadataPlugin {} + lla_plugin_interface::declare_plugin!(FileMetadataPlugin); From b377b96d81dd10d060b8f6216a42f69ab84b290a Mon Sep 17 00:00:00 2001 From: Mohamed Achaq Date: Fri, 27 Dec 2024 21:11:11 +0100 Subject: [PATCH 11/43] feat: enhance categorizer plugin with new dependencies and functionality - Added new dependencies: `lazy_static`, `parking_lot`, and `serde` to improve plugin functionality and configuration management. - Updated `Cargo.toml` for the `categorizer` plugin to include `lla_plugin_utils` and `serde` with derive features. - Refactored the plugin to utilize `lazy_static` for action registry and `parking_lot` for improved concurrency. - Introduced new actions for adding categories and subcategories, along with statistics display. - Enhanced the `handle_raw_request` method to support new plugin actions and improved response formatting. --- Cargo.lock | 5 +- plugins/categorizer/Cargo.toml | 13 +- plugins/categorizer/src/lib.rs | 610 ++++++++++++++++++--------------- 3 files changed, 348 insertions(+), 280 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6eceb31..9675bce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -106,12 +106,15 @@ checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" [[package]] name = "categorizer" -version = "0.3.0" +version = "0.3.1" dependencies = [ "bytes", "colored", "dirs", + "lazy_static", "lla_plugin_interface", + "lla_plugin_utils", + "parking_lot", "prost", "serde", "toml", diff --git a/plugins/categorizer/Cargo.toml b/plugins/categorizer/Cargo.toml index ec69351..fcc4d38 100644 --- a/plugins/categorizer/Cargo.toml +++ b/plugins/categorizer/Cargo.toml @@ -1,17 +1,20 @@ [package] name = "categorizer" description = "Categorizes files based on their extensions and metadata" -version = "0.3.0" +version = "0.3.1" edition = "2021" [dependencies] -colored = "2.0.0" lla_plugin_interface = { path = "../../lla_plugin_interface" } -dirs = "5.0.1" -serde = { version = "1.0.200", features = ["derive"] } -toml = "0.8.8" +lla_plugin_utils = { path = "../../lla_plugin_utils" } +colored = "2.0" prost = "0.12" bytes = "1.5" +serde = { version = "1.0", features = ["derive"] } +lazy_static = "1.4" +parking_lot = "0.12" +toml = "0.8" +dirs = "5.0" [lib] crate-type = ["cdylib"] diff --git a/plugins/categorizer/src/lib.rs b/plugins/categorizer/src/lib.rs index fb831d2..cc0e8b8 100644 --- a/plugins/categorizer/src/lib.rs +++ b/plugins/categorizer/src/lib.rs @@ -1,10 +1,174 @@ -use colored::Colorize; -use dirs::config_dir; -use lla_plugin_interface::{DecoratedEntry, Plugin}; +use lazy_static::lazy_static; +use lla_plugin_interface::{DecoratedEntry, Plugin, PluginRequest, PluginResponse}; +use lla_plugin_utils::{ + config::PluginConfig, + ui::{ + components::{BoxComponent, BoxStyle, HelpFormatter, KeyValue, List}, + format_size, TextBlock, + }, + ActionRegistry, BasePlugin, ConfigurablePlugin, ProtobufHandler, +}; +use parking_lot::RwLock; use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use std::fs; -use std::path::PathBuf; +use std::{collections::HashMap, fs, path::PathBuf}; + +lazy_static! { + static ref ACTION_REGISTRY: RwLock = RwLock::new({ + let mut registry = ActionRegistry::new(); + + lla_plugin_utils::define_action!( + registry, + "add-category", + "add-category [description]", + "Add a new category", + vec!["lla plugin --name categorizer --action add-category Documents blue txt,doc,pdf \"Text documents\""], + |args| { + if args.len() < 3 { + return Err("Usage: add-category [description]".to_string()); + } + let mut rule = CategoryRule::default(); + rule.name = args[0].clone(); + rule.color = args[1].clone(); + rule.extensions = args[2].split(',').map(String::from).collect(); + if let Some(desc) = args.get(3) { + rule.description = desc.clone(); + } + PLUGIN_STATE.write().add_rule(rule); + Ok(()) + } + ); + + lla_plugin_utils::define_action!( + registry, + "add-subcategory", + "add-subcategory ", + "Add a subcategory to an existing category", + vec!["lla plugin --name categorizer --action add-subcategory Documents Text txt,md"], + |args| { + if args.len() != 3 { + return Err( + "Usage: add-subcategory " + .to_string(), + ); + } + PLUGIN_STATE + .write() + .add_subcategory(&args[0], &args[1], &args[2]) + } + ); + + lla_plugin_utils::define_action!( + registry, + "show-stats", + "show-stats", + "Show category statistics", + vec!["lla plugin --name categorizer --action show-stats"], + |_| { + let state = PLUGIN_STATE.read(); + println!("{}", state.format_stats()); + Ok(()) + } + ); + + lla_plugin_utils::define_action!( + registry, + "list-categories", + "list-categories", + "List all categories and their details", + vec!["lla plugin --name categorizer --action list-categories"], + |_| { + let state = PLUGIN_STATE.read(); + let mut list = List::new(); + for rule in &state.rules { + let mut details = Vec::new(); + details.push(format!("Extensions: {}", rule.extensions.join(", "))); + + if !rule.subcategories.is_empty() { + details.push("Subcategories:".to_string()); + for (sub, exts) in &rule.subcategories { + details.push(format!(" {}: {}", sub, exts.join(", "))); + } + } + + list.add_item( + KeyValue::new(&rule.name, &rule.description) + .key_color(&rule.color) + .key_width(15) + .render(), + ); + + for detail in details { + list.add_item(" ".to_string() + &detail); + } + } + + println!( + "{}", + BoxComponent::new(list.render()) + .style(BoxStyle::Minimal) + .padding(1) + .render() + ); + Ok(()) + } + ); + + lla_plugin_utils::define_action!( + registry, + "help", + "help", + "Show help information", + vec!["lla plugin --name categorizer --action help"], + |_| { + let mut help = HelpFormatter::new("File Categorizer Plugin".to_string()); + help.add_section("Description".to_string()).add_command( + "".to_string(), + "Categorizes files based on their extensions and metadata".to_string(), + vec![], + ); + + help.add_section("Actions".to_string()) + .add_command( + "add-category".to_string(), + "Add a new category".to_string(), + vec!["lla plugin --name categorizer --action add-category Documents blue txt,doc,pdf \"Text documents\"".to_string()], + ) + .add_command( + "add-subcategory".to_string(), + "Add a subcategory to an existing category".to_string(), + vec!["lla plugin --name categorizer --action add-subcategory Documents Text txt,md".to_string()], + ) + .add_command( + "show-stats".to_string(), + "Show category statistics".to_string(), + vec!["lla plugin --name categorizer --action show-stats".to_string()], + ) + .add_command( + "list-categories".to_string(), + "List all categories and their details".to_string(), + vec!["lla plugin --name categorizer --action list-categories".to_string()], + ) + .add_command( + "help".to_string(), + "Show this help information".to_string(), + vec!["lla plugin --name categorizer --action help".to_string()], + ); + + println!( + "{}", + BoxComponent::new(help.render(&CategorizerConfig::default().colors)) + .style(BoxStyle::Minimal) + .padding(2) + .render() + ); + Ok(()) + } + ); + + registry + }); + static ref PLUGIN_STATE: RwLock = RwLock::new(PluginState::new()); +} #[derive(Debug, Clone, Serialize, Deserialize)] struct CategoryRule { @@ -36,15 +200,15 @@ struct CategoryStats { subcategory_counts: HashMap, } -pub struct FileCategoryPlugin { +struct PluginState { rules: Vec, config_path: PathBuf, stats: HashMap, } -impl FileCategoryPlugin { - pub fn new() -> Self { - let config_path = config_dir() +impl PluginState { + fn new() -> Self { + let config_path = dirs::config_dir() .unwrap_or_else(|| PathBuf::from(".")) .join("lla") .join("categorizer.toml"); @@ -53,7 +217,7 @@ impl FileCategoryPlugin { vec![ CategoryRule { name: "Document".to_string(), - color: "blue".to_string(), + color: "bright_blue".to_string(), extensions: vec!["txt", "md", "doc", "docx", "pdf", "rtf", "odt"] .into_iter() .map(String::from) @@ -76,37 +240,9 @@ impl FileCategoryPlugin { }, description: "Text documents and office files".to_string(), }, - CategoryRule { - name: "Image".to_string(), - color: "green".to_string(), - extensions: vec!["jpg", "jpeg", "png", "gif", "bmp", "svg", "webp", "tiff"] - .into_iter() - .map(String::from) - .collect(), - size_ranges: Some(vec![(0, 52_428_800)]), - subcategories: { - let mut map = HashMap::new(); - map.insert( - "Raster".to_string(), - vec!["jpg", "jpeg", "png", "gif", "bmp"] - .into_iter() - .map(String::from) - .collect(), - ); - map.insert( - "Vector".to_string(), - vec!["svg", "ai", "eps"] - .into_iter() - .map(String::from) - .collect(), - ); - map - }, - description: "Image files in various formats".to_string(), - }, CategoryRule { name: "Code".to_string(), - color: "cyan".to_string(), + color: "bright_cyan".to_string(), extensions: vec![ "rs", "py", "js", "ts", "java", "c", "cpp", "h", "hpp", "go", "rb", "php", "cs", "swift", "kt", @@ -145,27 +281,13 @@ impl FileCategoryPlugin { ] }); - FileCategoryPlugin { + Self { rules, config_path, stats: HashMap::new(), } } - fn encode_error(&self, error: &str) -> Vec { - use prost::Message; - let error_msg = lla_plugin_interface::proto::PluginMessage { - message: Some( - lla_plugin_interface::proto::plugin_message::Message::ErrorResponse( - error.to_string(), - ), - ), - }; - let mut buf = bytes::BytesMut::with_capacity(error_msg.encoded_len()); - error_msg.encode(&mut buf).unwrap(); - buf.to_vec() - } - fn load_rules(path: &PathBuf) -> Option> { fs::read_to_string(path) .ok() @@ -181,6 +303,29 @@ impl FileCategoryPlugin { } } + fn add_rule(&mut self, rule: CategoryRule) { + self.rules.push(rule); + self.save_rules(); + } + + fn add_subcategory( + &mut self, + category: &str, + subcategory: &str, + extensions: &str, + ) -> Result<(), String> { + if let Some(rule) = self.rules.iter_mut().find(|r| r.name == category) { + rule.subcategories.insert( + subcategory.to_string(), + extensions.split(',').map(String::from).collect(), + ); + self.save_rules(); + Ok(()) + } else { + Err(format!("Category '{}' not found", category)) + } + } + fn get_category_info( &self, entry: &DecoratedEntry, @@ -217,251 +362,154 @@ impl FileCategoryPlugin { } } - fn string_to_color(color: &str) -> colored::Color { - match color.to_lowercase().as_str() { - "black" => colored::Color::Black, - "red" => colored::Color::Red, - "green" => colored::Color::Green, - "yellow" => colored::Color::Yellow, - "blue" => colored::Color::Blue, - "magenta" => colored::Color::Magenta, - "cyan" => colored::Color::Cyan, - _ => colored::Color::White, - } - } - fn format_stats(&self) -> String { - let mut output = String::new(); - output.push_str("Category Statistics:\n"); - + let mut list = List::new(); for (category, stats) in &self.stats { - let color = self - .rules - .iter() - .find(|r| &r.name == category) - .map(|r| Self::string_to_color(&r.color)) - .unwrap_or(colored::Color::White); - - output.push_str(&format!( - "\n{} ({} files, {})\n", - category.color(color), - stats.count, - Self::format_size(stats.total_size) - )); + let rule = self.rules.iter().find(|r| &r.name == category); + let white = "white".to_string(); + let color = rule.map(|r| &r.color).unwrap_or(&white); + + let header = KeyValue::new( + category, + &format!("{} files, {}", stats.count, format_size(stats.total_size)), + ) + .key_color(color) + .key_width(15) + .render(); + + list.add_item(header); for (sub, count) in &stats.subcategory_counts { - output.push_str(&format!(" {} ({} files)\n", sub, count)); + list.add_item(format!(" {} ({} files)", sub, count)); } } - output + + BoxComponent::new(list.render()) + .style(BoxStyle::Minimal) + .padding(1) + .render() } +} - fn format_size(size: u64) -> String { - const KB: u64 = 1024; - const MB: u64 = KB * 1024; - const GB: u64 = MB * 1024; - - if size >= GB { - format!("{:.2} GB", size as f64 / GB as f64) - } else if size >= MB { - format!("{:.2} MB", size as f64 / MB as f64) - } else if size >= KB { - format!("{:.2} KB", size as f64 / KB as f64) - } else { - format!("{} B", size) +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CategorizerConfig { + #[serde(default = "default_colors")] + colors: HashMap, +} + +fn default_colors() -> HashMap { + let mut colors = HashMap::new(); + colors.insert("success".to_string(), "bright_green".to_string()); + colors.insert("info".to_string(), "bright_blue".to_string()); + colors.insert("name".to_string(), "bright_yellow".to_string()); + colors +} + +impl Default for CategorizerConfig { + fn default() -> Self { + Self { + colors: default_colors(), } } } -impl Plugin for FileCategoryPlugin { - fn handle_raw_request(&mut self, request: &[u8]) -> Vec { - use lla_plugin_interface::proto::{self, plugin_message}; - use prost::Message as ProstMessage; - - let proto_msg = match proto::PluginMessage::decode(request) { - Ok(msg) => msg, - Err(e) => { - let error_msg = proto::PluginMessage { - message: Some(plugin_message::Message::ErrorResponse(format!( - "Failed to decode request: {}", - e - ))), - }; - let mut buf = bytes::BytesMut::with_capacity(error_msg.encoded_len()); - error_msg.encode(&mut buf).unwrap(); - return buf.to_vec(); - } - }; +impl PluginConfig for CategorizerConfig {} - let response_msg = match proto_msg.message { - Some(plugin_message::Message::GetName(_)) => { - plugin_message::Message::NameResponse(env!("CARGO_PKG_NAME").to_string()) - } - Some(plugin_message::Message::GetVersion(_)) => { - plugin_message::Message::VersionResponse(env!("CARGO_PKG_VERSION").to_string()) - } - Some(plugin_message::Message::GetDescription(_)) => { - plugin_message::Message::DescriptionResponse( - env!("CARGO_PKG_DESCRIPTION").to_string(), - ) - } - Some(plugin_message::Message::GetSupportedFormats(_)) => { - plugin_message::Message::FormatsResponse(proto::SupportedFormatsResponse { - formats: vec!["default".to_string(), "long".to_string()], - }) - } - Some(plugin_message::Message::Decorate(entry)) => { - let mut decorated_entry = match DecoratedEntry::try_from(entry.clone()) { - Ok(e) => e, - Err(e) => { - return self.encode_error(&format!("Failed to convert entry: {}", e)); - } - }; +pub struct FileCategoryPlugin { + base: BasePlugin, +} + +impl FileCategoryPlugin { + pub fn new() -> Self { + Self { + base: BasePlugin::new(), + } + } - if let Some((category, color, subcategory)) = - self.get_category_info(&decorated_entry) - { - decorated_entry - .custom_fields - .insert("category".to_string(), category.clone()); - decorated_entry - .custom_fields - .insert("category_color".to_string(), color); - if let Some(sub) = &subcategory { - decorated_entry - .custom_fields - .insert("subcategory".to_string(), sub.clone()); + fn format_file_info(&self, entry: &DecoratedEntry, format: &str) -> Option { + match ( + entry.custom_fields.get("category"), + entry.custom_fields.get("category_color"), + entry.custom_fields.get("subcategory"), + ) { + (Some(category), Some(color), subcategory) => match format { + "default" => Some( + TextBlock::new(format!("[{}]", category)) + .color(color) + .build(), + ), + "long" => { + let base = TextBlock::new(format!("[{}]", category)) + .color(color) + .build(); + if let Some(sub) = subcategory { + Some(format!( + "{} ({})", + base, + TextBlock::new(sub).color("bright_black").build() + )) + } else { + Some(base) } - self.update_stats(&decorated_entry, &category, subcategory.as_deref()); } + _ => None, + }, + _ => None, + } + } +} - plugin_message::Message::DecoratedResponse(decorated_entry.into()) - } - Some(plugin_message::Message::FormatField(req)) => { - let entry = match req.entry { - Some(e) => match DecoratedEntry::try_from(e) { - Ok(entry) => entry, - Err(e) => { - return self.encode_error(&format!("Failed to convert entry: {}", e)); - } - }, - None => return self.encode_error("Missing entry in format field request"), - }; - - let formatted = match req.format.as_str() { - "default" => entry.custom_fields.get("category").map(|category| { - let color = entry - .custom_fields - .get("category_color") - .and_then(|c| c.parse::().ok()) - .unwrap_or(colored::Color::White); - format!("[{}]", category.color(color)) - }), - "long" => entry.custom_fields.get("category").map(|category| { - let color = entry - .custom_fields - .get("category_color") - .and_then(|c| c.parse::().ok()) - .unwrap_or(colored::Color::White); - let base = format!("[{}]", category.color(color)); - if let Some(sub) = entry.custom_fields.get("subcategory") { - format!("{} ({})", base, sub.bright_black()) - } else { - base - } - }), - _ => None, - }; - - plugin_message::Message::FieldResponse(proto::FormattedFieldResponse { - field: formatted, - }) - } - Some(plugin_message::Message::Action(req)) => { - let result: Result<(), String> = match req.action.as_str() { - "add-category" => { - if req.args.len() < 3 { - Err( - "Usage: add-category [description]" - .to_string(), - ) - } else { - let mut rule = CategoryRule::default(); - rule.name = req.args[0].clone(); - rule.color = req.args[1].clone(); - rule.extensions = req.args[2].split(',').map(String::from).collect(); - if let Some(desc) = req.args.get(3) { - rule.description = desc.clone(); - } - self.rules.push(rule); - self.save_rules(); - Ok(()) - } +impl Plugin for FileCategoryPlugin { + fn handle_raw_request(&mut self, request: &[u8]) -> Vec { + match self.decode_request(request) { + Ok(request) => { + let response = match request { + PluginRequest::GetName => { + PluginResponse::Name(env!("CARGO_PKG_NAME").to_string()) } - "add-subcategory" => { - if req.args.len() != 4 { - Err( - "Usage: add-subcategory " - .to_string(), - ) - } else if let Some(rule) = - self.rules.iter_mut().find(|r| r.name == req.args[0]) - { - rule.subcategories.insert( - req.args[1].clone(), - req.args[2].split(',').map(String::from).collect(), - ); - self.save_rules(); - Ok(()) - } else { - Err(format!("Category '{}' not found", req.args[0])) - } + PluginRequest::GetVersion => { + PluginResponse::Version(env!("CARGO_PKG_VERSION").to_string()) } - "show-stats" => { - println!("{}", self.format_stats()); - Ok(()) + PluginRequest::GetDescription => { + PluginResponse::Description(env!("CARGO_PKG_DESCRIPTION").to_string()) } - "list-categories" => { - for rule in &self.rules { - let color = Self::string_to_color(&rule.color); - println!("\n{} ({})", rule.name.color(color), rule.description); - println!(" Extensions: {}", rule.extensions.join(", ")); - if !rule.subcategories.is_empty() { - println!(" Subcategories:"); - for (sub, exts) in &rule.subcategories { - println!(" {}: {}", sub, exts.join(", ")); - } + PluginRequest::GetSupportedFormats => PluginResponse::SupportedFormats(vec![ + "default".to_string(), + "long".to_string(), + ]), + PluginRequest::Decorate(mut entry) => { + let mut state = PLUGIN_STATE.write(); + if let Some((category, color, subcategory)) = + state.get_category_info(&entry) + { + entry + .custom_fields + .insert("category".to_string(), category.clone()); + entry + .custom_fields + .insert("category_color".to_string(), color); + if let Some(sub) = &subcategory { + entry + .custom_fields + .insert("subcategory".to_string(), sub.clone()); } + state.update_stats(&entry, &category, subcategory.as_deref()); } - Ok(()) + PluginResponse::Decorated(entry) } - "help" => { - let help_text = "Available actions:\n\ - add-category [description] - Add a new category\n\ - add-subcategory - Add a subcategory\n\ - show-stats - Show category statistics\n\ - list-categories - List all categories and their details\n\ - help - Show this help message\n\n"; - println!("{}", help_text); - Ok(()) + PluginRequest::FormatField(entry, format) => { + let field = self.format_file_info(&entry, &format); + PluginResponse::FormattedField(field) + } + PluginRequest::PerformAction(action, args) => { + let result = ACTION_REGISTRY.read().handle(&action, &args); + PluginResponse::ActionResult(result) } - _ => Err(format!("Unknown action: {}", req.action)), }; - - plugin_message::Message::ActionResponse(proto::ActionResponse { - success: result.is_ok(), - error: result.err(), - }) + self.encode_response(response) } - _ => plugin_message::Message::ErrorResponse("Invalid request type".to_string()), - }; - - let response = proto::PluginMessage { - message: Some(response_msg), - }; - let mut buf = bytes::BytesMut::with_capacity(response.encoded_len()); - response.encode(&mut buf).unwrap(); - buf.to_vec() + Err(e) => self.encode_error(&e), + } } } @@ -471,4 +519,18 @@ impl Default for FileCategoryPlugin { } } +impl ConfigurablePlugin for FileCategoryPlugin { + type Config = CategorizerConfig; + + fn config(&self) -> &Self::Config { + self.base.config() + } + + fn config_mut(&mut self) -> &mut Self::Config { + self.base.config_mut() + } +} + +impl ProtobufHandler for FileCategoryPlugin {} + lla_plugin_interface::declare_plugin!(FileCategoryPlugin); From 3b9d2f4ccfd5bea6acffd8e6d7fb4f626e6db71d Mon Sep 17 00:00:00 2001 From: Mohamed Achaq Date: Fri, 27 Dec 2024 21:11:39 +0100 Subject: [PATCH 12/43] chore: bump file_meta plugin version to 0.3.1 - Updated version in Cargo.toml to reflect the new release. --- Cargo.lock | 2 +- plugins/file_meta/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9675bce..6564ca5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -451,7 +451,7 @@ dependencies = [ [[package]] name = "file_meta" -version = "0.3.0" +version = "0.3.1" dependencies = [ "bytes", "chrono", diff --git a/plugins/file_meta/Cargo.toml b/plugins/file_meta/Cargo.toml index dabac29..77a921d 100644 --- a/plugins/file_meta/Cargo.toml +++ b/plugins/file_meta/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "file_meta" description = "Displays the file metadata of each file" -version = "0.3.0" +version = "0.3.1" edition = "2021" [dependencies] From 3f90d2296791a696cd5093d60fd3fb2153248a12 Mon Sep 17 00:00:00 2001 From: Mohamed Achaq Date: Fri, 27 Dec 2024 21:29:17 +0100 Subject: [PATCH 13/43] feat: update code_complexity plugin to version 0.3.1 with new dependencies and enhanced functionality - Bumped version to 0.3.1 in Cargo.toml and Cargo.lock. - Added new dependencies: `lazy_static`, `parking_lot`, and `lla_plugin_utils` to improve concurrency and configuration management. - Refactored the plugin to utilize `lazy_static` for action registry and `parking_lot` for thread-safe state management. - Introduced new actions for setting complexity thresholds and displaying detailed reports. - Enhanced the report generation and metrics formatting for better user experience. --- Cargo.lock | 5 +- plugins/code_complexity/Cargo.toml | 15 +- plugins/code_complexity/src/lib.rs | 649 +++++++++++++++++------------ 3 files changed, 390 insertions(+), 279 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6564ca5..5630b36 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -184,12 +184,15 @@ dependencies = [ [[package]] name = "code_complexity" -version = "0.3.0" +version = "0.3.1" dependencies = [ "bytes", "colored", "dirs", + "lazy_static", "lla_plugin_interface", + "lla_plugin_utils", + "parking_lot", "prost", "serde", "toml", diff --git a/plugins/code_complexity/Cargo.toml b/plugins/code_complexity/Cargo.toml index b0dc23b..68034d1 100644 --- a/plugins/code_complexity/Cargo.toml +++ b/plugins/code_complexity/Cargo.toml @@ -1,17 +1,20 @@ [package] name = "code_complexity" -description = "Analyzes code complexity using various metrics" -version = "0.3.0" +description = "Analyzes code complexity and provides metrics" +version = "0.3.1" edition = "2021" [dependencies] -colored = "2.0.0" lla_plugin_interface = { path = "../../lla_plugin_interface" } -dirs = "5.0.1" -serde = { version = "1.0.200", features = ["derive"] } -toml = "0.8.8" +lla_plugin_utils = { path = "../../lla_plugin_utils" } +colored = "2.0" prost = "0.12" bytes = "1.5" +serde = { version = "1.0", features = ["derive"] } +lazy_static = "1.4" +parking_lot = "0.12" +toml = "0.8" +dirs = "5.0" [lib] crate-type = ["cdylib"] diff --git a/plugins/code_complexity/src/lib.rs b/plugins/code_complexity/src/lib.rs index 78d2f08..0e9b49f 100644 --- a/plugins/code_complexity/src/lib.rs +++ b/plugins/code_complexity/src/lib.rs @@ -1,11 +1,139 @@ -use colored::Colorize; -use dirs::config_dir; -use lla_plugin_interface::{DecoratedEntry, Plugin}; +use lazy_static::lazy_static; +use lla_plugin_interface::{DecoratedEntry, Plugin, PluginRequest, PluginResponse}; +use lla_plugin_utils::{ + config::PluginConfig, + ui::{ + components::{BoxComponent, BoxStyle, HelpFormatter, KeyValue, List}, + TextBlock, + }, + ActionRegistry, BasePlugin, ConfigurablePlugin, ProtobufHandler, +}; +use parking_lot::RwLock; use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use std::fs::{self, File}; -use std::io::{BufRead, BufReader}; -use std::path::PathBuf; +use std::{ + collections::HashMap, + fs::{self, File}, + io::{BufRead, BufReader}, + path::PathBuf, +}; + +lazy_static! { + static ref ACTION_REGISTRY: RwLock = RwLock::new({ + let mut registry = ActionRegistry::new(); + + lla_plugin_utils::define_action!( + registry, + "set-thresholds", + "set-thresholds ", + "Set complexity thresholds", + vec!["lla plugin --name code_complexity --action set-thresholds 10 20 30 40"], + |args| { + if args.len() != 4 { + return Err( + "Usage: set-thresholds ".to_string() + ); + } + if let (Ok(low), Ok(medium), Ok(high), Ok(very_high)) = ( + args[0].parse::(), + args[1].parse::(), + args[2].parse::(), + args[3].parse::(), + ) { + let mut state = PLUGIN_STATE.write(); + state.config.thresholds = ComplexityThresholds { + low, + medium, + high, + very_high, + }; + state.save_config(); + println!( + "{}", + TextBlock::new("Updated complexity thresholds") + .color("bright_green") + .build() + ); + Ok(()) + } else { + Err("Invalid threshold values".to_string()) + } + } + ); + + lla_plugin_utils::define_action!( + registry, + "show-report", + "show-report", + "Show detailed complexity report", + vec!["lla plugin --name code_complexity --action show-report"], + |_| { + let state = PLUGIN_STATE.read(); + println!("{}", state.generate_report()); + Ok(()) + } + ); + + lla_plugin_utils::define_action!( + registry, + "help", + "help", + "Show help information", + vec!["lla plugin --name code_complexity --action help"], + |_| { + let mut help = HelpFormatter::new("Code Complexity Plugin".to_string()); + help.add_section("Description".to_string()).add_command( + "".to_string(), + "Analyzes code complexity using various metrics".to_string(), + vec![], + ); + + help.add_section("Actions".to_string()) + .add_command( + "set-thresholds".to_string(), + "Set complexity thresholds".to_string(), + vec![ + "lla plugin --name code_complexity --action set-thresholds 10 20 30 40" + .to_string(), + ], + ) + .add_command( + "show-report".to_string(), + "Show detailed complexity report".to_string(), + vec!["lla plugin --name code_complexity --action show-report".to_string()], + ) + .add_command( + "help".to_string(), + "Show this help information".to_string(), + vec!["lla plugin --name code_complexity --action help".to_string()], + ); + + help.add_section("Formats".to_string()) + .add_command( + "default".to_string(), + "Show basic complexity metrics".to_string(), + vec![], + ) + .add_command( + "long".to_string(), + "Show detailed complexity metrics and suggestions".to_string(), + vec![], + ); + + println!( + "{}", + BoxComponent::new(help.render(&ComplexityConfig::default().colors)) + .style(BoxStyle::Minimal) + .padding(2) + .render() + ); + Ok(()) + } + ); + + registry + }); + static ref PLUGIN_STATE: RwLock = RwLock::new(PluginState::new()); +} #[derive(Debug, Clone, Serialize, Deserialize)] struct LanguageRules { @@ -39,11 +167,39 @@ impl Default for LanguageRules { } #[derive(Debug, Clone, Serialize, Deserialize)] -struct ComplexityConfig { +pub struct ComplexityConfig { languages: HashMap, thresholds: ComplexityThresholds, + #[serde(default = "default_colors")] + colors: HashMap, +} + +fn default_colors() -> HashMap { + let mut colors = HashMap::new(); + colors.insert("low".to_string(), "bright_green".to_string()); + colors.insert("medium".to_string(), "bright_yellow".to_string()); + colors.insert("high".to_string(), "bright_red".to_string()); + colors.insert("very_high".to_string(), "red".to_string()); + colors.insert("success".to_string(), "bright_green".to_string()); + colors.insert("info".to_string(), "bright_blue".to_string()); + colors.insert("name".to_string(), "bright_yellow".to_string()); + colors } +impl Default for ComplexityConfig { + fn default() -> Self { + let mut languages = HashMap::new(); + languages.insert("Rust".to_string(), LanguageRules::default()); + Self { + languages, + thresholds: ComplexityThresholds::default(), + colors: default_colors(), + } + } +} + +impl PluginConfig for ComplexityConfig {} + #[derive(Debug, Clone, Serialize, Deserialize)] struct ComplexityThresholds { low: f32, @@ -96,115 +252,28 @@ impl Default for ComplexityMetrics { } } -pub struct CodeComplexityEstimatorPlugin { +struct PluginState { config: ComplexityConfig, config_path: PathBuf, stats: HashMap>, } -impl CodeComplexityEstimatorPlugin { - pub fn new() -> Self { - let config_path = config_dir() +impl PluginState { + fn new() -> Self { + let config_path = dirs::config_dir() .unwrap_or_else(|| PathBuf::from(".")) .join("lla") .join("code_complexity.toml"); - let config = Self::load_config(&config_path).unwrap_or_else(|| { - let mut languages = HashMap::new(); - - languages.insert( - "Rust".to_string(), - LanguageRules { - extensions: vec!["rs".to_string()], - function_patterns: vec!["fn ".to_string()], - class_patterns: vec![ - "struct ".to_string(), - "impl ".to_string(), - "trait ".to_string(), - ], - branch_patterns: vec![ - "if ".to_string(), - "match ".to_string(), - "else".to_string(), - ], - loop_patterns: vec![ - "for ".to_string(), - "while ".to_string(), - "loop".to_string(), - ], - comment_patterns: vec!["//".to_string(), "/*".to_string()], - max_line_length: 100, - max_function_lines: 50, - }, - ); - - languages.insert( - "Python".to_string(), - LanguageRules { - extensions: vec!["py".to_string()], - function_patterns: vec!["def ".to_string()], - class_patterns: vec!["class ".to_string()], - branch_patterns: vec![ - "if ".to_string(), - "elif ".to_string(), - "else:".to_string(), - ], - loop_patterns: vec!["for ".to_string(), "while ".to_string()], - comment_patterns: vec!["#".to_string()], - max_line_length: 88, - max_function_lines: 50, - }, - ); + let config = Self::load_config(&config_path).unwrap_or_default(); - languages.insert( - "JavaScript".to_string(), - LanguageRules { - extensions: vec!["js".to_string(), "ts".to_string()], - function_patterns: vec!["function ".to_string(), "=> ".to_string()], - class_patterns: vec!["class ".to_string()], - branch_patterns: vec![ - "if ".to_string(), - "else ".to_string(), - "switch ".to_string(), - ], - loop_patterns: vec![ - "for ".to_string(), - "while ".to_string(), - "do ".to_string(), - ], - comment_patterns: vec!["//".to_string(), "/*".to_string()], - max_line_length: 80, - max_function_lines: 40, - }, - ); - - ComplexityConfig { - languages, - thresholds: ComplexityThresholds::default(), - } - }); - - CodeComplexityEstimatorPlugin { + Self { config, config_path, stats: HashMap::new(), } } - fn encode_error(&self, error: &str) -> Vec { - use prost::Message; - let error_msg = lla_plugin_interface::proto::PluginMessage { - message: Some( - lla_plugin_interface::proto::plugin_message::Message::ErrorResponse( - error.to_string(), - ), - ), - }; - let mut buf = bytes::BytesMut::with_capacity(error_msg.encoded_len()); - error_msg.encode(&mut buf).unwrap(); - buf.to_vec() - } - fn load_config(path: &PathBuf) -> Option { fs::read_to_string(path) .ok() @@ -302,83 +371,148 @@ impl CodeComplexityEstimatorPlugin { Some(metrics) } - fn get_complexity_color(&self, metrics: &ComplexityMetrics) -> colored::Color { + fn get_complexity_color(&self, metrics: &ComplexityMetrics) -> String { let score = metrics.cyclomatic_complexity as f32 * 0.4 + metrics.cognitive_complexity as f32 * 0.3 + (100.0 - metrics.maintainability_index) * 0.3; if score < self.config.thresholds.low { - colored::Color::Green + self.config + .colors + .get("low") + .unwrap_or(&"bright_green".to_string()) + .clone() } else if score < self.config.thresholds.medium { - colored::Color::Yellow + self.config + .colors + .get("medium") + .unwrap_or(&"bright_yellow".to_string()) + .clone() } else if score < self.config.thresholds.high { - colored::Color::Red + self.config + .colors + .get("high") + .unwrap_or(&"bright_red".to_string()) + .clone() } else { - colored::Color::BrightRed + self.config + .colors + .get("very_high") + .unwrap_or(&"red".to_string()) + .clone() } } fn format_metrics(&self, metrics: &ComplexityMetrics, detailed: bool) -> String { let color = self.get_complexity_color(metrics); - let mut output = format!( - "\nComplexity: {} (MI: {:.1})", - metrics.cyclomatic_complexity.to_string().color(color), - metrics.maintainability_index + let mut list = List::new(); + + list.add_item( + KeyValue::new( + "Complexity", + &format!( + "{} (MI: {:.1})", + metrics.cyclomatic_complexity, metrics.maintainability_index + ), + ) + .key_color(color) + .key_width(15) + .render(), ); if detailed { - output.push_str(&format!("\n Lines: {}", metrics.lines)); - output.push_str(&format!("\n Functions: {}", metrics.functions)); - output.push_str(&format!("\n Classes: {}", metrics.classes)); - output.push_str(&format!("\n Branches: {}", metrics.branches)); - output.push_str(&format!("\n Loops: {}", metrics.loops)); - output.push_str(&format!("\n Comments: {}", metrics.comments)); - output.push_str(&format!("\n Long lines: {}", metrics.long_lines)); + list.add_item( + KeyValue::new("Lines", metrics.lines.to_string()) + .key_width(15) + .render(), + ); + list.add_item( + KeyValue::new("Functions", metrics.functions.to_string()) + .key_width(15) + .render(), + ); + list.add_item( + KeyValue::new("Classes", metrics.classes.to_string()) + .key_width(15) + .render(), + ); + list.add_item( + KeyValue::new("Branches", metrics.branches.to_string()) + .key_width(15) + .render(), + ); + list.add_item( + KeyValue::new("Loops", metrics.loops.to_string()) + .key_width(15) + .render(), + ); + list.add_item( + KeyValue::new("Comments", metrics.comments.to_string()) + .key_width(15) + .render(), + ); + list.add_item( + KeyValue::new("Long lines", metrics.long_lines.to_string()) + .key_width(15) + .render(), + ); if !metrics.long_functions.is_empty() { - output.push_str("\n Long functions:"); + list.add_item("Long functions:".to_string()); for (name, lines) in &metrics.long_functions { - output.push_str(&format!("\n {} ({} lines)", name, lines)); + list.add_item(format!(" {} ({} lines)", name, lines)); } } if metrics.maintainability_index < 65.0 { - output.push_str("\n\nSuggestions:"); + list.add_item("\nSuggestions:".to_string()); if metrics.long_functions.len() > 2 { - output.push_str("\n - Consider breaking down long functions"); + list.add_item(" - Consider breaking down long functions".to_string()); } if metrics.comments < metrics.lines / 10 { - output.push_str("\n - Add more documentation"); + list.add_item(" - Add more documentation".to_string()); } if metrics.cyclomatic_complexity > 10 { - output.push_str("\n - Reduce nested conditionals"); + list.add_item(" - Reduce nested conditionals".to_string()); } if metrics.cognitive_complexity > 15 { - output.push_str("\n - Simplify complex logic"); + list.add_item(" - Simplify complex logic".to_string()); } } } - output + BoxComponent::new(list.render()) + .style(BoxStyle::Minimal) + .padding(1) + .render() } fn generate_report(&self) -> String { - let mut output = String::new(); - output.push_str("Code Complexity Report\n\n"); + let mut list = List::new(); + list.add_item( + TextBlock::new("Code Complexity Report") + .color("bright_blue") + .build(), + ); for (language, files) in &self.stats { - output.push_str(&format!("{}:\n", language.bright_cyan())); + list.add_item(TextBlock::new(language).color("bright_cyan").build()); let mut total_metrics = ComplexityMetrics::default(); let mut file_count = 0; for (path, metrics) in files { - output.push_str(&format!( - " {}: {} (MI: {:.1})\n", - path.display(), - metrics.cyclomatic_complexity, - metrics.maintainability_index - )); + list.add_item( + KeyValue::new( + &format!(" {}", path.display()), + &format!( + "{} (MI: {:.1})", + metrics.cyclomatic_complexity, metrics.maintainability_index + ), + ) + .key_color(self.get_complexity_color(&metrics)) + .render(), + ); total_metrics.lines += metrics.lines; total_metrics.functions += metrics.functions; @@ -392,162 +526,119 @@ impl CodeComplexityEstimatorPlugin { } if file_count > 0 { - output.push_str(&format!( - "\n Average metrics:\n Lines per file: {:.1}\n Cyclomatic complexity: {:.1}\n Maintainability index: {:.1}\n\n", - total_metrics.lines as f32 / file_count as f32, - total_metrics.cyclomatic_complexity as f32 / file_count as f32, + list.add_item("\nAverage metrics:".to_string()); + list.add_item(format!( + " Lines per file: {:.1}", + total_metrics.lines as f32 / file_count as f32 + )); + list.add_item(format!( + " Cyclomatic complexity: {:.1}", + total_metrics.cyclomatic_complexity as f32 / file_count as f32 + )); + list.add_item(format!( + " Maintainability index: {:.1}\n", total_metrics.maintainability_index / file_count as f32 )); } } - output + BoxComponent::new(list.render()) + .style(BoxStyle::Minimal) + .padding(1) + .render() + } +} + +pub struct CodeComplexityEstimatorPlugin { + base: BasePlugin, +} + +impl CodeComplexityEstimatorPlugin { + pub fn new() -> Self { + Self { + base: BasePlugin::new(), + } + } + + fn format_file_info(&self, entry: &DecoratedEntry, format: &str) -> Option { + entry + .custom_fields + .get("complexity_metrics") + .and_then(|toml_str| toml::from_str::(toml_str).ok()) + .map(|metrics| { + PLUGIN_STATE + .read() + .format_metrics(&metrics, format == "long") + }) } } impl Plugin for CodeComplexityEstimatorPlugin { fn handle_raw_request(&mut self, request: &[u8]) -> Vec { - use lla_plugin_interface::proto::{self, plugin_message}; - use prost::Message as ProstMessage; - - let proto_msg = match proto::PluginMessage::decode(request) { - Ok(msg) => msg, - Err(e) => { - let error_msg = proto::PluginMessage { - message: Some(plugin_message::Message::ErrorResponse(format!( - "Failed to decode request: {}", - e - ))), - }; - let mut buf = bytes::BytesMut::with_capacity(error_msg.encoded_len()); - error_msg.encode(&mut buf).unwrap(); - return buf.to_vec(); - } - }; - - let response_msg = match proto_msg.message { - Some(plugin_message::Message::GetName(_)) => { - plugin_message::Message::NameResponse(env!("CARGO_PKG_NAME").to_string()) - } - Some(plugin_message::Message::GetVersion(_)) => { - plugin_message::Message::VersionResponse(env!("CARGO_PKG_VERSION").to_string()) - } - Some(plugin_message::Message::GetDescription(_)) => { - plugin_message::Message::DescriptionResponse( - env!("CARGO_PKG_DESCRIPTION").to_string(), - ) - } - Some(plugin_message::Message::GetSupportedFormats(_)) => { - plugin_message::Message::FormatsResponse(proto::SupportedFormatsResponse { - formats: vec!["default".to_string(), "long".to_string()], - }) - } - Some(plugin_message::Message::Decorate(entry)) => { - let mut entry = match DecoratedEntry::try_from(entry.clone()) { - Ok(e) => e, - Err(e) => { - return self.encode_error(&format!("Failed to convert entry: {}", e)); + match self.decode_request(request) { + Ok(request) => { + let response = match request { + PluginRequest::GetName => { + PluginResponse::Name(env!("CARGO_PKG_NAME").to_string()) } - }; - if entry.path.is_file() { - if let Some(metrics) = self.analyze_file(&entry.path) { - entry.custom_fields.insert( - "complexity_metrics".to_string(), - toml::to_string(&metrics).unwrap_or_default(), - ); - - if let Some(ext) = entry.path.extension().and_then(|e| e.to_str()) { - if let Some((lang, _)) = self - .config - .languages - .iter() - .find(|(_, rules)| rules.extensions.iter().any(|e| e == ext)) - { - self.stats - .entry(lang.clone()) - .or_default() - .push((entry.path.clone(), metrics)); - } - } + PluginRequest::GetVersion => { + PluginResponse::Version(env!("CARGO_PKG_VERSION").to_string()) } - } - plugin_message::Message::DecoratedResponse(entry.into()) - } - Some(plugin_message::Message::FormatField(req)) => { - let entry = match req.entry { - Some(e) => match DecoratedEntry::try_from(e) { - Ok(entry) => entry, - Err(e) => { - return self.encode_error(&format!("Failed to convert entry: {}", e)); - } - }, - None => return self.encode_error("Missing entry in format field request"), - }; - let formatted = entry - .custom_fields - .get("complexity_metrics") - .and_then(|toml_str| toml::from_str::(toml_str).ok()) - .map(|metrics| self.format_metrics(&metrics, req.format == "long")); - plugin_message::Message::FieldResponse(proto::FormattedFieldResponse { - field: formatted, - }) - } - Some(plugin_message::Message::Action(req)) => { - let result: Result<(), String> = match req.action.as_str() { - "set-thresholds" => { - if req.args.len() != 4 { - return self.encode_error( - "Usage: set-thresholds ", - ); - } - if let (Ok(low), Ok(medium), Ok(high), Ok(very_high)) = ( - req.args[0].parse::(), - req.args[1].parse::(), - req.args[2].parse::(), - req.args[3].parse::(), - ) { - self.config.thresholds = ComplexityThresholds { - low, - medium, - high, - very_high, - }; - self.save_config(); - println!("Updated complexity thresholds"); - Ok(()) - } else { - Err("Invalid threshold values".to_string()) + PluginRequest::GetDescription => { + PluginResponse::Description(env!("CARGO_PKG_DESCRIPTION").to_string()) + } + PluginRequest::GetSupportedFormats => PluginResponse::SupportedFormats(vec![ + "default".to_string(), + "long".to_string(), + ]), + PluginRequest::Decorate(mut entry) => { + if entry.path.is_file() { + let metrics = PLUGIN_STATE.read().analyze_file(&entry.path); + if let Some(metrics) = metrics { + entry.custom_fields.insert( + "complexity_metrics".to_string(), + toml::to_string(&metrics).unwrap_or_default(), + ); + + if let Some(ext) = entry.path.extension().and_then(|e| e.to_str()) { + let lang = { + let state = PLUGIN_STATE.read(); + state + .config + .languages + .iter() + .find(|(_, rules)| { + rules.extensions.iter().any(|e| e == ext) + }) + .map(|(lang, _)| lang.clone()) + }; + + if let Some(lang) = lang { + PLUGIN_STATE + .write() + .stats + .entry(lang) + .or_default() + .push((entry.path.clone(), metrics)); + } + } + } } + PluginResponse::Decorated(entry) } - "show-report" => { - println!("{}", self.generate_report()); - Ok(()) + PluginRequest::FormatField(entry, format) => { + let field = self.format_file_info(&entry, &format); + PluginResponse::FormattedField(field) } - "help" => { - let help_text = "Available actions:\n\ - set-thresholds - Set complexity thresholds\n\ - show-report - Show detailed complexity report\n\ - help - Show this help message\n\n"; - println!("{}", help_text); - Ok(()) + PluginRequest::PerformAction(action, args) => { + let result = ACTION_REGISTRY.read().handle(&action, &args); + PluginResponse::ActionResult(result) } - _ => Err(format!("Unknown action: {}", req.action)), }; - - plugin_message::Message::ActionResponse(proto::ActionResponse { - success: result.is_ok(), - error: result.err(), - }) + self.encode_response(response) } - _ => plugin_message::Message::ErrorResponse("Invalid request type".to_string()), - }; - - let response = proto::PluginMessage { - message: Some(response_msg), - }; - let mut buf = bytes::BytesMut::with_capacity(response.encoded_len()); - response.encode(&mut buf).unwrap(); - buf.to_vec() + Err(e) => self.encode_error(&e), + } } } @@ -557,4 +648,18 @@ impl Default for CodeComplexityEstimatorPlugin { } } +impl ConfigurablePlugin for CodeComplexityEstimatorPlugin { + type Config = ComplexityConfig; + + fn config(&self) -> &Self::Config { + self.base.config() + } + + fn config_mut(&mut self) -> &mut Self::Config { + self.base.config_mut() + } +} + +impl ProtobufHandler for CodeComplexityEstimatorPlugin {} + lla_plugin_interface::declare_plugin!(CodeComplexityEstimatorPlugin); From 2c4ee92d71bdda8e667aebee31c86ede6633ac40 Mon Sep 17 00:00:00 2001 From: Mohamed Achaq Date: Fri, 27 Dec 2024 21:46:22 +0100 Subject: [PATCH 14/43] feat: enhance file_hash plugin with new dependencies and functionality - Updated Cargo.toml and Cargo.lock to include new dependencies: `lazy_static`, `parking_lot`, and `serde` for improved concurrency and configuration management. - Refactored the plugin to utilize `lazy_static` for action registry and `parking_lot` for thread-safe state management. - Introduced `FileHashConfig` struct for managing plugin configuration with default color settings for hash display. - Enhanced the `handle_raw_request` method to support new plugin actions and improved response formatting. - Added functionality for formatting hash information and displaying help documentation for user actions. --- Cargo.lock | 4 + plugins/file_hash/Cargo.toml | 4 + plugins/file_hash/src/lib.rs | 326 ++++++++++++++++++++++------------- 3 files changed, 218 insertions(+), 116 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5630b36..a70253c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -446,8 +446,12 @@ version = "0.3.0" dependencies = [ "bytes", "colored", + "lazy_static", "lla_plugin_interface", + "lla_plugin_utils", + "parking_lot", "prost", + "serde", "sha1", "sha2", ] diff --git a/plugins/file_hash/Cargo.toml b/plugins/file_hash/Cargo.toml index 33fd978..29d289e 100644 --- a/plugins/file_hash/Cargo.toml +++ b/plugins/file_hash/Cargo.toml @@ -8,9 +8,13 @@ edition = "2021" sha1 = "0.10.1" sha2 = "0.10.2" lla_plugin_interface = { path = "../../lla_plugin_interface" } +lla_plugin_utils = { path = "../../lla_plugin_utils" } colored = "2.0.0" prost = "0.12" bytes = "1.5" +lazy_static = "1.4" +parking_lot = "0.12" +serde = { version = "1.0", features = ["derive"] } [lib] crate-type = ["cdylib"] diff --git a/plugins/file_hash/src/lib.rs b/plugins/file_hash/src/lib.rs index 064aed8..0549c77 100644 --- a/plugins/file_hash/src/lib.rs +++ b/plugins/file_hash/src/lib.rs @@ -1,33 +1,107 @@ -use colored::Colorize; -use lla_plugin_interface::{ - proto::{self, plugin_message::Message}, - Plugin, +use lazy_static::lazy_static; +use lla_plugin_interface::{Plugin, PluginRequest, PluginResponse}; +use lla_plugin_utils::{ + config::PluginConfig, + ui::components::{BoxComponent, BoxStyle, HelpFormatter, KeyValue, List, Spinner}, + ActionRegistry, BasePlugin, ConfigurablePlugin, ProtobufHandler, }; -use prost::Message as _; +use parking_lot::RwLock; +use serde::{Deserialize, Serialize}; use sha1::Sha1; use sha2::{Digest, Sha256}; -use std::fs::File; -use std::io::{BufReader, Read}; +use std::{ + collections::HashMap, + fs::File, + io::{BufReader, Read}, +}; -pub struct FileHashPlugin; +lazy_static! { + static ref SPINNER: RwLock = RwLock::new(Spinner::new()); + static ref ACTION_REGISTRY: RwLock = RwLock::new({ + let mut registry = ActionRegistry::new(); -impl FileHashPlugin { - pub fn new() -> Self { - FileHashPlugin + lla_plugin_utils::define_action!( + registry, + "help", + "help", + "Show help information", + vec!["lla plugin --name file_hash --action help"], + |_| { + let mut help = HelpFormatter::new("File Hash Plugin".to_string()); + help.add_section("Description".to_string()).add_command( + "".to_string(), + "Calculates SHA1 and SHA256 hashes for files.".to_string(), + vec![], + ); + + help.add_section("Actions".to_string()).add_command( + "help".to_string(), + "Show this help information".to_string(), + vec!["lla plugin --name file_hash --action help".to_string()], + ); + + help.add_section("Formats".to_string()) + .add_command( + "default".to_string(), + "Show basic hash information (first 8 characters)".to_string(), + vec![], + ) + .add_command( + "long".to_string(), + "Show complete hash values".to_string(), + vec![], + ); + + println!( + "{}", + BoxComponent::new(help.render(&FileHashConfig::default().colors)) + .style(BoxStyle::Minimal) + .padding(2) + .render() + ); + Ok(()) + } + ); + + registry + }); +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FileHashConfig { + #[serde(default = "default_colors")] + colors: HashMap, +} + +fn default_colors() -> HashMap { + let mut colors = HashMap::new(); + colors.insert("sha1".to_string(), "bright_green".to_string()); + colors.insert("sha256".to_string(), "bright_yellow".to_string()); + colors.insert("success".to_string(), "bright_green".to_string()); + colors.insert("info".to_string(), "bright_blue".to_string()); + colors.insert("name".to_string(), "bright_yellow".to_string()); + colors +} + +impl Default for FileHashConfig { + fn default() -> Self { + Self { + colors: default_colors(), + } } +} - fn encode_error(&self, error: &str) -> Vec { - use prost::Message; - let error_msg = lla_plugin_interface::proto::PluginMessage { - message: Some( - lla_plugin_interface::proto::plugin_message::Message::ErrorResponse( - error.to_string(), - ), - ), - }; - let mut buf = bytes::BytesMut::with_capacity(error_msg.encoded_len()); - error_msg.encode(&mut buf).unwrap(); - buf.to_vec() +impl PluginConfig for FileHashConfig {} + +pub struct FileHashPlugin { + base: BasePlugin, +} + +impl FileHashPlugin { + pub fn new() -> Self { + Self { + base: BasePlugin::new(), + } } fn calculate_hashes(path: &std::path::Path) -> Option<(String, String)> { @@ -41,111 +115,117 @@ impl FileHashPlugin { Some((sha1, sha256)) } -} -impl Plugin for FileHashPlugin { - fn handle_raw_request(&mut self, request: &[u8]) -> Vec { - let proto_msg = match proto::PluginMessage::decode(request) { - Ok(msg) => msg, - Err(e) => { - let error_msg = proto::PluginMessage { - message: Some(Message::ErrorResponse(format!( - "Failed to decode request: {}", - e - ))), - }; - let mut buf = bytes::BytesMut::with_capacity(error_msg.encoded_len()); - error_msg.encode(&mut buf).unwrap(); - return buf.to_vec(); - } + fn format_hash_info( + &self, + entry: &lla_plugin_interface::DecoratedEntry, + format: &str, + ) -> Option { + if !entry.metadata.is_file { + return None; + } + + let (sha1, sha256) = match ( + entry.custom_fields.get("sha1"), + entry.custom_fields.get("sha256"), + ) { + (Some(s1), Some(s2)) => (s1, s2), + _ => return None, }; - let response_msg = match proto_msg.message { - Some(Message::GetName(_)) => Message::NameResponse(env!("CARGO_PKG_NAME").to_string()), - Some(Message::GetVersion(_)) => { - Message::VersionResponse(env!("CARGO_PKG_VERSION").to_string()) - } - Some(Message::GetDescription(_)) => { - Message::DescriptionResponse(env!("CARGO_PKG_DESCRIPTION").to_string()) + let colors = &self.base.config().colors; + let mut list = List::new().style(BoxStyle::Minimal).key_width(12); + + match format { + "long" => { + list.add_item( + KeyValue::new("SHA1", sha1) + .key_color(colors.get("sha1").unwrap_or(&"white".to_string())) + .value_color(colors.get("sha1").unwrap_or(&"white".to_string())) + .key_width(12) + .render(), + ); + + list.add_item( + KeyValue::new("SHA256", sha256) + .key_color(colors.get("sha256").unwrap_or(&"white".to_string())) + .value_color(colors.get("sha256").unwrap_or(&"white".to_string())) + .key_width(12) + .render(), + ); } - Some(Message::GetSupportedFormats(_)) => { - Message::FormatsResponse(proto::SupportedFormatsResponse { - formats: vec!["default".to_string(), "long".to_string()], - }) + "default" => { + let sha1_short = &sha1[..8]; + let sha256_short = &sha256[..8]; + + list.add_item( + KeyValue::new("SHA1", sha1_short) + .key_color(colors.get("sha1").unwrap_or(&"white".to_string())) + .value_color(colors.get("sha1").unwrap_or(&"white".to_string())) + .key_width(12) + .render(), + ); + + list.add_item( + KeyValue::new("SHA256", sha256_short) + .key_color(colors.get("sha256").unwrap_or(&"white".to_string())) + .value_color(colors.get("sha256").unwrap_or(&"white".to_string())) + .key_width(12) + .render(), + ); } - Some(Message::Decorate(entry)) => { - let mut entry = match lla_plugin_interface::DecoratedEntry::try_from(entry.clone()) - { - Ok(e) => e, - Err(e) => { - return self.encode_error(&format!("Failed to convert entry: {}", e)); - } - }; + _ => return None, + }; - if entry.path.is_file() { - if let Some((sha1, sha256)) = Self::calculate_hashes(&entry.path) { - entry.custom_fields.insert("sha1".to_string(), sha1); - entry.custom_fields.insert("sha256".to_string(), sha256); + Some(format!("\n{}", list.render())) + } +} + +impl Plugin for FileHashPlugin { + fn handle_raw_request(&mut self, request: &[u8]) -> Vec { + match self.decode_request(request) { + Ok(request) => { + let response = match request { + PluginRequest::GetName => { + PluginResponse::Name(env!("CARGO_PKG_NAME").to_string()) } - } - Message::DecoratedResponse(entry.into()) - } - Some(Message::FormatField(req)) => { - let entry = match req.entry { - Some(e) => match lla_plugin_interface::DecoratedEntry::try_from(e) { - Ok(entry) => entry, - Err(e) => { - return self.encode_error(&format!("Failed to convert entry: {}", e)); - } - }, - None => return self.encode_error("Missing entry in format field request"), - }; + PluginRequest::GetVersion => { + PluginResponse::Version(env!("CARGO_PKG_VERSION").to_string()) + } + PluginRequest::GetDescription => { + PluginResponse::Description(env!("CARGO_PKG_DESCRIPTION").to_string()) + } + PluginRequest::GetSupportedFormats => PluginResponse::SupportedFormats(vec![ + "default".to_string(), + "long".to_string(), + ]), + PluginRequest::Decorate(mut entry) => { + if entry.metadata.is_file { + let spinner = SPINNER.write(); + spinner.set_status("Calculating hashes...".to_string()); + + if let Some((sha1, sha256)) = Self::calculate_hashes(&entry.path) { + entry.custom_fields.insert("sha1".to_string(), sha1); + entry.custom_fields.insert("sha256".to_string(), sha256); + } - let formatted = match req.format.as_str() { - "long" | "default" => { - if entry.path.is_dir() { - None - } else { - let sha1 = entry - .custom_fields - .get("sha1") - .map(|s| s[..8].to_string()) - .unwrap_or_default(); - let sha256 = entry - .custom_fields - .get("sha256") - .map(|s| s[..8].to_string()) - .unwrap_or_default(); - Some(format!( - "\n{} {} {}{}\n{} {} {}{}", - "┌".bright_black(), - "SHA1".bright_green().bold(), - "→".bright_black(), - sha1.green(), - "└".bright_black(), - "SHA256".bright_yellow().bold(), - "→".bright_black(), - sha256.yellow() - )) + spinner.finish(); } + PluginResponse::Decorated(entry) + } + PluginRequest::FormatField(entry, format) => { + let field = self.format_hash_info(&entry, &format); + PluginResponse::FormattedField(field) + } + PluginRequest::PerformAction(action, args) => { + let result = ACTION_REGISTRY.read().handle(&action, &args); + PluginResponse::ActionResult(result) } - _ => None, }; - Message::FieldResponse(proto::FormattedFieldResponse { field: formatted }) + self.encode_response(response) } - Some(Message::Action(_)) => Message::ActionResponse(proto::ActionResponse { - success: true, - error: None, - }), - _ => Message::ErrorResponse("Invalid request type".to_string()), - }; - - let response = proto::PluginMessage { - message: Some(response_msg), - }; - let mut buf = bytes::BytesMut::with_capacity(response.encoded_len()); - response.encode(&mut buf).unwrap(); - buf.to_vec() + Err(e) => self.encode_error(&e), + } } } @@ -155,4 +235,18 @@ impl Default for FileHashPlugin { } } +impl ConfigurablePlugin for FileHashPlugin { + type Config = FileHashConfig; + + fn config(&self) -> &Self::Config { + self.base.config() + } + + fn config_mut(&mut self) -> &mut Self::Config { + self.base.config_mut() + } +} + +impl ProtobufHandler for FileHashPlugin {} + lla_plugin_interface::declare_plugin!(FileHashPlugin); From af53ef4159a82eb636521c8ef07bc4d9b18cf234 Mon Sep 17 00:00:00 2001 From: Mohamed Achaq Date: Fri, 27 Dec 2024 21:53:53 +0100 Subject: [PATCH 15/43] feat: enhance duplicate_file_detector plugin with new dependencies and functionality - Updated Cargo.toml and Cargo.lock to include new dependencies: `lla_plugin_utils` and `serde` for improved configuration management and serialization. - Refactored the plugin to utilize `lazy_static` for action registry and `parking_lot` for thread-safe state management. - Introduced `DuplicateConfig` struct for managing plugin configuration with default color settings for duplicate file display. - Enhanced the `handle_raw_request` method to support new plugin actions, including cache clearing and help documentation. - Improved formatting of duplicate information for better user experience. --- Cargo.lock | 2 + plugins/duplicate_file_detector/Cargo.toml | 13 +- plugins/duplicate_file_detector/src/lib.rs | 406 ++++++++++++++------- 3 files changed, 294 insertions(+), 127 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a70253c..a4d7c63 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -401,8 +401,10 @@ dependencies = [ "colored", "lazy_static", "lla_plugin_interface", + "lla_plugin_utils", "parking_lot", "prost", + "serde", "sha2", ] diff --git a/plugins/duplicate_file_detector/Cargo.toml b/plugins/duplicate_file_detector/Cargo.toml index d938ec2..a95dc55 100644 --- a/plugins/duplicate_file_detector/Cargo.toml +++ b/plugins/duplicate_file_detector/Cargo.toml @@ -1,16 +1,19 @@ [package] name = "duplicate_file_detector" -description = "A plugin for the LLA that detects duplicate files." +description = "Detects duplicate files by comparing their content hashes" version = "0.3.0" edition = "2021" [dependencies] -colored = "2.0.0" +sha2 = "0.10.2" lla_plugin_interface = { path = "../../lla_plugin_interface" } -sha2 = "0.10.8" -parking_lot = "0.12.1" -lazy_static = "1.4.0" +lla_plugin_utils = { path = "../../lla_plugin_utils" } +colored = "2.0.0" prost = "0.12" bytes = "1.5" +lazy_static = "1.4" +parking_lot = "0.12" +serde = { version = "1.0", features = ["derive"] } + [lib] crate-type = ["cdylib"] diff --git a/plugins/duplicate_file_detector/src/lib.rs b/plugins/duplicate_file_detector/src/lib.rs index a3fbf2f..adc67ef 100644 --- a/plugins/duplicate_file_detector/src/lib.rs +++ b/plugins/duplicate_file_detector/src/lib.rs @@ -1,17 +1,23 @@ -use colored::Colorize; use lazy_static::lazy_static; -use lla_plugin_interface::{ - proto::{self, plugin_message::Message}, - DecoratedEntry, Plugin, +use lla_plugin_interface::{DecoratedEntry, Plugin, PluginRequest, PluginResponse}; +use lla_plugin_utils::{ + config::PluginConfig, + ui::{ + components::{BoxComponent, BoxStyle, HelpFormatter, KeyValue, List, Spinner}, + TextBlock, + }, + ActionRegistry, BasePlugin, ConfigurablePlugin, ProtobufHandler, }; use parking_lot::RwLock; -use prost::Message as _; +use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; -use std::collections::HashMap; -use std::fs::File; -use std::io::Read; -use std::path::{Path, PathBuf}; -use std::time::SystemTime; +use std::{ + collections::HashMap, + fs::File, + io::Read, + path::{Path, PathBuf}, + time::SystemTime, +}; #[derive(Clone)] struct FileInfo { @@ -21,33 +27,128 @@ struct FileInfo { lazy_static! { static ref CACHE: RwLock>> = RwLock::new(HashMap::new()); + static ref SPINNER: RwLock = RwLock::new(Spinner::new()); + static ref ACTION_REGISTRY: RwLock = RwLock::new({ + let mut registry = ActionRegistry::new(); + + lla_plugin_utils::define_action!( + registry, + "clear-cache", + "clear-cache", + "Clear the duplicate file detection cache", + vec!["lla plugin --name duplicate_file_detector --action clear-cache"], + |_| { + let spinner = SPINNER.write(); + spinner.set_status("Clearing cache...".to_string()); + CACHE.write().clear(); + spinner.finish(); + println!( + "{}", + BoxComponent::new( + TextBlock::new("Cache cleared successfully") + .color("bright_green") + .build() + ) + .style(BoxStyle::Minimal) + .padding(1) + .render() + ); + Ok(()) + } + ); + + lla_plugin_utils::define_action!( + registry, + "help", + "help", + "Show help information", + vec!["lla plugin --name duplicate_file_detector --action help"], + |_| { + let mut help = HelpFormatter::new("Duplicate File Detector Plugin".to_string()); + help.add_section("Description".to_string()).add_command( + "".to_string(), + "Detects duplicate files by comparing their content hashes.".to_string(), + vec![], + ); + + help.add_section("Actions".to_string()) + .add_command( + "clear-cache".to_string(), + "Clear the duplicate file detection cache".to_string(), + vec![ + "lla plugin --name duplicate_file_detector --action clear-cache" + .to_string(), + ], + ) + .add_command( + "help".to_string(), + "Show this help information".to_string(), + vec!["lla plugin --name duplicate_file_detector --action help".to_string()], + ); + + help.add_section("Formats".to_string()) + .add_command( + "default".to_string(), + "Show basic duplicate information".to_string(), + vec![], + ) + .add_command( + "long".to_string(), + "Show detailed duplicate information including paths".to_string(), + vec![], + ); + + println!( + "{}", + BoxComponent::new(help.render(&DuplicateConfig::default().colors)) + .style(BoxStyle::Minimal) + .padding(2) + .render() + ); + Ok(()) + } + ); + + registry + }); } -pub struct DuplicateFileDetectorPlugin; +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DuplicateConfig { + #[serde(default = "default_colors")] + colors: HashMap, +} -impl Default for DuplicateFileDetectorPlugin { +fn default_colors() -> HashMap { + let mut colors = HashMap::new(); + colors.insert("duplicate".to_string(), "bright_red".to_string()); + colors.insert("has_duplicates".to_string(), "bright_yellow".to_string()); + colors.insert("path".to_string(), "bright_cyan".to_string()); + colors.insert("success".to_string(), "bright_green".to_string()); + colors.insert("info".to_string(), "bright_blue".to_string()); + colors.insert("name".to_string(), "bright_yellow".to_string()); + colors +} + +impl Default for DuplicateConfig { fn default() -> Self { - Self::new() + Self { + colors: default_colors(), + } } } +impl PluginConfig for DuplicateConfig {} + +pub struct DuplicateFileDetectorPlugin { + base: BasePlugin, +} + impl DuplicateFileDetectorPlugin { pub fn new() -> Self { - DuplicateFileDetectorPlugin - } - - fn encode_error(&self, error: &str) -> Vec { - use prost::Message; - let error_msg = lla_plugin_interface::proto::PluginMessage { - message: Some( - lla_plugin_interface::proto::plugin_message::Message::ErrorResponse( - error.to_string(), - ), - ), - }; - let mut buf = bytes::BytesMut::with_capacity(error_msg.encoded_len()); - error_msg.encode(&mut buf).unwrap(); - buf.to_vec() + Self { + base: BasePlugin::new(), + } } fn get_file_hash(path: &Path) -> Option { @@ -70,6 +171,9 @@ impl DuplicateFileDetectorPlugin { return entry; } + let spinner = SPINNER.write(); + spinner.set_status("Checking for duplicates...".to_string()); + if let Some(hash) = Self::get_file_hash(&entry.path) { let mut cache = CACHE.write(); let entries = cache.entry(hash).or_default(); @@ -118,113 +222,171 @@ impl DuplicateFileDetectorPlugin { } } + spinner.finish(); entry } -} -impl Plugin for DuplicateFileDetectorPlugin { - fn handle_raw_request(&mut self, request: &[u8]) -> Vec { - let proto_msg = match proto::PluginMessage::decode(request) { - Ok(msg) => msg, - Err(e) => { - return self.encode_error(&format!("Failed to decode request: {}", e)); - } - }; + fn format_duplicate_info(&self, entry: &DecoratedEntry, format: &str) -> Option { + let colors = &self.base.config().colors; + let mut list = List::new().style(BoxStyle::Minimal).key_width(15); - let response_msg = match proto_msg.message { - Some(Message::GetName(_)) => Message::NameResponse(env!("CARGO_PKG_NAME").to_string()), - Some(Message::GetVersion(_)) => { - Message::VersionResponse(env!("CARGO_PKG_VERSION").to_string()) - } - Some(Message::GetDescription(_)) => { - Message::DescriptionResponse(env!("CARGO_PKG_DESCRIPTION").to_string()) - } - Some(Message::GetSupportedFormats(_)) => { - Message::FormatsResponse(proto::SupportedFormatsResponse { - formats: vec!["default".to_string(), "long".to_string()], - }) - } - Some(Message::Decorate(entry)) => { - let entry = match DecoratedEntry::try_from(entry.clone()) { - Ok(e) => e, - Err(e) => { - return self.encode_error(&format!("Failed to convert entry: {}", e)); + if entry.custom_fields.get("has_duplicates").is_some() { + match format { + "long" => { + list.add_item( + KeyValue::new("Status", "HAS DUPLICATES") + .key_color(colors.get("info").unwrap_or(&"white".to_string())) + .value_color( + colors.get("has_duplicates").unwrap_or(&"white".to_string()), + ) + .key_width(15) + .render(), + ); + + if let Some(paths) = entry.custom_fields.get("duplicate_paths") { + list.add_item( + KeyValue::new("Duplicate Copies", paths) + .key_color(colors.get("info").unwrap_or(&"white".to_string())) + .value_color(colors.get("path").unwrap_or(&"white".to_string())) + .key_width(15) + .render(), + ); } - }; - Message::DecoratedResponse(self.process_entry(entry).into()) + } + "default" => { + if let Some(paths) = entry.custom_fields.get("duplicate_paths") { + list.add_item( + KeyValue::new("Status", format!("HAS DUPLICATES: {}", paths)) + .key_color(colors.get("info").unwrap_or(&"white".to_string())) + .value_color( + colors.get("has_duplicates").unwrap_or(&"white".to_string()), + ) + .key_width(15) + .render(), + ); + } else { + list.add_item( + KeyValue::new("Status", "HAS DUPLICATES") + .key_color(colors.get("info").unwrap_or(&"white".to_string())) + .value_color( + colors.get("has_duplicates").unwrap_or(&"white".to_string()), + ) + .key_width(15) + .render(), + ); + } + } + _ => return None, } - Some(Message::FormatField(req)) => { - let entry = match req.entry { - Some(e) => match DecoratedEntry::try_from(e) { - Ok(entry) => entry, - Err(e) => { - return self.encode_error(&format!("Failed to convert entry: {}", e)); - } - }, - None => return self.encode_error("Missing entry in format field request"), - }; + } else if entry.custom_fields.get("is_duplicate").is_some() { + match format { + "long" => { + list.add_item( + KeyValue::new("Status", "DUPLICATE") + .key_color(colors.get("info").unwrap_or(&"white".to_string())) + .value_color(colors.get("duplicate").unwrap_or(&"white".to_string())) + .key_width(15) + .render(), + ); - let formatted = if entry.custom_fields.get("has_duplicates").is_some() { - match req.format.as_str() { - "long" => Some(format!( - "{} {}", - "HAS DUPLICATES".bright_yellow(), - format!( - "copies: {}", - entry.custom_fields.get("duplicate_paths").unwrap() - ) - .bright_cyan() - )), - "default" => Some(format!("{}", "HAS DUPLICATES".bright_yellow())), - _ => None, + if let Some(original) = entry.custom_fields.get("original_path") { + list.add_item( + KeyValue::new("Original File", original) + .key_color(colors.get("info").unwrap_or(&"white".to_string())) + .value_color(colors.get("path").unwrap_or(&"white".to_string())) + .key_width(15) + .render(), + ); } - } else { - let cache = CACHE.read(); - let mut original_path = None; - let is_duplicate = cache.values().any(|entries| { - if let Some(oldest) = entries.iter().min_by_key(|f| f.modified) { - let is_dup = entries - .iter() - .any(|f| f.path == entry.path && oldest.path != entry.path); - if is_dup { - original_path = Some(oldest.path.to_string_lossy().to_string()); - } - is_dup - } else { - false - } - }); - - if is_duplicate { - match req.format.as_str() { - "long" => Some(format!( - "{} {}", - "DUPLICATE".bright_red(), - format!("of: {}", original_path.unwrap()).bright_cyan() - )), - "default" => Some(format!("{}", "DUPLICATE".bright_red())), - _ => None, - } + } + "default" => { + if let Some(original) = entry.custom_fields.get("original_path") { + list.add_item( + KeyValue::new("Status", format!("DUPLICATE of {}", original)) + .key_color(colors.get("info").unwrap_or(&"white".to_string())) + .value_color( + colors.get("duplicate").unwrap_or(&"white".to_string()), + ) + .key_width(15) + .render(), + ); } else { - None + list.add_item( + KeyValue::new("Status", "DUPLICATE") + .key_color(colors.get("info").unwrap_or(&"white".to_string())) + .value_color( + colors.get("duplicate").unwrap_or(&"white".to_string()), + ) + .key_width(15) + .render(), + ); + } + } + _ => return None, + } + } else { + return None; + } + + Some(format!("\n{}", list.render())) + } +} + +impl Plugin for DuplicateFileDetectorPlugin { + fn handle_raw_request(&mut self, request: &[u8]) -> Vec { + match self.decode_request(request) { + Ok(request) => { + let response = match request { + PluginRequest::GetName => { + PluginResponse::Name(env!("CARGO_PKG_NAME").to_string()) + } + PluginRequest::GetVersion => { + PluginResponse::Version(env!("CARGO_PKG_VERSION").to_string()) + } + PluginRequest::GetDescription => { + PluginResponse::Description(env!("CARGO_PKG_DESCRIPTION").to_string()) + } + PluginRequest::GetSupportedFormats => PluginResponse::SupportedFormats(vec![ + "default".to_string(), + "long".to_string(), + ]), + PluginRequest::Decorate(entry) => { + PluginResponse::Decorated(self.process_entry(entry)) + } + PluginRequest::FormatField(entry, format) => { + let field = self.format_duplicate_info(&entry, &format); + PluginResponse::FormattedField(field) + } + PluginRequest::PerformAction(action, args) => { + let result = ACTION_REGISTRY.read().handle(&action, &args); + PluginResponse::ActionResult(result) } }; - Message::FieldResponse(proto::FormattedFieldResponse { field: formatted }) + self.encode_response(response) } - Some(Message::Action(_)) => Message::ActionResponse(proto::ActionResponse { - success: true, - error: None, - }), - _ => Message::ErrorResponse("Invalid request type".to_string()), - }; - - let response = proto::PluginMessage { - message: Some(response_msg), - }; - let mut buf = bytes::BytesMut::with_capacity(response.encoded_len()); - response.encode(&mut buf).unwrap(); - buf.to_vec() + Err(e) => self.encode_error(&e), + } + } +} + +impl Default for DuplicateFileDetectorPlugin { + fn default() -> Self { + Self::new() } } +impl ConfigurablePlugin for DuplicateFileDetectorPlugin { + type Config = DuplicateConfig; + + fn config(&self) -> &Self::Config { + self.base.config() + } + + fn config_mut(&mut self) -> &mut Self::Config { + self.base.config_mut() + } +} + +impl ProtobufHandler for DuplicateFileDetectorPlugin {} + lla_plugin_interface::declare_plugin!(DuplicateFileDetectorPlugin); From fd99e020c7c1ebb70973294cf4a3c2cb3e10a48c Mon Sep 17 00:00:00 2001 From: Mohamed Achaq Date: Fri, 27 Dec 2024 21:59:04 +0100 Subject: [PATCH 16/43] feat: enhance file_tagger plugin with new dependencies and functionality - Updated Cargo.toml and Cargo.lock to include new dependencies: `lazy_static`, `parking_lot`, and `serde` for improved configuration management and concurrency. - Refactored the plugin to utilize `lazy_static` for action registry and `parking_lot` for thread-safe state management. - Introduced `TaggerConfig` struct for managing plugin configuration with default color settings for tag display. - Enhanced the `handle_raw_request` method to support new actions for adding, removing, and listing tags, along with improved help documentation. - Improved formatting of tag information for better user experience. --- Cargo.lock | 4 + plugins/file_tagger/Cargo.toml | 10 +- plugins/file_tagger/src/lib.rs | 485 ++++++++++++++++++++------------- 3 files changed, 314 insertions(+), 185 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a4d7c63..b82397a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -480,8 +480,12 @@ dependencies = [ "bytes", "colored", "dirs", + "lazy_static", "lla_plugin_interface", + "lla_plugin_utils", + "parking_lot", "prost", + "serde", ] [[package]] diff --git a/plugins/file_tagger/Cargo.toml b/plugins/file_tagger/Cargo.toml index 5d56163..d09faaf 100644 --- a/plugins/file_tagger/Cargo.toml +++ b/plugins/file_tagger/Cargo.toml @@ -1,15 +1,19 @@ [package] name = "file_tagger" -description = "A plugin for tagging files and filtering by tags" +description = "Add and manage tags for files" version = "0.3.0" edition = "2021" [dependencies] -colored = "2.0.0" lla_plugin_interface = { path = "../../lla_plugin_interface" } -dirs = "5.0.1" +lla_plugin_utils = { path = "../../lla_plugin_utils" } +colored = "2.0.0" prost = "0.12" bytes = "1.5" +lazy_static = "1.4" +parking_lot = "0.12" +serde = { version = "1.0", features = ["derive"] } +dirs = "5.0" [lib] crate-type = ["cdylib"] diff --git a/plugins/file_tagger/src/lib.rs b/plugins/file_tagger/src/lib.rs index bcf6b02..1e51420 100644 --- a/plugins/file_tagger/src/lib.rs +++ b/plugins/file_tagger/src/lib.rs @@ -1,25 +1,215 @@ -use colored::Colorize; -use lla_plugin_interface::{ - proto::{self, plugin_message::Message}, - Plugin, +use lazy_static::lazy_static; +use lla_plugin_interface::{Plugin, PluginRequest, PluginResponse}; +use lla_plugin_utils::{ + config::PluginConfig, + ui::{ + components::{BoxComponent, BoxStyle, HelpFormatter, KeyValue, List, Spinner}, + TextBlock, + }, + ActionRegistry, BasePlugin, ConfigurablePlugin, ProtobufHandler, +}; +use parking_lot::RwLock; +use serde::{Deserialize, Serialize}; +use std::{ + collections::HashMap, + fs::File, + io::{BufRead, BufReader, Write}, + path::PathBuf, }; -use prost::Message as _; -use std::collections::HashMap; -use std::fs::File; -use std::io::{BufRead, BufReader, Write}; -use std::path::PathBuf; -pub struct FileTaggerPlugin { - tag_file: PathBuf, - tags: HashMap>, +lazy_static! { + static ref SPINNER: RwLock = RwLock::new(Spinner::new()); + static ref ACTION_REGISTRY: RwLock = RwLock::new({ + let mut registry = ActionRegistry::new(); + + lla_plugin_utils::define_action!( + registry, + "add-tag", + "add-tag ", + "Add a tag to a file", + vec![ + "lla plugin --name file_tagger --action add-tag --args \"/path/to/file\" \"mytag\"" + ], + |args| { + if args.len() != 2 { + return Err("Usage: add-tag ".to_string()); + } + let mut plugin = FileTaggerPlugin::new(); + plugin.add_tag(&args[0], &args[1]); + println!( + "{}", + BoxComponent::new( + TextBlock::new(format!("Added tag '{}' to {}", args[1], args[0])) + .color("bright_green") + .build() + ) + .style(BoxStyle::Minimal) + .padding(1) + .render() + ); + Ok(()) + } + ); + + lla_plugin_utils::define_action!( + registry, + "remove-tag", + "remove-tag ", + "Remove a tag from a file", + vec!["lla plugin --name file_tagger --action remove-tag --args \"/path/to/file\" \"mytag\""], + |args| { + if args.len() != 2 { + return Err("Usage: remove-tag ".to_string()); + } + let mut plugin = FileTaggerPlugin::new(); + plugin.remove_tag(&args[0], &args[1]); + println!( + "{}", + BoxComponent::new( + TextBlock::new(format!("Removed tag '{}' from {}", args[1], args[0])) + .color("bright_green") + .build() + ) + .style(BoxStyle::Minimal) + .padding(1) + .render() + ); + Ok(()) + } + ); + + lla_plugin_utils::define_action!( + registry, + "list-tags", + "list-tags ", + "List all tags for a file", + vec!["lla plugin --name file_tagger --action list-tags --args \"/path/to/file\""], + |args| { + if args.len() != 1 { + return Err("Usage: list-tags ".to_string()); + } + let plugin = FileTaggerPlugin::new(); + let tags = plugin.get_tags(&args[0]); + let mut list = List::new().style(BoxStyle::Minimal).key_width(12); + + if tags.is_empty() { + list.add_item( + KeyValue::new("Info", format!("No tags found for {}", args[0])) + .key_color("bright_blue") + .value_color("bright_yellow") + .key_width(12) + .render(), + ); + } else { + list.add_item( + KeyValue::new("Tags", tags.join(", ")) + .key_color("bright_green") + .value_color("bright_cyan") + .key_width(12) + .render(), + ); + } + + println!("\n{}", list.render()); + Ok(()) + } + ); + + lla_plugin_utils::define_action!( + registry, + "help", + "help", + "Show help information", + vec!["lla plugin --name file_tagger --action help"], + |_| { + let mut help = HelpFormatter::new("File Tagger Plugin".to_string()); + help.add_section("Description".to_string()).add_command( + "".to_string(), + "Add and manage tags for files.".to_string(), + vec![], + ); + + help.add_section("Actions".to_string()) + .add_command( + "add-tag".to_string(), + "Add a tag to a file".to_string(), + vec!["lla plugin --name file_tagger --action add-tag --args \"/path/to/file\" \"mytag\"".to_string()], + ) + .add_command( + "remove-tag".to_string(), + "Remove a tag from a file".to_string(), + vec!["lla plugin --name file_tagger --action remove-tag --args \"/path/to/file\" \"mytag\"".to_string()], + ) + .add_command( + "list-tags".to_string(), + "List all tags for a file".to_string(), + vec!["lla plugin --name file_tagger --action list-tags --args \"/path/to/file\"".to_string()], + ) + .add_command( + "help".to_string(), + "Show this help information".to_string(), + vec!["lla plugin --name file_tagger --action help".to_string()], + ); + + help.add_section("Formats".to_string()) + .add_command( + "default".to_string(), + "Show tags in a compact format".to_string(), + vec![], + ) + .add_command( + "long".to_string(), + "Show tags in a detailed format".to_string(), + vec![], + ); + + println!( + "{}", + BoxComponent::new(help.render(&TaggerConfig::default().colors)) + .style(BoxStyle::Minimal) + .padding(2) + .render() + ); + Ok(()) + } + ); + + registry + }); } -impl Default for FileTaggerPlugin { +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TaggerConfig { + #[serde(default = "default_colors")] + colors: HashMap, +} + +fn default_colors() -> HashMap { + let mut colors = HashMap::new(); + colors.insert("tag".to_string(), "bright_cyan".to_string()); + colors.insert("tag_label".to_string(), "bright_green".to_string()); + colors.insert("success".to_string(), "bright_green".to_string()); + colors.insert("info".to_string(), "bright_blue".to_string()); + colors.insert("name".to_string(), "bright_yellow".to_string()); + colors +} + +impl Default for TaggerConfig { fn default() -> Self { - Self::new() + Self { + colors: default_colors(), + } } } +impl PluginConfig for TaggerConfig {} + +pub struct FileTaggerPlugin { + base: BasePlugin, + tag_file: PathBuf, + tags: HashMap>, +} + impl FileTaggerPlugin { pub fn new() -> Self { let tag_file = dirs::config_dir() @@ -27,21 +217,11 @@ impl FileTaggerPlugin { .join("lla") .join("file_tags.txt"); let tags = Self::load_tags(&tag_file); - FileTaggerPlugin { tag_file, tags } - } - - fn encode_error(&self, error: &str) -> Vec { - use prost::Message; - let error_msg = lla_plugin_interface::proto::PluginMessage { - message: Some( - lla_plugin_interface::proto::plugin_message::Message::ErrorResponse( - error.to_string(), - ), - ), - }; - let mut buf = bytes::BytesMut::with_capacity(error_msg.encoded_len()); - error_msg.encode(&mut buf).unwrap(); - buf.to_vec() + Self { + base: BasePlugin::new(), + tag_file, + tags, + } } fn load_tags(path: &PathBuf) -> HashMap> { @@ -94,174 +274,115 @@ impl FileTaggerPlugin { fn get_tags(&self, file_path: &str) -> Vec { self.tags.get(file_path).cloned().unwrap_or_default() } -} -impl Plugin for FileTaggerPlugin { - fn handle_raw_request(&mut self, request: &[u8]) -> Vec { - let proto_msg = match proto::PluginMessage::decode(request) { - Ok(msg) => msg, - Err(e) => { - let error_msg = proto::PluginMessage { - message: Some(Message::ErrorResponse(format!( - "Failed to decode request: {}", - e - ))), - }; - let mut buf = bytes::BytesMut::with_capacity(error_msg.encoded_len()); - error_msg.encode(&mut buf).unwrap(); - return buf.to_vec(); - } - }; + fn format_tags( + &self, + entry: &lla_plugin_interface::DecoratedEntry, + format: &str, + ) -> Option { + let tags = entry.custom_fields.get("tags")?; + if tags.is_empty() { + return None; + } - let response_msg = match proto_msg.message { - Some(Message::GetName(_)) => Message::NameResponse(env!("CARGO_PKG_NAME").to_string()), - Some(Message::GetVersion(_)) => { - Message::VersionResponse(env!("CARGO_PKG_VERSION").to_string()) - } - Some(Message::GetDescription(_)) => { - Message::DescriptionResponse(env!("CARGO_PKG_DESCRIPTION").to_string()) - } - Some(Message::GetSupportedFormats(_)) => { - Message::FormatsResponse(proto::SupportedFormatsResponse { - formats: vec!["default".to_string()], - }) - } - Some(Message::Decorate(entry)) => { - let mut entry = match lla_plugin_interface::DecoratedEntry::try_from(entry.clone()) - { - Ok(e) => e, - Err(e) => { - return self.encode_error(&format!("Failed to convert entry: {}", e)); - } - }; + let colors = &self.base.config().colors; + let mut list = List::new().style(BoxStyle::Minimal).key_width(12); - let tags = self.get_tags(entry.path.to_str().unwrap_or("")); - if !tags.is_empty() { - entry - .custom_fields - .insert("tags".to_string(), tags.join(", ")); + match format { + "long" => { + for tag in tags.split(", ") { + list.add_item( + KeyValue::new("Tag", tag) + .key_color(colors.get("tag_label").unwrap_or(&"white".to_string())) + .value_color(colors.get("tag").unwrap_or(&"white".to_string())) + .key_width(12) + .render(), + ); } - Message::DecoratedResponse(entry.into()) + Some(format!("\n{}", list.render())) } - Some(Message::FormatField(req)) => { - let entry = match req.entry { - Some(e) => match lla_plugin_interface::DecoratedEntry::try_from(e) { - Ok(entry) => entry, - Err(e) => { - return self.encode_error(&format!("Failed to convert entry: {}", e)); - } - }, - None => return self.encode_error("Missing entry in format field request"), - }; - - let formatted = entry.custom_fields.get("tags").map(|tags| { - format!( - "[{}]", + "default" => { + list.add_item( + KeyValue::new( + "Tags", tags.split(", ") - .map(|t| t.cyan().to_string()) + .map(|t| format!("[{}]", t)) .collect::>() - .join(", ") + .join(" "), ) - }); - Message::FieldResponse(proto::FormattedFieldResponse { field: formatted }) + .key_color(colors.get("tag_label").unwrap_or(&"white".to_string())) + .value_color(colors.get("tag").unwrap_or(&"white".to_string())) + .key_width(12) + .render(), + ); + Some(format!("\n{}", list.render())) } - Some(Message::Action(req)) => match req.action.as_str() { - "add-tag" => { - if req.args.len() != 2 { - println!("{} add-tag ", "Usage:".bright_cyan()); - return self.encode_error("Invalid number of arguments for add-tag"); + _ => None, + } + } +} + +impl Plugin for FileTaggerPlugin { + fn handle_raw_request(&mut self, request: &[u8]) -> Vec { + match self.decode_request(request) { + Ok(request) => { + let response = match request { + PluginRequest::GetName => { + PluginResponse::Name(env!("CARGO_PKG_NAME").to_string()) } - self.add_tag(&req.args[0], &req.args[1]); - println!( - "{} tag '{}' to {}", - "Added".bright_green(), - req.args[1].cyan(), - req.args[0].bright_blue() - ); - Message::ActionResponse(proto::ActionResponse { - success: true, - error: None, - }) - } - "remove-tag" => { - if req.args.len() != 2 { - println!("{} remove-tag ", "Usage:".bright_cyan()); - return self.encode_error("Invalid number of arguments for remove-tag"); + PluginRequest::GetVersion => { + PluginResponse::Version(env!("CARGO_PKG_VERSION").to_string()) } - self.remove_tag(&req.args[0], &req.args[1]); - println!( - "{} tag '{}' from {}", - "Removed".bright_green(), - req.args[1].cyan(), - req.args[0].bright_blue() - ); - Message::ActionResponse(proto::ActionResponse { - success: true, - error: None, - }) - } - "list-tags" => { - if req.args.len() != 1 { - println!("{} list-tags ", "Usage:".bright_cyan()); - return self.encode_error("Invalid number of arguments for list-tags"); + PluginRequest::GetDescription => { + PluginResponse::Description(env!("CARGO_PKG_DESCRIPTION").to_string()) } - let tags = self.get_tags(&req.args[0]); - if tags.is_empty() { - println!( - "{} No tags found for {}", - "Info:".bright_blue(), - req.args[0].bright_yellow() - ); - } else { - println!( - "{} for {}:", - "Tags".bright_green(), - req.args[0].bright_blue() - ); - for tag in tags { - println!(" {} {}", "→".bright_cyan(), tag.bright_yellow()); + PluginRequest::GetSupportedFormats => PluginResponse::SupportedFormats(vec![ + "default".to_string(), + "long".to_string(), + ]), + PluginRequest::Decorate(mut entry) => { + let tags = self.get_tags(entry.path.to_str().unwrap_or("")); + if !tags.is_empty() { + entry + .custom_fields + .insert("tags".to_string(), tags.join(", ")); } + PluginResponse::Decorated(entry) } - Message::ActionResponse(proto::ActionResponse { - success: true, - error: None, - }) - } - "help" => { - println!("{}", "File Tagger Commands".bright_green().bold()); - println!(); - println!("{}", "Available actions:".bright_yellow()); - println!(" {} ", "add-tag".bright_cyan()); - println!(" Add a tag to a file"); - println!(); - println!(" {} ", "remove-tag".bright_cyan()); - println!(" Remove a tag from a file"); - println!(); - println!(" {} ", "list-tags".bright_cyan()); - println!(" List all tags for a file"); - Message::ActionResponse(proto::ActionResponse { - success: true, - error: None, - }) - } - _ => { - println!("{} Unknown action: {}", "Error:".bright_red(), req.action); - Message::ActionResponse(proto::ActionResponse { - success: false, - error: Some(format!("Unknown action: {}", req.action)), - }) - } - }, - _ => Message::ErrorResponse("Invalid request type".to_string()), - }; - - let response = proto::PluginMessage { - message: Some(response_msg), - }; - let mut buf = bytes::BytesMut::with_capacity(response.encoded_len()); - response.encode(&mut buf).unwrap(); - buf.to_vec() + PluginRequest::FormatField(entry, format) => { + let field = self.format_tags(&entry, &format); + PluginResponse::FormattedField(field) + } + PluginRequest::PerformAction(action, args) => { + let result = ACTION_REGISTRY.read().handle(&action, &args); + PluginResponse::ActionResult(result) + } + }; + self.encode_response(response) + } + Err(e) => self.encode_error(&e), + } } } +impl Default for FileTaggerPlugin { + fn default() -> Self { + Self::new() + } +} + +impl ConfigurablePlugin for FileTaggerPlugin { + type Config = TaggerConfig; + + fn config(&self) -> &Self::Config { + self.base.config() + } + + fn config_mut(&mut self) -> &mut Self::Config { + self.base.config_mut() + } +} + +impl ProtobufHandler for FileTaggerPlugin {} + lla_plugin_interface::declare_plugin!(FileTaggerPlugin); From aa8083b48fb603579007c443139494fcd77e16bd Mon Sep 17 00:00:00 2001 From: Mohamed Achaq Date: Fri, 27 Dec 2024 22:04:52 +0100 Subject: [PATCH 17/43] feat: enhance git_status plugin with new dependencies and functionality - Updated Cargo.toml and Cargo.lock to include new dependencies: `lazy_static`, `parking_lot`, and `serde` for improved configuration management and concurrency. - Refactored the plugin to utilize `lazy_static` for action registry and `parking_lot` for thread-safe state management. - Introduced `GitConfig` struct for managing plugin configuration with default color settings for Git status display. - Enhanced the `handle_raw_request` method to support new actions for formatting and displaying Git repository status information. - Improved help documentation and formatting for better user experience. --- Cargo.lock | 4 + plugins/git_status/Cargo.toml | 8 +- plugins/git_status/src/lib.rs | 518 ++++++++++++++++++++-------------- 3 files changed, 322 insertions(+), 208 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b82397a..421dafc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -521,8 +521,12 @@ version = "0.3.0" dependencies = [ "bytes", "colored", + "lazy_static", "lla_plugin_interface", + "lla_plugin_utils", + "parking_lot", "prost", + "serde", ] [[package]] diff --git a/plugins/git_status/Cargo.toml b/plugins/git_status/Cargo.toml index 77aec61..7c8b532 100644 --- a/plugins/git_status/Cargo.toml +++ b/plugins/git_status/Cargo.toml @@ -1,14 +1,18 @@ [package] name = "git_status" -description = "Shows the git status of each file" +description = "Shows Git repository status information" version = "0.3.0" edition = "2021" [dependencies] lla_plugin_interface = { path = "../../lla_plugin_interface" } -colored = "2.1.0" +lla_plugin_utils = { path = "../../lla_plugin_utils" } +colored = "2.0.0" prost = "0.12" bytes = "1.5" +lazy_static = "1.4" +parking_lot = "0.12" +serde = { version = "1.0", features = ["derive"] } [lib] crate-type = ["cdylib"] diff --git a/plugins/git_status/src/lib.rs b/plugins/git_status/src/lib.rs index bcd287c..845fe6f 100644 --- a/plugins/git_status/src/lib.rs +++ b/plugins/git_status/src/lib.rs @@ -1,38 +1,107 @@ -use colored::Colorize; -use lla_plugin_interface::{ - proto::{self, plugin_message::Message}, - Plugin, +use lazy_static::lazy_static; +use lla_plugin_interface::{Plugin, PluginRequest, PluginResponse}; +use lla_plugin_utils::{ + config::PluginConfig, + ui::components::{BoxComponent, BoxStyle, HelpFormatter, KeyValue, List, Spinner}, + ActionRegistry, BasePlugin, ConfigurablePlugin, ProtobufHandler, }; -use prost::Message as _; -use std::env; -use std::path::Path; -use std::process::Command; +use parking_lot::RwLock; +use serde::{Deserialize, Serialize}; +use std::{collections::HashMap, path::Path, process::Command}; -pub struct GitStatusPlugin; +lazy_static! { + static ref SPINNER: RwLock = RwLock::new(Spinner::new()); + static ref ACTION_REGISTRY: RwLock = RwLock::new({ + let mut registry = ActionRegistry::new(); -impl Default for GitStatusPlugin { + lla_plugin_utils::define_action!( + registry, + "help", + "help", + "Show help information", + vec!["lla plugin --name git_status --action help"], + |_| { + let mut help = HelpFormatter::new("Git Status Plugin".to_string()); + help.add_section("Description".to_string()).add_command( + "".to_string(), + "Shows Git repository status information for files and directories." + .to_string(), + vec![], + ); + + help.add_section("Actions".to_string()).add_command( + "help".to_string(), + "Show this help information".to_string(), + vec!["lla plugin --name git_status --action help".to_string()], + ); + + help.add_section("Formats".to_string()) + .add_command( + "default".to_string(), + "Show basic Git status information".to_string(), + vec![], + ) + .add_command( + "long".to_string(), + "Show detailed Git status information including branch and commit details" + .to_string(), + vec![], + ); + + println!( + "{}", + BoxComponent::new(help.render(&GitConfig::default().colors)) + .style(BoxStyle::Minimal) + .padding(2) + .render() + ); + Ok(()) + } + ); + + registry + }); +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GitConfig { + #[serde(default = "default_colors")] + colors: HashMap, +} + +fn default_colors() -> HashMap { + let mut colors = HashMap::new(); + colors.insert("clean".to_string(), "bright_green".to_string()); + colors.insert("modified".to_string(), "bright_yellow".to_string()); + colors.insert("staged".to_string(), "bright_green".to_string()); + colors.insert("untracked".to_string(), "bright_blue".to_string()); + colors.insert("conflict".to_string(), "bright_red".to_string()); + colors.insert("branch".to_string(), "bright_cyan".to_string()); + colors.insert("commit".to_string(), "bright_yellow".to_string()); + colors.insert("info".to_string(), "bright_blue".to_string()); + colors.insert("name".to_string(), "bright_yellow".to_string()); + colors +} + +impl Default for GitConfig { fn default() -> Self { - Self::new() + Self { + colors: default_colors(), + } } } +impl PluginConfig for GitConfig {} + +pub struct GitStatusPlugin { + base: BasePlugin, +} + impl GitStatusPlugin { pub fn new() -> Self { - GitStatusPlugin - } - - fn encode_error(&self, error: &str) -> Vec { - use prost::Message; - let error_msg = lla_plugin_interface::proto::PluginMessage { - message: Some( - lla_plugin_interface::proto::plugin_message::Message::ErrorResponse( - error.to_string(), - ), - ), - }; - let mut buf = bytes::BytesMut::with_capacity(error_msg.encoded_len()); - error_msg.encode(&mut buf).unwrap(); - buf.to_vec() + Self { + base: BasePlugin::new(), + } } fn is_git_repo(path: &Path) -> bool { @@ -102,236 +171,273 @@ impl GitStatusPlugin { match (index_status, worktree_status) { ('M', ' ') => { staged += 1; - formatted_entries.push(format!("{} staged", "✓".bright_green())); + formatted_entries.push("staged"); } (' ', 'M') => { modified += 1; - formatted_entries.push(format!("{} modified", "±".bright_yellow())); + formatted_entries.push("modified"); } ('M', 'M') => { staged += 1; modified += 1; - formatted_entries.push(format!("{} staged & modified", "±".bright_yellow())); + formatted_entries.push("staged & modified"); } ('A', ' ') => { staged += 1; - formatted_entries.push(format!("{} new file", "✚".bright_green())); + formatted_entries.push("new file"); } ('D', ' ') | (' ', 'D') => { modified += 1; - formatted_entries.push(format!("{} deleted", "✖".bright_red())); + formatted_entries.push("deleted"); } ('R', _) => { staged += 1; - formatted_entries.push(format!("{} renamed", "➜".bright_purple())); + formatted_entries.push("renamed"); } ('C', _) => { staged += 1; - formatted_entries.push(format!("{} copied", "↠".bright_cyan())); + formatted_entries.push("copied"); } ('U', _) | (_, 'U') => { conflicts += 1; - formatted_entries.push(format!("{} conflict", "⚡".bright_magenta())); + formatted_entries.push("conflict"); } (' ', '?') => { untracked += 1; - formatted_entries.push(format!("{} untracked", "?".bright_blue())); + formatted_entries.push("untracked"); } _ => {} } } let status_summary = if formatted_entries.is_empty() { - "clean".bright_green().to_string() + "clean".to_string() } else { formatted_entries.join(", ") }; (status_summary, staged, modified, untracked, conflicts) } -} -impl Plugin for GitStatusPlugin { - fn handle_raw_request(&mut self, request: &[u8]) -> Vec { - let proto_msg = match proto::PluginMessage::decode(request) { - Ok(msg) => msg, - Err(e) => { - let error_msg = proto::PluginMessage { - message: Some(Message::ErrorResponse(format!( - "Failed to decode request: {}", - e - ))), - }; - let mut buf = bytes::BytesMut::with_capacity(error_msg.encoded_len()); - error_msg.encode(&mut buf).unwrap(); - return buf.to_vec(); - } - }; + fn format_git_info( + &self, + entry: &lla_plugin_interface::DecoratedEntry, + format: &str, + ) -> Option { + let colors = &self.base.config().colors; + let mut list = List::new().style(BoxStyle::Minimal).key_width(12); - let response_msg = match proto_msg.message { - Some(Message::GetName(_)) => Message::NameResponse(env!("CARGO_PKG_NAME").to_string()), - Some(Message::GetVersion(_)) => { - Message::VersionResponse(env!("CARGO_PKG_VERSION").to_string()) - } - Some(Message::GetDescription(_)) => { - Message::DescriptionResponse(env!("CARGO_PKG_DESCRIPTION").to_string()) - } - Some(Message::GetSupportedFormats(_)) => { - Message::FormatsResponse(proto::SupportedFormatsResponse { - formats: vec!["default".to_string(), "long".to_string()], - }) - } - Some(Message::Decorate(entry)) => { - let mut entry = match lla_plugin_interface::DecoratedEntry::try_from(entry.clone()) - { - Ok(e) => e, - Err(e) => { - return self.encode_error(&format!("Failed to convert entry: {}", e)); + if let (Some(status), Some(branch), Some(commit)) = ( + entry.custom_fields.get("git_status"), + entry.custom_fields.get("git_branch"), + entry.custom_fields.get("git_commit"), + ) { + match format { + "long" => { + // Branch info + let key_color = colors + .get("info") + .unwrap_or(&"white".to_string()) + .to_string(); + let value_color = colors + .get("branch") + .unwrap_or(&"white".to_string()) + .to_string(); + let kv = KeyValue::new("Branch", branch) + .key_color(&key_color) + .value_color(&value_color) + .key_width(12); + list.add_item(kv.render()); + + // Commit info + let commit_parts: Vec<&str> = commit.split_whitespace().collect(); + if let Some((hash, msg)) = commit_parts.split_first() { + let key_color = colors + .get("info") + .unwrap_or(&"white".to_string()) + .to_string(); + let value_color = colors + .get("commit") + .unwrap_or(&"white".to_string()) + .to_string(); + let kv = KeyValue::new("Commit", format!("{} {}", hash, msg.join(" "))) + .key_color(&key_color) + .value_color(&value_color) + .key_width(12); + list.add_item(kv.render()); } - }; - if let Some((status, branch, commit)) = Self::get_git_info(&entry.path) { - let (status_summary, staged, modified, untracked, conflicts) = - Self::format_git_status(&status); - entry - .custom_fields - .insert("git_status".to_string(), status_summary); - entry.custom_fields.insert("git_branch".to_string(), branch); - entry.custom_fields.insert("git_commit".to_string(), commit); - entry - .custom_fields - .insert("git_staged".to_string(), staged.to_string()); - entry - .custom_fields - .insert("git_modified".to_string(), modified.to_string()); - entry - .custom_fields - .insert("git_untracked".to_string(), untracked.to_string()); - entry - .custom_fields - .insert("git_conflicts".to_string(), conflicts.to_string()); + // Status counts + let mut status_items = Vec::new(); + if let Some(staged) = entry.custom_fields.get("git_staged") { + if let Ok(count) = staged.parse::() { + if count > 0 { + status_items.push(format!("{} staged", count)); + } + } + } + if let Some(modified) = entry.custom_fields.get("git_modified") { + if let Ok(count) = modified.parse::() { + if count > 0 { + status_items.push(format!("{} modified", count)); + } + } + } + if let Some(untracked) = entry.custom_fields.get("git_untracked") { + if let Ok(count) = untracked.parse::() { + if count > 0 { + status_items.push(format!("{} untracked", count)); + } + } + } + if let Some(conflicts) = entry.custom_fields.get("git_conflicts") { + if let Ok(count) = conflicts.parse::() { + if count > 0 { + status_items.push(format!("{} conflicts", count)); + } + } + } + + let status_text = if status_items.is_empty() { + "working tree clean".to_string() + } else { + status_items.join(", ") + }; + + let key_color = colors + .get("info") + .unwrap_or(&"white".to_string()) + .to_string(); + let value_color = if status_items.is_empty() { + colors + .get("clean") + .unwrap_or(&"white".to_string()) + .to_string() + } else { + colors + .get("modified") + .unwrap_or(&"white".to_string()) + .to_string() + }; + let kv = KeyValue::new("Status", status_text) + .key_color(&key_color) + .value_color(&value_color) + .key_width(12); + list.add_item(kv.render()); } - Message::DecoratedResponse(entry.into()) + "default" => { + let key_color = colors + .get("info") + .unwrap_or(&"white".to_string()) + .to_string(); + let value_color = if status == "clean" { + colors + .get("clean") + .unwrap_or(&"white".to_string()) + .to_string() + } else { + colors + .get("modified") + .unwrap_or(&"white".to_string()) + .to_string() + }; + let kv = KeyValue::new("Git", status) + .key_color(&key_color) + .value_color(&value_color) + .key_width(12); + list.add_item(kv.render()); + } + _ => return None, } - Some(Message::FormatField(req)) => { - let entry = match req.entry { - Some(e) => match lla_plugin_interface::DecoratedEntry::try_from(e) { - Ok(entry) => entry, - Err(e) => { - return self.encode_error(&format!("Failed to convert entry: {}", e)); - } - }, - None => return self.encode_error("Missing entry in format field request"), - }; - let formatted = match req.format.as_str() { - "long" => { - if let (Some(_status), Some(branch), Some(commit)) = ( - entry.custom_fields.get("git_status"), - entry.custom_fields.get("git_branch"), - entry.custom_fields.get("git_commit"), - ) { - let stats = [ - entry.custom_fields.get("git_staged").and_then(|s| { - let count: usize = s.parse().unwrap_or(0); - if count > 0 { - Some(format!("{} staged", count.to_string().bright_green())) - } else { - None - } - }), - entry.custom_fields.get("git_modified").and_then(|s| { - let count: usize = s.parse().unwrap_or(0); - if count > 0 { - Some(format!( - "{} modified", - count.to_string().bright_yellow() - )) - } else { - None - } - }), - entry.custom_fields.get("git_untracked").and_then(|s| { - let count: usize = s.parse().unwrap_or(0); - if count > 0 { - Some(format!( - "{} untracked", - count.to_string().bright_blue() - )) - } else { - None - } - }), - entry.custom_fields.get("git_conflicts").and_then(|s| { - let count: usize = s.parse().unwrap_or(0); - if count > 0 { - Some(format!( - "{} conflicts", - count.to_string().bright_red() - )) - } else { - None - } - }), - ] - .into_iter() - .flatten() - .collect::>(); - - let status_line = if stats.is_empty() { - "Status: working tree clean".bright_green().to_string() - } else { - format!("Status: {}", stats.join(", ")) - }; - - let branch_symbol = "⎇".bright_blue(); - let commit_parts: Vec<&str> = commit.split_whitespace().collect(); - let (commit_hash, commit_msg) = match commit_parts.split_first() { - Some((hash, msg)) => (*hash, msg.join(" ")), - None => (commit.as_str(), String::new()), - }; - - Some(format!( - "\n{}\n{}\n{}\n{}", - format!("Branch: {} {}", branch_symbol, branch.bright_cyan()), - format!( - "Commit: {} {}", - commit_hash.bright_yellow(), - commit_msg.bright_white() - ), - status_line, - format!( - "Repo: {}", - if !stats.is_empty() { - "has changes".bright_yellow() - } else { - "clean".bright_green() - } - ) - )) - } else { - None + Some(format!("\n{}", list.render())) + } else { + None + } + } +} + +impl Plugin for GitStatusPlugin { + fn handle_raw_request(&mut self, request: &[u8]) -> Vec { + match self.decode_request(request) { + Ok(request) => { + let response = match request { + PluginRequest::GetName => { + PluginResponse::Name(env!("CARGO_PKG_NAME").to_string()) + } + PluginRequest::GetVersion => { + PluginResponse::Version(env!("CARGO_PKG_VERSION").to_string()) + } + PluginRequest::GetDescription => { + PluginResponse::Description(env!("CARGO_PKG_DESCRIPTION").to_string()) + } + PluginRequest::GetSupportedFormats => PluginResponse::SupportedFormats(vec![ + "default".to_string(), + "long".to_string(), + ]), + PluginRequest::Decorate(mut entry) => { + let spinner = SPINNER.write(); + spinner.set_status("Checking Git status...".to_string()); + + if let Some((status, branch, commit)) = Self::get_git_info(&entry.path) { + let (status_summary, staged, modified, untracked, conflicts) = + Self::format_git_status(&status); + entry + .custom_fields + .insert("git_status".to_string(), status_summary); + entry.custom_fields.insert("git_branch".to_string(), branch); + entry.custom_fields.insert("git_commit".to_string(), commit); + entry + .custom_fields + .insert("git_staged".to_string(), staged.to_string()); + entry + .custom_fields + .insert("git_modified".to_string(), modified.to_string()); + entry + .custom_fields + .insert("git_untracked".to_string(), untracked.to_string()); + entry + .custom_fields + .insert("git_conflicts".to_string(), conflicts.to_string()); } + + spinner.finish(); + PluginResponse::Decorated(entry) + } + PluginRequest::FormatField(entry, format) => { + let field = self.format_git_info(&entry, &format); + PluginResponse::FormattedField(field) + } + PluginRequest::PerformAction(action, args) => { + let result = ACTION_REGISTRY.read().handle(&action, &args); + PluginResponse::ActionResult(result) } - "default" => entry.custom_fields.get("git_status").cloned(), - _ => None, }; - Message::FieldResponse(proto::FormattedFieldResponse { field: formatted }) + self.encode_response(response) } - Some(Message::Action(_)) => Message::ActionResponse(proto::ActionResponse { - success: true, - error: None, - }), - _ => Message::ErrorResponse("Invalid request type".to_string()), - }; + Err(e) => self.encode_error(&e), + } + } +} - let response = proto::PluginMessage { - message: Some(response_msg), - }; - let mut buf = bytes::BytesMut::with_capacity(response.encoded_len()); - response.encode(&mut buf).unwrap(); - buf.to_vec() +impl Default for GitStatusPlugin { + fn default() -> Self { + Self::new() } } +impl ConfigurablePlugin for GitStatusPlugin { + type Config = GitConfig; + + fn config(&self) -> &Self::Config { + self.base.config() + } + + fn config_mut(&mut self) -> &mut Self::Config { + self.base.config_mut() + } +} + +impl ProtobufHandler for GitStatusPlugin {} + lla_plugin_interface::declare_plugin!(GitStatusPlugin); From b9f83d98ee5069532c1196406562de836e80e443 Mon Sep 17 00:00:00 2001 From: Mohamed Achaq Date: Fri, 27 Dec 2024 22:08:35 +0100 Subject: [PATCH 18/43] feat: enhance last_git_commit plugin with new dependencies and functionality - Updated Cargo.toml and Cargo.lock to include new dependencies: `lazy_static`, `parking_lot`, and `serde` for improved configuration management and concurrency. - Refactored the plugin to utilize `lazy_static` for action registry and `parking_lot` for thread-safe state management. - Introduced `CommitConfig` struct for managing plugin configuration with default color settings for commit information display. - Enhanced the `handle_raw_request` method to support new actions for formatting and displaying last Git commit information. - Improved help documentation and formatting for better user experience. --- Cargo.lock | 4 + plugins/git_status/src/lib.rs | 3 - plugins/last_git_commit/Cargo.toml | 4 + plugins/last_git_commit/src/lib.rs | 338 +++++++++++++++++++---------- 4 files changed, 228 insertions(+), 121 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 421dafc..beffff1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -703,8 +703,12 @@ version = "0.3.0" dependencies = [ "bytes", "colored", + "lazy_static", "lla_plugin_interface", + "lla_plugin_utils", + "parking_lot", "prost", + "serde", ] [[package]] diff --git a/plugins/git_status/src/lib.rs b/plugins/git_status/src/lib.rs index 845fe6f..9194076 100644 --- a/plugins/git_status/src/lib.rs +++ b/plugins/git_status/src/lib.rs @@ -234,7 +234,6 @@ impl GitStatusPlugin { ) { match format { "long" => { - // Branch info let key_color = colors .get("info") .unwrap_or(&"white".to_string()) @@ -249,7 +248,6 @@ impl GitStatusPlugin { .key_width(12); list.add_item(kv.render()); - // Commit info let commit_parts: Vec<&str> = commit.split_whitespace().collect(); if let Some((hash, msg)) = commit_parts.split_first() { let key_color = colors @@ -267,7 +265,6 @@ impl GitStatusPlugin { list.add_item(kv.render()); } - // Status counts let mut status_items = Vec::new(); if let Some(staged) = entry.custom_fields.get("git_staged") { if let Ok(count) = staged.parse::() { diff --git a/plugins/last_git_commit/Cargo.toml b/plugins/last_git_commit/Cargo.toml index 53a6af0..dbc2fbb 100644 --- a/plugins/last_git_commit/Cargo.toml +++ b/plugins/last_git_commit/Cargo.toml @@ -7,8 +7,12 @@ edition = "2021" [dependencies] colored = "2.0.0" lla_plugin_interface = { path = "../../lla_plugin_interface" } +lla_plugin_utils = { path = "../../lla_plugin_utils" } prost = "0.12" bytes = "1.5" +lazy_static = "1.4" +parking_lot = "0.12" +serde = { version = "1.0", features = ["derive"] } [lib] crate-type = ["cdylib"] diff --git a/plugins/last_git_commit/src/lib.rs b/plugins/last_git_commit/src/lib.rs index a606f6f..f03fb6d 100644 --- a/plugins/last_git_commit/src/lib.rs +++ b/plugins/last_git_commit/src/lib.rs @@ -1,31 +1,101 @@ -use colored::Colorize; -use lla_plugin_interface::{ - proto::{self, plugin_message::Message}, - Plugin, +use lazy_static::lazy_static; +use lla_plugin_interface::{Plugin, PluginRequest, PluginResponse}; +use lla_plugin_utils::{ + config::PluginConfig, + ui::components::{BoxComponent, BoxStyle, HelpFormatter, KeyValue, List, Spinner}, + ActionRegistry, BasePlugin, ConfigurablePlugin, ProtobufHandler, }; -use prost::Message as _; -use std::path::Path; -use std::process::Command; +use parking_lot::RwLock; +use serde::{Deserialize, Serialize}; +use std::{collections::HashMap, path::Path, process::Command}; -pub struct LastGitCommitPlugin; +lazy_static! { + static ref SPINNER: RwLock = RwLock::new(Spinner::new()); + static ref ACTION_REGISTRY: RwLock = RwLock::new({ + let mut registry = ActionRegistry::new(); -impl LastGitCommitPlugin { - pub fn new() -> Self { - LastGitCommitPlugin + lla_plugin_utils::define_action!( + registry, + "help", + "help", + "Show help information", + vec!["lla plugin --name last_git_commit --action help"], + |_| { + let mut help = HelpFormatter::new("Last Git Commit Plugin".to_string()); + help.add_section("Description".to_string()).add_command( + "".to_string(), + "Shows information about the last Git commit for files.".to_string(), + vec![], + ); + + help.add_section("Actions".to_string()).add_command( + "help".to_string(), + "Show this help information".to_string(), + vec!["lla plugin --name last_git_commit --action help".to_string()], + ); + + help.add_section("Formats".to_string()) + .add_command( + "default".to_string(), + "Show basic commit information".to_string(), + vec![], + ) + .add_command( + "long".to_string(), + "Show detailed commit information including author".to_string(), + vec![], + ); + + println!( + "{}", + BoxComponent::new(help.render(&CommitConfig::default().colors)) + .style(BoxStyle::Minimal) + .padding(2) + .render() + ); + Ok(()) + } + ); + + registry + }); +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CommitConfig { + #[serde(default = "default_colors")] + colors: HashMap, +} + +fn default_colors() -> HashMap { + let mut colors = HashMap::new(); + colors.insert("hash".to_string(), "bright_yellow".to_string()); + colors.insert("author".to_string(), "bright_cyan".to_string()); + colors.insert("time".to_string(), "bright_green".to_string()); + colors.insert("info".to_string(), "bright_blue".to_string()); + colors.insert("name".to_string(), "bright_yellow".to_string()); + colors +} + +impl Default for CommitConfig { + fn default() -> Self { + Self { + colors: default_colors(), + } } +} + +impl PluginConfig for CommitConfig {} + +pub struct LastGitCommitPlugin { + base: BasePlugin, +} - fn encode_error(&self, error: &str) -> Vec { - use prost::Message; - let error_msg = lla_plugin_interface::proto::PluginMessage { - message: Some( - lla_plugin_interface::proto::plugin_message::Message::ErrorResponse( - error.to_string(), - ), - ), - }; - let mut buf = bytes::BytesMut::with_capacity(error_msg.encoded_len()); - error_msg.encode(&mut buf).unwrap(); - buf.to_vec() +impl LastGitCommitPlugin { + pub fn new() -> Self { + Self { + base: BasePlugin::new(), + } } fn get_last_commit_info(path: &Path) -> Option<(String, String, String)> { @@ -47,113 +117,131 @@ impl LastGitCommitPlugin { None } } + + fn format_commit_info( + &self, + entry: &lla_plugin_interface::DecoratedEntry, + format: &str, + ) -> Option { + let colors = &self.base.config().colors; + let mut list = List::new().style(BoxStyle::Minimal).key_width(12); + + if let (Some(hash), Some(author), Some(time)) = ( + entry.custom_fields.get("commit_hash"), + entry.custom_fields.get("commit_author"), + entry.custom_fields.get("commit_time"), + ) { + match format { + "long" => { + let key_color = colors + .get("info") + .unwrap_or(&"white".to_string()) + .to_string(); + let hash_color = colors + .get("hash") + .unwrap_or(&"white".to_string()) + .to_string(); + let kv = KeyValue::new("Commit", hash) + .key_color(&key_color) + .value_color(&hash_color) + .key_width(12); + list.add_item(kv.render()); + + let author_color = colors + .get("author") + .unwrap_or(&"white".to_string()) + .to_string(); + let kv = KeyValue::new("Author", author) + .key_color(&key_color) + .value_color(&author_color) + .key_width(12); + list.add_item(kv.render()); + + let time_color = colors + .get("time") + .unwrap_or(&"white".to_string()) + .to_string(); + let kv = KeyValue::new("Time", time) + .key_color(&key_color) + .value_color(&time_color) + .key_width(12); + list.add_item(kv.render()); + } + "default" => { + let key_color = colors + .get("info") + .unwrap_or(&"white".to_string()) + .to_string(); + let hash_color = colors + .get("hash") + .unwrap_or(&"white".to_string()) + .to_string(); + let kv = KeyValue::new("Commit", format!("{} {}", hash, time)) + .key_color(&key_color) + .value_color(&hash_color) + .key_width(12); + list.add_item(kv.render()); + } + _ => return None, + } + + Some(format!("\n{}", list.render())) + } else { + None + } + } } impl Plugin for LastGitCommitPlugin { fn handle_raw_request(&mut self, request: &[u8]) -> Vec { - let proto_msg = match proto::PluginMessage::decode(request) { - Ok(msg) => msg, - Err(e) => { - let error_msg = proto::PluginMessage { - message: Some(Message::ErrorResponse(format!( - "Failed to decode request: {}", - e - ))), - }; - let mut buf = bytes::BytesMut::with_capacity(error_msg.encoded_len()); - error_msg.encode(&mut buf).unwrap(); - return buf.to_vec(); - } - }; - - let response_msg = match proto_msg.message { - Some(Message::GetName(_)) => Message::NameResponse(env!("CARGO_PKG_NAME").to_string()), - Some(Message::GetVersion(_)) => { - Message::VersionResponse(env!("CARGO_PKG_VERSION").to_string()) - } - Some(Message::GetDescription(_)) => { - Message::DescriptionResponse(env!("CARGO_PKG_DESCRIPTION").to_string()) - } - Some(Message::GetSupportedFormats(_)) => { - Message::FormatsResponse(proto::SupportedFormatsResponse { - formats: vec!["default".to_string(), "long".to_string()], - }) - } - Some(Message::Decorate(entry)) => { - let mut entry = match lla_plugin_interface::DecoratedEntry::try_from(entry.clone()) - { - Ok(e) => e, - Err(e) => { - return self.encode_error(&format!("Failed to convert entry: {}", e)); + match self.decode_request(request) { + Ok(request) => { + let response = match request { + PluginRequest::GetName => { + PluginResponse::Name(env!("CARGO_PKG_NAME").to_string()) } - }; + PluginRequest::GetVersion => { + PluginResponse::Version(env!("CARGO_PKG_VERSION").to_string()) + } + PluginRequest::GetDescription => { + PluginResponse::Description(env!("CARGO_PKG_DESCRIPTION").to_string()) + } + PluginRequest::GetSupportedFormats => PluginResponse::SupportedFormats(vec![ + "default".to_string(), + "long".to_string(), + ]), + PluginRequest::Decorate(mut entry) => { + let spinner = SPINNER.write(); + spinner.set_status("Checking last commit...".to_string()); - if let Some((commit_hash, author, time)) = Self::get_last_commit_info(&entry.path) { - entry - .custom_fields - .insert("commit_hash".to_string(), commit_hash); - entry - .custom_fields - .insert("commit_author".to_string(), author); - entry.custom_fields.insert("commit_time".to_string(), time); - } - Message::DecoratedResponse(entry.into()) - } - Some(Message::FormatField(req)) => { - let entry = match req.entry { - Some(e) => match lla_plugin_interface::DecoratedEntry::try_from(e) { - Ok(entry) => entry, - Err(e) => { - return self.encode_error(&format!("Failed to convert entry: {}", e)); + if let Some((commit_hash, author, time)) = + Self::get_last_commit_info(&entry.path) + { + entry + .custom_fields + .insert("commit_hash".to_string(), commit_hash); + entry + .custom_fields + .insert("commit_author".to_string(), author); + entry.custom_fields.insert("commit_time".to_string(), time); } - }, - None => return self.encode_error("Missing entry in format field request"), - }; - let formatted = match req.format.as_str() { - "long" => { - if let (Some(hash), Some(author), Some(time)) = ( - entry.custom_fields.get("commit_hash"), - entry.custom_fields.get("commit_author"), - entry.custom_fields.get("commit_time"), - ) { - Some(format!( - "Last commit: {} by {} {}", - hash.bright_yellow(), - author.bright_cyan(), - time.bright_green() - )) - } else { - None - } + spinner.finish(); + PluginResponse::Decorated(entry) } - "default" => { - if let (Some(hash), Some(time)) = ( - entry.custom_fields.get("commit_hash"), - entry.custom_fields.get("commit_time"), - ) { - Some(format!("Commit: {} {}", hash, time)) - } else { - None - } + PluginRequest::FormatField(entry, format) => { + let field = self.format_commit_info(&entry, &format); + PluginResponse::FormattedField(field) + } + PluginRequest::PerformAction(action, args) => { + let result = ACTION_REGISTRY.read().handle(&action, &args); + PluginResponse::ActionResult(result) } - _ => None, }; - Message::FieldResponse(proto::FormattedFieldResponse { field: formatted }) + self.encode_response(response) } - Some(Message::Action(_)) => Message::ActionResponse(proto::ActionResponse { - success: true, - error: None, - }), - _ => Message::ErrorResponse("Invalid request type".to_string()), - }; - - let response = proto::PluginMessage { - message: Some(response_msg), - }; - let mut buf = bytes::BytesMut::with_capacity(response.encoded_len()); - response.encode(&mut buf).unwrap(); - buf.to_vec() + Err(e) => self.encode_error(&e), + } } } @@ -163,4 +251,18 @@ impl Default for LastGitCommitPlugin { } } +impl ConfigurablePlugin for LastGitCommitPlugin { + type Config = CommitConfig; + + fn config(&self) -> &Self::Config { + self.base.config() + } + + fn config_mut(&mut self) -> &mut Self::Config { + self.base.config_mut() + } +} + +impl ProtobufHandler for LastGitCommitPlugin {} + lla_plugin_interface::declare_plugin!(LastGitCommitPlugin); From 1ff0b2de304b1d4b7f87265b9343b5d93046db2b Mon Sep 17 00:00:00 2001 From: Mohamed Achaq Date: Fri, 27 Dec 2024 22:20:39 +0100 Subject: [PATCH 19/43] feat: enhance sizeviz plugin with new dependencies and functionality - Updated Cargo.toml and Cargo.lock to include new dependencies: `lazy_static`, `parking_lot`, and `serde` for improved configuration management and concurrency. - Refactored the plugin to utilize `lazy_static` for managing the action registry and `parking_lot` for thread-safe state management. - Introduced `SizeConfig` struct for managing plugin configuration with default color settings for size visualization. - Enhanced the `handle_raw_request` method to support new actions for formatting and displaying file size information. - Improved help documentation and formatting for better user experience. --- Cargo.lock | 4 + plugins/sizeviz/Cargo.toml | 4 + plugins/sizeviz/src/lib.rs | 384 +++++++++++++++++++++++-------------- 3 files changed, 251 insertions(+), 141 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index beffff1..33fcd76 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1245,8 +1245,12 @@ version = "0.3.0" dependencies = [ "bytes", "colored", + "lazy_static", "lla_plugin_interface", + "lla_plugin_utils", + "parking_lot", "prost", + "serde", ] [[package]] diff --git a/plugins/sizeviz/Cargo.toml b/plugins/sizeviz/Cargo.toml index dfa945e..1be179e 100644 --- a/plugins/sizeviz/Cargo.toml +++ b/plugins/sizeviz/Cargo.toml @@ -7,8 +7,12 @@ edition = "2021" [dependencies] colored = "2.0.0" lla_plugin_interface = { path = "../../lla_plugin_interface" } +lla_plugin_utils = { path = "../../lla_plugin_utils" } prost = "0.12" bytes = "1.5" +lazy_static = "1.4" +parking_lot = "0.12" +serde = { version = "1.0", features = ["derive"] } [lib] crate-type = ["cdylib"] diff --git a/plugins/sizeviz/src/lib.rs b/plugins/sizeviz/src/lib.rs index 1f254c2..845495f 100644 --- a/plugins/sizeviz/src/lib.rs +++ b/plugins/sizeviz/src/lib.rs @@ -1,30 +1,105 @@ use colored::Colorize; -use lla_plugin_interface::{ - proto::{self, plugin_message::Message}, - Plugin, +use lazy_static::lazy_static; +use lla_plugin_interface::{Plugin, PluginRequest, PluginResponse}; +use lla_plugin_utils::{ + config::PluginConfig, + ui::components::{BoxComponent, BoxStyle, HelpFormatter, Spinner}, + ActionRegistry, BasePlugin, ConfigurablePlugin, ProtobufHandler, }; -use prost::Message as _; -use std::cmp; +use parking_lot::RwLock; +use serde::{Deserialize, Serialize}; +use std::{cmp, collections::HashMap}; -pub struct FileSizeVisualizerPlugin; +lazy_static! { + static ref SPINNER: RwLock = RwLock::new(Spinner::new()); + static ref ACTION_REGISTRY: RwLock = RwLock::new({ + let mut registry = ActionRegistry::new(); -impl FileSizeVisualizerPlugin { - pub fn new() -> Self { - FileSizeVisualizerPlugin + lla_plugin_utils::define_action!( + registry, + "help", + "help", + "Show help information", + vec!["lla plugin --name sizeviz --action help"], + |_| { + let mut help = HelpFormatter::new("Size Visualizer Plugin".to_string()); + help.add_section("Description".to_string()).add_command( + "".to_string(), + "Visualizes file sizes with bars and percentage indicators.".to_string(), + vec![], + ); + + help.add_section("Actions".to_string()).add_command( + "help".to_string(), + "Show this help information".to_string(), + vec!["lla plugin --name sizeviz --action help".to_string()], + ); + + help.add_section("Formats".to_string()) + .add_command( + "default".to_string(), + "Show basic size visualization".to_string(), + vec![], + ) + .add_command( + "long".to_string(), + "Show detailed size visualization with percentage".to_string(), + vec![], + ); + + println!( + "{}", + BoxComponent::new(help.render(&SizeConfig::default().colors)) + .style(BoxStyle::Minimal) + .padding(2) + .render() + ); + Ok(()) + } + ); + + registry + }); +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SizeConfig { + #[serde(default = "default_colors")] + colors: HashMap, +} + +fn default_colors() -> HashMap { + let mut colors = HashMap::new(); + colors.insert("tiny".to_string(), "bright_green".to_string()); + colors.insert("small".to_string(), "bright_cyan".to_string()); + colors.insert("medium".to_string(), "bright_yellow".to_string()); + colors.insert("large".to_string(), "bright_red".to_string()); + colors.insert("huge".to_string(), "bright_magenta".to_string()); + colors.insert("info".to_string(), "bright_blue".to_string()); + colors.insert("size".to_string(), "bright_yellow".to_string()); + colors.insert("percentage".to_string(), "bright_magenta".to_string()); + colors +} + +impl Default for SizeConfig { + fn default() -> Self { + Self { + colors: default_colors(), + } } +} - fn encode_error(&self, error: &str) -> Vec { - use prost::Message; - let error_msg = lla_plugin_interface::proto::PluginMessage { - message: Some( - lla_plugin_interface::proto::plugin_message::Message::ErrorResponse( - error.to_string(), - ), - ), - }; - let mut buf = bytes::BytesMut::with_capacity(error_msg.encoded_len()); - error_msg.encode(&mut buf).unwrap(); - buf.to_vec() +impl PluginConfig for SizeConfig {} + +pub struct FileSizeVisualizerPlugin { + base: BasePlugin, +} + +impl FileSizeVisualizerPlugin { + pub fn new() -> Self { + Self { + base: BasePlugin::new(), + } } fn format_size(size: u64) -> String { @@ -64,7 +139,8 @@ impl FileSizeVisualizerPlugin { format!("{}{}{}", full_blocks, partial_block, spaces) } - fn size_to_color(size: u64) -> colored::Color { + fn get_size_color(&self, size: u64) -> String { + let colors = &self.base.config().colors; const KB: u64 = 1024; const KB_1: u64 = KB + 1; const KB_10: u64 = KB * 10; @@ -78,13 +154,34 @@ impl FileSizeVisualizerPlugin { const GB: u64 = MB * 1024; match size { - 0..=KB => colored::Color::Green, - KB_1..=KB_10 => colored::Color::BrightGreen, - KB_10_1..=MB => colored::Color::Cyan, - MB_1..=MB_10 => colored::Color::Blue, - MB_10_1..=MB_100 => colored::Color::Yellow, - MB_100_1..=GB => colored::Color::Red, - _ => colored::Color::Magenta, + 0..=KB => colors + .get("tiny") + .unwrap_or(&"white".to_string()) + .to_string(), + KB_1..=KB_10 => colors + .get("small") + .unwrap_or(&"white".to_string()) + .to_string(), + KB_10_1..=MB => colors + .get("small") + .unwrap_or(&"white".to_string()) + .to_string(), + MB_1..=MB_10 => colors + .get("medium") + .unwrap_or(&"white".to_string()) + .to_string(), + MB_10_1..=MB_100 => colors + .get("large") + .unwrap_or(&"white".to_string()) + .to_string(), + MB_100_1..=GB => colors + .get("large") + .unwrap_or(&"white".to_string()) + .to_string(), + _ => colors + .get("huge") + .unwrap_or(&"white".to_string()) + .to_string(), } } @@ -95,128 +192,119 @@ impl FileSizeVisualizerPlugin { (size as f64 / total_size as f64) * 100.0 } } + + fn format_size_info( + &self, + entry: &lla_plugin_interface::DecoratedEntry, + format: &str, + ) -> Option { + entry + .custom_fields + .get("size") + .and_then(|size_str| size_str.parse::().ok()) + .map(|size| { + let max_size = 1_073_741_824; + let result = match format { + "long" => { + let bar = Self::size_to_bar(size, max_size, 20); + let bar_color = self.get_size_color(size); + let percentage = Self::get_percentage(size, max_size); + + format!( + "\n{}\n{}\n{}\n{}", + format!( + "┌─ {} ─{}", + "Size".bright_blue(), + "─".repeat(40).bright_black() + ), + format!( + "│ {} {}", + bar.color(match bar_color.as_str() { + "bright_green" => colored::Color::BrightGreen, + "bright_cyan" => colored::Color::BrightCyan, + "bright_yellow" => colored::Color::BrightYellow, + "bright_red" => colored::Color::BrightRed, + "bright_magenta" => colored::Color::BrightMagenta, + _ => colored::Color::White, + }), + Self::format_size(size).bright_yellow() + ), + format!( + "│ {}% of reference (1GB)", + format!("{:.1}", percentage).bright_magenta() + ), + format!("└{}", "─".repeat(50).bright_black()) + ) + } + "default" => { + let bar = Self::size_to_bar(size, max_size, 10); + let bar_color = self.get_size_color(size); + format!( + "{} {}", + bar.color(match bar_color.as_str() { + "bright_green" => colored::Color::BrightGreen, + "bright_cyan" => colored::Color::BrightCyan, + "bright_yellow" => colored::Color::BrightYellow, + "bright_red" => colored::Color::BrightRed, + "bright_magenta" => colored::Color::BrightMagenta, + _ => colored::Color::White, + }), + Self::format_size(size).bright_yellow() + ) + } + _ => return None, + }; + Some(result) + }) + .flatten() + } } impl Plugin for FileSizeVisualizerPlugin { fn handle_raw_request(&mut self, request: &[u8]) -> Vec { - let proto_msg = match proto::PluginMessage::decode(request) { - Ok(msg) => msg, - Err(e) => { - let error_msg = proto::PluginMessage { - message: Some(Message::ErrorResponse(format!( - "Failed to decode request: {}", - e - ))), - }; - let mut buf = bytes::BytesMut::with_capacity(error_msg.encoded_len()); - error_msg.encode(&mut buf).unwrap(); - return buf.to_vec(); - } - }; - - let response_msg = match proto_msg.message { - Some(Message::GetName(_)) => Message::NameResponse(env!("CARGO_PKG_NAME").to_string()), - Some(Message::GetVersion(_)) => { - Message::VersionResponse(env!("CARGO_PKG_VERSION").to_string()) - } - Some(Message::GetDescription(_)) => { - Message::DescriptionResponse(env!("CARGO_PKG_DESCRIPTION").to_string()) - } - Some(Message::GetSupportedFormats(_)) => { - Message::FormatsResponse(proto::SupportedFormatsResponse { - formats: vec!["default".to_string(), "long".to_string()], - }) - } - Some(Message::Decorate(entry)) => { - let mut entry = match lla_plugin_interface::DecoratedEntry::try_from(entry.clone()) - { - Ok(e) => e, - Err(e) => { - return self.encode_error(&format!("Failed to convert entry: {}", e)); + match self.decode_request(request) { + Ok(request) => { + let response = match request { + PluginRequest::GetName => { + PluginResponse::Name(env!("CARGO_PKG_NAME").to_string()) } - }; + PluginRequest::GetVersion => { + PluginResponse::Version(env!("CARGO_PKG_VERSION").to_string()) + } + PluginRequest::GetDescription => { + PluginResponse::Description(env!("CARGO_PKG_DESCRIPTION").to_string()) + } + PluginRequest::GetSupportedFormats => PluginResponse::SupportedFormats(vec![ + "default".to_string(), + "long".to_string(), + ]), + PluginRequest::Decorate(mut entry) => { + let spinner = SPINNER.write(); + spinner.set_status("Calculating size...".to_string()); - if entry.path.is_file() { - let size = entry.metadata.size; - entry - .custom_fields - .insert("size".to_string(), size.to_string()); - } - Message::DecoratedResponse(entry.into()) - } - Some(Message::FormatField(req)) => { - let entry = match req.entry { - Some(e) => match lla_plugin_interface::DecoratedEntry::try_from(e) { - Ok(entry) => entry, - Err(e) => { - return self.encode_error(&format!("Failed to convert entry: {}", e)); + if entry.path.is_file() { + let size = entry.metadata.size; + entry + .custom_fields + .insert("size".to_string(), size.to_string()); } - }, - None => return self.encode_error("Missing entry in format field request"), - }; - - let formatted = match req.format.as_str() { - "long" => entry - .custom_fields - .get("size") - .and_then(|size_str| size_str.parse::().ok()) - .map(|size| { - let max_size = 1_073_741_824; - let bar = Self::size_to_bar(size, max_size, 20); - let color = Self::size_to_color(size); - let formatted_size = Self::format_size(size); - let percentage = Self::get_percentage(size, max_size); - format!( - "\n{}\n{}\n{}\n{}", - format!( - "┌─ {} ─{}", - "Size".bright_cyan(), - "─".repeat(40).bright_black() - ), - format!( - "│ {} {}", - bar.color(color), - formatted_size.bright_yellow() - ), - format!( - "│ {}% of reference (1GB)", - format!("{:.2}", percentage).bright_magenta() - ), - format!("└{}", "─".repeat(50).bright_black()) - ) - }), - "default" => entry - .custom_fields - .get("size") - .and_then(|size_str| size_str.parse::().ok()) - .map(|size| { - let max_size = 1_073_741_824; - let bar = Self::size_to_bar(size, max_size, 10); - let color = Self::size_to_color(size); - format!( - "{} {}", - bar.color(color), - Self::format_size(size).bright_yellow() - ) - }), - _ => None, + spinner.finish(); + PluginResponse::Decorated(entry) + } + PluginRequest::FormatField(entry, format) => { + let field = self.format_size_info(&entry, &format); + PluginResponse::FormattedField(field) + } + PluginRequest::PerformAction(action, args) => { + let result = ACTION_REGISTRY.read().handle(&action, &args); + PluginResponse::ActionResult(result) + } }; - Message::FieldResponse(proto::FormattedFieldResponse { field: formatted }) + self.encode_response(response) } - Some(Message::Action(_)) => Message::ActionResponse(proto::ActionResponse { - success: true, - error: None, - }), - _ => Message::ErrorResponse("Invalid request type".to_string()), - }; - - let response = proto::PluginMessage { - message: Some(response_msg), - }; - let mut buf = bytes::BytesMut::with_capacity(response.encoded_len()); - response.encode(&mut buf).unwrap(); - buf.to_vec() + Err(e) => self.encode_error(&e), + } } } @@ -226,4 +314,18 @@ impl Default for FileSizeVisualizerPlugin { } } +impl ConfigurablePlugin for FileSizeVisualizerPlugin { + type Config = SizeConfig; + + fn config(&self) -> &Self::Config { + self.base.config() + } + + fn config_mut(&mut self) -> &mut Self::Config { + self.base.config_mut() + } +} + +impl ProtobufHandler for FileSizeVisualizerPlugin {} + lla_plugin_interface::declare_plugin!(FileSizeVisualizerPlugin); From b54ad3b9a6bd1007d4261cdb4386b1f7e0dd3c67 Mon Sep 17 00:00:00 2001 From: Mohamed Achaq Date: Sat, 28 Dec 2024 09:12:07 +0100 Subject: [PATCH 20/43] feat: enhance lla_plugin_utils with syntax highlighting and interactive features - Updated Cargo.toml to include new optional dependencies: `syntect`, `lazy_static`, and `dialoguer` for syntax highlighting and interactive selection. - Introduced `syntax` module for code highlighting functionality using `syntect`. - Added `CodeHighlighter` struct with methods for highlighting code and retrieving available themes. - Implemented `InteractiveSelector` for user interaction, allowing single and multiple selections, confirmations, and custom inputs. - Refactored UI components to support new features, including enhanced `BoxComponent` styling options. - Improved module organization by separating text-related functionality into a new `text` module. --- lla_plugin_utils/Cargo.toml | 7 +- lla_plugin_utils/src/lib.rs | 3 + lla_plugin_utils/src/syntax.rs | 60 +++++++++ lla_plugin_utils/src/ui/components.rs | 171 ++++++++++++++++++++++++-- lla_plugin_utils/src/ui/mod.rs | 99 +-------------- lla_plugin_utils/src/ui/selector.rs | 115 +++++++++++++++++ lla_plugin_utils/src/ui/text.rs | 95 ++++++++++++++ 7 files changed, 443 insertions(+), 107 deletions(-) create mode 100644 lla_plugin_utils/src/syntax.rs create mode 100644 lla_plugin_utils/src/ui/selector.rs create mode 100644 lla_plugin_utils/src/ui/text.rs diff --git a/lla_plugin_utils/Cargo.toml b/lla_plugin_utils/Cargo.toml index 4168bd4..b8f2a13 100644 --- a/lla_plugin_utils/Cargo.toml +++ b/lla_plugin_utils/Cargo.toml @@ -18,9 +18,14 @@ chrono = { workspace = true } users = { workspace = true } indicatif = { workspace = true } console = "0.15.8" +syntect = { version = "5.1.0", optional = true } +lazy_static = { version = "1.4", optional = true } +dialoguer = { version = "0.11.0", optional = true } [features] -default = ["config", "ui", "format"] +default = ["config", "ui", "format", "syntax", "interactive"] config = [] ui = [] format = [] +syntax = ["syntect", "lazy_static"] +interactive = ["dialoguer"] diff --git a/lla_plugin_utils/src/lib.rs b/lla_plugin_utils/src/lib.rs index 3ea0b49..5a00cad 100644 --- a/lla_plugin_utils/src/lib.rs +++ b/lla_plugin_utils/src/lib.rs @@ -1,12 +1,15 @@ pub mod actions; pub mod config; pub mod format; +pub mod syntax; pub mod ui; pub use actions::{Action, ActionHelp, ActionRegistry}; pub use config::PluginConfig; +pub use syntax::CodeHighlighter; pub use ui::{ components::{BoxComponent, BoxStyle, HelpFormatter, KeyValue, List, Spinner}, + selector::InteractiveSelector, TextBlock, TextStyle, }; diff --git a/lla_plugin_utils/src/syntax.rs b/lla_plugin_utils/src/syntax.rs new file mode 100644 index 0000000..d8ed5a6 --- /dev/null +++ b/lla_plugin_utils/src/syntax.rs @@ -0,0 +1,60 @@ +#[cfg(feature = "syntax")] +use lazy_static::lazy_static; +#[cfg(feature = "syntax")] +use syntect::{ + easy::HighlightLines, + highlighting::{Style, ThemeSet}, + parsing::SyntaxSet, + util::{as_24_bit_terminal_escaped, LinesWithEndings}, +}; + +#[cfg(feature = "syntax")] +lazy_static! { + static ref SYNTAX_SET: SyntaxSet = SyntaxSet::load_defaults_newlines(); + static ref THEME_SET: ThemeSet = ThemeSet::load_defaults(); +} + +pub struct CodeHighlighter; + +impl CodeHighlighter { + #[cfg(feature = "syntax")] + pub fn highlight(code: &str, language: &str) -> String { + let syntax = SYNTAX_SET + .find_syntax_by_token(language) + .unwrap_or_else(|| SYNTAX_SET.find_syntax_plain_text()); + let mut h = HighlightLines::new(syntax, &THEME_SET.themes["base16-ocean.dark"]); + + let mut highlighted = String::new(); + for line in LinesWithEndings::from(code) { + let ranges: Vec<(Style, &str)> = + h.highlight_line(line, &SYNTAX_SET).unwrap_or_default(); + let escaped = as_24_bit_terminal_escaped(&ranges[..], false); + highlighted.push_str(&escaped); + } + highlighted + } + + #[cfg(not(feature = "syntax"))] + pub fn highlight(code: &str, _language: &str) -> String { + code.to_string() + } + + pub fn highlight_with_line_numbers(code: &str, language: &str, start_line: usize) -> String { + let highlighted = Self::highlight(code, language); + let mut result = String::new(); + for (i, line) in highlighted.lines().enumerate() { + result.push_str(&format!("{:4} │ {}\n", i + start_line, line)); + } + result + } +} + +#[cfg(feature = "syntax")] +pub fn get_available_themes() -> Vec { + THEME_SET.themes.keys().cloned().collect() +} + +#[cfg(not(feature = "syntax"))] +pub fn get_available_themes() -> Vec { + vec![] +} diff --git a/lla_plugin_utils/src/ui/components.rs b/lla_plugin_utils/src/ui/components.rs index 6655b24..5dff765 100644 --- a/lla_plugin_utils/src/ui/components.rs +++ b/lla_plugin_utils/src/ui/components.rs @@ -1,5 +1,6 @@ use super::{TextBlock, TextStyle}; use indicatif::{ProgressBar, ProgressStyle}; +use std::cmp; use std::time::Duration; pub struct Spinner { @@ -212,6 +213,7 @@ impl List { } } +#[derive(Clone, Copy)] pub enum BoxStyle { Minimal, Rounded, @@ -220,45 +222,194 @@ pub enum BoxStyle { Dashed, } +impl BoxStyle { + fn get_chars(&self) -> BoxChars { + match self { + BoxStyle::Minimal => BoxChars { + top_left: '┌', + top_right: '┐', + bottom_left: '└', + bottom_right: '┘', + horizontal: '─', + vertical: '│', + left_t: '├', + right_t: '┤', + top_t: '┬', + bottom_t: '┴', + cross: '┼', + }, + BoxStyle::Rounded => BoxChars { + top_left: '╭', + top_right: '╮', + bottom_left: '╰', + bottom_right: '╯', + horizontal: '─', + vertical: '│', + left_t: '├', + right_t: '┤', + top_t: '┬', + bottom_t: '┴', + cross: '┼', + }, + BoxStyle::Double => BoxChars { + top_left: '╔', + top_right: '╗', + bottom_left: '╚', + bottom_right: '╝', + horizontal: '═', + vertical: '║', + left_t: '╠', + right_t: '╣', + top_t: '╦', + bottom_t: '╩', + cross: '╬', + }, + BoxStyle::Heavy => BoxChars { + top_left: '┏', + top_right: '┓', + bottom_left: '┗', + bottom_right: '┛', + horizontal: '━', + vertical: '┃', + left_t: '┣', + right_t: '┫', + top_t: '┳', + bottom_t: '┻', + cross: '╋', + }, + BoxStyle::Dashed => BoxChars { + top_left: '┌', + top_right: '┐', + bottom_left: '└', + bottom_right: '┘', + horizontal: '┄', + vertical: '┆', + left_t: '├', + right_t: '┤', + top_t: '┬', + bottom_t: '┴', + cross: '┼', + }, + } + } +} + +#[allow(dead_code)] +struct BoxChars { + top_left: char, + top_right: char, + bottom_left: char, + bottom_right: char, + horizontal: char, + vertical: char, + left_t: char, + right_t: char, + top_t: char, + bottom_t: char, + cross: char, +} + pub struct BoxComponent { content: String, + style: BoxStyle, + width: Option, + padding: usize, + title: Option, } impl BoxComponent { pub fn new(content: impl Into) -> Self { Self { content: content.into(), + style: BoxStyle::Minimal, + width: None, + padding: 0, + title: None, } } - pub fn style(self, _style: BoxStyle) -> Self { + pub fn style(mut self, style: BoxStyle) -> Self { + self.style = style; self } - pub fn width(self, _width: usize) -> Self { + pub fn width(mut self, width: usize) -> Self { + self.width = Some(width); self } - pub fn padding(self, _padding: usize) -> Self { + pub fn padding(mut self, padding: usize) -> Self { + self.padding = padding; + self + } + + pub fn title(mut self, title: impl Into) -> Self { + self.title = Some(title.into()); self } pub fn render(&self) -> String { + let chars = self.style.get_chars(); let mut output = String::new(); - output.push('┌'); - output.push('─'); + let lines: Vec<&str> = self.content.lines().collect(); + + let content_width = lines + .iter() + .map(|line| console::measure_text_width(line)) + .max() + .unwrap_or(0); + let title_width = self + .title + .as_ref() + .map(|t| console::measure_text_width(t)) + .unwrap_or(0); + let inner_width = cmp::max(content_width, title_width) + self.padding * 2; + let total_width = self.width.unwrap_or(inner_width); + + output.push(chars.top_left); + if let Some(title) = &self.title { + output.push(chars.horizontal); + output.push(' '); + output.push_str(title); + output.push(' '); + let remaining = total_width.saturating_sub(title_width + 4); + output.push_str(&chars.horizontal.to_string().repeat(remaining)); + } else { + output.push_str(&chars.horizontal.to_string().repeat(total_width)); + } + output.push(chars.top_right); output.push('\n'); - for line in self.content.lines() { - output.push('│'); - output.push(' '); + for _ in 0..self.padding { + output.push(chars.vertical); + output.push_str(&" ".repeat(total_width)); + output.push(chars.vertical); + output.push('\n'); + } + + for line in lines { + output.push(chars.vertical); + output.push_str(&" ".repeat(self.padding)); output.push_str(line); + let padding = + total_width.saturating_sub(console::measure_text_width(line) + self.padding); + output.push_str(&" ".repeat(padding)); + output.push(chars.vertical); output.push('\n'); } - output.push('└'); - output.push('─'); + for _ in 0..self.padding { + output.push(chars.vertical); + output.push_str(&" ".repeat(total_width)); + output.push(chars.vertical); + output.push('\n'); + } + + output.push(chars.bottom_left); + output.push_str(&chars.horizontal.to_string().repeat(total_width)); + output.push(chars.bottom_right); output.push('\n'); + output } } diff --git a/lla_plugin_utils/src/ui/mod.rs b/lla_plugin_utils/src/ui/mod.rs index b0e619a..6269d50 100644 --- a/lla_plugin_utils/src/ui/mod.rs +++ b/lla_plugin_utils/src/ui/mod.rs @@ -1,98 +1,5 @@ pub mod components; +pub mod selector; +pub mod text; -use std::fmt::Display; - -#[derive(Clone, Copy)] -pub enum TextStyle { - Normal, - Bold, - Italic, - Underline, -} - -pub struct TextBlock { - content: String, - color: Option, - style: TextStyle, -} - -impl TextBlock { - pub fn new(content: impl Into) -> Self { - Self { - content: content.into(), - color: None, - style: TextStyle::Normal, - } - } - - pub fn color(mut self, color: impl Into) -> Self { - self.color = Some(color.into()); - self - } - - pub fn style(mut self, style: TextStyle) -> Self { - self.style = style; - self - } - - pub fn build(&self) -> String { - let mut text = self.content.clone(); - - if let Some(color) = &self.color { - text = match color.as_str() { - "black" => text.black().to_string(), - "red" => text.red().to_string(), - "green" => text.green().to_string(), - "yellow" => text.yellow().to_string(), - "blue" => text.blue().to_string(), - "magenta" => text.magenta().to_string(), - "cyan" => text.cyan().to_string(), - "white" => text.white().to_string(), - "bright_black" => text.bright_black().to_string(), - "bright_red" => text.bright_red().to_string(), - "bright_green" => text.bright_green().to_string(), - "bright_yellow" => text.bright_yellow().to_string(), - "bright_blue" => text.bright_blue().to_string(), - "bright_magenta" => text.bright_magenta().to_string(), - "bright_cyan" => text.bright_cyan().to_string(), - "bright_white" => text.bright_white().to_string(), - "dimmed" => text.dimmed().to_string(), - _ => text, - }; - } - - match self.style { - TextStyle::Normal => text, - TextStyle::Bold => text.bold().to_string(), - TextStyle::Italic => text.italic().to_string(), - TextStyle::Underline => text.underline().to_string(), - } - } -} - -impl Display for TextBlock { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.build()) - } -} - -use colored::Colorize; - -pub fn format_size(size: u64) -> String { - const KB: u64 = 1024; - const MB: u64 = KB * 1024; - const GB: u64 = MB * 1024; - const TB: u64 = GB * 1024; - - if size >= TB { - format!("{:.2} TB", size as f64 / TB as f64) - } else if size >= GB { - format!("{:.2} GB", size as f64 / GB as f64) - } else if size >= MB { - format!("{:.2} MB", size as f64 / MB as f64) - } else if size >= KB { - format!("{:.2} KB", size as f64 / KB as f64) - } else { - format!("{} B", size) - } -} +pub use text::{format_size, TextBlock, TextStyle}; diff --git a/lla_plugin_utils/src/ui/selector.rs b/lla_plugin_utils/src/ui/selector.rs new file mode 100644 index 0000000..0bd6e68 --- /dev/null +++ b/lla_plugin_utils/src/ui/selector.rs @@ -0,0 +1,115 @@ +#[cfg(feature = "interactive")] +use dialoguer::{theme::ColorfulTheme, MultiSelect, Select}; + +pub struct InteractiveSelector; + +impl InteractiveSelector { + #[cfg(feature = "interactive")] + pub fn select_one( + items: &[T], + prompt: &str, + default: Option, + ) -> Result, String> { + Select::with_theme(&ColorfulTheme::default()) + .with_prompt(prompt) + .items(&items.iter().map(|i| i.to_string()).collect::>()) + .default(default.unwrap_or(0)) + .interact_opt() + .map_err(|e| format!("Failed to show selector: {}", e)) + } + + #[cfg(not(feature = "interactive"))] + pub fn select_one( + _items: &[T], + _prompt: &str, + _default: Option, + ) -> Result, String> { + Err("Interactive features are not enabled".to_string()) + } + + #[cfg(feature = "interactive")] + pub fn select_multiple( + items: &[T], + prompt: &str, + defaults: Option<&[bool]>, + ) -> Result, String> { + MultiSelect::with_theme(&ColorfulTheme::default()) + .with_prompt(prompt) + .items(&items.iter().map(|i| i.to_string()).collect::>()) + .defaults(defaults.unwrap_or(&vec![false; items.len()])) + .interact() + .map_err(|e| format!("Failed to show selector: {}", e)) + } + + #[cfg(not(feature = "interactive"))] + pub fn select_multiple( + _items: &[T], + _prompt: &str, + _defaults: Option<&[bool]>, + ) -> Result, String> { + Err("Interactive features are not enabled".to_string()) + } + + #[cfg(feature = "interactive")] + pub fn confirm(prompt: &str, default: bool) -> Result { + dialoguer::Confirm::with_theme(&ColorfulTheme::default()) + .with_prompt(prompt) + .default(default) + .interact() + .map_err(|e| format!("Failed to show prompt: {}", e)) + } + + #[cfg(not(feature = "interactive"))] + pub fn confirm(_prompt: &str, _default: bool) -> Result { + Err("Interactive features are not enabled".to_string()) + } + + #[cfg(feature = "interactive")] + pub fn input(prompt: &str) -> Result { + dialoguer::Input::::with_theme(&ColorfulTheme::default()) + .with_prompt(prompt) + .interact_text() + .map_err(|e| format!("Failed to get input: {}", e)) + } + + #[cfg(not(feature = "interactive"))] + pub fn input(_prompt: &str) -> Result { + Err("Interactive features are not enabled".to_string()) + } + + #[cfg(feature = "interactive")] + pub fn select_with_custom( + items: &[T], + prompt: &str, + custom_prompt: &str, + ) -> Result, String> { + let mut display_items = items.iter().map(|i| i.to_string()).collect::>(); + display_items.push("(Custom)".to_string()); + + let selection = Select::with_theme(&ColorfulTheme::default()) + .with_prompt(prompt) + .items(&display_items) + .default(0) + .interact() + .map_err(|e| format!("Failed to show selector: {}", e))?; + + if selection == items.len() { + dialoguer::Input::::with_theme(&ColorfulTheme::default()) + .with_prompt(custom_prompt) + .interact_text() + .map(Some) + .map_err(|e| format!("Failed to get input: {}", e)) + } else { + Ok(Some(items[selection].to_string())) + } + } + + #[cfg(not(feature = "interactive"))] + pub fn select_with_custom( + _items: &[T], + _prompt: &str, + _custom_prompt: &str, + ) -> Result, String> { + Err("Interactive features are not enabled".to_string()) + } +} diff --git a/lla_plugin_utils/src/ui/text.rs b/lla_plugin_utils/src/ui/text.rs new file mode 100644 index 0000000..4d909fe --- /dev/null +++ b/lla_plugin_utils/src/ui/text.rs @@ -0,0 +1,95 @@ +use colored::Colorize; +use std::fmt::Display; + +#[derive(Clone, Copy)] +pub enum TextStyle { + Normal, + Bold, + Italic, + Underline, +} + +pub struct TextBlock { + content: String, + color: Option, + style: TextStyle, +} + +impl TextBlock { + pub fn new(content: impl Into) -> Self { + Self { + content: content.into(), + color: None, + style: TextStyle::Normal, + } + } + + pub fn color(mut self, color: impl Into) -> Self { + self.color = Some(color.into()); + self + } + + pub fn style(mut self, style: TextStyle) -> Self { + self.style = style; + self + } + + pub fn build(&self) -> String { + let mut text = self.content.clone(); + + if let Some(color) = &self.color { + text = match color.as_str() { + "black" => text.black().to_string(), + "red" => text.red().to_string(), + "green" => text.green().to_string(), + "yellow" => text.yellow().to_string(), + "blue" => text.blue().to_string(), + "magenta" => text.magenta().to_string(), + "cyan" => text.cyan().to_string(), + "white" => text.white().to_string(), + "bright_black" => text.bright_black().to_string(), + "bright_red" => text.bright_red().to_string(), + "bright_green" => text.bright_green().to_string(), + "bright_yellow" => text.bright_yellow().to_string(), + "bright_blue" => text.bright_blue().to_string(), + "bright_magenta" => text.bright_magenta().to_string(), + "bright_cyan" => text.bright_cyan().to_string(), + "bright_white" => text.bright_white().to_string(), + "dimmed" => text.dimmed().to_string(), + _ => text, + }; + } + + match self.style { + TextStyle::Normal => text, + TextStyle::Bold => text.bold().to_string(), + TextStyle::Italic => text.italic().to_string(), + TextStyle::Underline => text.underline().to_string(), + } + } +} + +impl Display for TextBlock { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.build()) + } +} + +pub fn format_size(size: u64) -> String { + const KB: u64 = 1024; + const MB: u64 = KB * 1024; + const GB: u64 = MB * 1024; + const TB: u64 = GB * 1024; + + if size >= TB { + format!("{:.2} TB", size as f64 / TB as f64) + } else if size >= GB { + format!("{:.2} GB", size as f64 / GB as f64) + } else if size >= MB { + format!("{:.2} MB", size as f64 / MB as f64) + } else if size >= KB { + format!("{:.2} KB", size as f64 / KB as f64) + } else { + format!("{} B", size) + } +} From 6c2e3cba64b1cae2aa5083f5e1c199b0a15e8938 Mon Sep 17 00:00:00 2001 From: Mohamed Achaq Date: Sat, 28 Dec 2024 09:12:23 +0100 Subject: [PATCH 21/43] feat: update code_snippet_extractor plugin with new features and dependencies - Bumped version to 0.3.1 in Cargo.toml. - Enhanced functionality with new dependencies: `arboard`, `uuid`, `chrono`, `fuzzy-matcher`, and `syntect` for improved snippet management and user interaction. - Refactored code to support tagging and categorization of snippets, including new actions for adding and removing tags. - Implemented clipboard support for snippet extraction and improved snippet rendering with syntax highlighting. - Updated the plugin's configuration management using `lazy_static` and `parking_lot` for thread safety. - Improved help documentation and user experience with clearer command usage and output formatting. --- Cargo.lock | 548 ++++++- plugins/code_snippet_extractor/Cargo.toml | 27 +- plugins/code_snippet_extractor/src/lib.rs | 1576 ++++++++++++++------- 3 files changed, 1631 insertions(+), 520 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 33fcd76..c7bcecf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + [[package]] name = "aho-corasick" version = "1.1.3" @@ -32,6 +38,24 @@ version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7" +[[package]] +name = "arboard" +version = "3.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df099ccb16cd014ff054ac1bf392c67feeef57164b05c42f037cd40f5d4357f4" +dependencies = [ + "clipboard-win", + "core-graphics", + "image", + "log", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "parking_lot", + "windows-sys 0.48.0", + "x11rb", +] + [[package]] name = "arrayvec" version = "0.5.2" @@ -55,12 +79,27 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -82,6 +121,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2", +] + [[package]] name = "bstr" version = "1.6.2" @@ -98,6 +146,18 @@ version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +[[package]] +name = "bytemuck" +version = "1.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3" + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + [[package]] name = "bytes" version = "1.9.0" @@ -182,6 +242,15 @@ dependencies = [ "os_str_bytes", ] +[[package]] +name = "clipboard-win" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15efe7a882b08f34e38556b14f2fb3daa98769d06c7f0c1b076dfd0d983bc892" +dependencies = [ + "error-code", +] + [[package]] name = "code_complexity" version = "0.3.1" @@ -200,17 +269,26 @@ dependencies = [ [[package]] name = "code_snippet_extractor" -version = "0.3.0" +version = "0.3.1" dependencies = [ - "base64", - "bytes", + "arboard", + "base64 0.21.7", + "chrono", "colored", + "console", + "dialoguer", "dirs", + "fuzzy-matcher", + "lazy_static", "lla_plugin_interface", - "prost", + "lla_plugin_utils", + "parking_lot", "ring", "serde", + "serde_json", + "syntect", "toml", + "uuid", ] [[package]] @@ -236,12 +314,46 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "core-graphics" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "libc", +] + [[package]] name = "cpufeatures" version = "0.2.16" @@ -251,6 +363,15 @@ dependencies = [ "libc", ] +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + [[package]] name = "crossbeam-channel" version = "0.5.14" @@ -333,6 +454,15 @@ dependencies = [ "parking_lot_core", ] +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + [[package]] name = "dialoguer" version = "0.11.0" @@ -436,12 +566,27 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "error-code" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d9305ccc6942a704f4335694ecd3de2ea531b114ac2d51f5f843750787a92f" + [[package]] name = "fastrand" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "486f806e73c5707928240ddc295403b1b93c96a02038563881c4a2fd84b81ac4" +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + [[package]] name = "file_hash" version = "0.3.0" @@ -494,6 +639,58 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" +[[package]] +name = "flate2" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "fuzzy-matcher" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94" +dependencies = [ + "thread_local", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -504,6 +701,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "gethostname" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818" +dependencies = [ + "libc", + "windows-targets 0.48.5", +] + [[package]] name = "getrandom" version = "0.2.15" @@ -626,6 +833,19 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "image" +version = "0.25.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd6f44aed642f18953a158afeb30206f4d50da59fbc66ecb53c66488de73563b" +dependencies = [ + "bytemuck", + "byteorder-lite", + "num-traits", + "png", + "tiff", +] + [[package]] name = "indexmap" version = "1.9.3" @@ -674,6 +894,12 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" +[[package]] +name = "jpeg-decoder" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" + [[package]] name = "js-sys" version = "0.3.72" @@ -743,6 +969,12 @@ dependencies = [ "libc", ] +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + [[package]] name = "linux-raw-sys" version = "0.4.14" @@ -804,11 +1036,14 @@ dependencies = [ "chrono", "colored", "console", + "dialoguer", "dirs", "indicatif", + "lazy_static", "lla_plugin_interface", "prost", "serde", + "syntect", "toml", "users", ] @@ -835,6 +1070,16 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "miniz_oxide" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394" +dependencies = [ + "adler2", + "simd-adler32", +] + [[package]] name = "mio" version = "0.8.11" @@ -853,6 +1098,12 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03" +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-traits" version = "0.2.19" @@ -878,12 +1129,133 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" +[[package]] +name = "objc-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" + +[[package]] +name = "objc2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" +dependencies = [ + "objc-sys", + "objc2-encode", +] + +[[package]] +name = "objc2-app-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" +dependencies = [ + "bitflags 2.6.0", + "block2", + "libc", + "objc2", + "objc2-core-data", + "objc2-core-image", + "objc2-foundation", + "objc2-quartz-core", +] + +[[package]] +name = "objc2-core-data" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" +dependencies = [ + "bitflags 2.6.0", + "block2", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-image" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" +dependencies = [ + "block2", + "objc2", + "objc2-foundation", + "objc2-metal", +] + +[[package]] +name = "objc2-encode" +version = "4.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7891e71393cd1f227313c9379a26a584ff3d7e6e7159e988851f0934c993f0f8" + +[[package]] +name = "objc2-foundation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +dependencies = [ + "bitflags 2.6.0", + "block2", + "libc", + "objc2", +] + +[[package]] +name = "objc2-metal" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" +dependencies = [ + "bitflags 2.6.0", + "block2", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" +dependencies = [ + "bitflags 2.6.0", + "block2", + "objc2", + "objc2-foundation", + "objc2-metal", +] + [[package]] name = "once_cell" version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +[[package]] +name = "onig" +version = "6.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c4b31c8722ad9171c6d77d3557db078cab2bd50afcc9d09c8b315c59df8ca4f" +dependencies = [ + "bitflags 1.3.2", + "libc", + "once_cell", + "onig_sys", +] + +[[package]] +name = "onig_sys" +version = "69.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b829e3d7e9cc74c7e315ee8edb185bf4190da5acde74afd7fc59c35b1f086e7" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "option-ext" version = "0.2.0" @@ -929,12 +1301,50 @@ dependencies = [ "indexmap 2.6.0", ] +[[package]] +name = "pkg-config" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" + +[[package]] +name = "plist" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42cf17e9a1800f5f396bc67d193dc9411b59012a5876445ef450d449881e1016" +dependencies = [ + "base64 0.22.1", + "indexmap 2.6.0", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + [[package]] name = "portable-atomic" version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "280dc24453071f1b63954171985a0b0d30058d287960968b9b2aca264c8d4ee6" +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "prettyplease" version = "0.2.25" @@ -1007,6 +1417,15 @@ dependencies = [ "prost", ] +[[package]] +name = "quick-xml" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d3a6e5838b60e0e8fa7a43f22ade549a37d61f8bdbe636d0d7816191de969c2" +dependencies = [ + "memchr", +] + [[package]] name = "quote" version = "1.0.37" @@ -1239,6 +1658,12 @@ dependencies = [ "libc", ] +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + [[package]] name = "sizeviz" version = "0.3.0" @@ -1291,6 +1716,28 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syntect" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "874dcfa363995604333cf947ae9f751ca3af4522c60886774c4963943b4746b1" +dependencies = [ + "bincode", + "bitflags 1.3.2", + "flate2", + "fnv", + "once_cell", + "onig", + "plist", + "regex-syntax", + "serde", + "serde_derive", + "serde_json", + "thiserror", + "walkdir", + "yaml-rust", +] + [[package]] name = "tempfile" version = "3.14.0" @@ -1349,6 +1796,58 @@ dependencies = [ "syn", ] +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "tiff" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e" +dependencies = [ + "flate2", + "jpeg-decoder", + "weezl", +] + +[[package]] +name = "time" +version = "0.3.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tinyvec" version = "1.8.0" @@ -1453,6 +1952,15 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "uuid" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" +dependencies = [ + "getrandom", +] + [[package]] name = "version_check" version = "0.9.5" @@ -1561,6 +2069,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "weezl" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" + [[package]] name = "winapi" version = "0.3.9" @@ -1758,6 +2272,32 @@ dependencies = [ "memchr", ] +[[package]] +name = "x11rb" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d91ffca73ee7f68ce055750bf9f6eca0780b8c85eff9bc046a3b0da41755e12" +dependencies = [ + "gethostname", + "rustix", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d" + +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + [[package]] name = "zeroize" version = "1.8.1" diff --git a/plugins/code_snippet_extractor/Cargo.toml b/plugins/code_snippet_extractor/Cargo.toml index 6c3efcd..e04f220 100644 --- a/plugins/code_snippet_extractor/Cargo.toml +++ b/plugins/code_snippet_extractor/Cargo.toml @@ -1,19 +1,28 @@ [package] name = "code_snippet_extractor" -description = "A plugin for extracting and managing code snippets" -version = "0.3.0" +version = "0.3.1" edition = "2021" +description = "Extract and manage code snippets with tagging support" [dependencies] -colored = "2.0.0" lla_plugin_interface = { path = "../../lla_plugin_interface" } -dirs = "5.0.1" -base64 = { version = "0.22.1", features = ["std"] } -ring = "0.17.8" +lla_plugin_utils = { path = "../../lla_plugin_utils" } +colored = "2.0" +toml = "0.8" serde = { version = "1.0", features = ["derive"] } -toml = "0.8.8" -prost = "0.12" -bytes = "1.5" +dirs = "5.0" +ring = "0.17" +base64 = "0.21" +uuid = { version = "1.4", features = ["v4"] } +chrono = "0.4" +syntect = "5.1" +lazy_static = "1.4" +parking_lot = "0.12" +dialoguer = "0.11.0" +fuzzy-matcher = "0.3.7" +arboard = "3.3.0" +serde_json = "1.0" +console = "0.15.7" [lib] crate-type = ["cdylib"] diff --git a/plugins/code_snippet_extractor/src/lib.rs b/plugins/code_snippet_extractor/src/lib.rs index e7a9914..bd9c49f 100644 --- a/plugins/code_snippet_extractor/src/lib.rs +++ b/plugins/code_snippet_extractor/src/lib.rs @@ -1,16 +1,66 @@ +use arboard::Clipboard; use base64::Engine as _; +use chrono::{TimeZone, Utc}; use colored::Colorize; -use lla_plugin_interface::{DecoratedEntry, Plugin}; +use dialoguer::{theme::ColorfulTheme, Select}; +use fuzzy_matcher::skim::SkimMatcherV2; +use fuzzy_matcher::FuzzyMatcher; +use lazy_static::lazy_static; +use lla_plugin_interface::{Plugin, PluginRequest, PluginResponse}; +use lla_plugin_utils::{ + config::PluginConfig, + ui::components::{BoxComponent, BoxStyle, HelpFormatter}, + ActionRegistry, BasePlugin, ProtobufHandler, +}; +use parking_lot::RwLock; use ring::digest; use serde::{Deserialize, Serialize}; -use std::collections::{HashMap, HashSet}; -use std::fs::File; -use std::io::{BufRead, BufReader}; -use std::path::PathBuf; -use std::time::{SystemTime, UNIX_EPOCH}; +use std::{ + collections::{HashMap, HashSet}, + fs::File, + io::{BufRead, BufReader}, + ops::Deref, + path::PathBuf, + time::{SystemTime, UNIX_EPOCH}, +}; +use syntect::{ + easy::HighlightLines, + highlighting::{Style, ThemeSet}, + parsing::SyntaxSet, + util::{as_24_bit_terminal_escaped, LinesWithEndings}, +}; +use uuid::Uuid; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SnippetConfig { + #[serde(default = "default_colors")] + colors: HashMap, +} + +fn default_colors() -> HashMap { + let mut colors = HashMap::new(); + colors.insert("success".to_string(), "bright_green".to_string()); + colors.insert("info".to_string(), "bright_blue".to_string()); + colors.insert("error".to_string(), "bright_red".to_string()); + colors.insert("name".to_string(), "bright_yellow".to_string()); + colors.insert("language".to_string(), "bright_cyan".to_string()); + colors.insert("tag".to_string(), "bright_magenta".to_string()); + colors +} + +impl Default for SnippetConfig { + fn default() -> Self { + Self { + colors: default_colors(), + } + } +} + +impl PluginConfig for SnippetConfig {} #[derive(Clone, Serialize, Deserialize)] struct CodeSnippet { + id: String, name: String, content: String, language: String, @@ -21,18 +71,22 @@ struct CodeSnippet { context_before: Option, context_after: Option, hash: String, + source_file: String, + category: Option, } impl CodeSnippet { - fn new(name: String, content: String, language: String) -> Self { + fn new(name: String, content: String, language: String, source_file: String) -> Self { let timestamp = SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap() .as_secs(); let hash = Self::compute_hash(&content); + let id = Uuid::new_v4().to_string(); Self { + id, name, content, language, @@ -43,6 +97,8 @@ impl CodeSnippet { context_before: None, context_after: None, hash, + source_file, + category: None, } } @@ -52,40 +108,147 @@ impl CodeSnippet { } } -#[derive(Clone)] +lazy_static! { + static ref ACTION_REGISTRY: RwLock = RwLock::new({ + let mut registry = ActionRegistry::new(); + + lla_plugin_utils::define_action!( + registry, + "extract", + "extract [context_lines]", + "Extract a code snippet from a file", + vec!["lla plugin --name code_snippet_extractor --action extract --args \"file.rs\" \"my_func\" 10 20"], + |args| CodeSnippetExtractorPlugin::extract_action(args) + ); + + lla_plugin_utils::define_action!( + registry, + "list", + "list [file_path]", + "List all snippets, optionally filtered by file", + vec![ + "lla plugin --name code_snippet_extractor --action list", + "lla plugin --name code_snippet_extractor --action list --args \"file.rs\"" + ], + |args| CodeSnippetExtractorPlugin::list_action(args) + ); + + lla_plugin_utils::define_action!( + registry, + "get", + "get ", + "Get a specific snippet by ID", + vec!["lla plugin --name code_snippet_extractor --action get --args \"abc123\""], + |args| CodeSnippetExtractorPlugin::get_action(args) + ); + + lla_plugin_utils::define_action!( + registry, + "search", + "search ", + "Search through all snippets", + vec!["lla plugin --name code_snippet_extractor --action search --args \"function\""], + |args| CodeSnippetExtractorPlugin::search_action(args) + ); + + lla_plugin_utils::define_action!( + registry, + "add-tags", + "add-tags [tag2...]", + "Add tags to a snippet", + vec!["lla plugin --name code_snippet_extractor --action add-tags --args \"abc123\" \"rust\" \"function\""], + |args| CodeSnippetExtractorPlugin::add_tags_action(args) + ); + + lla_plugin_utils::define_action!( + registry, + "remove-tags", + "remove-tags [tag2...]", + "Remove tags from a snippet", + vec!["lla plugin --name code_snippet_extractor --action remove-tags --args \"abc123\" \"rust\""], + |args| CodeSnippetExtractorPlugin::remove_tags_action(args) + ); + + lla_plugin_utils::define_action!( + registry, + "help", + "help", + "Show help information", + vec!["lla plugin --name code_snippet_extractor --action help"], + |_| CodeSnippetExtractorPlugin::help_action() + ); + + lla_plugin_utils::define_action!( + registry, + "list-categories", + "list-categories", + "List all available categories", + vec!["lla plugin --name code_snippet_extractor --action list-categories"], + |_| CodeSnippetExtractorPlugin::list_categories_action() + ); + + lla_plugin_utils::define_action!( + registry, + "list-by-category", + "list-by-category ", + "List all snippets in a category", + vec!["lla plugin --name code_snippet_extractor --action list-by-category --args \"algorithms\""], + |args| CodeSnippetExtractorPlugin::list_by_category_action(args) + ); + + lla_plugin_utils::define_action!( + registry, + "set-category", + "set-category ", + "Set or change the category of a snippet", + vec!["lla plugin --name code_snippet_extractor --action set-category --args \"abc123\" \"algorithms\""], + |args| CodeSnippetExtractorPlugin::set_category_action(args) + ); + + lla_plugin_utils::define_action!( + registry, + "export", + "export [snippet_ids...]", + "Export snippets to a JSON file", + vec![ + "lla plugin --name code_snippet_extractor --action export --args \"snippets.json\"", + "lla plugin --name code_snippet_extractor --action export --args \"snippets.json\" \"abc123\" \"def456\"" + ], + |args| CodeSnippetExtractorPlugin::export_action(args) + ); + + lla_plugin_utils::define_action!( + registry, + "import", + "import ", + "Import snippets from a JSON file", + vec![ + "lla plugin --name code_snippet_extractor --action import --args \"snippets.json\"" + ], + |args| CodeSnippetExtractorPlugin::import_action(args) + ); + + registry + }); +} + pub struct CodeSnippetExtractorPlugin { - snippet_file: PathBuf, - snippets: HashMap>, + base: BasePlugin, + snippets: HashMap, } impl CodeSnippetExtractorPlugin { pub fn new() -> Self { + let base = BasePlugin::new(); let snippet_file = dirs::config_dir() .unwrap_or_else(|| PathBuf::from(".")) .join("lla") .join("code_snippets.toml"); let snippets = Self::load_snippets(&snippet_file); - CodeSnippetExtractorPlugin { - snippet_file, - snippets, - } + Self { base, snippets } } - fn encode_error(&self, error: &str) -> Vec { - use prost::Message; - let error_msg = lla_plugin_interface::proto::PluginMessage { - message: Some( - lla_plugin_interface::proto::plugin_message::Message::ErrorResponse( - error.to_string(), - ), - ), - }; - let mut buf = bytes::BytesMut::with_capacity(error_msg.encoded_len()); - error_msg.encode(&mut buf).unwrap(); - buf.to_vec() - } - - fn load_snippets(path: &PathBuf) -> HashMap> { + fn load_snippets(path: &PathBuf) -> HashMap { if let Ok(content) = std::fs::read_to_string(path) { if let Ok(snippets) = toml::from_str(&content) { return snippets; @@ -95,12 +258,17 @@ impl CodeSnippetExtractorPlugin { } fn save_snippets(&self) -> Result<(), String> { - if let Some(parent) = self.snippet_file.parent() { + let snippet_file = dirs::config_dir() + .unwrap_or_else(|| PathBuf::from(".")) + .join("lla") + .join("code_snippets.toml"); + + if let Some(parent) = snippet_file.parent() { std::fs::create_dir_all(parent).map_err(|e| e.to_string())?; } let content = toml::to_string_pretty(&self.snippets) .map_err(|e| format!("Failed to serialize snippets: {}", e))?; - std::fs::write(&self.snippet_file, content).map_err(|e| e.to_string())?; + std::fs::write(&snippet_file, content).map_err(|e| e.to_string())?; Ok(()) } @@ -136,7 +304,7 @@ impl CodeSnippetExtractorPlugin { start_line: usize, end_line: usize, context_lines: Option, - ) -> Result<(), String> { + ) -> Result { let file = File::open(file_path).map_err(|e| format!("Failed to open file: {}", e))?; let reader = BufReader::new(file); let lines: Vec = reader.lines().map_while(Result::ok).collect(); @@ -160,535 +328,929 @@ impl CodeSnippetExtractorPlugin { let content = lines[start_line - 1..end_line].join("\n"); let language = Self::detect_language(file_path); - let mut snippet = CodeSnippet::new(name.to_string(), content, language); + let mut snippet = + CodeSnippet::new(name.to_string(), content, language, file_path.to_string()); snippet.context_before = context_before; snippet.context_after = context_after; - self.snippets - .entry(file_path.to_string()) - .or_default() - .push(snippet); - + let id = snippet.id.clone(); + self.snippets.insert(id.clone(), snippet); self.save_snippets()?; - Ok(()) + Ok(id) + } + + fn get_snippet(&self, id: &str) -> Option<&CodeSnippet> { + self.snippets.get(id) + } + + fn list_snippets(&self) -> Vec<&CodeSnippet> { + self.snippets.values().collect() } - fn list_snippets(&self, file_path: &str) -> Vec { + fn list_snippets_by_file(&self, file_path: &str) -> Vec<&CodeSnippet> { self.snippets - .get(file_path) - .map(|snippets| { - snippets + .values() + .filter(|s| s.source_file == file_path) + .collect() + } + + fn search_snippets(&self, query: &str) -> Vec<&CodeSnippet> { + let matcher = SkimMatcherV2::default(); + let query = query.to_lowercase(); + let mut matches: Vec<(&CodeSnippet, i64)> = self + .snippets + .values() + .filter_map(|s| { + let name_score = matcher + .fuzzy_match(&s.name.to_lowercase(), &query) + .unwrap_or(0); + let content_score = matcher + .fuzzy_match(&s.content.to_lowercase(), &query) + .unwrap_or(0); + let tags_score = s + .tags .iter() - .map(|s| { - format!( - "{} [v{}] [{}] {}", - s.name, - s.version, - s.language, - s.tags - .iter() - .map(|t| format!("#{}", t)) - .collect::>() - .join(" ") - ) - }) - .collect() + .filter_map(|t| matcher.fuzzy_match(&t.to_lowercase(), &query)) + .max() + .unwrap_or(0); + let source_score = matcher + .fuzzy_match(&s.source_file.to_lowercase(), &query) + .unwrap_or(0); + + let total_score = name_score + content_score + tags_score + source_score; + if total_score > 0 { + Some((s, total_score)) + } else { + None + } }) - .unwrap_or_default() + .collect(); + + matches.sort_by(|a, b| b.1.cmp(&a.1)); + matches.into_iter().map(|(snippet, _)| snippet).collect() } - fn get_snippet(&self, file_path: &str, name: &str) -> Option<&CodeSnippet> { - self.snippets - .get(file_path)? - .iter() - .find(|s| s.name == name) + fn add_tags(&mut self, id: &str, tags: &[String]) -> Result<(), String> { + let snippet = self.snippets.get_mut(id).ok_or("Snippet not found")?; + snippet.tags.extend(tags.iter().cloned()); + self.save_snippets()?; + Ok(()) } - fn search_snippets(&self, query: &str) -> Vec<(String, &CodeSnippet)> { - let query = query.to_lowercase(); - let mut results = Vec::new(); + fn remove_tags(&mut self, id: &str, tags: &[String]) -> Result<(), String> { + let snippet = self.snippets.get_mut(id).ok_or("Snippet not found")?; + for tag in tags { + snippet.tags.remove(tag); + } + self.save_snippets()?; + Ok(()) + } + + fn format_timestamp(&self, timestamp: u64) -> String { + if let Some(datetime) = Utc.timestamp_opt(timestamp as i64, 0).single() { + datetime.format("%Y-%m-%d %H:%M:%S UTC").to_string() + } else { + "Invalid timestamp".to_string() + } + } + + fn count_lines(text: &str) -> usize { + text.lines().count() + } + + fn truncate_str(&self, s: &str, max_width: usize) -> String { + if console::measure_text_width(s) <= max_width { + s.to_string() + } else { + let mut width = 0; + let mut result = String::new(); + let mut chars = s.chars(); + + while let Some(c) = chars.next() { + let char_width = console::measure_text_width(&c.to_string()); + if width + char_width + 3 > max_width { + result.push_str("..."); + break; + } + width += char_width; + result.push(c); + } + result + } + } + + fn render_snippet(&self, snippet: &CodeSnippet) -> String { + let colors = &self.base.config().colors; + let mut output = String::new(); + let max_width = 100; + let content_width = max_width - 8; + + output.push_str(&format!("─{}─\n", "─".bright_black().repeat(max_width - 2))); + output.push_str(&format!( + " {} \n", + self.truncate_str(&snippet.name.bright_white().to_string(), max_width - 3) + )); + + let metadata = format!( + " {} {} • {} {} • {} {}", + "ID:".bright_yellow(), + snippet.id.bright_magenta(), + "Language:".bright_yellow(), + snippet.language.bright_cyan(), + "Version:".bright_yellow(), + format!("v{}", snippet.version).bright_white() + ); + output.push_str(&format!( + "{}\n", + self.truncate_str(&metadata, max_width - 1) + )); + + output.push_str(&format!("─{}─\n", "─".bright_black().repeat(max_width - 2))); + let source_info = format!( + " 📂 {} {}", + "Source:".bright_yellow(), + snippet.source_file.bright_blue() + ); + output.push_str(&format!( + "{}\n", + self.truncate_str(&source_info, max_width - 1) + )); + + let tag_list = if snippet.tags.is_empty() { + "No tags".dimmed().to_string() + } else { + snippet + .tags + .iter() + .map(|t| { + format!( + "#{}", + t.color(colors.get("tag").unwrap_or(&"white".to_string()).as_str()) + ) + }) + .collect::>() + .join(" ") + }; + + let tags_info = format!(" 🏷️ {} {}", "Tags:".bright_yellow(), tag_list); + output.push_str(&format!( + "{}\n", + self.truncate_str(&tags_info, max_width - 1) + )); - for (file_path, snippets) in &self.snippets { + let category_info = format!( + " 📁 {} {}", + "Category:".bright_yellow(), + snippet + .category + .as_ref() + .map(|c| c.bright_cyan().to_string()) + .unwrap_or_else(|| "None".dimmed().to_string()) + ); + output.push_str(&format!( + "{}\n", + self.truncate_str(&category_info, max_width - 1) + )); + + let timestamps = format!( + " 🕒 Created: {} • Modified: {}", + self.format_timestamp(snippet.created_at).bright_white(), + self.format_timestamp(snippet.modified_at).bright_white() + ); + output.push_str(&format!( + "{}\n", + self.truncate_str(×tamps, max_width - 1) + )); + + if let Some(ctx) = &snippet.context_before { + let ctx_lines = Self::count_lines(ctx); + output.push_str(&format!("─{}─\n", "─".bright_black().repeat(max_width - 2))); + output.push_str(&format!( + " {} {}\n", + "◀ Context".bright_blue(), + format!("({} lines)", ctx_lines).bright_black() + )); + + for (i, line) in ctx.lines().enumerate() { + let line_num = format!(" {:>4} │ ", -(ctx_lines as i32 - i as i32)).bright_black(); + let truncated_line = + self.truncate_str(&line.bright_black().to_string(), content_width); + output.push_str(&format!("{}{}\n", line_num, truncated_line)); + } + } + + let content_lines = snippet.content.lines().count(); + output.push_str(&format!("─{}─\n", "─".bright_black().repeat(max_width - 2))); + output.push_str(&format!( + " {} {}\n", + "▶ Code".bright_green(), + format!("({} lines)", content_lines).bright_black() + )); + + let highlighted = self.highlight_code(&snippet.content, &snippet.language); + for (i, line) in highlighted.lines().enumerate() { + let line_num = format!(" {:>4} │ ", i + 1).bright_black(); + let truncated_line = self.truncate_str(line, content_width); + output.push_str(&format!("{}{}\n", line_num, truncated_line)); + } + + if let Some(ctx) = &snippet.context_after { + let ctx_lines = Self::count_lines(ctx); + output.push_str(&format!("─{}─\n", "─".bright_black().repeat(max_width - 2))); + output.push_str(&format!( + " {} {}\n", + "▼ Context".bright_blue(), + format!("({} lines)", ctx_lines).bright_black() + )); + + for (i, line) in ctx.lines().enumerate() { + let line_num = format!(" {:>4} │ ", content_lines + i + 1).bright_black(); + let truncated_line = + self.truncate_str(&line.bright_black().to_string(), content_width); + output.push_str(&format!("{}{}\n", line_num, truncated_line)); + } + } + + output.push_str(&format!("─{}─\n", "─".bright_black().repeat(max_width - 2))); + let stats = format!( + " 📊 {} characters in {} lines", + snippet.content.len(), + content_lines + ); + output.push_str(&format!("{}\n", self.truncate_str(&stats, max_width - 1))); + + output + } + + fn extract_action(args: &[String]) -> Result<(), String> { + if args.len() < 4 || args.len() > 5 { + return Err( + "Usage: extract [context_lines]" + .to_string(), + ); + } + + let start_line = args[2] + .parse::() + .map_err(|_| "Invalid start line".to_string())?; + let end_line = args[3] + .parse::() + .map_err(|_| "Invalid end line".to_string())?; + let context_lines = args.get(4).and_then(|s| s.parse().ok()); + + let mut plugin = Self::new(); + let snippet_id = + plugin.extract_snippet(&args[0], &args[1], start_line, end_line, context_lines)?; + + println!( + "{} extracted snippet '{}' from {} (lines {}-{}) with ID: {}", + "Successfully".bright_green(), + args[1].bright_yellow(), + args[0].bright_blue(), + start_line.to_string().bright_cyan(), + end_line.to_string().bright_cyan(), + snippet_id.bright_magenta() + ); + Ok(()) + } + + fn list_action(args: &[String]) -> Result<(), String> { + let plugin = Self::new(); + let snippets = if let Some(file_path) = args.get(0) { + plugin.list_snippets_by_file(file_path) + } else { + plugin.list_snippets() + }; + + if snippets.is_empty() { + println!("{} No snippets found", "Info:".bright_blue(),); + } else { + println!("{} snippets:", "Found".bright_green()); for snippet in snippets { - if snippet.name.to_lowercase().contains(&query) - || snippet.content.to_lowercase().contains(&query) - || snippet + println!( + " {} [{}] {} [{}] {}", + "→".bright_cyan(), + snippet.id.bright_magenta(), + snippet.name.bright_yellow(), + snippet.language.bright_blue(), + snippet .tags .iter() - .any(|t| t.to_lowercase().contains(&query)) - { - results.push((file_path.clone(), snippet)); + .map(|t| format!("#{}", t.bright_magenta())) + .collect::>() + .join(" ") + ); + } + } + Ok(()) + } + + fn get_action(args: &[String]) -> Result<(), String> { + if args.is_empty() { + return Err("Usage: get ".to_string()); + } + + let plugin = Self::new(); + let snippet = plugin + .get_snippet(&args[0]) + .ok_or("Snippet not found".to_string())?; + + println!("{}", plugin.render_snippet(snippet)); + Ok(()) + } + + fn search_action(args: &[String]) -> Result<(), String> { + if args.is_empty() { + return Err("Usage: search ".to_string()); + } + + let plugin = Self::new(); + let results = plugin.search_snippets(&args[0]); + + if results.is_empty() { + println!( + "{} No matching snippets found for query: {}", + "Info:".bright_blue(), + args[0].bright_yellow() + ); + return Ok(()); + } + + println!( + "{} snippets for query: {}", + "Found".bright_green(), + args[0].bright_yellow() + ); + + let selection_items: Vec = results + .iter() + .map(|snippet| { + format!( + "[{}] {} [{}] {} {} from {}", + snippet.id.bright_magenta(), + snippet.name.bright_yellow(), + snippet.language.bright_blue(), + snippet + .category + .as_ref() + .map(|c| format!("({})", c.bright_cyan())) + .unwrap_or_default(), + snippet + .tags + .iter() + .map(|t| format!("#{}", t.bright_magenta())) + .collect::>() + .join(" "), + snippet.source_file.bright_blue() + ) + }) + .collect(); + + let selections = dialoguer::MultiSelect::with_theme(&ColorfulTheme::default()) + .with_prompt("Select snippets (Space to select, Enter to confirm)") + .items(&selection_items) + .defaults(&vec![false; selection_items.len()]) + .interact() + .map_err(|e| format!("Failed to show selector: {}", e))?; + + if selections.is_empty() { + return Ok(()); + } + + let selected_snippets: Vec<&CodeSnippet> = selections.iter().map(|&i| results[i]).collect(); + + let actions = vec![ + "View snippets", + "Copy to clipboard", + "Add tags", + "Remove tags", + "Set category", + ]; + + let action_selection = Select::with_theme(&ColorfulTheme::default()) + .with_prompt("Choose action") + .items(&actions) + .default(0) + .interact() + .map_err(|e| format!("Failed to show action menu: {}", e))?; + + match action_selection { + 0 => { + for snippet in selected_snippets { + println!("\n{}", plugin.render_snippet(snippet)); } } + 1 => { + let content = selected_snippets + .iter() + .map(|s| s.content.clone()) + .collect::>() + .join("\n\n"); + plugin.copy_to_clipboard(&content)?; + println!( + "{} Snippets copied to clipboard!", + "Success:".bright_green() + ); + } + 2 => { + let input = dialoguer::Input::::with_theme(&ColorfulTheme::default()) + .with_prompt("Enter tags (space-separated)") + .interact_text() + .map_err(|e| format!("Failed to get input: {}", e))?; + let tags: Vec = input.split_whitespace().map(String::from).collect(); + let ids: Vec = selected_snippets.iter().map(|s| s.id.clone()).collect(); + let mut plugin = Self::new(); + plugin.batch_add_tags(&ids, &tags)?; + println!( + "{} Added tags to {} snippets", + "Success:".bright_green(), + ids.len() + ); + } + 3 => { + let input = dialoguer::Input::::with_theme(&ColorfulTheme::default()) + .with_prompt("Enter tags to remove (space-separated)") + .interact_text() + .map_err(|e| format!("Failed to get input: {}", e))?; + let tags: Vec = input.split_whitespace().map(String::from).collect(); + let ids: Vec = selected_snippets.iter().map(|s| s.id.clone()).collect(); + let mut plugin = Self::new(); + plugin.batch_remove_tags(&ids, &tags)?; + println!( + "{} Removed tags from {} snippets", + "Success:".bright_green(), + ids.len() + ); + } + 4 => { + let categories: Vec = plugin.list_categories().into_iter().collect(); + let mut category_items = vec!["(None)".to_string(), "(New category)".to_string()]; + category_items.extend(categories); + + let category_selection = Select::with_theme(&ColorfulTheme::default()) + .with_prompt("Choose category") + .items(&category_items) + .default(0) + .interact() + .map_err(|e| format!("Failed to show category menu: {}", e))?; + + let category = match category_selection { + 0 => None, + 1 => { + let input = + dialoguer::Input::::with_theme(&ColorfulTheme::default()) + .with_prompt("Enter new category name") + .interact_text() + .map_err(|e| format!("Failed to get input: {}", e))?; + Some(input) + } + i => Some(category_items[i].clone()), + }; + + let ids: Vec = selected_snippets.iter().map(|s| s.id.clone()).collect(); + let mut plugin = Self::new(); + plugin.batch_set_category(&ids, category.clone())?; + println!( + "{} Set category {} for {} snippets", + "Success:".bright_green(), + category.unwrap_or_else(|| "None".to_string()), + ids.len() + ); + } + _ => unreachable!(), } - results + Ok(()) } - fn add_tags(&mut self, file_path: &str, name: &str, tags: &[String]) -> Result<(), String> { - let snippets = self.snippets.get_mut(file_path).ok_or("File not found")?; - let snippet = snippets - .iter_mut() - .find(|s| s.name == name) - .ok_or("Snippet not found")?; + fn add_tags_action(args: &[String]) -> Result<(), String> { + if args.len() < 2 { + return Err("Usage: add-tags [tag2...]".to_string()); + } + + let mut plugin = Self::new(); + let tags: Vec = args[1..].iter().map(|s| s.to_string()).collect(); + plugin.add_tags(&args[0], &tags)?; - snippet.tags.extend(tags.iter().cloned()); + println!( + "{} tags {} to snippet", + "Added".bright_green(), + tags.iter() + .map(|t| format!("#{}", t.bright_magenta())) + .collect::>() + .join(" ") + ); + Ok(()) + } + + fn remove_tags_action(args: &[String]) -> Result<(), String> { + if args.len() < 2 { + return Err("Usage: remove-tags [tag2...]".to_string()); + } + + let mut plugin = Self::new(); + let tags: Vec = args[1..].iter().map(|s| s.to_string()).collect(); + plugin.remove_tags(&args[0], &tags)?; + + println!( + "{} tags {} from snippet", + "Removed".bright_green(), + tags.iter() + .map(|t| format!("#{}", t.bright_magenta())) + .collect::>() + .join(" ") + ); + Ok(()) + } + + fn help_action() -> Result<(), String> { + let mut help = HelpFormatter::new("Code Snippet Extractor".to_string()); + help.add_section("Description".to_string()).add_command( + "".to_string(), + "Extract, manage, and search code snippets with tagging support".to_string(), + vec![], + ); + + help.add_section("Basic Commands".to_string()) + .add_command( + "extract".to_string(), + "Extract a code snippet from a file".to_string(), + vec!["extract file.rs my_func 10 20".to_string()], + ) + .add_command( + "list".to_string(), + "List all snippets, optionally filtered by file".to_string(), + vec!["list".to_string(), "list file.rs".to_string()], + ) + .add_command( + "get".to_string(), + "Get a specific snippet by ID".to_string(), + vec!["get abc123".to_string()], + ); + + help.add_section("Search & Organization".to_string()) + .add_command( + "search".to_string(), + "Search through all snippets (with multi-select and batch operations)".to_string(), + vec!["search function".to_string()], + ) + .add_command( + "add-tags".to_string(), + "Add tags to a snippet".to_string(), + vec!["add-tags abc123 rust function".to_string()], + ) + .add_command( + "remove-tags".to_string(), + "Remove tags from a snippet".to_string(), + vec!["remove-tags abc123 rust".to_string()], + ); + + help.add_section("Category Management".to_string()) + .add_command( + "list-categories".to_string(), + "List all available categories".to_string(), + vec!["list-categories".to_string()], + ) + .add_command( + "list-by-category".to_string(), + "List all snippets in a category".to_string(), + vec!["list-by-category algorithms".to_string()], + ) + .add_command( + "set-category".to_string(), + "Set or change the category of a snippet".to_string(), + vec!["set-category abc123 algorithms".to_string()], + ); + + help.add_section("Import & Export".to_string()) + .add_command( + "export".to_string(), + "Export snippets to a JSON file".to_string(), + vec![ + "export snippets.json".to_string(), + "export snippets.json abc123 def456".to_string(), + ], + ) + .add_command( + "import".to_string(), + "Import snippets from a JSON file".to_string(), + vec!["import snippets.json".to_string()], + ); + + println!( + "{}", + BoxComponent::new(help.render(&SnippetConfig::default().colors)) + .style(BoxStyle::Minimal) + .padding(1) + .render() + ); + Ok(()) + } + + fn copy_to_clipboard(&self, content: &str) -> Result<(), String> { + let mut clipboard = + Clipboard::new().map_err(|e| format!("Failed to access clipboard: {}", e))?; + clipboard + .set_text(content) + .map_err(|e| format!("Failed to copy to clipboard: {}", e)) + } + + fn set_category(&mut self, id: &str, category: Option) -> Result<(), String> { + let snippet = self.snippets.get_mut(id).ok_or("Snippet not found")?; + snippet.category = category; self.save_snippets()?; Ok(()) } - fn remove_tags(&mut self, file_path: &str, name: &str, tags: &[String]) -> Result<(), String> { - let snippets = self.snippets.get_mut(file_path).ok_or("File not found")?; - let snippet = snippets - .iter_mut() - .find(|s| s.name == name) - .ok_or("Snippet not found")?; + fn list_categories(&self) -> HashSet { + self.snippets + .values() + .filter_map(|s| s.category.clone()) + .collect() + } - for tag in tags { - snippet.tags.remove(tag); + fn list_snippets_by_category(&self, category: &str) -> Vec<&CodeSnippet> { + self.snippets + .values() + .filter(|s| s.category.as_deref() == Some(category)) + .collect() + } + + fn batch_add_tags(&mut self, ids: &[String], tags: &[String]) -> Result<(), String> { + for id in ids { + self.add_tags(id, tags)?; + } + Ok(()) + } + + fn batch_remove_tags(&mut self, ids: &[String], tags: &[String]) -> Result<(), String> { + for id in ids { + self.remove_tags(id, tags)?; } - self.save_snippets()?; Ok(()) } - fn export_snippets(&self, file_path: &str) -> Result { - let snippets = self.snippets.get(file_path).ok_or("File not found")?; - toml::to_string_pretty(snippets).map_err(|e| e.to_string()) + fn batch_set_category( + &mut self, + ids: &[String], + category: Option, + ) -> Result<(), String> { + for id in ids { + self.set_category(id, category.clone())?; + } + Ok(()) } - fn import_snippets(&mut self, file_path: &str, toml_data: &str) -> Result<(), String> { - let imported: Vec = - toml::from_str(toml_data).map_err(|e| format!("Invalid TOML format: {}", e))?; + fn list_categories_action() -> Result<(), String> { + let plugin = Self::new(); + let categories = plugin.list_categories(); - self.snippets.insert(file_path.to_string(), imported); - self.save_snippets()?; + if categories.is_empty() { + println!("{} No categories found", "Info:".bright_blue()); + } else { + println!("{} categories:", "Found".bright_green()); + for category in categories { + let snippets = plugin.list_snippets_by_category(&category); + println!( + " {} {} ({} snippets)", + "→".bright_cyan(), + category.bright_yellow(), + snippets.len().to_string().bright_blue() + ); + } + } Ok(()) } -} -impl Plugin for CodeSnippetExtractorPlugin { - fn handle_raw_request(&mut self, request: &[u8]) -> Vec { - use lla_plugin_interface::proto::{self, plugin_message}; - use prost::Message as ProstMessage; - - let proto_msg = match proto::PluginMessage::decode(request) { - Ok(msg) => msg, - Err(e) => { - let error_msg = proto::PluginMessage { - message: Some(plugin_message::Message::ErrorResponse(format!( - "Failed to decode request: {}", - e - ))), - }; - let mut buf = bytes::BytesMut::with_capacity(error_msg.encoded_len()); - error_msg.encode(&mut buf).unwrap(); - return buf.to_vec(); + fn list_by_category_action(args: &[String]) -> Result<(), String> { + if args.is_empty() { + return Err("Usage: list-by-category ".to_string()); + } + + let plugin = Self::new(); + let snippets = plugin.list_snippets_by_category(&args[0]); + + if snippets.is_empty() { + println!( + "{} No snippets found in category: {}", + "Info:".bright_blue(), + args[0].bright_yellow() + ); + } else { + println!( + "{} snippets in category {}:", + "Found".bright_green(), + args[0].bright_yellow() + ); + for snippet in snippets { + println!( + " {} [{}] {} [{}] {}", + "→".bright_cyan(), + snippet.id.bright_magenta(), + snippet.name.bright_yellow(), + snippet.language.bright_blue(), + snippet + .tags + .iter() + .map(|t| format!("#{}", t.bright_magenta())) + .collect::>() + .join(" ") + ); } + } + Ok(()) + } + + fn set_category_action(args: &[String]) -> Result<(), String> { + if args.len() != 2 { + return Err("Usage: set-category ".to_string()); + } + + let mut plugin = Self::new(); + let category = if args[1].to_lowercase() == "none" { + None + } else { + Some(args[1].clone()) }; - let response_msg: plugin_message::Message = match proto_msg.message { - Some(plugin_message::Message::GetName(_)) => { - plugin_message::Message::NameResponse(env!("CARGO_PKG_NAME").to_string()) - } - Some(plugin_message::Message::GetVersion(_)) => { - plugin_message::Message::VersionResponse(env!("CARGO_PKG_VERSION").to_string()) - } - Some(plugin_message::Message::GetDescription(_)) => { - plugin_message::Message::DescriptionResponse( - env!("CARGO_PKG_DESCRIPTION").to_string(), - ) - } - Some(plugin_message::Message::GetSupportedFormats(_)) => { - plugin_message::Message::FormatsResponse(proto::SupportedFormatsResponse { - formats: vec!["default".to_string(), "long".to_string()], - }) + plugin.set_category(&args[0], category.clone())?; + println!( + "{} Set category {} for snippet", + "Success:".bright_green(), + category + .unwrap_or_else(|| "None".to_string()) + .bright_yellow() + ); + Ok(()) + } + + fn highlight_code(&self, code: &str, language: &str) -> String { + lazy_static! { + static ref PS: SyntaxSet = SyntaxSet::load_defaults_newlines(); + static ref TS: ThemeSet = ThemeSet::load_defaults(); + } + + let syntax = PS + .find_syntax_by_token(language) + .unwrap_or_else(|| PS.find_syntax_plain_text()); + let mut h = HighlightLines::new(syntax, &TS.themes["base16-ocean.dark"]); + + let mut highlighted = String::new(); + for line in LinesWithEndings::from(code) { + let ranges: Vec<(Style, &str)> = h.highlight_line(line, &PS).unwrap_or_default(); + let escaped = as_24_bit_terminal_escaped(&ranges[..], false); + highlighted.push_str(&escaped); + } + highlighted + } + + fn export_snippets(&self, path: &str, ids: &[String]) -> Result<(), String> { + let snippets: Vec<&CodeSnippet> = + ids.iter().filter_map(|id| self.snippets.get(id)).collect(); + + let content = serde_json::to_string_pretty(&snippets) + .map_err(|e| format!("Failed to serialize snippets: {}", e))?; + + std::fs::write(path, content).map_err(|e| format!("Failed to write file: {}", e))?; + Ok(()) + } + + fn import_snippets(&mut self, path: &str) -> Result, String> { + let content = + std::fs::read_to_string(path).map_err(|e| format!("Failed to read file: {}", e))?; + + let imported_snippets: Vec = serde_json::from_str(&content) + .map_err(|e| format!("Failed to parse snippets: {}", e))?; + + let mut imported_ids = Vec::new(); + for mut snippet in imported_snippets { + snippet.id = Uuid::new_v4().to_string(); + imported_ids.push(snippet.id.clone()); + self.snippets.insert(snippet.id.clone(), snippet); + } + + self.save_snippets()?; + Ok(imported_ids) + } + + fn export_action(args: &[String]) -> Result<(), String> { + if args.is_empty() { + return Err("Usage: export [snippet_ids...]".to_string()); + } + + let plugin = Self::new(); + let export_path = &args[0]; + let snippet_ids = if args.len() > 1 { + args[1..].to_vec() + } else { + plugin.snippets.keys().cloned().collect() + }; + + plugin.export_snippets(export_path, &snippet_ids)?; + println!( + "{} Exported {} snippets to {}", + "Success:".bright_green(), + snippet_ids.len(), + export_path.bright_blue() + ); + Ok(()) + } + + fn import_action(args: &[String]) -> Result<(), String> { + if args.is_empty() { + return Err("Usage: import ".to_string()); + } + + let mut plugin = Self::new(); + let import_path = &args[0]; + let imported_ids = plugin.import_snippets(import_path)?; + + println!( + "{} Imported {} snippets from {}", + "Success:".bright_green(), + imported_ids.len(), + import_path.bright_blue() + ); + + println!("\n{} Imported snippets:", "Info:".bright_blue()); + for id in &imported_ids { + if let Some(snippet) = plugin.get_snippet(id) { + println!( + " {} [{}] {} [{}] {}", + "→".bright_cyan(), + snippet.id.bright_magenta(), + snippet.name.bright_yellow(), + snippet.language.bright_blue(), + snippet + .tags + .iter() + .map(|t| format!("#{}", t.bright_magenta())) + .collect::>() + .join(" ") + ); } - Some(plugin_message::Message::Action(req)) => { - let result: Result<(), String> = match req.action.as_str() { - "extract" => { - if req.args.len() < 4 || req.args.len() > 5 { - return self.encode_error("{} extract [context_lines]"); - } - let start_line = match req.args[2].parse() { - Ok(n) => n, - Err(_) => { - return self.encode_error( - format!("{} Invalid start line", "Error:".bright_red()) - .as_str(), - ); - } - }; - let end_line = match req.args[3].parse() { - Ok(n) => n, - Err(_) => { - return self.encode_error( - format!("{} Invalid end line", "Error:".bright_red()).as_str(), - ); - } - }; - let context_lines = req.args.get(4).and_then(|s| s.parse().ok()); - - match self.extract_snippet( - &req.args[0], - &req.args[1], - start_line, - end_line, - context_lines, - ) { - Ok(()) => { - println!( - "{} extracted snippet '{}' from {} (lines {}-{})", - "Successfully".bright_green(), - req.args[1].bright_yellow(), - req.args[0].bright_blue(), - start_line.to_string().bright_cyan(), - end_line.to_string().bright_cyan() - ); - Ok(()) - } - Err(e) => { - println!("{} {}", "Error:".bright_red(), e); - Ok(()) - } - } - } - "list" => { - if req.args.len() != 1 { - return self.encode_error( - format!("{} list ", "Usage:".bright_cyan()).as_str(), - ); - } - let snippets = self.list_snippets(&req.args[0]); - if snippets.is_empty() { - println!( - "{} No snippets found in {}", - "Info:".bright_blue(), - req.args[0].bright_yellow() - ); - } else { - println!( - "{} in {}:", - "Snippets".bright_green(), - req.args[0].bright_blue() - ); - for snippet in snippets { - println!(" {}", snippet); - } - } - Ok(()) - } - "get" => { - if req.args.len() != 2 { - return self.encode_error( - format!( - "{} get ", - "Usage:".bright_cyan() - ) - .as_str(), - ); - } - match self.get_snippet(&req.args[0], &req.args[1]) { - Some(snippet) => { - println!("{}", "┌─ Context Before ─────────────────".bright_cyan()); - if let Some(ctx) = &snippet.context_before { - println!("{}", ctx.dimmed()); - } - println!("{}", "├─ Snippet Content ───────────────".bright_green()); - println!("{}", snippet.content.bright_white()); - println!("{}", "├─ Context After ──────────────────".bright_cyan()); - if let Some(ctx) = &snippet.context_after { - println!("{}", ctx.dimmed()); - } - println!("{}", "├─ Metadata ─────────────────────".bright_yellow()); - println!("│ {}: {}", "Language".bright_blue(), snippet.language); - println!("│ {}: {}", "Version".bright_blue(), snippet.version); - println!( - "│ {}: {}", - "Tags".bright_blue(), - snippet - .tags - .iter() - .map(|t| format!("#{}", t.bright_magenta())) - .collect::>() - .join(" ") - ); - println!("{}", "└────────────────────────────────".bright_cyan()); - Ok(()) - } - None => { - println!( - "{} Snippet '{}' not found in {}", - "Error:".bright_red(), - req.args[1].bright_yellow(), - req.args[0].bright_blue() - ); - Ok(()) - } - } - } - "search" => { - if req.args.len() != 1 { - return self.encode_error( - format!("{} search ", "Usage:".bright_cyan()).as_str(), - ); - } - let results = self.search_snippets(&req.args[0]); - if results.is_empty() { - println!( - "{} No matching snippets found for query: {}", - "Info:".bright_blue(), - req.args[0].bright_yellow() - ); - } else { - println!( - "{} snippets for query: {}", - "Found".bright_green(), - req.args[0].bright_yellow() - ); - for (file, snippet) in results { - println!( - " {} {} [{}] {}", - "→".bright_cyan(), - file.bright_blue(), - snippet.name.bright_yellow(), - snippet - .tags - .iter() - .map(|t| format!("#{}", t.bright_magenta())) - .collect::>() - .join(" ") - ); - } - } - Ok(()) - } - "add-tags" => { - if req.args.len() < 3 { - return self.encode_error( - format!( - "{} add-tags [tag2...]", - "Usage:".bright_cyan() - ) - .as_str(), - ); - } - let tags: Vec = - req.args[2..].iter().map(|s| s.to_string()).collect(); - match self.add_tags(&req.args[0], &req.args[1], &tags) { - Ok(()) => { - println!( - "{} tags {} to snippet '{}'", - "Added".bright_green(), - tags.iter() - .map(|t| format!("#{}", t.bright_magenta())) - .collect::>() - .join(" "), - req.args[1].bright_yellow() - ); - Ok(()) - } - Err(e) => { - println!("{} {}", "Error:".bright_red(), e); - Ok(()) - } - } + } + + Ok(()) + } +} + +impl Deref for CodeSnippetExtractorPlugin { + type Target = SnippetConfig; + + fn deref(&self) -> &Self::Target { + self.base.config() + } +} + +impl Plugin for CodeSnippetExtractorPlugin { + fn handle_raw_request(&mut self, request: &[u8]) -> Vec { + match self.decode_request(request) { + Ok(request) => { + let response = match request { + PluginRequest::GetName => { + PluginResponse::Name(env!("CARGO_PKG_NAME").to_string()) } - "remove-tags" => { - if req.args.len() < 3 { - return self.encode_error( - format!( - "{} remove-tags [tag2...]", - "Usage:".bright_cyan() - ) - .as_str(), - ); - } - let tags: Vec = - req.args[2..].iter().map(|s| s.to_string()).collect(); - match self.remove_tags(&req.args[0], &req.args[1], &tags) { - Ok(()) => { - println!( - "{} tags {} from snippet '{}'", - "Removed".bright_green(), - tags.iter() - .map(|t| format!("#{}", t.bright_magenta())) - .collect::>() - .join(" "), - req.args[1].bright_yellow() - ); - Ok(()) - } - Err(e) => { - println!("{} {}", "Error:".bright_red(), e); - Ok(()) - } - } + PluginRequest::GetVersion => { + PluginResponse::Version(env!("CARGO_PKG_VERSION").to_string()) } - "export" => { - if req.args.len() != 1 { - return self.encode_error( - format!("{} export ", "Usage:".bright_cyan()).as_str(), - ); - } - match self.export_snippets(&req.args[0]) { - Ok(toml) => { - println!( - "{} Exported snippets from {}", - "Successfully".bright_green(), - req.args[0].bright_blue() - ); - println!("{}", toml); - Ok(()) - } - Err(e) => { - println!("{} {}", "Error:".bright_red(), e); - Ok(()) - } - } + PluginRequest::GetDescription => { + PluginResponse::Description(env!("CARGO_PKG_DESCRIPTION").to_string()) } - "import" => { - if req.args.len() != 2 { - return self.encode_error( - format!( - "{} import ", - "Usage:".bright_cyan() - ) - .as_str(), - ); - } - match self.import_snippets(&req.args[0], &req.args[1]) { - Ok(()) => { - println!( - "{} imported snippets to {}", - "Successfully".bright_green(), - req.args[0].bright_blue() + PluginRequest::GetSupportedFormats => PluginResponse::SupportedFormats(vec![ + "default".to_string(), + "long".to_string(), + ]), + PluginRequest::Decorate(mut entry) => { + if let Some(file_path) = entry.path.to_str() { + let snippet_count = self.list_snippets_by_file(file_path).len(); + if snippet_count > 0 { + entry.custom_fields.insert( + "snippet_count".to_string(), + format!("[{} snippets]", snippet_count), ); - Ok(()) - } - Err(e) => { - println!("{} {}", "Error:".bright_red(), e); - Ok(()) } } + PluginResponse::Decorated(entry) } - "help" => { - println!( - "{}", - "Code Snippet Extractor Commands".bright_green().bold() - ); - println!(); - println!("{}", "Basic Commands:".bright_yellow()); - println!(" {} [context_lines]", "extract".bright_cyan()); - println!(" Extract a code snippet from a file"); - println!(); - println!(" {} ", "list".bright_cyan()); - println!(" List all snippets in a file"); - println!(); - println!(" {} ", "get".bright_cyan()); - println!(" Get a specific snippet with context"); - println!(); - println!("{}", "Search & Organization:".bright_yellow()); - println!(" {} ", "search".bright_cyan()); - println!(" Search through all snippets"); - println!(); - println!( - " {} [tag2...]", - "add-tags".bright_cyan() - ); - println!(" Add tags to a snippet"); - println!(); - println!( - " {} [tag2...]", - "remove-tags".bright_cyan() - ); - println!(" Remove tags from a snippet"); - println!(); - println!("{}", "Import/Export:".bright_yellow()); - println!(" {} ", "export".bright_cyan()); - println!(" Export snippets to TOML format"); - println!(); - println!(" {} ", "import".bright_cyan()); - println!(" Import snippets from TOML"); - println!(); - println!("{}", "Examples:".bright_yellow()); - println!(" {} Extract lines 10-20 from a file:", "→".bright_cyan()); - println!(" lla plugin --name code_snippet_extractor --action extract --args \"file.rs\" \"my_func\" 10 20"); - println!(); - println!(" {} Add tags to a snippet:", "→".bright_cyan()); - println!(" lla plugin --name code_snippet_extractor --action add-tags --args \"file.rs\" \"my_func\" \"rust\" \"function\""); - Ok(()) - } - _ => { - println!("{} Unknown action: {}", "Error:".bright_red(), req.action); - Ok(()) - } - }; - plugin_message::Message::ActionResponse(proto::ActionResponse { - success: result.is_ok(), - error: result.err(), - }) - } - Some(plugin_message::Message::Decorate(entry)) => { - let mut entry = match DecoratedEntry::try_from(entry.clone()) { - Ok(e) => e, - Err(e) => { - return self.encode_error(&format!("Failed to convert entry: {}", e)); + PluginRequest::FormatField(entry, format) => { + let field = if format == "snippet_count" { + entry.custom_fields.get("snippet_count").cloned() + } else { + None + }; + PluginResponse::FormattedField(field) } - }; - if let Some(file_path) = entry.path.to_str() { - let snippet_count = self.snippets.get(file_path).map(|s| s.len()).unwrap_or(0); - if snippet_count > 0 { - entry.custom_fields.insert( - "snippet_count".to_string(), - format!("[{} snippets]", snippet_count.to_string().bright_yellow()), - ); + PluginRequest::PerformAction(action, args) => { + let result = ACTION_REGISTRY.read().handle(&action, &args); + PluginResponse::ActionResult(result) } - } - plugin_message::Message::DecoratedResponse(entry.into()) - } - Some(plugin_message::Message::FormatField(req)) => { - let entry = match req.entry { - Some(e) => match DecoratedEntry::try_from(e) { - Ok(entry) => entry, - Err(e) => { - return self.encode_error(&format!("Failed to convert entry: {}", e)); - } - }, - None => return self.encode_error("Missing entry in format field request"), }; - - if req.format == "snippet_count" { - if let Some(count) = entry.custom_fields.get("snippet_count") { - plugin_message::Message::FieldResponse(proto::FormattedFieldResponse { - field: Some(format!("[{} snippets]", count.bright_yellow())), - }) - } else { - plugin_message::Message::FieldResponse(proto::FormattedFieldResponse { - field: None, - }) - } - } else { - plugin_message::Message::FieldResponse(proto::FormattedFieldResponse { - field: None, - }) - } + self.encode_response(response) } - _ => plugin_message::Message::ErrorResponse("Invalid request type".to_string()), - }; - - let response = proto::PluginMessage { - message: Some(response_msg), - }; - let mut buf = bytes::BytesMut::with_capacity(response.encoded_len()); - response.encode(&mut buf).unwrap(); - buf.to_vec() - } -} - -impl Default for CodeSnippetExtractorPlugin { - fn default() -> Self { - Self::new() + Err(e) => self.encode_error(&e), + } } } -lla_plugin_interface::declare_plugin!(CodeSnippetExtractorPlugin); +lla_plugin_utils::create_plugin!(CodeSnippetExtractorPlugin); From e09133e58ffc1c99d20f6f7d4761c8701c4ea4ed Mon Sep 17 00:00:00 2001 From: Mohamed Achaq Date: Sat, 28 Dec 2024 09:12:59 +0100 Subject: [PATCH 22/43] chore: bump versions of multiple plugins to 0.3.1 - Updated Cargo.toml for the following plugins: `duplicate_file_detector`, `file_hash`, `file_tagger`, `git_status`, `keyword_search`, `last_git_commit`, and `sizeviz`. - Incremented version from 0.3.0 to 0.3.1 for each plugin to reflect the latest release. --- Cargo.toml | 2 +- plugins/duplicate_file_detector/Cargo.toml | 2 +- plugins/file_hash/Cargo.toml | 2 +- plugins/file_tagger/Cargo.toml | 2 +- plugins/git_status/Cargo.toml | 2 +- plugins/keyword_search/Cargo.toml | 2 +- plugins/last_git_commit/Cargo.toml | 2 +- plugins/sizeviz/Cargo.toml | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b7f3271..aaf6905 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,7 @@ parking_lot = "0.12" once_cell = "1.18" unicode-width = "0.1.11" strip-ansi-escapes = "0.1.1" -terminal_size = "0.3.0" +terminal_size = "0.3.1" dialoguer = "0.11.0" atty = "0.2.14" indicatif = "0.17.9" diff --git a/plugins/duplicate_file_detector/Cargo.toml b/plugins/duplicate_file_detector/Cargo.toml index a95dc55..cf0f13a 100644 --- a/plugins/duplicate_file_detector/Cargo.toml +++ b/plugins/duplicate_file_detector/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "duplicate_file_detector" description = "Detects duplicate files by comparing their content hashes" -version = "0.3.0" +version = "0.3.1" edition = "2021" [dependencies] diff --git a/plugins/file_hash/Cargo.toml b/plugins/file_hash/Cargo.toml index 29d289e..177994d 100644 --- a/plugins/file_hash/Cargo.toml +++ b/plugins/file_hash/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "file_hash" description = "Displays the hash of each file" -version = "0.3.0" +version = "0.3.1" edition = "2021" [dependencies] diff --git a/plugins/file_tagger/Cargo.toml b/plugins/file_tagger/Cargo.toml index d09faaf..19cdd33 100644 --- a/plugins/file_tagger/Cargo.toml +++ b/plugins/file_tagger/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "file_tagger" description = "Add and manage tags for files" -version = "0.3.0" +version = "0.3.1" edition = "2021" [dependencies] diff --git a/plugins/git_status/Cargo.toml b/plugins/git_status/Cargo.toml index 7c8b532..221f684 100644 --- a/plugins/git_status/Cargo.toml +++ b/plugins/git_status/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "git_status" description = "Shows Git repository status information" -version = "0.3.0" +version = "0.3.1" edition = "2021" [dependencies] diff --git a/plugins/keyword_search/Cargo.toml b/plugins/keyword_search/Cargo.toml index 8a477a9..8af81e5 100644 --- a/plugins/keyword_search/Cargo.toml +++ b/plugins/keyword_search/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "keyword_search" description = "Searches file contents for user-specified keywords" -version = "0.3.0" +version = "0.3.1" edition = "2021" [dependencies] diff --git a/plugins/last_git_commit/Cargo.toml b/plugins/last_git_commit/Cargo.toml index dbc2fbb..3cd3918 100644 --- a/plugins/last_git_commit/Cargo.toml +++ b/plugins/last_git_commit/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "last_git_commit" description = "A plugin for the LLA that provides the last git commit hash" -version = "0.3.0" +version = "0.3.1" edition = "2021" [dependencies] diff --git a/plugins/sizeviz/Cargo.toml b/plugins/sizeviz/Cargo.toml index 1be179e..3d1afba 100644 --- a/plugins/sizeviz/Cargo.toml +++ b/plugins/sizeviz/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "sizeviz" description = "File size visualizer plugin for LLA" -version = "0.3.0" +version = "0.3.1" edition = "2021" [dependencies] From fb0884d8c70b4a8f34e3ebc804bc888cabd93525 Mon Sep 17 00:00:00 2001 From: Mohamed Achaq Date: Sat, 28 Dec 2024 09:15:22 +0100 Subject: [PATCH 23/43] chore: update multiple package versions to 0.3.1 and 0.4.1 - Bumped versions of `duplicate_file_detector`, `file_hash`, `file_tagger`, `git_status`, `keyword_search`, `last_git_commit`, and `sizeviz` from 0.3.0 to 0.3.1 in Cargo.lock. - Updated `terminal_size` version from 0.3.1 to 0.4.1 in Cargo.toml, along with its dependencies. - Ensured compatibility with the latest releases for improved functionality and performance. --- Cargo.lock | 20 ++++++++++---------- Cargo.toml | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c7bcecf..bbbee73 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -525,7 +525,7 @@ dependencies = [ [[package]] name = "duplicate_file_detector" -version = "0.3.0" +version = "0.3.1" dependencies = [ "bytes", "colored", @@ -589,7 +589,7 @@ dependencies = [ [[package]] name = "file_hash" -version = "0.3.0" +version = "0.3.1" dependencies = [ "bytes", "colored", @@ -620,7 +620,7 @@ dependencies = [ [[package]] name = "file_tagger" -version = "0.3.0" +version = "0.3.1" dependencies = [ "bytes", "colored", @@ -724,7 +724,7 @@ dependencies = [ [[package]] name = "git_status" -version = "0.3.0" +version = "0.3.1" dependencies = [ "bytes", "colored", @@ -911,7 +911,7 @@ dependencies = [ [[package]] name = "keyword_search" -version = "0.3.0" +version = "0.3.1" dependencies = [ "bytes", "colored", @@ -925,7 +925,7 @@ dependencies = [ [[package]] name = "last_git_commit" -version = "0.3.0" +version = "0.3.1" dependencies = [ "bytes", "colored", @@ -1666,7 +1666,7 @@ checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" [[package]] name = "sizeviz" -version = "0.3.0" +version = "0.3.1" dependencies = [ "bytes", "colored", @@ -1762,12 +1762,12 @@ dependencies = [ [[package]] name = "terminal_size" -version = "0.3.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7" +checksum = "5352447f921fda68cf61b4101566c0bdb5104eff6804d0678e5227580ab6a4e9" dependencies = [ "rustix", - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index aaf6905..e431958 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,7 @@ parking_lot = "0.12" once_cell = "1.18" unicode-width = "0.1.11" strip-ansi-escapes = "0.1.1" -terminal_size = "0.3.1" +terminal_size = "0.4.1" dialoguer = "0.11.0" atty = "0.2.14" indicatif = "0.17.9" From 27438bbabd4a0fb2f70c10a4fcf58844dcd237bf Mon Sep 17 00:00:00 2001 From: Mohamed Achaq Date: Sat, 28 Dec 2024 09:44:23 +0100 Subject: [PATCH 24/43] feat: enhance keyword_search plugin with new features and dependencies - Updated Cargo.toml to include new dependencies: `arboard`, `chrono`, `console`, `dialoguer`, `itertools`, `lazy_static`, `parking_lot`, and `syntect` for improved functionality and user interaction. - Refactored the plugin to support enhanced UI features, including interactive selection and syntax highlighting. - Introduced a new `colors` field in `SearchConfig` for customizable keyword highlighting. - Improved match rendering with detailed context and enhanced formatting for better user experience. - Updated the plugin description to reflect the new capabilities. --- Cargo.lock | 33 +- plugins/keyword_search/Cargo.toml | 12 +- plugins/keyword_search/src/lib.rs | 989 +++++++++++++++++------------- 3 files changed, 615 insertions(+), 419 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bbbee73..24a469c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -913,13 +913,23 @@ dependencies = [ name = "keyword_search" version = "0.3.1" dependencies = [ + "arboard", "bytes", + "chrono", "colored", + "console", + "dialoguer", "dirs", + "itertools", + "lazy_static", "lla_plugin_interface", + "lla_plugin_utils", + "parking_lot", "prost", "regex", "serde", + "strip-ansi-escapes 0.2.0", + "syntect", "toml", ] @@ -1009,7 +1019,7 @@ dependencies = [ "regex", "serde", "serde_json", - "strip-ansi-escapes", + "strip-ansi-escapes 0.1.1", "tempfile", "terminal_size", "toml", @@ -1696,7 +1706,16 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "011cbb39cf7c1f62871aea3cc46e5817b0937b49e9447370c93cacbe93a766d8" dependencies = [ - "vte", + "vte 0.10.1", +] + +[[package]] +name = "strip-ansi-escapes" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55ff8ef943b384c414f54aefa961dd2bd853add74ec75e7ac74cf91dba62bcfa" +dependencies = [ + "vte 0.11.1", ] [[package]] @@ -1978,6 +1997,16 @@ dependencies = [ "vte_generate_state_changes", ] +[[package]] +name = "vte" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5022b5fbf9407086c180e9557be968742d839e68346af7792b8592489732197" +dependencies = [ + "utf8parse", + "vte_generate_state_changes", +] + [[package]] name = "vte_generate_state_changes" version = "0.1.2" diff --git a/plugins/keyword_search/Cargo.toml b/plugins/keyword_search/Cargo.toml index 8af81e5..65b6de0 100644 --- a/plugins/keyword_search/Cargo.toml +++ b/plugins/keyword_search/Cargo.toml @@ -1,18 +1,28 @@ [package] name = "keyword_search" -description = "Searches file contents for user-specified keywords" +description = "Searches file contents for user-specified keywords with enhanced UI and features" version = "0.3.1" edition = "2021" [dependencies] colored = "2.0.0" lla_plugin_interface = { path = "../../lla_plugin_interface" } +lla_plugin_utils = { path = "../../lla_plugin_utils" } dirs = "5.0.1" serde = { version = "1.0.200", features = ["derive"] } toml = "0.8.8" regex = "1.11.1" prost = "0.12" bytes = "1.5" +lazy_static = "1.4" +parking_lot = "0.12" +dialoguer = "0.11.0" +console = "0.15.7" +syntect = "5.1" +chrono = "0.4" +arboard = "3.3.0" +itertools = "0.12.0" +strip-ansi-escapes = "0.2.0" [lib] crate-type = ["cdylib"] diff --git a/plugins/keyword_search/src/lib.rs b/plugins/keyword_search/src/lib.rs index f1273d0..bef87f4 100644 --- a/plugins/keyword_search/src/lib.rs +++ b/plugins/keyword_search/src/lib.rs @@ -1,15 +1,27 @@ +use arboard::Clipboard; use colored::Colorize; -use lla_plugin_interface::{ - proto::{self, plugin_message::Message}, - Plugin, +use dialoguer::{theme::ColorfulTheme, MultiSelect, Select}; +use itertools::Itertools; +use lazy_static::lazy_static; +use lla_plugin_interface::{Plugin, PluginRequest, PluginResponse}; +use lla_plugin_utils::{ + config::PluginConfig, + ui::components::{BoxComponent, BoxStyle, HelpFormatter}, + BasePlugin, ConfigurablePlugin, ProtobufHandler, }; -use prost::Message as _; use regex::RegexBuilder; use serde::{Deserialize, Serialize}; -use std::fs::{self, File}; -use std::io::{BufRead, BufReader}; -use std::path::PathBuf; -use std::sync::{Arc, Mutex}; +use std::{ + collections::HashMap, + fs::{self, File}, + io::{BufRead, BufReader}, +}; +use syntect::{ + easy::HighlightLines, + highlighting::{Style, ThemeSet}, + parsing::SyntaxSet, + util::as_24_bit_terminal_escaped, +}; #[derive(Debug, Clone, Serialize, Deserialize)] struct KeywordMatch { @@ -21,13 +33,26 @@ struct KeywordMatch { } #[derive(Debug, Clone, Serialize, Deserialize)] -struct SearchConfig { - keywords: Vec, - case_sensitive: bool, - use_regex: bool, - context_lines: usize, - max_matches: usize, - file_extensions: Vec, +pub struct SearchConfig { + pub keywords: Vec, + pub case_sensitive: bool, + pub use_regex: bool, + pub context_lines: usize, + pub max_matches: usize, + pub file_extensions: Vec, + #[serde(default = "default_colors")] + pub colors: HashMap, +} + +fn default_colors() -> HashMap { + let mut colors = HashMap::new(); + colors.insert("keyword".to_string(), "bright_red".to_string()); + colors.insert("line_number".to_string(), "bright_yellow".to_string()); + colors.insert("context".to_string(), "bright_black".to_string()); + colors.insert("file".to_string(), "bright_blue".to_string()); + colors.insert("success".to_string(), "bright_green".to_string()); + colors.insert("info".to_string(), "bright_cyan".to_string()); + colors } impl Default for SearchConfig { @@ -61,158 +86,131 @@ impl Default for SearchConfig { "ini".to_string(), "conf".to_string(), ], + colors: default_colors(), } } } +impl PluginConfig for SearchConfig {} + pub struct KeywordSearchPlugin { - config: Arc>, - config_path: PathBuf, + base: BasePlugin, } impl KeywordSearchPlugin { pub fn new() -> Self { - let config_path = dirs::home_dir() - .expect("Failed to get home directory") - .join(".config") - .join("lla") - .join("plugins") - .join("keyword_search.toml"); - - if let Some(parent) = config_path.parent() { - if let Err(e) = fs::create_dir_all(parent) { - eprintln!( - "{} Failed to create plugin directory: {}", - "Warning:".bright_yellow(), - e + Self { + base: BasePlugin::new(), + } + } + + fn highlight_match(&self, line: &str, keyword: &str) -> String { + lazy_static! { + static ref PS: SyntaxSet = SyntaxSet::load_defaults_newlines(); + static ref TS: ThemeSet = ThemeSet::load_defaults(); + } + + let syntax = PS.find_syntax_plain_text(); + let mut h = HighlightLines::new(syntax, &TS.themes["base16-ocean.dark"]); + let ranges: Vec<(Style, &str)> = h.highlight_line(line, &PS).unwrap_or_default(); + let highlighted = as_24_bit_terminal_escaped(&ranges[..], false); + let mut result = highlighted.clone(); + + if let Some(pattern) = RegexBuilder::new(®ex::escape(keyword)) + .case_insensitive(true) + .build() + .ok() + { + for mat in pattern.find_iter(&highlighted) { + let matched_text = &highlighted[mat.start()..mat.end()]; + result.replace_range( + mat.start()..mat.end(), + &matched_text.bright_red().to_string(), ); } } - let config = Arc::new(Mutex::new(Self::load_config(&config_path))); - KeywordSearchPlugin { - config, - config_path, - } + result } - fn encode_error(&self, error: &str) -> Vec { - use prost::Message; - let error_msg = lla_plugin_interface::proto::PluginMessage { - message: Some( - lla_plugin_interface::proto::plugin_message::Message::ErrorResponse( - error.to_string(), - ), - ), + fn render_match(&self, m: &KeywordMatch, file_path: &str) -> String { + let mut output = String::new(); + let separator_width = 98; + let line_number_width = 4; + let prefix_width = line_number_width + 3; + let max_line_width = separator_width - prefix_width; + + output.push_str(&format!("─{}─\n", "─".repeat(separator_width))); + let truncated_path = if file_path.len() > max_line_width { + format!("{}...", &file_path[..max_line_width - 3]) + } else { + file_path.to_string() }; - let mut buf = bytes::BytesMut::with_capacity(error_msg.encoded_len()); - error_msg.encode(&mut buf).unwrap(); - buf.to_vec() - } + output.push_str(&format!(" 📂 {}\n", truncated_path.bright_blue())); + output.push_str(&format!("─{}─\n", "─".repeat(separator_width))); - fn load_config(path: &PathBuf) -> SearchConfig { - match fs::read_to_string(path) { - Ok(contents) => match toml::from_str(&contents) { - Ok(config) => config, - Err(e) => { - eprintln!( - "{} Failed to parse config: {}", - "Warning:".bright_yellow(), - e - ); - if let Err(e) = fs::rename(path, path.with_extension("toml.bak")) { - eprintln!( - "{} Failed to backup corrupted config: {}", - "Warning:".bright_yellow(), - e - ); - } - let default = SearchConfig::default(); - if let Ok(contents) = toml::to_string_pretty(&default) { - if let Err(e) = fs::write(path, contents) { - eprintln!( - "{} Failed to write default config: {}", - "Warning:".bright_yellow(), - e - ); - } else { - println!("{} Created new default config", "Info:".bright_blue()); - } + let format_line = |line: &str, is_match: bool| -> String { + let stripped = strip_ansi_escapes::strip(line.as_bytes()); + let visible_len = String::from_utf8_lossy(&stripped).chars().count(); + + if visible_len > max_line_width { + let mut truncated = String::new(); + let mut current_len = 0; + let mut chars = line.chars(); + + while let Some(c) = chars.next() { + if !c.is_ascii_control() { + current_len += 1; } - default - } - }, - Err(e) if e.kind() == std::io::ErrorKind::NotFound => { - let default = SearchConfig::default(); - if let Some(parent) = path.parent() { - if let Err(e) = fs::create_dir_all(parent) { - eprintln!( - "{} Failed to create config directory: {}", - "Warning:".bright_yellow(), - e - ); - return default; + truncated.push(c); + if current_len >= max_line_width - 3 { + break; } } - if let Ok(contents) = toml::to_string_pretty(&default) { - match fs::write(path, contents) { - Ok(_) => println!("{} Created new default config", "Info:".bright_blue()), - Err(e) => eprintln!( - "{} Failed to write default config: {}", - "Warning:".bright_yellow(), - e - ), - } + + if is_match { + format!("{}...", truncated) + } else { + format!("{}...", truncated).bright_black().to_string() } - default - } - Err(e) => { - eprintln!( - "{} Failed to read config: {}", - "Warning:".bright_yellow(), - e - ); - SearchConfig::default() + } else if is_match { + line.to_string() + } else { + line.bright_black().to_string() } - } - } + }; - fn save_config(&self) -> Result<(), String> { - if let Some(parent) = self.config_path.parent() { - fs::create_dir_all(parent) - .map_err(|e| format!("Failed to create config directory: {}", e))?; + for (i, line) in m.context_before.iter().enumerate() { + let line_num = m.line_number - (m.context_before.len() - i); + output.push_str(&format!( + " {:>4} │ {}\n", + format!("{}", line_num).bright_black(), + format_line(line, false) + )); } - let config = self - .config - .lock() - .map_err(|_| "Failed to acquire config lock".to_string())?; - - let temp_path = self.config_path.with_extension("toml.tmp"); - let contents = toml::to_string_pretty(&*config) - .map_err(|e| format!("Failed to serialize config: {}", e))?; - - fs::write(&temp_path, contents) - .map_err(|e| format!("Failed to write temporary config: {}", e))?; - - match fs::rename(&temp_path, &self.config_path) { - Ok(_) => { - println!( - "{} Config saved to: {}", - "Info:".bright_blue(), - self.config_path.display().to_string().bright_yellow() - ); - Ok(()) - } - Err(e) => { - let _ = fs::remove_file(&temp_path); - Err(format!("Failed to save config: {}", e)) - } + let highlighted = self.highlight_match(&m.line, &m.keyword); + output.push_str(&format!( + " {:>4} │ {}\n", + format!("{}", m.line_number).bright_yellow(), + format_line(&highlighted, true) + )); + + for (i, line) in m.context_after.iter().enumerate() { + let line_num = m.line_number + i + 1; + output.push_str(&format!( + " {:>4} │ {}\n", + format!("{}", line_num).bright_black(), + format_line(line, false) + )); } + + output.push_str(&format!("─{}─\n", "─".repeat(separator_width))); + output } fn search_file(&self, path: &std::path::Path) -> Option> { - let config = self.config.lock().unwrap(); + let config = self.base.config(); if let Some(ext) = path.extension() { if !config @@ -269,309 +267,454 @@ impl KeywordSearchPlugin { } } - fn format_matches(&self, matches: &[KeywordMatch], long: bool) -> String { - let mut output = String::new(); - for m in matches { - if long { - for (i, line) in m.context_before.iter().enumerate() { - let line_num = m.line_number - (m.context_before.len() - i); - output.push_str(&format!( - " {}: {}\n", - line_num.to_string().bright_black(), - line - )); - } - - output.push_str(&format!( - "→ {}: {}\n", + fn interactive_search( + &self, + matches: Vec, + file_path: &str, + ) -> Result<(), String> { + let items: Vec = matches + .iter() + .map(|m| { + format!( + "{} Line {}: {} {}", + "►".bright_cyan(), m.line_number.to_string().bright_yellow(), - m.line - .replace(&m.keyword, &m.keyword.bright_red().to_string()) - )); - - for (i, line) in m.context_after.iter().enumerate() { - let line_num = m.line_number + i + 1; - output.push_str(&format!( - " {}: {}\n", - line_num.to_string().bright_black(), - line - )); - } - output.push('\n'); - } else { - output.push_str(&format!( - "{}:{} - {}\n", - m.line_number, - m.keyword.bright_red(), - m.line.trim() - )); - } + self.highlight_match(&m.line, &m.keyword), + format!("[{}]", m.keyword).bright_magenta() + ) + }) + .collect(); + + let selection = MultiSelect::with_theme(&ColorfulTheme::default()) + .with_prompt(format!("{} Select matches to process", "🔍".bright_cyan())) + .items(&items) + .defaults(&vec![true; items.len()]) + .interact() + .map_err(|e| format!("Failed to show selector: {}", e))?; + + if selection.is_empty() { + println!("{} No matches selected", "Info:".bright_blue()); + return Ok(()); } - output - } -} -impl Plugin for KeywordSearchPlugin { - fn handle_raw_request(&mut self, request: &[u8]) -> Vec { - let proto_msg = match proto::PluginMessage::decode(request) { - Ok(msg) => msg, - Err(e) => { - let error_msg = proto::PluginMessage { - message: Some(Message::ErrorResponse(format!( - "Failed to decode request: {}", + let selected_matches: Vec<&KeywordMatch> = selection.iter().map(|&i| &matches[i]).collect(); + + let actions = vec![ + "📝 View detailed matches", + "📋 Copy to clipboard", + "💾 Save to file", + "📊 Show statistics", + "🔍 Filter matches", + "📈 Advanced analysis", + ]; + + let action_selection = Select::with_theme(&ColorfulTheme::default()) + .with_prompt(format!("{} Choose action", "⚡".bright_cyan())) + .items(&actions) + .default(0) + .interact() + .map_err(|e| format!("Failed to show action menu: {}", e))?; + + match action_selection { + 0 => { + println!( + "\n{} Showing {} selected matches:", + "Info:".bright_blue(), + selected_matches.len() + ); + for m in selected_matches { + println!("\n{}", self.render_match(m, file_path)); + } + } + 1 => { + let content = selected_matches + .iter() + .map(|m| { + format!( + "File: {}\nLine {}: {}\nKeyword: {}\nContext:\n{}\n", + file_path, + m.line_number, + m.line, + m.keyword, + m.context_before + .iter() + .chain(std::iter::once(&m.line)) + .chain(m.context_after.iter()) + .enumerate() + .map(|(i, line)| format!( + " {}: {}", + (m.line_number - m.context_before.len() + i), + line + )) + .collect::>() + .join("\n") + ) + }) + .collect::>() + .join("\n---\n"); + + match Clipboard::new() { + Ok(mut clipboard) => { + if let Err(e) = clipboard.set_text(&content) { + println!( + "{} Failed to copy to clipboard: {}", + "Error:".bright_red(), + e + ); + } else { + println!( + "{} {} matches copied to clipboard with full context!", + "Success:".bright_green(), + selected_matches.len() + ); + } + } + Err(e) => println!( + "{} Failed to access clipboard: {}", + "Error:".bright_red(), e - ))), - }; - let mut buf = bytes::BytesMut::with_capacity(error_msg.encoded_len()); - error_msg.encode(&mut buf).unwrap(); - return buf.to_vec(); + ), + } } - }; + 2 => { + let default_filename = format!( + "keyword_matches_{}.txt", + chrono::Local::now().format("%Y%m%d_%H%M%S") + ); + let input = dialoguer::Input::::with_theme(&ColorfulTheme::default()) + .with_prompt("Enter file path to save") + .with_initial_text(&default_filename) + .interact_text() + .map_err(|e| format!("Failed to get input: {}", e))?; + + let content = selected_matches + .iter() + .map(|m| { + format!( + "Match Details:\n File: {}\n Line: {}\n Keyword: {}\n Content: {}\n Context:\n{}", + file_path, + m.line_number, + m.keyword, + m.line, + m.context_before + .iter() + .chain(std::iter::once(&m.line)) + .chain(m.context_after.iter()) + .enumerate() + .map(|(i, line)| format!( + " {}: {}", + (m.line_number - m.context_before.len() + i), + line + )) + .collect::>() + .join("\n") + ) + }) + .collect::>() + .join("\n\n"); - let response_msg = match proto_msg.message { - Some(Message::GetName(_)) => Message::NameResponse(env!("CARGO_PKG_NAME").to_string()), - Some(Message::GetVersion(_)) => { - Message::VersionResponse(env!("CARGO_PKG_VERSION").to_string()) - } - Some(Message::GetDescription(_)) => { - Message::DescriptionResponse(env!("CARGO_PKG_DESCRIPTION").to_string()) - } - Some(Message::GetSupportedFormats(_)) => { - Message::FormatsResponse(proto::SupportedFormatsResponse { - formats: vec!["default".to_string(), "long".to_string()], - }) + fs::write(&input, content).map_err(|e| format!("Failed to write file: {}", e))?; + println!( + "{} {} matches saved to {} with full context", + "Success:".bright_green(), + selected_matches.len(), + input.bright_blue() + ); } - Some(Message::Decorate(entry)) => { - let mut entry = match lla_plugin_interface::DecoratedEntry::try_from(entry.clone()) - { - Ok(e) => e, - Err(e) => { - return self.encode_error(&format!("Failed to convert entry: {}", e)); - } - }; + 3 => { + let total_matches = selected_matches.len(); + let unique_keywords: std::collections::HashSet<_> = + selected_matches.iter().map(|m| &m.keyword).collect(); + let avg_context_lines = selected_matches + .iter() + .map(|m| m.context_before.len() + m.context_after.len()) + .sum::() as f64 + / total_matches as f64; + + println!("\n{} Match Statistics:", "📊".bright_cyan()); + println!("─{}─", "─".repeat(50)); + println!( + " • Total matches: {}", + total_matches.to_string().bright_yellow() + ); + println!( + " • Unique keywords: {}", + unique_keywords.len().to_string().bright_yellow() + ); + println!( + " • Average context lines: {:.1}", + avg_context_lines.to_string().bright_yellow() + ); + println!(" • File: {}", file_path.bright_blue()); - if let Some(matches) = entry - .path - .is_file() - .then(|| self.search_file(&entry.path)) - .flatten() - { - entry.custom_fields.insert( - "keyword_matches".to_string(), - toml::to_string(&matches).unwrap_or_default(), + println!("\n{} Keyword Frequency:", "📈".bright_cyan()); + let mut keyword_freq: HashMap<&String, usize> = HashMap::new(); + for m in selected_matches.iter() { + *keyword_freq.entry(&m.keyword).or_insert(0) += 1; + } + for (keyword, count) in keyword_freq.iter() { + println!( + " • {}: {}", + keyword.bright_magenta(), + count.to_string().bright_yellow() ); } - Message::DecoratedResponse(entry.into()) + println!("─{}─\n", "─".repeat(50)); } - Some(Message::FormatField(req)) => { - let entry = match req.entry { - Some(e) => match lla_plugin_interface::DecoratedEntry::try_from(e) { - Ok(entry) => entry, - Err(e) => { - return self.encode_error(&format!("Failed to convert entry: {}", e)); - } - }, - None => return self.encode_error("Missing entry in format field request"), - }; + 4 => { + let keywords: Vec<_> = selected_matches + .iter() + .map(|m| &m.keyword) + .collect::>() + .into_iter() + .collect(); + + let keyword_selection = MultiSelect::with_theme(&ColorfulTheme::default()) + .with_prompt("Filter by keywords") + .items(&keywords) + .interact() + .map_err(|e| format!("Failed to show keyword selector: {}", e))?; + + if keyword_selection.is_empty() { + println!("{} No keywords selected", "Info:".bright_blue()); + return Ok(()); + } - let formatted = entry - .custom_fields - .get("keyword_matches") - .and_then(|toml_str| toml::from_str::>(toml_str).ok()) - .map(|matches| self.format_matches(&matches[..], req.format == "long")); - Message::FieldResponse(proto::FormattedFieldResponse { field: formatted }) + let filtered_matches: Vec<_> = selected_matches + .into_iter() + .filter(|m| keyword_selection.iter().any(|&i| keywords[i] == &m.keyword)) + .collect(); + + println!( + "\n{} Showing {} filtered matches:", + "Info:".bright_blue(), + filtered_matches.len() + ); + for m in filtered_matches { + println!("\n{}", self.render_match(m, file_path)); + } } - Some(Message::Action(req)) => match req.action.as_str() { - "set-keywords" => { - if req.args.is_empty() { - return self.encode_error("Usage: set-keywords [keyword2 ...]"); - } - let mut config = self.config.lock().unwrap(); - config.keywords = req.args.to_vec(); - drop(config); - if let Err(e) = self.save_config() { - return self.encode_error(&e); - } - println!( - "Set keywords to: {}", - req.args - .iter() - .map(|k| k.yellow().to_string()) - .collect::>() - .join(", ") - ); - Message::ActionResponse(proto::ActionResponse { - success: true, - error: None, - }) + 5 => { + println!("\n{} Advanced Analysis:", "📈".bright_cyan()); + println!("─{}─", "─".repeat(50)); + + let mut line_dist: HashMap = HashMap::new(); + for m in selected_matches.iter() { + let bucket = (m.line_number / 10) * 10; + *line_dist.entry(bucket).or_insert(0) += 1; } - "show-config" => { - let config = self.config.lock().unwrap(); - println!("Current configuration:"); - println!( - " Keywords: {}", - config - .keywords - .iter() - .map(|k| k.yellow().to_string()) - .collect::>() - .join(", ") - ); + println!("\n{} Line Distribution:", "📊".bright_blue()); + for (bucket, count) in line_dist.iter().sorted_by_key(|k| k.0) { println!( - " Case sensitive: {}", - config.case_sensitive.to_string().cyan() - ); - println!(" Use regex: {}", config.use_regex.to_string().cyan()); - println!( - " Context lines: {}", - config.context_lines.to_string().cyan() - ); - println!( - " Max matches per file: {}", - config.max_matches.to_string().cyan() + " • Lines {}-{}: {}", + bucket, + bucket + 9, + "█".repeat(*count).bright_yellow() ); + } + + println!("\n{} Keyword Patterns:", "🔍".bright_blue()); + let mut patterns: HashMap<(&String, &String), usize> = HashMap::new(); + for window in selected_matches.windows(2) { + if let [a, b] = window { + *patterns.entry((&a.keyword, &b.keyword)).or_insert(0) += 1; + } + } + for ((k1, k2), count) in patterns.iter().filter(|(_, &c)| c > 1) { println!( - " File extensions: {}", - config - .file_extensions - .iter() - .map(|e| e.cyan().to_string()) - .collect::>() - .join(", ") + " • {} → {}: {} occurrences", + k1.bright_magenta(), + k2.bright_magenta(), + count.to_string().bright_yellow() ); - Message::ActionResponse(proto::ActionResponse { - success: true, - error: None, - }) } - "set-case-sensitive" => { - let value = req.args.first().map(|s| s == "true").unwrap_or(false); - let mut config = self.config.lock().unwrap(); - config.case_sensitive = value; - drop(config); - if let Err(e) = self.save_config() { - return self.encode_error(&e); + println!("─{}─\n", "─".repeat(50)); + } + _ => unreachable!(), + } + + Ok(()) + } +} + +impl Plugin for KeywordSearchPlugin { + fn handle_raw_request(&mut self, request: &[u8]) -> Vec { + match self.decode_request(request) { + Ok(request) => { + let response = match request { + PluginRequest::GetName => { + PluginResponse::Name(env!("CARGO_PKG_NAME").to_string()) } - println!("Case sensitive search: {}", value.to_string().cyan()); - Message::ActionResponse(proto::ActionResponse { - success: true, - error: None, - }) - } - "set-context-lines" => { - if let Some(lines) = req.args.first().and_then(|s| s.parse().ok()) { - let mut config = self.config.lock().unwrap(); - config.context_lines = lines; - drop(config); - if let Err(e) = self.save_config() { - return self.encode_error(&e); - } - println!("Context lines set to: {}", lines.to_string().cyan()); - Message::ActionResponse(proto::ActionResponse { - success: true, - error: None, - }) - } else { - return self.encode_error("Invalid number of context lines"); + PluginRequest::GetVersion => { + PluginResponse::Version(env!("CARGO_PKG_VERSION").to_string()) } - } - "set-max-matches" => { - if let Some(max) = req.args.first().and_then(|s| s.parse().ok()) { - let mut config = self.config.lock().unwrap(); - config.max_matches = max; - drop(config); - if let Err(e) = self.save_config() { - return self.encode_error(&e); + PluginRequest::GetDescription => { + PluginResponse::Description(env!("CARGO_PKG_DESCRIPTION").to_string()) + } + PluginRequest::GetSupportedFormats => PluginResponse::SupportedFormats(vec![ + "default".to_string(), + "long".to_string(), + ]), + PluginRequest::Decorate(mut entry) => { + if let Some(matches) = entry + .path + .is_file() + .then(|| self.search_file(&entry.path)) + .flatten() + { + entry.custom_fields.insert( + "keyword_matches".to_string(), + toml::to_string(&matches).unwrap_or_default(), + ); } - println!("Max matches per file set to: {}", max.to_string().cyan()); - Message::ActionResponse(proto::ActionResponse { - success: true, - error: None, - }) - } else { - return self.encode_error("Invalid max matches value"); + PluginResponse::Decorated(entry) } - } - "help" => { - println!("{}", "Keyword Search Plugin".bright_green().bold()); - println!(); - println!("{}", "Actions:".bright_yellow()); - println!( - " {} [keyword2 ...]", - "set-keywords".bright_cyan() - ); - println!(" Set keywords to search for in files"); - println!(); - println!(" {}", "show-config".bright_cyan()); - println!(" Display current plugin configuration"); - println!(); - println!(" {} [true|false]", "set-case-sensitive".bright_cyan()); - println!(" Enable or disable case-sensitive search"); - println!(); - println!(" {} ", "set-context-lines".bright_cyan()); - println!(" Set number of context lines to show around matches"); - println!(); - println!(" {} ", "set-max-matches".bright_cyan()); - println!(" Set maximum number of matches to show per file"); - println!(); - println!(" {} ", "search".bright_cyan()); - println!(" Search for configured keywords in a specific file"); - println!(); - println!(" Configure search:"); - println!( - " {} --name keyword_search --action set-context-lines --args 3", - "lla plugin".bright_blue() - ); - println!( - " {} --name keyword_search --action set-case-sensitive --args true", - "lla plugin".bright_blue() - ); - Message::ActionResponse(proto::ActionResponse { - success: true, - error: None, - }) - } - "search" => { - if req.args.is_empty() { - return self.encode_error("Usage: search "); + PluginRequest::FormatField(entry, _format) => { + let field = entry + .custom_fields + .get("keyword_matches") + .and_then(|toml_str| toml::from_str::>(toml_str).ok()) + .map(|matches| { + matches + .iter() + .map(|m| self.render_match(m, &entry.path.to_string_lossy())) + .collect::>() + .join("\n") + }); + PluginResponse::FormattedField(field) } - let path = std::path::Path::new(&req.args[0]); - if let Some(matches) = self.search_file(path) { - println!( - "\n{} in {}:", - "Matches".bright_green().bold(), - path.display().to_string().bright_blue() - ); - println!("{}", self.format_matches(&matches[..], true)); - Message::ActionResponse(proto::ActionResponse { - success: true, - error: None, - }) - } else { - println!( - "{} No matches found in {}", - "Info:".bright_blue(), - path.display().to_string().bright_yellow() - ); - Message::ActionResponse(proto::ActionResponse { - success: true, - error: None, - }) + PluginRequest::PerformAction(action, _args) => { + let response = match action.as_str() { + "search" => { + let result = (|| { + let config = self.base.config(); + if config.keywords.is_empty() { + let input = dialoguer::Input::::with_theme( + &ColorfulTheme::default(), + ) + .with_prompt("Enter keywords (space-separated)") + .interact_text() + .map_err(|e| format!("Failed to get keywords: {}", e))?; + + let keywords: Vec = input + .split_whitespace() + .map(|s| s.to_string()) + .collect(); + + if keywords.is_empty() { + return Err("No keywords provided".to_string()); + } + + self.base.config_mut().keywords = keywords; + } + + let mut files: Vec = Vec::new(); + for entry in std::fs::read_dir(".") + .map_err(|e| format!("Failed to read directory: {}", e))? + { + let entry = entry + .map_err(|e| format!("Failed to read entry: {}", e))?; + let path = entry.path(); + if path.is_file() { + if let Some(ext) = path.extension() { + if self + .base + .config() + .file_extensions + .contains(&ext.to_string_lossy().to_string()) + { + files.push(path.to_string_lossy().to_string()); + } + } + } + } + + if files.is_empty() { + return Err( + "No supported files found in current directory" + .to_string(), + ); + } + + let selection = + MultiSelect::with_theme(&ColorfulTheme::default()) + .with_prompt("Select files to search") + .items(&files) + .interact() + .map_err(|e| { + format!("Failed to show file selector: {}", e) + })?; + + if selection.is_empty() { + return Err("No files selected".to_string()); + } + + let mut all_matches = Vec::new(); + for &idx in &selection { + let path = std::path::Path::new(&files[idx]); + if let Some(matches) = self.search_file(path) { + all_matches.extend(matches); + } + } + + if all_matches.is_empty() { + println!( + "{} No matches found in selected files", + "Info:".bright_blue() + ); + Ok(()) + } else { + self.interactive_search(all_matches, "Selected Files") + } + })(); + PluginResponse::ActionResult(result) + } + "help" => { + let result = { + let mut help = + HelpFormatter::new("Keyword Search Plugin".to_string()); + help.add_section("Description".to_string()) + .add_command( + "".to_string(), + "Search for keywords in files with interactive selection and actions.".to_string(), + vec![], + ); + + help.add_section("Actions".to_string()) + .add_command( + "search".to_string(), + "Search for keywords in files".to_string(), + vec!["search".to_string()], + ) + .add_command( + "help".to_string(), + "Show this help information".to_string(), + vec!["help".to_string()], + ); + + println!( + "{}", + BoxComponent::new(help.render(&self.base.config().colors)) + .style(BoxStyle::Minimal) + .padding(1) + .render() + ); + Ok(()) + }; + PluginResponse::ActionResult(result) + } + _ => PluginResponse::ActionResult(Err(format!( + "Unknown action: {}", + action + ))), + }; + response } - } - _ => { - return self.encode_error(&format!("Unknown action: {}", req.action)); - } - }, - _ => Message::ErrorResponse("Invalid request type".to_string()), - }; - - let response = proto::PluginMessage { - message: Some(response_msg), - }; - let mut buf = bytes::BytesMut::with_capacity(response.encoded_len()); - response.encode(&mut buf).unwrap(); - buf.to_vec() + }; + self.encode_response(response) + } + Err(e) => self.encode_error(&e), + } } } @@ -581,4 +724,18 @@ impl Default for KeywordSearchPlugin { } } +impl ConfigurablePlugin for KeywordSearchPlugin { + type Config = SearchConfig; + + fn config(&self) -> &Self::Config { + self.base.config() + } + + fn config_mut(&mut self) -> &mut Self::Config { + self.base.config_mut() + } +} + +impl ProtobufHandler for KeywordSearchPlugin {} + lla_plugin_interface::declare_plugin!(KeywordSearchPlugin); From 72fe359d5a7dfbaef8286af36d13c2c1d92d8f84 Mon Sep 17 00:00:00 2001 From: Mohamed Achaq Date: Sat, 28 Dec 2024 09:44:57 +0100 Subject: [PATCH 25/43] chore: update keyword_search plugin description in Cargo.toml - Simplified the plugin description by removing references to enhanced UI and features, focusing on the core functionality of searching file contents for user-specified keywords. --- plugins/keyword_search/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/keyword_search/Cargo.toml b/plugins/keyword_search/Cargo.toml index 65b6de0..92152cc 100644 --- a/plugins/keyword_search/Cargo.toml +++ b/plugins/keyword_search/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "keyword_search" -description = "Searches file contents for user-specified keywords with enhanced UI and features" +description = "Searches file contents for user-specified keywords" version = "0.3.1" edition = "2021" From 6b57b801f60572a3170b9e7d85f6d81f2ef70cc4 Mon Sep 17 00:00:00 2001 From: Mohamed Achaq Date: Sat, 28 Dec 2024 11:07:19 +0100 Subject: [PATCH 26/43] feat: enhance configuration management in lla_plugin_utils - Refactored ConfigManager to create and manage plugin configuration files more robustly, ensuring directories are created if they do not exist. - Updated the initialization process to handle loading existing configurations or creating a new default configuration if none exists. - Simplified the BasePlugin structure by integrating ConfigManager, improving the overall design and usability of the plugin configuration system. --- lla_plugin_utils/src/config.rs | 37 ++++++++++++++++++++++++++------ lla_plugin_utils/src/lib.rs | 39 +++++++++++----------------------- 2 files changed, 42 insertions(+), 34 deletions(-) diff --git a/lla_plugin_utils/src/config.rs b/lla_plugin_utils/src/config.rs index 773047a..37edcf1 100644 --- a/lla_plugin_utils/src/config.rs +++ b/lla_plugin_utils/src/config.rs @@ -14,13 +14,40 @@ pub struct ConfigManager { impl ConfigManager { pub fn new(plugin_name: &str) -> Self { - let config_path = dirs::config_dir() + let config_path = dirs::home_dir() .unwrap_or_else(|| PathBuf::from(".")) + .join(".config") .join("lla") + .join("plugins") .join(plugin_name) - .with_extension("toml"); + .join("config.toml"); - let config = Self::load_config(&config_path).unwrap_or_default(); + if let Some(parent) = config_path.parent() { + if let Err(e) = std::fs::create_dir_all(parent) { + eprintln!("[ConfigManager] Failed to create config directory: {}", e); + } + } + + let config = if config_path.exists() { + Self::load_config(&config_path).unwrap_or_else(|e| { + eprintln!( + "[ConfigManager] Failed to load config: {}, using default", + e + ); + T::default() + }) + } else { + let config = T::default(); + match toml::to_string_pretty(&config) { + Ok(content) => { + if let Err(e) = std::fs::write(&config_path, content) { + eprintln!("[ConfigManager] Failed to write initial config: {}", e); + } + } + Err(e) => eprintln!("[ConfigManager] Failed to serialize default config: {}", e), + } + config + }; Self { config, @@ -38,18 +65,14 @@ impl ConfigManager { pub fn save(&self) -> Result<(), String> { self.config.validate()?; - if let Some(parent) = self.config_path.parent() { std::fs::create_dir_all(parent) .map_err(|e| format!("Failed to create config directory: {}", e))?; } - let content = toml::to_string_pretty(&self.config) .map_err(|e| format!("Failed to serialize config: {}", e))?; - std::fs::write(&self.config_path, content) .map_err(|e| format!("Failed to write config file: {}", e))?; - Ok(()) } diff --git a/lla_plugin_utils/src/lib.rs b/lla_plugin_utils/src/lib.rs index 5a00cad..b1f8453 100644 --- a/lla_plugin_utils/src/lib.rs +++ b/lla_plugin_utils/src/lib.rs @@ -5,7 +5,7 @@ pub mod syntax; pub mod ui; pub use actions::{Action, ActionHelp, ActionRegistry}; -pub use config::PluginConfig; +pub use config::{ConfigManager, PluginConfig}; pub use syntax::CodeHighlighter; pub use ui::{ components::{BoxComponent, BoxStyle, HelpFormatter, KeyValue, List, Spinner}, @@ -14,50 +14,35 @@ pub use ui::{ }; use lla_plugin_interface::{proto, PluginRequest, PluginResponse}; -use std::path::PathBuf; pub struct BasePlugin { - config: C, - config_file: PathBuf, + config_manager: ConfigManager, } impl BasePlugin { pub fn new() -> Self { - let config_file = dirs::config_dir() - .unwrap_or_else(|| PathBuf::from(".")) - .join("lla") - .join("plugins") - .join(env!("CARGO_PKG_NAME")) - .join("config.toml"); - - let config = if let Ok(content) = std::fs::read_to_string(&config_file) { - toml::from_str(&content).unwrap_or_default() - } else { - C::default() - }; + let plugin_name = env!("CARGO_PKG_NAME"); + Self { + config_manager: ConfigManager::new(plugin_name), + } + } + pub fn with_name(plugin_name: &str) -> Self { Self { - config, - config_file, + config_manager: ConfigManager::new(plugin_name), } } pub fn config(&self) -> &C { - &self.config + self.config_manager.get() } pub fn config_mut(&mut self) -> &mut C { - &mut self.config + self.config_manager.get_mut() } pub fn save_config(&self) -> Result<(), String> { - if let Some(parent) = self.config_file.parent() { - std::fs::create_dir_all(parent).map_err(|e| e.to_string())?; - } - let content = toml::to_string_pretty(&self.config) - .map_err(|e| format!("Failed to serialize config: {}", e))?; - std::fs::write(&self.config_file, content).map_err(|e| e.to_string())?; - Ok(()) + self.config_manager.save() } } From 7f5e559c89e193184b9acea261422ef58fbd9667 Mon Sep 17 00:00:00 2001 From: Mohamed Achaq Date: Sat, 28 Dec 2024 11:16:02 +0100 Subject: [PATCH 27/43] feat: enhance categorizer plugin with hierarchical organization and detailed metadata - Expanded the README to provide a comprehensive overview of features, including smart file categorization, hierarchical organization, size-based rules, and rich formatting options. - Introduced default categories with specific configurations for Documents and Code, including subcategories and size ranges. - Refactored the plugin's internal logic to improve category and subcategory management, ensuring rules are saved correctly. - Updated usage examples in the README to reflect new commands for adding categories and managing subcategories. - Enhanced configuration management by integrating default rules and improving the overall structure of the plugin. --- plugins/categorizer/README.md | 83 +++++++--- plugins/categorizer/src/lib.rs | 277 ++++++++++++--------------------- 2 files changed, 168 insertions(+), 192 deletions(-) diff --git a/plugins/categorizer/README.md b/plugins/categorizer/README.md index f609a6c..7ea9a36 100644 --- a/plugins/categorizer/README.md +++ b/plugins/categorizer/README.md @@ -1,42 +1,89 @@ # LLA Categorizer Plugin -A file categorization plugin for `lla` that automatically categorizes files based on their extensions and provides detailed statistics about file categories in your directories. +A powerful file categorization plugin for `lla` that automatically organizes and labels files based on their extensions, with support for hierarchical categorization and detailed metadata tracking. ## Features -- Automatic file categorization with colored output -- Category statistics with file counts and sizes -- Subcategory support for organization -- Configurable categories and rules +- **Smart File Categorization**: Automatically categorizes files based on extensions with colored labels +- **Hierarchical Organization**: Support for categories and subcategories +- **Size-Based Rules**: Optional file size ranges for more precise categorization +- **Rich Formatting**: Two display formats (default and long) with colored output +- **Statistics Tracking**: Maintains counts and size statistics for categories and subcategories +- **Fully Configurable**: Easy to add and customize categories, colors, and rules + +## Default Categories + +The plugin comes with pre-configured categories: + +### Documents + +- Color: bright_blue +- Extensions: txt, md, doc, docx, pdf, rtf, odt +- Size Range: 0-10MB +- Subcategories: + - Text: txt, md + - Office: doc, docx, xls, xlsx, ppt, pptx + +### Code + +- Color: bright_cyan +- Extensions: rs, py, js, ts, java, c, cpp, h, hpp, go, rb, php, cs, swift, kt +- Size Range: 0-1MB +- Subcategories: + - Systems: rs, c, cpp, h, hpp + - Web: js, ts, html, css, php + - Scripts: py, rb, sh, bash ## Usage -The following actions are available through the plugin interface: +### Adding Categories ```bash # Add a new category -lla plugin --name categorizer --action add-category --args "Audio" "yellow" "mp3,wav,flac,ogg" "Audio files" +lla plugin --name categorizer --action add-category "Images" "yellow" "jpg,png,gif" "Image files" +``` +### Managing Subcategories + +```bash # Add a subcategory to an existing category -lla plugin --name categorizer --action add-subcategory --args "Audio" "Lossless" "flac,wav" +lla plugin --name categorizer --action add-subcategory "Images" "Raster" "jpg,png,gif" +``` -# Show statistics about file categories -lla plugin --name categorizer --action show-stats +### Viewing Categories -# List all configured categories +```bash +# List all configured categories and their details lla plugin --name categorizer --action list-categories -# Show help and available actions +# Show help and available commands lla plugin --name categorizer --action help ``` ## Configuration -The plugin stores its configuration in `~/.config/lla/categorizer.toml`. The configuration includes: +The plugin configuration is stored in `~/.config/lla/plugins/categorizer/config.toml` and includes: + +- Category definitions (name, color, description) +- File extension mappings +- Size range rules +- Subcategory configurations +- UI color schemes + +The configuration is automatically created with sensible defaults on first run and can be customized as needed. + +## Display Formats + +The plugin supports two display formats: + +- **default**: Shows category in colored brackets (e.g., `[Documents]`) +- **long**: Shows category with subcategory (e.g., `[Documents] (Text)`) + +## Development -- Category names and colors -- File extensions for each category -- Subcategories and their extensions -- Optional size ranges for categorization +This plugin is built using Rust and integrates with the LLA plugin system. It uses: -Default categories include Documents, Images, and Code, each with their own subcategories and extensions. +- `serde` for configuration serialization +- `colored` for terminal output +- `parking_lot` for thread-safe state management +- `toml` for configuration file handling diff --git a/plugins/categorizer/src/lib.rs b/plugins/categorizer/src/lib.rs index cc0e8b8..c87553f 100644 --- a/plugins/categorizer/src/lib.rs +++ b/plugins/categorizer/src/lib.rs @@ -4,13 +4,13 @@ use lla_plugin_utils::{ config::PluginConfig, ui::{ components::{BoxComponent, BoxStyle, HelpFormatter, KeyValue, List}, - format_size, TextBlock, + TextBlock, }, ActionRegistry, BasePlugin, ConfigurablePlugin, ProtobufHandler, }; use parking_lot::RwLock; use serde::{Deserialize, Serialize}; -use std::{collections::HashMap, fs, path::PathBuf}; +use std::collections::HashMap; lazy_static! { static ref ACTION_REGISTRY: RwLock = RwLock::new({ @@ -33,7 +33,9 @@ lazy_static! { if let Some(desc) = args.get(3) { rule.description = desc.clone(); } - PLUGIN_STATE.write().add_rule(rule); + let mut plugin = FileCategoryPlugin::new(); + plugin.config_mut().rules.push(rule); + plugin.base.save_config().map_err(|e| e.to_string())?; Ok(()) } ); @@ -51,22 +53,20 @@ lazy_static! { .to_string(), ); } - PLUGIN_STATE - .write() - .add_subcategory(&args[0], &args[1], &args[2]) - } - ); - lla_plugin_utils::define_action!( - registry, - "show-stats", - "show-stats", - "Show category statistics", - vec!["lla plugin --name categorizer --action show-stats"], - |_| { - let state = PLUGIN_STATE.read(); - println!("{}", state.format_stats()); - Ok(()) + let mut plugin = FileCategoryPlugin::new(); + let config = plugin.config_mut(); + + if let Some(rule) = config.rules.iter_mut().find(|r| r.name == args[0]) { + rule.subcategories.insert( + args[1].to_string(), + args[2].split(',').map(String::from).collect(), + ); + plugin.base.save_config().map_err(|e| e.to_string())?; + Ok(()) + } else { + Err(format!("Category '{}' not found", args[0])) + } } ); @@ -77,9 +77,9 @@ lazy_static! { "List all categories and their details", vec!["lla plugin --name categorizer --action list-categories"], |_| { - let state = PLUGIN_STATE.read(); + let plugin = FileCategoryPlugin::new(); let mut list = List::new(); - for rule in &state.rules { + for rule in &plugin.config().rules { let mut details = Vec::new(); details.push(format!("Extensions: {}", rule.extensions.join(", "))); @@ -120,6 +120,7 @@ lazy_static! { "Show help information", vec!["lla plugin --name categorizer --action help"], |_| { + let plugin = FileCategoryPlugin::new(); let mut help = HelpFormatter::new("File Categorizer Plugin".to_string()); help.add_section("Description".to_string()).add_command( "".to_string(), @@ -138,11 +139,6 @@ lazy_static! { "Add a subcategory to an existing category".to_string(), vec!["lla plugin --name categorizer --action add-subcategory Documents Text txt,md".to_string()], ) - .add_command( - "show-stats".to_string(), - "Show category statistics".to_string(), - vec!["lla plugin --name categorizer --action show-stats".to_string()], - ) .add_command( "list-categories".to_string(), "List all categories and their details".to_string(), @@ -156,7 +152,7 @@ lazy_static! { println!( "{}", - BoxComponent::new(help.render(&CategorizerConfig::default().colors)) + BoxComponent::new(help.render(&plugin.config().colors)) .style(BoxStyle::Minimal) .padding(2) .render() @@ -201,139 +197,24 @@ struct CategoryStats { } struct PluginState { - rules: Vec, - config_path: PathBuf, stats: HashMap, } impl PluginState { fn new() -> Self { - let config_path = dirs::config_dir() - .unwrap_or_else(|| PathBuf::from(".")) - .join("lla") - .join("categorizer.toml"); - - let rules = Self::load_rules(&config_path).unwrap_or_else(|| { - vec![ - CategoryRule { - name: "Document".to_string(), - color: "bright_blue".to_string(), - extensions: vec!["txt", "md", "doc", "docx", "pdf", "rtf", "odt"] - .into_iter() - .map(String::from) - .collect(), - size_ranges: Some(vec![(0, 10_485_760)]), - subcategories: { - let mut map = HashMap::new(); - map.insert( - "Text".to_string(), - vec!["txt", "md"].into_iter().map(String::from).collect(), - ); - map.insert( - "Office".to_string(), - vec!["doc", "docx", "xls", "xlsx", "ppt", "pptx"] - .into_iter() - .map(String::from) - .collect(), - ); - map - }, - description: "Text documents and office files".to_string(), - }, - CategoryRule { - name: "Code".to_string(), - color: "bright_cyan".to_string(), - extensions: vec![ - "rs", "py", "js", "ts", "java", "c", "cpp", "h", "hpp", "go", "rb", "php", - "cs", "swift", "kt", - ] - .into_iter() - .map(String::from) - .collect(), - size_ranges: Some(vec![(0, 1_048_576)]), - subcategories: { - let mut map = HashMap::new(); - map.insert( - "Systems".to_string(), - vec!["rs", "c", "cpp", "h", "hpp"] - .into_iter() - .map(String::from) - .collect(), - ); - map.insert( - "Web".to_string(), - vec!["js", "ts", "html", "css", "php"] - .into_iter() - .map(String::from) - .collect(), - ); - map.insert( - "Scripts".to_string(), - vec!["py", "rb", "sh", "bash"] - .into_iter() - .map(String::from) - .collect(), - ); - map - }, - description: "Source code files".to_string(), - }, - ] - }); - Self { - rules, - config_path, stats: HashMap::new(), } } - fn load_rules(path: &PathBuf) -> Option> { - fs::read_to_string(path) - .ok() - .and_then(|content| toml::from_str(&content).ok()) - } - - fn save_rules(&self) { - if let Some(parent) = self.config_path.parent() { - fs::create_dir_all(parent).ok(); - } - if let Ok(content) = toml::to_string_pretty(&self.rules) { - fs::write(&self.config_path, content).ok(); - } - } - - fn add_rule(&mut self, rule: CategoryRule) { - self.rules.push(rule); - self.save_rules(); - } - - fn add_subcategory( - &mut self, - category: &str, - subcategory: &str, - extensions: &str, - ) -> Result<(), String> { - if let Some(rule) = self.rules.iter_mut().find(|r| r.name == category) { - rule.subcategories.insert( - subcategory.to_string(), - extensions.split(',').map(String::from).collect(), - ); - self.save_rules(); - Ok(()) - } else { - Err(format!("Category '{}' not found", category)) - } - } - fn get_category_info( - &self, + rules: &[CategoryRule], entry: &DecoratedEntry, ) -> Option<(String, String, Option)> { let extension = entry.path.extension()?.to_str()?.to_lowercase(); let size = entry.metadata.size; - for rule in &self.rules { + for rule in rules { if rule.extensions.iter().any(|ext| ext == &extension) { if let Some(ranges) = &rule.size_ranges { if !ranges.iter().any(|(min, max)| size >= *min && size <= *max) { @@ -361,40 +242,14 @@ impl PluginState { *stats.subcategory_counts.entry(sub.to_string()).or_default() += 1; } } - - fn format_stats(&self) -> String { - let mut list = List::new(); - for (category, stats) in &self.stats { - let rule = self.rules.iter().find(|r| &r.name == category); - let white = "white".to_string(); - let color = rule.map(|r| &r.color).unwrap_or(&white); - - let header = KeyValue::new( - category, - &format!("{} files, {}", stats.count, format_size(stats.total_size)), - ) - .key_color(color) - .key_width(15) - .render(); - - list.add_item(header); - - for (sub, count) in &stats.subcategory_counts { - list.add_item(format!(" {} ({} files)", sub, count)); - } - } - - BoxComponent::new(list.render()) - .style(BoxStyle::Minimal) - .padding(1) - .render() - } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CategorizerConfig { #[serde(default = "default_colors")] colors: HashMap, + #[serde(default = "default_rules")] + rules: Vec, } fn default_colors() -> HashMap { @@ -405,10 +260,79 @@ fn default_colors() -> HashMap { colors } +fn default_rules() -> Vec { + vec![ + CategoryRule { + name: "Document".to_string(), + color: "bright_blue".to_string(), + extensions: vec!["txt", "md", "doc", "docx", "pdf", "rtf", "odt"] + .into_iter() + .map(String::from) + .collect(), + size_ranges: Some(vec![(0, 10_485_760)]), + subcategories: { + let mut map = HashMap::new(); + map.insert( + "Text".to_string(), + vec!["txt", "md"].into_iter().map(String::from).collect(), + ); + map.insert( + "Office".to_string(), + vec!["doc", "docx", "xls", "xlsx", "ppt", "pptx"] + .into_iter() + .map(String::from) + .collect(), + ); + map + }, + description: "Text documents and office files".to_string(), + }, + CategoryRule { + name: "Code".to_string(), + color: "bright_cyan".to_string(), + extensions: vec![ + "rs", "py", "js", "ts", "java", "c", "cpp", "h", "hpp", "go", "rb", "php", "cs", + "swift", "kt", + ] + .into_iter() + .map(String::from) + .collect(), + size_ranges: Some(vec![(0, 1_048_576)]), + subcategories: { + let mut map = HashMap::new(); + map.insert( + "Systems".to_string(), + vec!["rs", "c", "cpp", "h", "hpp"] + .into_iter() + .map(String::from) + .collect(), + ); + map.insert( + "Web".to_string(), + vec!["js", "ts", "html", "css", "php"] + .into_iter() + .map(String::from) + .collect(), + ); + map.insert( + "Scripts".to_string(), + vec!["py", "rb", "sh", "bash"] + .into_iter() + .map(String::from) + .collect(), + ); + map + }, + description: "Source code files".to_string(), + }, + ] +} + impl Default for CategorizerConfig { fn default() -> Self { Self { colors: default_colors(), + rules: default_rules(), } } } @@ -421,9 +345,14 @@ pub struct FileCategoryPlugin { impl FileCategoryPlugin { pub fn new() -> Self { - Self { - base: BasePlugin::new(), + let plugin_name = env!("CARGO_PKG_NAME"); + let plugin = Self { + base: BasePlugin::with_name(plugin_name), + }; + if let Err(e) = plugin.base.save_config() { + eprintln!("[FileCategoryPlugin] Failed to save config: {}", e); } + plugin } fn format_file_info(&self, entry: &DecoratedEntry, format: &str) -> Option { @@ -480,7 +409,7 @@ impl Plugin for FileCategoryPlugin { PluginRequest::Decorate(mut entry) => { let mut state = PLUGIN_STATE.write(); if let Some((category, color, subcategory)) = - state.get_category_info(&entry) + PluginState::get_category_info(&self.config().rules, &entry) { entry .custom_fields From 920c68c2fa327e2a372099f6756f965429c0511a Mon Sep 17 00:00:00 2001 From: Mohamed Achaq Date: Sat, 28 Dec 2024 11:40:04 +0100 Subject: [PATCH 28/43] feat: improve BoxComponent rendering and enhance ComplexityConfig defaults - Added a newline character to the output in the BoxComponent's render method for better formatting. - Introduced default values for `languages` and `thresholds` in the ComplexityConfig struct, streamlining the default configuration process. - Refactored the CodeComplexityEstimatorPlugin initialization to include error handling when saving the plugin configuration, improving robustness. --- lla_plugin_utils/src/ui/components.rs | 1 + plugins/code_complexity/src/lib.rs | 35 ++++++++++++++++++++++----- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/lla_plugin_utils/src/ui/components.rs b/lla_plugin_utils/src/ui/components.rs index 5dff765..88a99c1 100644 --- a/lla_plugin_utils/src/ui/components.rs +++ b/lla_plugin_utils/src/ui/components.rs @@ -351,6 +351,7 @@ impl BoxComponent { pub fn render(&self) -> String { let chars = self.style.get_chars(); let mut output = String::new(); + output.push('\n'); let lines: Vec<&str> = self.content.lines().collect(); let content_width = lines diff --git a/plugins/code_complexity/src/lib.rs b/plugins/code_complexity/src/lib.rs index 0e9b49f..fa1e86e 100644 --- a/plugins/code_complexity/src/lib.rs +++ b/plugins/code_complexity/src/lib.rs @@ -168,7 +168,9 @@ impl Default for LanguageRules { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ComplexityConfig { + #[serde(default = "default_languages")] languages: HashMap, + #[serde(default = "default_thresholds")] thresholds: ComplexityThresholds, #[serde(default = "default_colors")] colors: HashMap, @@ -186,13 +188,26 @@ fn default_colors() -> HashMap { colors } +fn default_thresholds() -> ComplexityThresholds { + ComplexityThresholds { + low: 10.0, + medium: 20.0, + high: 30.0, + very_high: 40.0, + } +} + +fn default_languages() -> HashMap { + let mut languages = HashMap::new(); + languages.insert("Rust".to_string(), LanguageRules::default()); + languages +} + impl Default for ComplexityConfig { fn default() -> Self { - let mut languages = HashMap::new(); - languages.insert("Rust".to_string(), LanguageRules::default()); Self { - languages, - thresholds: ComplexityThresholds::default(), + languages: default_languages(), + thresholds: default_thresholds(), colors: default_colors(), } } @@ -555,9 +570,17 @@ pub struct CodeComplexityEstimatorPlugin { impl CodeComplexityEstimatorPlugin { pub fn new() -> Self { - Self { - base: BasePlugin::new(), + let plugin_name = env!("CARGO_PKG_NAME"); + let plugin = Self { + base: BasePlugin::with_name(plugin_name), + }; + if let Err(e) = plugin.base.save_config() { + eprintln!( + "[CodeComplexityEstimatorPlugin] Failed to save config: {}", + e + ); } + plugin } fn format_file_info(&self, entry: &DecoratedEntry, format: &str) -> Option { From dc70472604c849acb7eec05c048a18cccfabd771 Mon Sep 17 00:00:00 2001 From: Mohamed Achaq Date: Sat, 28 Dec 2024 11:51:50 +0100 Subject: [PATCH 29/43] feat: enhance code_snippet_extractor plugin with new configuration options - Introduced `syntax_themes` and `max_preview_lines` fields in `SnippetConfig` for improved customization. - Added default values for syntax themes and maximum preview lines to streamline configuration. - Refactored `CodeSnippetExtractorPlugin` initialization to utilize a plugin name and improved error handling for configuration saving. - Implemented `ConfigurablePlugin` trait for better configuration management and access. - Updated plugin declaration to support new features and maintain consistency with the plugin interface. --- plugins/code_snippet_extractor/src/lib.rs | 59 ++++++++++++++++++++--- 1 file changed, 52 insertions(+), 7 deletions(-) diff --git a/plugins/code_snippet_extractor/src/lib.rs b/plugins/code_snippet_extractor/src/lib.rs index bd9c49f..646e7eb 100644 --- a/plugins/code_snippet_extractor/src/lib.rs +++ b/plugins/code_snippet_extractor/src/lib.rs @@ -10,7 +10,7 @@ use lla_plugin_interface::{Plugin, PluginRequest, PluginResponse}; use lla_plugin_utils::{ config::PluginConfig, ui::components::{BoxComponent, BoxStyle, HelpFormatter}, - ActionRegistry, BasePlugin, ProtobufHandler, + ActionRegistry, BasePlugin, ConfigurablePlugin, ProtobufHandler, }; use parking_lot::RwLock; use ring::digest; @@ -35,6 +35,10 @@ use uuid::Uuid; pub struct SnippetConfig { #[serde(default = "default_colors")] colors: HashMap, + #[serde(default = "default_syntax_themes")] + syntax_themes: HashMap, + #[serde(default = "default_max_preview_lines")] + max_preview_lines: usize, } fn default_colors() -> HashMap { @@ -48,10 +52,22 @@ fn default_colors() -> HashMap { colors } +fn default_syntax_themes() -> HashMap { + let mut themes = HashMap::new(); + themes.insert("default".to_string(), "Solarized (dark)".to_string()); + themes +} + +fn default_max_preview_lines() -> usize { + 10 +} + impl Default for SnippetConfig { fn default() -> Self { Self { colors: default_colors(), + syntax_themes: default_syntax_themes(), + max_preview_lines: default_max_preview_lines(), } } } @@ -239,13 +255,22 @@ pub struct CodeSnippetExtractorPlugin { impl CodeSnippetExtractorPlugin { pub fn new() -> Self { - let base = BasePlugin::new(); - let snippet_file = dirs::config_dir() + let plugin_name = env!("CARGO_PKG_NAME"); + let plugin = Self { + base: BasePlugin::with_name(plugin_name), + snippets: Self::load_snippets(&Self::get_snippets_path()), + }; + if let Err(e) = plugin.base.save_config() { + eprintln!("[CodeSnippetExtractorPlugin] Failed to save config: {}", e); + } + plugin + } + + fn get_snippets_path() -> PathBuf { + dirs::config_dir() .unwrap_or_else(|| PathBuf::from(".")) .join("lla") - .join("code_snippets.toml"); - let snippets = Self::load_snippets(&snippet_file); - Self { base, snippets } + .join("code_snippets.toml") } fn load_snippets(path: &PathBuf) -> HashMap { @@ -1253,4 +1278,24 @@ impl Plugin for CodeSnippetExtractorPlugin { } } -lla_plugin_utils::create_plugin!(CodeSnippetExtractorPlugin); +impl ConfigurablePlugin for CodeSnippetExtractorPlugin { + type Config = SnippetConfig; + + fn config(&self) -> &Self::Config { + self.base.config() + } + + fn config_mut(&mut self) -> &mut Self::Config { + self.base.config_mut() + } +} + +impl ProtobufHandler for CodeSnippetExtractorPlugin {} + +lla_plugin_interface::declare_plugin!(CodeSnippetExtractorPlugin); + +impl Default for CodeSnippetExtractorPlugin { + fn default() -> Self { + Self::new() + } +} From 4bc42b589b82fc6ac8c0fb0a9a6eb7159f290fc3 Mon Sep 17 00:00:00 2001 From: Mohamed Achaq Date: Sat, 28 Dec 2024 11:54:48 +0100 Subject: [PATCH 30/43] feat: enhance DirsPlugin with new configuration options and improved initialization - Added `max_scan_depth` and `parallel_threshold` fields to `DirsConfig` with default values for better performance tuning. - Introduced `default_scan_depth` and `default_parallel_threshold` functions to streamline configuration management. - Refactored `DirsPlugin` initialization to include a new constructor method, improving error handling during configuration saving. - Updated the default implementation of `DirsPlugin` to utilize the new constructor for cleaner code structure. --- plugins/dirs_meta/src/lib.rs | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/plugins/dirs_meta/src/lib.rs b/plugins/dirs_meta/src/lib.rs index 15ab783..3cc6e66 100644 --- a/plugins/dirs_meta/src/lib.rs +++ b/plugins/dirs_meta/src/lib.rs @@ -132,6 +132,10 @@ pub struct DirsConfig { cache_size: usize, #[serde(default = "default_colors")] colors: HashMap, + #[serde(default = "default_scan_depth")] + max_scan_depth: usize, + #[serde(default = "default_parallel_threshold")] + parallel_threshold: usize, } fn default_cache_size() -> usize { @@ -150,11 +154,21 @@ fn default_colors() -> HashMap { colors } +fn default_scan_depth() -> usize { + 100 +} + +fn default_parallel_threshold() -> usize { + 1000 +} + impl Default for DirsConfig { fn default() -> Self { Self { cache_size: default_cache_size(), colors: default_colors(), + max_scan_depth: default_scan_depth(), + parallel_threshold: default_parallel_threshold(), } } } @@ -166,6 +180,17 @@ pub struct DirsPlugin { } impl DirsPlugin { + pub fn new() -> Self { + let plugin_name = env!("CARGO_PKG_NAME"); + let plugin = Self { + base: BasePlugin::with_name(plugin_name), + }; + if let Err(e) = plugin.base.save_config() { + eprintln!("[DirsPlugin] Failed to save config: {}", e); + } + plugin + } + fn analyze_directory(path: &Path) -> Option<(usize, usize, u64)> { let path_str = path.to_string_lossy().to_string(); @@ -409,9 +434,7 @@ impl Plugin for DirsPlugin { impl Default for DirsPlugin { fn default() -> Self { - Self { - base: BasePlugin::new(), - } + Self::new() } } From 48fcb64d1d6de6b6e21af493d14dab16bb11f806 Mon Sep 17 00:00:00 2001 From: Mohamed Achaq Date: Sat, 28 Dec 2024 11:59:39 +0100 Subject: [PATCH 31/43] feat: enhance DuplicateFileDetectorPlugin initialization and error handling - Updated the plugin constructor to use the plugin name for better identification. - Improved error handling by logging configuration save failures to standard error output. - Refactored the initialization process to streamline the creation of the DuplicateFileDetectorPlugin instance. --- plugins/duplicate_file_detector/src/lib.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/plugins/duplicate_file_detector/src/lib.rs b/plugins/duplicate_file_detector/src/lib.rs index adc67ef..e6bdd96 100644 --- a/plugins/duplicate_file_detector/src/lib.rs +++ b/plugins/duplicate_file_detector/src/lib.rs @@ -146,9 +146,14 @@ pub struct DuplicateFileDetectorPlugin { impl DuplicateFileDetectorPlugin { pub fn new() -> Self { - Self { - base: BasePlugin::new(), + let plugin_name = env!("CARGO_PKG_NAME"); + let plugin = Self { + base: BasePlugin::with_name(plugin_name), + }; + if let Err(e) = plugin.base.save_config() { + eprintln!("[DuplicateFileDetectorPlugin] Failed to save config: {}", e); } + plugin } fn get_file_hash(path: &Path) -> Option { From f6589c599f0752fef8f4bef40e716302f6d9fd13 Mon Sep 17 00:00:00 2001 From: Mohamed Achaq Date: Sat, 28 Dec 2024 12:01:41 +0100 Subject: [PATCH 32/43] feat: enhance FileHashPlugin initialization and error handling - Updated the constructor to use the plugin name for better identification. - Added error handling to log configuration save failures to standard error output. - Refactored the initialization process for improved clarity and robustness. --- plugins/file_hash/src/lib.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/plugins/file_hash/src/lib.rs b/plugins/file_hash/src/lib.rs index 0549c77..819392d 100644 --- a/plugins/file_hash/src/lib.rs +++ b/plugins/file_hash/src/lib.rs @@ -99,9 +99,14 @@ pub struct FileHashPlugin { impl FileHashPlugin { pub fn new() -> Self { - Self { - base: BasePlugin::new(), + let plugin_name = env!("CARGO_PKG_NAME"); + let plugin = Self { + base: BasePlugin::with_name(plugin_name), + }; + if let Err(e) = plugin.base.save_config() { + eprintln!("[FileHashPlugin] Failed to save config: {}", e); } + plugin } fn calculate_hashes(path: &std::path::Path) -> Option<(String, String)> { From e56ca4fe96975754675b33dfbe97da3dbfdf572f Mon Sep 17 00:00:00 2001 From: Mohamed Achaq Date: Sat, 28 Dec 2024 12:03:33 +0100 Subject: [PATCH 33/43] feat: enhance FileMetadataPlugin initialization and error handling - Updated the constructor to utilize the plugin name for better identification. - Added error handling to log configuration save failures to standard error output. - Refactored the initialization process for improved clarity and robustness. --- plugins/file_meta/src/lib.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/plugins/file_meta/src/lib.rs b/plugins/file_meta/src/lib.rs index e7954e5..e8cd932 100644 --- a/plugins/file_meta/src/lib.rs +++ b/plugins/file_meta/src/lib.rs @@ -100,9 +100,14 @@ pub struct FileMetadataPlugin { impl FileMetadataPlugin { pub fn new() -> Self { - Self { - base: BasePlugin::new(), + let plugin_name = env!("CARGO_PKG_NAME"); + let plugin = Self { + base: BasePlugin::with_name(plugin_name), + }; + if let Err(e) = plugin.base.save_config() { + eprintln!("[FileMetadataPlugin] Failed to save config: {}", e); } + plugin } fn format_timestamp(timestamp: SystemTime) -> String { From 2cc6df468b5575c33c6c505ebf6895460c1fd0d0 Mon Sep 17 00:00:00 2001 From: Mohamed Achaq Date: Sat, 28 Dec 2024 12:05:04 +0100 Subject: [PATCH 34/43] feat: enhance FileTaggerPlugin initialization and error handling - Updated the constructor to utilize the plugin name for better identification. - Added error handling to log configuration save failures to standard error output. - Refactored the initialization process for improved clarity and robustness. --- plugins/file_tagger/src/lib.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/plugins/file_tagger/src/lib.rs b/plugins/file_tagger/src/lib.rs index 1e51420..9cc912a 100644 --- a/plugins/file_tagger/src/lib.rs +++ b/plugins/file_tagger/src/lib.rs @@ -212,16 +212,21 @@ pub struct FileTaggerPlugin { impl FileTaggerPlugin { pub fn new() -> Self { + let plugin_name = env!("CARGO_PKG_NAME"); let tag_file = dirs::config_dir() .unwrap_or_else(|| PathBuf::from(".")) .join("lla") .join("file_tags.txt"); let tags = Self::load_tags(&tag_file); - Self { - base: BasePlugin::new(), + let plugin = Self { + base: BasePlugin::with_name(plugin_name), tag_file, tags, + }; + if let Err(e) = plugin.base.save_config() { + eprintln!("[FileTaggerPlugin] Failed to save config: {}", e); } + plugin } fn load_tags(path: &PathBuf) -> HashMap> { From b44fce92cdc4ec29799d40b91e3199a08d886506 Mon Sep 17 00:00:00 2001 From: Mohamed Achaq Date: Sat, 28 Dec 2024 12:06:19 +0100 Subject: [PATCH 35/43] feat: enhance GitStatusPlugin initialization and error handling - Updated the constructor to utilize the plugin name for better identification. - Added error handling to log configuration save failures to standard error output. - Refactored the initialization process for improved clarity and robustness. --- plugins/git_status/src/lib.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/plugins/git_status/src/lib.rs b/plugins/git_status/src/lib.rs index 9194076..3e49e34 100644 --- a/plugins/git_status/src/lib.rs +++ b/plugins/git_status/src/lib.rs @@ -99,9 +99,14 @@ pub struct GitStatusPlugin { impl GitStatusPlugin { pub fn new() -> Self { - Self { - base: BasePlugin::new(), + let plugin_name = env!("CARGO_PKG_NAME"); + let plugin = Self { + base: BasePlugin::with_name(plugin_name), + }; + if let Err(e) = plugin.base.save_config() { + eprintln!("[GitStatusPlugin] Failed to save config: {}", e); } + plugin } fn is_git_repo(path: &Path) -> bool { From 8e8990ab6f222f06b6f22ae06f279e35b30ca9db Mon Sep 17 00:00:00 2001 From: Mohamed Achaq Date: Sat, 28 Dec 2024 12:07:26 +0100 Subject: [PATCH 36/43] feat: enhance KeywordSearchPlugin initialization and error handling - Updated the constructor to utilize the plugin name for better identification. - Added error handling to log configuration save failures to standard error output. - Refactored the initialization process for improved clarity and robustness. --- plugins/keyword_search/src/lib.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/plugins/keyword_search/src/lib.rs b/plugins/keyword_search/src/lib.rs index bef87f4..6aebfc8 100644 --- a/plugins/keyword_search/src/lib.rs +++ b/plugins/keyword_search/src/lib.rs @@ -99,9 +99,14 @@ pub struct KeywordSearchPlugin { impl KeywordSearchPlugin { pub fn new() -> Self { - Self { - base: BasePlugin::new(), + let plugin_name = env!("CARGO_PKG_NAME"); + let plugin = Self { + base: BasePlugin::with_name(plugin_name), + }; + if let Err(e) = plugin.base.save_config() { + eprintln!("[KeywordSearchPlugin] Failed to save config: {}", e); } + plugin } fn highlight_match(&self, line: &str, keyword: &str) -> String { From cc1735accf148f52af5d96b8180ce1776e79c7c1 Mon Sep 17 00:00:00 2001 From: Mohamed Achaq Date: Sat, 28 Dec 2024 12:09:40 +0100 Subject: [PATCH 37/43] feat: enhance LastGitCommitPlugin initialization and error handling - Updated the constructor to utilize the plugin name for better identification. - Added error handling to log configuration save failures to standard error output. - Refactored the initialization process for improved clarity and robustness. --- plugins/last_git_commit/src/lib.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/plugins/last_git_commit/src/lib.rs b/plugins/last_git_commit/src/lib.rs index f03fb6d..5c8f0a1 100644 --- a/plugins/last_git_commit/src/lib.rs +++ b/plugins/last_git_commit/src/lib.rs @@ -93,9 +93,14 @@ pub struct LastGitCommitPlugin { impl LastGitCommitPlugin { pub fn new() -> Self { - Self { - base: BasePlugin::new(), + let plugin_name = env!("CARGO_PKG_NAME"); + let plugin = Self { + base: BasePlugin::with_name(plugin_name), + }; + if let Err(e) = plugin.base.save_config() { + eprintln!("[LastGitCommitPlugin] Failed to save config: {}", e); } + plugin } fn get_last_commit_info(path: &Path) -> Option<(String, String, String)> { From b9657a7ffb02de13b4626faccdfd8b85e4038012 Mon Sep 17 00:00:00 2001 From: Mohamed Achaq Date: Sat, 28 Dec 2024 12:12:07 +0100 Subject: [PATCH 38/43] feat: enhance FileSizeVisualizerPlugin initialization and error handling - Updated the constructor to utilize the plugin name for better identification. - Added error handling to log configuration save failures to standard error output. - Refactored the initialization process for improved clarity and robustness. --- plugins/sizeviz/src/lib.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/plugins/sizeviz/src/lib.rs b/plugins/sizeviz/src/lib.rs index 845495f..79c274b 100644 --- a/plugins/sizeviz/src/lib.rs +++ b/plugins/sizeviz/src/lib.rs @@ -97,9 +97,14 @@ pub struct FileSizeVisualizerPlugin { impl FileSizeVisualizerPlugin { pub fn new() -> Self { - Self { - base: BasePlugin::new(), + let plugin_name = env!("CARGO_PKG_NAME"); + let plugin = Self { + base: BasePlugin::with_name(plugin_name), + }; + if let Err(e) = plugin.base.save_config() { + eprintln!("[FileSizeVisualizerPlugin] Failed to save config: {}", e); } + plugin } fn format_size(size: u64) -> String { From 98fdc565d5a380297bc0769471cd2638e7ccdcaa Mon Sep 17 00:00:00 2001 From: Mohamed Achaq Date: Sat, 28 Dec 2024 12:49:54 +0100 Subject: [PATCH 39/43] feat: update plugin documentation and configuration for improved clarity and usability - Refined README files for multiple plugins, enhancing feature descriptions and usage instructions. - Standardized configuration examples across plugins for consistency. - Improved display formats and examples to better illustrate plugin functionality. - Updated color coding and metadata sections for better visibility and user experience. - Enhanced overall documentation structure to facilitate easier navigation and understanding. --- plugins/categorizer/README.md | 64 +++--------- plugins/code_complexity/README.md | 117 +++++++++++++++------- plugins/code_snippet_extractor/README.md | 112 +++++++++++---------- plugins/dirs_meta/README.md | 68 ++++++------- plugins/duplicate_file_detector/README.md | 71 +++++++++---- plugins/file_hash/README.md | 56 +++++++---- plugins/file_meta/README.md | 64 ++++++------ plugins/file_tagger/README.md | 59 +++++++---- plugins/git_status/README.md | 75 ++++++-------- plugins/keyword_search/README.md | 94 +++++++++-------- plugins/last_git_commit/README.md | 50 +++++---- plugins/sizeviz/README.md | 57 ++++++----- plugins/sizeviz/src/lib.rs | 2 +- 13 files changed, 488 insertions(+), 401 deletions(-) diff --git a/plugins/categorizer/README.md b/plugins/categorizer/README.md index 7ea9a36..f8f09ea 100644 --- a/plugins/categorizer/README.md +++ b/plugins/categorizer/README.md @@ -1,25 +1,20 @@ # LLA Categorizer Plugin -A powerful file categorization plugin for `lla` that automatically organizes and labels files based on their extensions, with support for hierarchical categorization and detailed metadata tracking. +File categorization plugin for `lla` that organizes files based on extensions, with hierarchical categorization support. ## Features -- **Smart File Categorization**: Automatically categorizes files based on extensions with colored labels -- **Hierarchical Organization**: Support for categories and subcategories -- **Size-Based Rules**: Optional file size ranges for more precise categorization -- **Rich Formatting**: Two display formats (default and long) with colored output -- **Statistics Tracking**: Maintains counts and size statistics for categories and subcategories -- **Fully Configurable**: Easy to add and customize categories, colors, and rules +- Automatic file categorization by extension with colored labels +- Hierarchical categories and subcategories +- Size-based rules and statistics tracking +- Configurable categories, colors, and rules ## Default Categories -The plugin comes with pre-configured categories: - ### Documents - Color: bright_blue - Extensions: txt, md, doc, docx, pdf, rtf, odt -- Size Range: 0-10MB - Subcategories: - Text: txt, md - Office: doc, docx, xls, xlsx, ppt, pptx @@ -27,8 +22,7 @@ The plugin comes with pre-configured categories: ### Code - Color: bright_cyan -- Extensions: rs, py, js, ts, java, c, cpp, h, hpp, go, rb, php, cs, swift, kt -- Size Range: 0-1MB +- Extensions: rs, py, js, ts, java, c, cpp, h, hpp, go, rb, php - Subcategories: - Systems: rs, c, cpp, h, hpp - Web: js, ts, html, css, php @@ -36,54 +30,26 @@ The plugin comes with pre-configured categories: ## Usage -### Adding Categories - ```bash -# Add a new category -lla plugin --name categorizer --action add-category "Images" "yellow" "jpg,png,gif" "Image files" -``` - -### Managing Subcategories +# Add category +lla plugin --name categorizer --action add-category "Images" "yellow" "jpg,png,gif" -```bash -# Add a subcategory to an existing category +# Add subcategory lla plugin --name categorizer --action add-subcategory "Images" "Raster" "jpg,png,gif" -``` - -### Viewing Categories -```bash -# List all configured categories and their details +# List categories lla plugin --name categorizer --action list-categories - -# Show help and available commands -lla plugin --name categorizer --action help ``` ## Configuration -The plugin configuration is stored in `~/.config/lla/plugins/categorizer/config.toml` and includes: +Config file: `~/.config/lla/plugins/categorizer/config.toml` -- Category definitions (name, color, description) -- File extension mappings -- Size range rules -- Subcategory configurations +- Category definitions and mappings +- Size rules and subcategories - UI color schemes -The configuration is automatically created with sensible defaults on first run and can be customized as needed. - ## Display Formats -The plugin supports two display formats: - -- **default**: Shows category in colored brackets (e.g., `[Documents]`) -- **long**: Shows category with subcategory (e.g., `[Documents] (Text)`) - -## Development - -This plugin is built using Rust and integrates with the LLA plugin system. It uses: - -- `serde` for configuration serialization -- `colored` for terminal output -- `parking_lot` for thread-safe state management -- `toml` for configuration file handling +- **default**: `[Documents]` +- **long**: `[Documents] (Text)` diff --git a/plugins/code_complexity/README.md b/plugins/code_complexity/README.md index c78354e..ddafe9e 100644 --- a/plugins/code_complexity/README.md +++ b/plugins/code_complexity/README.md @@ -1,61 +1,104 @@ # LLA Code Complexity Plugin -A code analysis plugin for `lla` that estimates code complexity using various metrics and provides detailed reports for Rust, Python, and JavaScript/TypeScript files. +A code analysis plugin for `lla` that performs real-time complexity analysis of source code. -## What it Does +## Features -- Calculates code complexity metrics including: - - Cyclomatic complexity - - Cognitive complexity - - Maintainability index (0-100) - - Lines of code, functions, classes, branches, loops - - Comment density and long lines detection -- Provides color-coded complexity indicators -- Generates detailed reports per language and file -- Supports configurable complexity thresholds +- **Multi-Metric Analysis** + - Cyclomatic & Cognitive Complexity + - Maintainability Index (0-100) + - Function/Class Analysis + - Control Flow & Volume Metrics +- **Smart Thresholds**: Configurable with color-coding +- **Real-Time Statistics**: Continuous metric tracking +- **Detailed Reports**: File and language-level insights -## Usage +## Default Configuration + +### Complexity Thresholds + +- Low: < 10.0 +- Medium: < 20.0 +- High: < 30.0 +- Very High: ≥ 40.0 -1. Generate Report: +### Language Support (Default: Rust) + +- Function: `fn` +- Class: `struct`, `impl`, `trait` +- Branch: `if`, `match`, `else` +- Loop: `for`, `while`, `loop` +- Comments: `//`, `/*` +- Max Line Length: 100 +- Max Function Length: 50 lines + +## Usage ```bash +# Set complexity thresholds +lla plugin --name code_complexity --action set-thresholds 10 20 30 40 + +# Show report lla plugin --name code_complexity --action show-report ``` -2. Configure Thresholds: +## Display Formats + +### Default -```bash -lla plugin --name code_complexity --action set-thresholds --args "10" "20" "30" "40" +``` +[Complexity: 12 (MI: 85.3)] ``` -3. View Help: +### Long -```bash -lla plugin --name code_complexity --action help +``` +[Complexity: 12 (MI: 85.3)] +├── Lines: 150 +├── Functions: 5 +├── Classes: 2 +├── Branches: 8 +├── Loops: 4 +├── Comments: 20 +└── Long functions: + ├── process_data (55 lines) + └── analyze_results (60 lines) ``` -### Output Examples +## Configuration -Basic view: +Config file: `~/.config/lla/code_complexity/config.toml` +### Language Settings + +```toml +[languages.Rust] +extensions = ["rs"] +function_patterns = ["fn "] +class_patterns = ["struct ", "impl ", "trait "] +branch_patterns = ["if ", "match ", "else"] +loop_patterns = ["for ", "while ", "loop"] +comment_patterns = ["//", "/*"] +max_line_length = 100 +max_function_lines = 50 ``` -[Complexity: 12 (MI: 85.3)] example.rs + +### Thresholds + +```toml +[thresholds] +low = 10.0 +medium = 20.0 +high = 30.0 +very_high = 40.0 ``` -Detailed view (`-l` flag): +### Colors +```toml +[colors] +low = "bright_green" +medium = "bright_yellow" +high = "bright_red" +very_high = "red" ``` -[Complexity: 12 (MI: 85.3)] - Lines: 150 - Functions: 5 - Classes: 2 - Branches: 8 - Loops: 4 - Comments: 20 - Long lines: 3 - Long functions: - process_data (55 lines) - analyze_results (60 lines) -``` - -Configuration is stored in `~/.config/lla/code_complexity.toml` and can be customized for each supported language. diff --git a/plugins/code_snippet_extractor/README.md b/plugins/code_snippet_extractor/README.md index a5449bd..33bd162 100644 --- a/plugins/code_snippet_extractor/README.md +++ b/plugins/code_snippet_extractor/README.md @@ -1,81 +1,89 @@ # LLA Code Snippet Extractor Plugin -A plugin for `lla` that helps you extract, organize, and manage code snippets from your files with tagging and search capabilities. +A plugin for `lla` that extracts, organizes, and manages code snippets with metadata and search capabilities. -## What it Does +## Features -- Extracts code snippets with customizable context lines -- Automatically detects language based on file extension -- Organizes snippets with tags -- Provides full-text search across all snippets -- Preserves code context (before/after the snippet) -- Supports import/export of snippets -- Tracks snippet versions and modifications +- **Smart Extraction**: Automatic language detection, contextual extraction +- **Organization**: Categories, tags, metadata tracking +- **Search**: Fuzzy search, multi-select operations +- **Interface**: Syntax highlighting, interactive CLI menus +- **Import/Export**: JSON-based snippet management -## Usage - -### Basic Operations +## Configuration -```bash -# Extract a snippet (lines 10-20 with 3 context lines) -lla plugin --name code_snippet_extractor --action extract --args "file.rs" "function_name" 10 20 3 +Config file: `~/.config/lla/code_snippets/config.toml` -# List snippets in a file -lla plugin --name code_snippet_extractor --action list --args "file.rs" +```toml +[colors] +success = "bright_green" +info = "bright_blue" +error = "bright_red" +name = "bright_yellow" +language = "bright_cyan" +tag = "bright_magenta" -# View a specific snippet -lla plugin --name code_snippet_extractor --action get --args "file.rs" "function_name" +[syntax_themes] +default = "Solarized (dark)" ``` -### Organization +## Usage + +### Basic Operations ```bash -# Search snippets -lla plugin --name code_snippet_extractor --action search --args "query" +# Extract snippet with context +lla plugin --name code_snippet_extractor --action extract "file.rs" "function_name" 10 20 3 -# Add tags -lla plugin --name code_snippet_extractor --action add-tags --args "file.rs" "function_name" "tag1" "tag2" +# List snippets +lla plugin --name code_snippet_extractor --action list -# Remove tags -lla plugin --name code_snippet_extractor --action remove-tags --args "file.rs" "function_name" "tag1" +# View snippet +lla plugin --name code_snippet_extractor --action get "snippet_id" ``` -### Import/Export +### Organization ```bash -# Export snippets -lla plugin --name code_snippet_extractor --action export --args "file.rs" +# Add/remove tags +lla plugin --name code_snippet_extractor --action add-tags "snippet_id" "tag1" "tag2" +lla plugin --name code_snippet_extractor --action remove-tags "snippet_id" "tag1" -# Import snippets -lla plugin --name code_snippet_extractor --action import --args "file.rs" "toml_data" +# Category management +lla plugin --name code_snippet_extractor --action set-category "snippet_id" "category_name" ``` -### View Help +### Import/Export ```bash -lla plugin --name code_snippet_extractor --action help +# Export/Import snippets +lla plugin --name code_snippet_extractor --action export "snippets.json" +lla plugin --name code_snippet_extractor --action import "snippets.json" ``` -### Output Example +## Display Format ``` -┌─ Context Before ───────────── -// Helper function for parsing - -├─ Snippet Content ────────────── -fn parse_input(input: &str) -> Option { - input.trim().parse().ok() -} - -├─ Context After ──────────────── -// Example usage: -// let num: i32 = parse_input("42").unwrap(); - -├─ Metadata ──────────────────── -│ Language: rust -│ Version: 1 -│ Tags: #parser #input -└────────────────────────────── +───────────────────────────────────── + Example Function + ID: abc123 • Language: rust • Version: v1 +───────────────────────────────────── + 📂 Source: src/example.rs + 🏷️ Tags: #rust #function #example + 📁 Category: Algorithms + 🕒 Created: 2024-01-20 10:30:00 UTC +───────────────────────────────────── + ◀ Context (3 lines) + 1 │ // Helper functions + ▶ Code (5 lines) + 4 │ fn parse_input(input: &str) -> Option { + 5 │ input.trim().parse().ok() + 6 │ } + ▼ Context (2 lines) + 10 │ // Example usage +───────────────────────────────────── ``` -Snippets are stored in `~/.config/lla/code_snippets.toml` and can be backed up or version controlled. +## Language Support + +Supports common languages: Rust, Python, JavaScript, TypeScript, Go, C/C++, Java, Ruby, PHP, Shell, HTML, CSS, Markdown, JSON, YAML, XML, SQL diff --git a/plugins/dirs_meta/README.md b/plugins/dirs_meta/README.md index 98d5650..47d98d0 100644 --- a/plugins/dirs_meta/README.md +++ b/plugins/dirs_meta/README.md @@ -1,51 +1,51 @@ -# LLA Directory Summary Plugin +# LLA Directory Metadata Plugin -A plugin for `lla` that provides quick directory statistics including file counts, sizes, and modification times with intelligent caching for better performance. +Real-time directory statistics with intelligent caching. -## What it Does +## Features -- Calculates directory statistics: - - Total number of files - - Number of subdirectories - - Total size of all files - - Last modification time -- Uses parallel processing for faster analysis -- Caches results for improved performance -- Automatically updates when directory contents change -- Provides human-readable size formatting +- **Analysis**: Parallel scanning, caching, configurable depth +- **Statistics**: File counts, sizes, subdirectories, modification times +- **Performance**: Multi-threaded, cache-optimized -## Display Formats +## Configuration -### Default View +`~/.config/lla/dirs_meta/config.toml`: -Shows basic directory information: +```toml +cache_size = 1000 # Max cached directories +max_scan_depth = 100 # Max scan depth +parallel_threshold = 1000 # Min entries for parallel -``` -Documents (15 files, 2.5 GB) +[colors] +files = "bright_cyan" +dirs = "bright_green" +size = "bright_yellow" +time = "bright_magenta" ``` -### Detailed View (`-l` flag) +## Usage -Shows complete directory information: +```bash +# Show stats +lla plugin --name dirs_meta --action stats "/path/to/directory" -``` -Documents (15 files, 3 dirs, 2.5 GB, modified 5 mins ago) +# Clear cache +lla plugin --name dirs_meta --action clear-cache ``` -### Size Units +## Display Formats -- Automatically adjusts units based on size: - - Bytes (B) for < 1 KB - - Kilobytes (KB) for < 1 MB - - Megabytes (MB) for < 1 GB - - Gigabytes (GB) for ≥ 1 GB +Default: `Documents (15 files, 2.5 GB)` -### Time Display +Long: -- Shows modification time in human-readable format: - - Seconds for < 1 minute - - Minutes for < 1 hour - - Hours for < 1 day - - Days for ≥ 1 day +``` +Documents +Files: 15 +Directories: 3 +Total Size: 2.5 GB +Modified: 5 mins ago +``` -The plugin automatically integrates with `lla`'s display system. +Units: B/KB/MB/GB, seconds/minutes/hours/days ago diff --git a/plugins/duplicate_file_detector/README.md b/plugins/duplicate_file_detector/README.md index 89024b3..7c140a0 100644 --- a/plugins/duplicate_file_detector/README.md +++ b/plugins/duplicate_file_detector/README.md @@ -1,39 +1,66 @@ # LLA Duplicate File Detector Plugin -A plugin for `lla` that identifies duplicate files by comparing their content using SHA-256 hashing. +A plugin for `lla` that identifies identical files using secure hash comparison. -## What it Does +## Features -- Detects duplicate files by comparing file contents -- Identifies original files and their duplicates -- Tracks file modification times to determine originals -- Uses SHA-256 hashing for reliable comparison -- Caches results for better performance +- SHA-256 content hashing with intelligent caching +- Original file and duplicate chain tracking +- Color-coded status display +- Performance optimized with chunk-based processing -## Display Formats - -### Default View +## Configuration -Shows basic duplicate information: +Located at `~/.config/lla/duplicate_file_detector/config.toml`: +```toml +[colors] +duplicate = "bright_red" # Duplicate file indicator +has_duplicates = "bright_yellow"# Original file with duplicates +path = "bright_cyan" # File path display +success = "bright_green" # Success messages +info = "bright_blue" # Information messages +name = "bright_yellow" # Name highlighting ``` -file.txt (DUPLICATE) -other.txt (HAS DUPLICATES) + +## Usage + +```bash +# Clear the detection cache +lla plugin --name duplicate_file_detector --action clear-cache + +# View help information +lla plugin --name duplicate_file_detector --action help ``` -### Detailed View (`-l` flag) +## Display Formats -Shows complete duplicate information: +### Default Format ``` -file.txt (DUPLICATE of: /path/to/original.txt) -other.txt (HAS DUPLICATES copies: /path/to/copy1.txt, /path/to/copy2.txt) +file.txt +Status: DUPLICATE of /path/to/original.txt + +other.txt +Status: HAS DUPLICATES: /path/to/copy1.txt, /path/to/copy2.txt ``` -### Color Coding +### Long Format + +``` +file.txt +Status: DUPLICATE +Original File: /path/to/original.txt + +other.txt +Status: HAS DUPLICATES +Duplicate Copies: /path/to/copy1.txt + /path/to/copy2.txt +``` -- Original files with duplicates: Yellow -- Duplicate files: Red -- File paths: Cyan +## Technical Details -The plugin automatically integrates with `lla`'s display system. +- Uses SHA-256 hashing with 8KB chunk-based reading +- Implements efficient caching with automatic invalidation +- Thread-safe operations +- Color-coded status indicators for duplicates and originals diff --git a/plugins/file_hash/README.md b/plugins/file_hash/README.md index e6d02da..2777374 100644 --- a/plugins/file_hash/README.md +++ b/plugins/file_hash/README.md @@ -1,34 +1,48 @@ # LLA File Hash Plugin -A plugin for `lla` that calculates and displays SHA-1 and SHA-256 hashes for files. +A high-performance file hashing plugin for `lla` that calculates secure cryptographic hashes (SHA-1 and SHA-256). -## What it Does +## Features -- Calculates two types of hashes for each file: - - SHA-1 (shown in green) - - SHA-256 (shown in yellow) -- Shows first 8 characters of each hash -- Uses efficient buffered reading -- Skips directories automatically -- Displays hashes in a clean, formatted box layout +- SHA-1 and SHA-256 hash calculation +- Efficient buffered reading +- Progress indication +- Rich display formatting -## Display Format +## Configuration -Shows hash information below each file: +Located at `~/.config/lla/file_hash/config.toml`: +```toml +[colors] +sha1 = "bright_green" # SHA-1 hash color +sha256 = "bright_yellow" # SHA-256 hash color +success = "bright_green" # Success messages +info = "bright_blue" # Information messages +name = "bright_yellow" # Name highlighting ``` -document.pdf -┌ SHA1 → a1b2c3d4 -└ SHA256 → e5f6g7h8 + +## Usage + +```bash +# View help information +lla plugin --name file_hash --action help ``` -### Color Coding +## Display Formats -- SHA-1: Green text -- SHA-256: Yellow text -- Box characters and arrows: Dark gray -- Hash names: Bold colored text +### Default Format + +``` +document.pdf +SHA1: a1b2c3d4 +SHA256: e5f6g7h8 +``` -The plugin automatically integrates with `lla`'s display system and works in both default and long view formats. +### Long Format -Note: The truncated 8-character display is for readability. For security-critical comparisons, use a dedicated hashing tool. +``` +document.pdf +SHA1: a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0 +SHA256: u1v2w3x4y5z6a7b8c9d0e1f2g3h4i5j6k7l8m9n0p1q2r3s4t5u6v7w8x9y0 +``` diff --git a/plugins/file_meta/README.md b/plugins/file_meta/README.md index 2953cc5..2a083d8 100644 --- a/plugins/file_meta/README.md +++ b/plugins/file_meta/README.md @@ -1,38 +1,46 @@ # LLA File Metadata Plugin -A plugin for `lla` that displays detailed file metadata including timestamps, ownership, size, and permissions. +A file metadata plugin for `lla` that provides comprehensive file information with rich formatting. + +## Features + +- Timestamp tracking (access, modify, create) +- Ownership and permission details +- Size statistics +- Color-coded information display + +## Configuration + +Located at `~/.config/lla/file_meta/config.toml`: + +```toml +[colors] +accessed = "bright_blue" # Access time color +modified = "bright_green" # Modification time color +created = "bright_yellow" # Creation time color +ownership = "bright_magenta" # UID/GID information color +size = "bright_cyan" # File size color +permissions = "bright_red" # Permissions color +success = "bright_green" # Success messages +info = "bright_blue" # Information messages +name = "bright_yellow" # Name highlighting +``` -## What it Does +## Usage -- Shows comprehensive file metadata: - - Access, modification, and creation timestamps - - User and group IDs (UID/GID) - - File size in bytes - - File permissions in octal format -- Formats timestamps in human-readable format (YYYY-MM-DD HH:MM:SS) -- Provides color-coded output for better readability +```bash +# View help information +lla plugin --name file_meta --action help +``` ## Display Format -Shows metadata information below each file: - ``` document.pdf -Accessed: 2024-03-15 14:30:22 -Modified: 2024-03-15 14:30:20 -Created: 2024-03-15 14:30:18 -UID/GID: 1000/1000 -Size: 1048576 -Perms: 644 +Accessed: 2024-03-15 14:30:22 +Modified: 2024-03-15 14:30:20 +Created: 2024-03-15 14:30:18 +UID/GID: 1000/1000 +Size: 1.0 MB +Permissions: 644 ``` - -### Color Coding - -- Access Time: Blue -- Modification Time: Green -- Creation Time: Yellow -- UID/GID: Magenta -- File Size: Cyan -- Permissions: Red - -The plugin automatically integrates with `lla`'s display system and works in both default and long view formats. diff --git a/plugins/file_tagger/README.md b/plugins/file_tagger/README.md index 3c93785..bf8d8a5 100644 --- a/plugins/file_tagger/README.md +++ b/plugins/file_tagger/README.md @@ -1,42 +1,57 @@ # LLA File Tagger Plugin -A plugin for `lla` that helps you organize files using custom tags with persistent storage. +A file tagging plugin for `lla` that provides persistent tag management. -## What it Does +## Features -- Manages custom tags for files: - - Add tags to files - - Remove tags from files - - List tags for files -- Displays tags next to file names -- Stores tags persistently -- Shows tags in cyan color for visibility +- Add, remove, and list file tags +- Persistent storage with efficient lookup +- Color-coded tag display +- Interactive commands -## Usage +## Configuration + +Config file: `~/.config/lla/file_tagger/config.toml` + +```toml +[colors] +tag = "bright_cyan" # Tag text +tag_label = "bright_green" # Tag label +success = "bright_green" # Success messages +info = "bright_blue" # Info messages +name = "bright_yellow" # Name highlighting +``` -### Basic Commands +## Usage ```bash -# Add a tag -lla plugin --name file_tagger --action add-tag --args "file.txt" "important" +# Add tag +lla plugin --name file_tagger --action add-tag --args "/path/to/file" "important" -# Remove a tag -lla plugin --name file_tagger --action remove-tag --args "file.txt" "important" +# Remove tag +lla plugin --name file_tagger --action remove-tag --args "/path/to/file" "important" # List tags -lla plugin --name file_tagger --action list-tags --args "file.txt" +lla plugin --name file_tagger --action list-tags --args "/path/to/file" -# View help +# Help lla plugin --name file_tagger --action help ``` -### Display Format +### Display Examples -Tags appear in cyan brackets next to file names: +Default format: ``` -document.pdf [important, work] -image.jpg [personal] +document.pdf +Tags: [important] [work] [urgent] ``` -The plugin stores tags in `~/.config/lla/file_tags.txt` and automatically integrates with `lla`'s display system. +Long format: + +``` +document.pdf +Tag: important +Tag: work +Tag: urgent +``` diff --git a/plugins/git_status/README.md b/plugins/git_status/README.md index 863e6f2..49d2acc 100644 --- a/plugins/git_status/README.md +++ b/plugins/git_status/README.md @@ -1,54 +1,45 @@ # LLA Git Status Plugin -A plugin for `lla` that shows Git repository status with color-coded symbols and detailed information. - -## What it Does - -- Shows Git status for files and directories: - - Staged, modified, and untracked files - - Branch and commit information - - Repository state (clean/changes) - - Merge conflicts -- Uses color-coded symbols for quick status recognition -- Provides both simple and detailed views -- Automatically detects Git repositories - -## Status Symbols - -| Symbol | Meaning | Color | -| ------ | --------- | ------- | -| ✓ | Staged | Green | -| ± | Modified | Yellow | -| ✚ | New file | Green | -| ✖ | Deleted | Red | -| ➜ | Renamed | Purple | -| ↠ | Copied | Cyan | -| ⚡ | Conflict | Magenta | -| ? | Untracked | Blue | -| ⎇ | Branch | Blue | - -## Display Formats +Git integration plugin for `lla` providing real-time repository status with rich formatting. + +## Features + +- Status tracking (staged, modified, untracked, conflicts) +- Repository info (branch, commits, working tree) +- Color-coded display +- Performance optimized + +## Configuration + +Config location: `~/.config/lla/git_status/config.toml` + +```toml +[colors] +clean = "bright_green" +modified = "bright_yellow" +staged = "bright_green" +untracked = "bright_blue" +conflict = "bright_red" +branch = "bright_cyan" +commit = "bright_yellow" +info = "bright_blue" +name = "bright_yellow" +``` -### Default View +## Display Examples -Shows file status with symbols: +Basic: ``` -document.txt [✓ staged] -script.py [± modified] -new.rs [? untracked] +document.txt +Git: modified, 2 staged ``` -### Detailed View (`-l` flag) - -Shows repository information: +Detailed: ``` project/ -Branch: ⎇ main -Commit: a1b2c3d Initial commit -Status: 2 staged, 1 modified, 3 untracked -Repo: has changes +Branch: main +Commit: a1b2c3d Initial commit +Status: 2 staged, 1 modified, 3 untracked ``` - -The plugin automatically integrates with `lla`'s display system. diff --git a/plugins/keyword_search/README.md b/plugins/keyword_search/README.md index ea2a52d..12751c0 100644 --- a/plugins/keyword_search/README.md +++ b/plugins/keyword_search/README.md @@ -1,67 +1,73 @@ # LLA Keyword Search Plugin -A plugin for `lla` that searches for keywords in files with configurable context and highlighting. +High-performance keyword search plugin for `lla` with interactive search and rich display features. -## What it Does +## Features -- Searches for configured keywords in files -- Shows matches with surrounding context lines -- Supports multiple search options: - - Case-sensitive search - - Multiple keywords - - Configurable context lines - - Maximum matches per file -- Highlights matches in color -- Supports many file extensions (txt, md, rs, py, js, etc.) +- **Smart Search**: Multi-keyword, case-sensitive, regex support +- **Interactive**: File selection, filtering, action menu +- **Rich Display**: Syntax highlighting, context visualization +- **Analysis**: Match statistics and pattern detection ## Usage -### Basic Commands - ```bash -# Set keywords to search for -lla plugin --name keyword_search --action set-keywords --args "TODO" "FIXME" - -# Search in a specific file -lla plugin --name keyword_search --action search --args "src/main.rs" - -# Show current configuration -lla plugin --name keyword_search --action show-config - -# View help -lla plugin --name keyword_search --action help +# Search in current directory +lla plugin --name keyword_search --action search + +# Available actions after finding matches: +1. View detailed matches +2. Copy to clipboard +3. Save to file +4. Show statistics +5. Filter matches +6. Advanced analysis ``` -### Configuration Commands +## Configuration -```bash -# Set case sensitivity -lla plugin --name keyword_search --action set-case-sensitive --args true +Config location: `~/.config/lla/keyword_search/config.toml` -# Set number of context lines -lla plugin --name keyword_search --action set-context-lines --args 3 +```toml +keywords = [] # Keywords to search for +case_sensitive = false # Case sensitivity +use_regex = false # Regular expression support +context_lines = 2 # Number of context lines +max_matches = 5 # Maximum matches per file -# Set maximum matches per file -lla plugin --name keyword_search --action set-max-matches --args 5 +[colors] +keyword = "bright_red" +line_number = "bright_yellow" +context = "bright_black" +file = "bright_blue" +success = "bright_green" +info = "bright_cyan" ``` -### Display Format +## Display Examples -Default view shows line numbers and matches: +Match View: ``` -15:TODO - Add error handling here -42:FIXME - Need to optimize this loop +───────────────────────────────── + 📂 src/main.rs +───────────────────────────────── + 123 │ function process() { + 124 │ let data = analyze(); +►125 │ // TODO: Implement error handling + 126 │ return data; + 127 │ } +───────────────────────────────── ``` -Detailed view (`-l` flag) shows context: +Statistics View: ``` - 12: function processData() { - 13: let data = []; -→ 14: // TODO: Add error handling here - 15: return data; - 16: } +📊 Match Statistics: +───────────────────────────────── + • Total matches: 5 + • Unique keywords: 2 + • Average context: 2.5 lines + • File: src/main.rs +───────────────────────────────── ``` - -The plugin stores configuration in `~/.config/lla/plugins/keyword_search.toml` and automatically integrates with `lla`'s display system. diff --git a/plugins/last_git_commit/README.md b/plugins/last_git_commit/README.md index 5ffc613..216abf8 100644 --- a/plugins/last_git_commit/README.md +++ b/plugins/last_git_commit/README.md @@ -1,36 +1,42 @@ # LLA Last Git Commit Plugin -A plugin for `lla` that shows the last Git commit information for files with colored output. +Git history plugin for `lla` providing real-time commit tracking with rich formatting. -## What it Does +## Features -- Shows last commit information for files: - - Commit hash (short version) - - Author name (in detailed view) - - Time since commit -- Uses color coding for better readability: - - Hash in yellow - - Author in cyan - - Time in green -- Automatically detects Git repositories -- Works with both files and directories +- Short hash, author, and timestamp display +- Path-specific history +- Color-coded information +- Multiple display formats +- Smart caching and quick lookups -## Display Formats - -### Default View +## Configuration -Shows basic commit information: +Config file: `~/.config/lla/last_git_commit/config.toml` -``` -document.txt [Commit: a1b2c3d 2 days ago] +```toml +[colors] +hash = "bright_yellow" +author = "bright_cyan" +time = "bright_green" +info = "bright_blue" +name = "bright_yellow" ``` -### Detailed View (`-l` flag) +## Display Formats -Shows complete commit information: +Default: ``` -document.txt [Last commit: a1b2c3d by John Doe 2 days ago] +document.txt +Commit: a1b2c3d 2 days ago ``` -The plugin automatically integrates with `lla`'s display system. +Long: + +``` +document.txt +Commit: a1b2c3d +Author: John Doe +Time: 2 days ago +``` diff --git a/plugins/sizeviz/README.md b/plugins/sizeviz/README.md index f5ef639..d5e8d35 100644 --- a/plugins/sizeviz/README.md +++ b/plugins/sizeviz/README.md @@ -1,42 +1,45 @@ # LLA Size Visualizer Plugin -A plugin for `lla` that visualizes file sizes using colored bars and human-readable formats. - -## What it Does - -- Shows file sizes in two ways: - - Visual bar representation - - Human-readable size format (B, KB, MB, GB, TB) -- Uses color coding based on size ranges: - - Green: ≤ 1KB - - Bright Green: 1KB - 10KB - - Cyan: 10KB - 1MB - - Blue: 1MB - 10MB - - Yellow: 10MB - 100MB - - Red: 100MB - 1GB - - Magenta: > 1GB -- Shows percentage relative to 1GB reference -- Provides both compact and detailed views +File size visualization plugin for `lla` providing real-time size analysis with rich formatting. + +## Features + +- Human-readable size formatting +- Visual progress bars with Unicode blocks +- Size-based color coding +- Multiple display formats +- Smart caching and quick analysis + +## Configuration + +Config file: `~/.config/lla/sizeviz/config.toml` + +```toml +[colors] +tiny = "bright_green" # ≤ 1KB +small = "bright_cyan" # 1KB - 1MB +medium = "bright_yellow" # 1MB - 10MB +large = "bright_red" # 10MB - 100MB +huge = "bright_magenta" # > 100MB +info = "bright_blue" +size = "bright_yellow" +percentage = "bright_magenta" +``` ## Display Formats -### Default View - -Shows a compact size visualization: +Default: ``` -file.txt █████ 2.5 MB +document.pdf +█████░░░░░ 2.5 MB ``` -### Detailed View (`-l` flag) - -Shows complete size information: +Long: ``` ┌─ Size ──────────────────────────────────── -│ █░░░░░░░░░░░░░░░░░░ 25.5 MB +│ ████████████░░░░░░░░░░ 25.5 MB │ 2.5% of reference (1GB) └────────────────────────────────────────── ``` - -The plugin automatically integrates with `lla`'s display system. diff --git a/plugins/sizeviz/src/lib.rs b/plugins/sizeviz/src/lib.rs index 79c274b..d08bd03 100644 --- a/plugins/sizeviz/src/lib.rs +++ b/plugins/sizeviz/src/lib.rs @@ -140,7 +140,7 @@ impl FileSizeVisualizerPlugin { _ => "", }; - let spaces = " ".repeat(max_width.saturating_sub(width)); + let spaces = "░".repeat(max_width.saturating_sub(width)); format!("{}{}{}", full_blocks, partial_block, spaces) } From a725e575a0ee6ff4fc3ec8b2fdae6876dbc35de8 Mon Sep 17 00:00:00 2001 From: Mohamed Achaq Date: Sat, 28 Dec 2024 12:52:30 +0100 Subject: [PATCH 40/43] feat: add README for lla_plugin_utils library - Introduced a new README.md file detailing the lla_plugin_utils library. - Documented core components including UI components, plugin infrastructure, and code utilities. - Highlighted key features such as TOML configuration, interactive CLI components, and syntax highlighting support. --- lla_plugin_utils/README.md | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 lla_plugin_utils/README.md diff --git a/lla_plugin_utils/README.md b/lla_plugin_utils/README.md new file mode 100644 index 0000000..fc11377 --- /dev/null +++ b/lla_plugin_utils/README.md @@ -0,0 +1,36 @@ +# lla_plugin_utils + +Utility library for building LLA plugins. + +## Core Components + +### UI Components + +- `BoxComponent`: Customizable bordered text boxes +- `HelpFormatter`: Command help text formatting +- `KeyValue`: Key-value pair display +- `List`: List display with borders +- `Spinner`: Progress indicator +- `TextBlock`: Styled text with colors +- `InteractiveSelector`: CLI selection menus + +### Plugin Infrastructure + +- `BasePlugin`: Base plugin implementation +- `ConfigManager`: Plugin configuration handling +- `ActionRegistry`: Plugin action registration and handling +- `ProtobufHandler`: Protocol buffer message handling + +### Code Utilities + +- `CodeHighlighter`: Syntax highlighting for code snippets +- `format`: File metadata formatting utilities +- `syntax`: Code syntax highlighting support + +## Features + +- Configurable via TOML +- Interactive CLI components +- Syntax highlighting (optional) +- Protobuf message handling +- Action registration system From a49e6d913313b287f59ff2f57751d22441de2c81 Mon Sep 17 00:00:00 2001 From: Mohamed Achaq Date: Sat, 28 Dec 2024 12:54:36 +0100 Subject: [PATCH 41/43] chore: update plugin versions and metadata in documentation - Incremented version numbers for all plugins from 0.3.0 to 0.3.1. - Renamed the 'dirs' plugin to 'dirs_meta' for better clarity. - Updated installation instructions and documentation links for consistency across all plugins. - Enhanced overall documentation structure to improve usability and navigation. --- plugins.md | 50 +++++++++++++++++++++++++++++++++++++------------- 1 file changed, 37 insertions(+), 13 deletions(-) diff --git a/plugins.md b/plugins.md index b122a77..fb16ed8 100644 --- a/plugins.md +++ b/plugins.md @@ -17,7 +17,7 @@ Or you can install individual plugins as described below. - [categorizer](#categorizer): Categorizes files based on their extensions and metadata - [code_complexity](#code_complexity): Analyzes code complexity using various metrics - [code_snippet_extractor](#code_snippet_extractor): A plugin for extracting and managing code snippets -- [dirs](#dirs): Shows directories metadata +- [dirs_meta](#dirs_meta): Shows directories metadata - [duplicate_file_detector](#duplicate_file_detector): A plugin for the LLA that detects duplicate files. - [file_hash](#file_hash): Displays the hash of each file - [file_meta](#file_meta): Displays the file metadata of each file @@ -31,18 +31,20 @@ Or you can install individual plugins as described below. **Description:** Categorizes files based on their extensions and metadata -**Version:** 0.3.0 +**Version:** 0.3.1 **Documentation:** [Documentation](plugins/categorizer/README.md) **Installation Options:** 1. Using LLA install command: + ```bash lla install --dir path/to/lla/plugins/categorizer/ ``` 2. Manual installation: + ```bash git clone https://github.com/triyanox/lla cd lla/plugins/categorizer/ @@ -55,18 +57,20 @@ Then, copy the generated `.so`, `.dll`, or `.dylib` file from the `target/releas **Description:** Analyzes code complexity using various metrics -**Version:** 0.3.0 +**Version:** 0.3.1 **Documentation:** [Documentation](plugins/code_complexity/README.md) **Installation Options:** 1. Using LLA install command: + ```bash lla install --dir path/to/lla/plugins/code_complexity/ ``` 2. Manual installation: + ```bash git clone https://github.com/triyanox/lla cd lla/plugins/code_complexity/ @@ -79,18 +83,20 @@ Then, copy the generated `.so`, `.dll`, or `.dylib` file from the `target/releas **Description:** A plugin for extracting and managing code snippets -**Version:** 0.3.0 +**Version:** 0.3.1 **Documentation:** [Documentation](plugins/code_snippet_extractor/README.md) **Installation Options:** 1. Using LLA install command: + ```bash lla install --dir path/to/lla/plugins/code_snippet_extractor/ ``` 2. Manual installation: + ```bash git clone https://github.com/triyanox/lla cd lla/plugins/code_snippet_extractor/ @@ -103,18 +109,20 @@ Then, copy the generated `.so`, `.dll`, or `.dylib` file from the `target/releas **Description:** Shows directories metadata -**Version:** 0.3.0 +**Version:** 0.3.1 **Documentation:** [Documentation](plugins/dirs/README.md) **Installation Options:** 1. Using LLA install command: + ```bash lla install --dir path/to/lla/plugins/dirs/ ``` 2. Manual installation: + ```bash git clone https://github.com/triyanox/lla cd lla/plugins/dirs/ @@ -127,18 +135,20 @@ Then, copy the generated `.so`, `.dll`, or `.dylib` file from the `target/releas **Description:** A plugin for the LLA that detects duplicate files. -**Version:** 0.3.0 +**Version:** 0.3.1 **Documentation:** [Documentation](plugins/duplicate_file_detector/README.md) **Installation Options:** 1. Using LLA install command: + ```bash lla install --dir path/to/lla/plugins/duplicate_file_detector/ ``` 2. Manual installation: + ```bash git clone https://github.com/triyanox/lla cd lla/plugins/duplicate_file_detector/ @@ -151,18 +161,20 @@ Then, copy the generated `.so`, `.dll`, or `.dylib` file from the `target/releas **Description:** Displays the hash of each file -**Version:** 0.3.0 +**Version:** 0.3.1 **Documentation:** [Documentation](plugins/file_hash/README.md) **Installation Options:** 1. Using LLA install command: + ```bash lla install --dir path/to/lla/plugins/file_hash/ ``` 2. Manual installation: + ```bash git clone https://github.com/triyanox/lla cd lla/plugins/file_hash/ @@ -175,18 +187,20 @@ Then, copy the generated `.so`, `.dll`, or `.dylib` file from the `target/releas **Description:** Displays the file metadata of each file -**Version:** 0.3.0 +**Version:** 0.3.1 **Documentation:** [Documentation](plugins/file_meta/README.md) **Installation Options:** 1. Using LLA install command: + ```bash lla install --dir path/to/lla/plugins/file_meta/ ``` 2. Manual installation: + ```bash git clone https://github.com/triyanox/lla cd lla/plugins/file_meta/ @@ -199,18 +213,20 @@ Then, copy the generated `.so`, `.dll`, or `.dylib` file from the `target/releas **Description:** A plugin for tagging files and filtering by tags -**Version:** 0.3.0 +**Version:** 0.3.1 **Documentation:** [Documentation](plugins/file_tagger/README.md) **Installation Options:** 1. Using LLA install command: + ```bash lla install --dir path/to/lla/plugins/file_tagger/ ``` 2. Manual installation: + ```bash git clone https://github.com/triyanox/lla cd lla/plugins/file_tagger/ @@ -223,18 +239,20 @@ Then, copy the generated `.so`, `.dll`, or `.dylib` file from the `target/releas **Description:** Shows the git status of each file -**Version:** 0.3.0 +**Version:** 0.3.1 **Documentation:** [Documentation](plugins/git_status/README.md) **Installation Options:** 1. Using LLA install command: + ```bash lla install --dir path/to/lla/plugins/git_status/ ``` 2. Manual installation: + ```bash git clone https://github.com/triyanox/lla cd lla/plugins/git_status/ @@ -247,18 +265,20 @@ Then, copy the generated `.so`, `.dll`, or `.dylib` file from the `target/releas **Description:** Searches file contents for user-specified keywords -**Version:** 0.3.0 +**Version:** 0.3.1 **Documentation:** [Documentation](plugins/keyword_search/README.md) **Installation Options:** 1. Using LLA install command: + ```bash lla install --dir path/to/lla/plugins/keyword_search/ ``` 2. Manual installation: + ```bash git clone https://github.com/triyanox/lla cd lla/plugins/keyword_search/ @@ -271,18 +291,20 @@ Then, copy the generated `.so`, `.dll`, or `.dylib` file from the `target/releas **Description:** A plugin for the LLA that provides the last git commit hash -**Version:** 0.3.0 +**Version:** 0.3.1 **Documentation:** [Documentation](plugins/last_git_commit/README.md) **Installation Options:** 1. Using LLA install command: + ```bash lla install --dir path/to/lla/plugins/last_git_commit/ ``` 2. Manual installation: + ```bash git clone https://github.com/triyanox/lla cd lla/plugins/last_git_commit/ @@ -295,18 +317,20 @@ Then, copy the generated `.so`, `.dll`, or `.dylib` file from the `target/releas **Description:** File size visualizer plugin for LLA -**Version:** 0.3.0 +**Version:** 0.3.1 **Documentation:** [Documentation](plugins/sizeviz/README.md) **Installation Options:** 1. Using LLA install command: + ```bash lla install --dir path/to/lla/plugins/sizeviz/ ``` 2. Manual installation: + ```bash git clone https://github.com/triyanox/lla cd lla/plugins/sizeviz/ From 67014c744bf216d3e1015596db48134212e14326 Mon Sep 17 00:00:00 2001 From: Mohamed Achaq Date: Sat, 28 Dec 2024 13:06:34 +0100 Subject: [PATCH 42/43] chore: update CHANGELOG for version 0.3.8 - Added new utility library `lla_plugin_utils` with UI components, plugin infrastructure utilities, code highlighting, and configuration management tools. - Introduced command-line arguments for enhanced file type filtering options. - Enhanced plugin functionality with updates to all official plugins and support for individual plugin updates. - Updated configuration to include a new `no_dotfiles` setting and improved documentation with examples. - Fixed an issue with the default listing format being overridden by config settings. --- CHANGELOG.md | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f24746..8bc2448 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,43 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.3.8] - 2024-12-21 + +### Added + +- New utility library `lla_plugin_utils` for building plugins: + + - UI components (BoxComponent, HelpFormatter, KeyValue, etc.) + - Plugin infrastructure utilities + - Code highlighting and syntax support + - Configuration management tools + +- New command-line arguments for file type filtering: + + - `--dirs-only`: Show only directories + - `--files-only`: Show only regular files + - `--symlinks-only`: Show only symbolic links + - `--dotfiles-only`: Show only dot files and directories + - `--no-dirs`: Hide directories + - `--no-files`: Hide regular files + - `--no-symlinks`: Hide symbolic links + - `--no-dotfiles`: Hide dot files and directories + +- Enhanced plugin functionality: + - All official plugins updated with new UI components and improved functionality + - Users can update their plugins using `lla update` command + - Individual plugin updates supported via `lla update ` + +### Changed + +- Updated configuration with new `no_dotfiles` setting to hide dot files by default +- Enhanced documentation with detailed examples of file type filtering +- Updated `terminal_size` dependency to version 0.4.1 + +### Fixed + +- Fix the issue with the default listing format from config overrides the args + ## [0.3.7] - 2024-12-20 ### Changed From 5b9bf2a9ccd3529fdac6fa31688ea2419bf538db Mon Sep 17 00:00:00 2001 From: Mohamed Achaq Date: Sat, 28 Dec 2024 13:08:17 +0100 Subject: [PATCH 43/43] chore: bump version to 0.3.8 for all packages - Updated version numbers for `lla`, `lla_plugin_interface`, and `lla_plugin_utils` from 0.3.7 to 0.3.8 in Cargo files. - Ensured consistency across package dependencies and workspace configuration. --- Cargo.lock | 6 +++--- Cargo.toml | 2 +- lla/Cargo.toml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 24a469c..563c295 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -993,7 +993,7 @@ checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] name = "lla" -version = "0.3.7" +version = "0.3.8" dependencies = [ "atty", "chrono", @@ -1031,7 +1031,7 @@ dependencies = [ [[package]] name = "lla_plugin_interface" -version = "0.3.7" +version = "0.3.8" dependencies = [ "prost", "prost-build", @@ -1040,7 +1040,7 @@ dependencies = [ [[package]] name = "lla_plugin_utils" -version = "0.3.7" +version = "0.3.8" dependencies = [ "bytes", "chrono", diff --git a/Cargo.toml b/Cargo.toml index e431958..b930868 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ members = ["lla", "lla_plugin_interface", "lla_plugin_utils", "plugins/*"] [workspace.package] description = "Blazing Fast and highly customizable ls Replacement with Superpowers" authors = ["Achaq "] -version = "0.3.7" +version = "0.3.8" categories = ["utilities", "file-system", "cli", "file-management"] edition = "2021" license = "MIT" diff --git a/lla/Cargo.toml b/lla/Cargo.toml index 3c1e641..129df41 100644 --- a/lla/Cargo.toml +++ b/lla/Cargo.toml @@ -28,7 +28,7 @@ walkdir.workspace = true tempfile.workspace = true users.workspace = true parking_lot.workspace = true -lla_plugin_interface = { version = "0.3.7", path = "../lla_plugin_interface" } +lla_plugin_interface = { version = "0.3.8", path = "../lla_plugin_interface" } once_cell.workspace = true dashmap.workspace = true unicode-width.workspace = true