From 9e337bc60160afea51b16cb81c9b354ff7e1be39 Mon Sep 17 00:00:00 2001 From: Dlurak <84224239+Dlurak@users.noreply.github.com> Date: Tue, 20 Aug 2024 20:41:14 +0200 Subject: [PATCH] Refactor the table and add a ls cmd for projects --- src/cli/mod.rs | 8 +- src/cli/project.rs | 20 +++++ src/cli/template.rs | 1 + src/commands/directory.rs | 9 ++- src/commands/mod.rs | 1 + src/commands/project.rs | 22 ++++++ src/commands/template.rs | 4 +- src/directories.rs | 8 +- src/init.rs | 7 +- src/main.rs | 2 + src/projects.rs | 152 ++++++++++++++++++++++++++++++++++++++ src/templates.rs | 19 +++-- src/widgets/table.rs | 150 +++++++++++++++++++++++++++---------- 13 files changed, 346 insertions(+), 57 deletions(-) create mode 100644 src/cli/project.rs create mode 100644 src/commands/project.rs create mode 100644 src/projects.rs diff --git a/src/cli/mod.rs b/src/cli/mod.rs index f8a6479..f364847 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -1,10 +1,9 @@ pub mod directory; +pub mod project; pub mod template; +use self::{directory::DirectoryCli, project::ProjectCli, template::TemplateCli}; use clap::{Parser, Subcommand}; -use self::{directory::DirectoryCli, template::TemplateCli}; - -// TODO: Make a `cli`directory with multiple files /// A CLI for tmux session management #[derive(Parser, Debug)] @@ -30,4 +29,7 @@ pub enum Commands { /// This command provides functionalities to interact with tmux sessions based on templates #[command(alias = "temp", alias = "templ")] Template(TemplateCli), + + #[command(alias = "proj", alias = "projects")] + Project(ProjectCli), } diff --git a/src/cli/project.rs b/src/cli/project.rs new file mode 100644 index 0000000..ade24d2 --- /dev/null +++ b/src/cli/project.rs @@ -0,0 +1,20 @@ +use clap::{Parser, Subcommand}; + +#[derive(Parser, Debug)] +pub struct ProjectCli { + #[command(subcommand)] + pub action: ProjectCommands, +} + +#[derive(Subcommand, Debug)] +pub enum ProjectCommands { + #[command(alias = "ls")] + List(ProjectListArgs), +} + +#[derive(Debug, Parser)] +pub struct ProjectListArgs { + /// Show minimal output for scripts + #[arg(short, long, default_value_t = false)] + pub minimal: bool, +} diff --git a/src/cli/template.rs b/src/cli/template.rs index ec307ef..e4fff22 100644 --- a/src/cli/template.rs +++ b/src/cli/template.rs @@ -1,5 +1,6 @@ use clap::{Parser, Subcommand}; use std::path::PathBuf; + #[derive(Parser, Debug)] pub struct TemplateCli { #[command(subcommand)] diff --git a/src/commands/directory.rs b/src/commands/directory.rs index ef350e9..303ef17 100644 --- a/src/commands/directory.rs +++ b/src/commands/directory.rs @@ -1,5 +1,10 @@ use crate::{ - cli::directory::{DirectoryCli, DirectoryCommands, ListDirectoryArgs, StartDirectoryArgs}, conditional_command, directories::{self, Directory}, helpers::{absolute_path, dir_name, Exit}, tmux::{attach, session_exists}, widgets::{heading::Heading, table::fmt_table} + cli::directory::{DirectoryCli, DirectoryCommands, ListDirectoryArgs, StartDirectoryArgs}, + conditional_command, + directories::{self, Directory}, + helpers::{absolute_path, dir_name, Exit}, + tmux::{attach, session_exists}, + widgets::{heading::Heading, table::Table}, }; use std::{collections::HashMap, path::PathBuf}; use tmux_interface::{NewSession, Tmux, TmuxCommand}; @@ -22,7 +27,7 @@ fn list_handler(args: ListDirectoryArgs) { for (key, value) in categories { println!("{}", Heading(key)); - println!("{}", fmt_table(value)); + println!("{}", Table::from_iter(value.iter())); } } diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 593b2c2..91b9a7c 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -1,3 +1,4 @@ pub mod directory; pub mod init; +pub mod project; pub mod template; diff --git a/src/commands/project.rs b/src/commands/project.rs new file mode 100644 index 0000000..358d442 --- /dev/null +++ b/src/commands/project.rs @@ -0,0 +1,22 @@ +use crate::{ + cli::project::{ProjectCli, ProjectCommands, ProjectListArgs}, + projects::parse_project_config, + widgets::{heading::Heading, table::Table}, +}; + +pub fn project_handler(args: ProjectCli) { + match args.action { + ProjectCommands::List(args) => list_handler(args), + } +} + +fn list_handler(args: ProjectListArgs) { + for proj in parse_project_config() { + if args.minimal { + println!("{}", proj.name); + } else { + println!("{}", Heading(proj.name)); + println!("{}", Table::from(proj.setup)); + } + } +} diff --git a/src/commands/template.rs b/src/commands/template.rs index befa758..d708891 100644 --- a/src/commands/template.rs +++ b/src/commands/template.rs @@ -4,7 +4,7 @@ use crate::{ helpers::{absolute_path, dir_name, Exit}, templates::{apply_template, parse_template_config}, tmux::{attach, session_exists}, - widgets::{heading::Heading, table::fmt_table}, + widgets::{heading::Heading, table::Table}, }; use std::path::PathBuf; use tmux_interface::{NewSession, Tmux, TmuxCommand}; @@ -32,7 +32,7 @@ fn list_handler(args: ListTemplateArgs) { println!("{}", template.name); } else { println!("{}", Heading(template.name)); - println!("{}", fmt_table(template.windows)); + println!("{}", Table::from_iter(template.windows.iter())); } } } diff --git a/src/directories.rs b/src/directories.rs index e51210d..b166451 100644 --- a/src/directories.rs +++ b/src/directories.rs @@ -34,16 +34,16 @@ impl fmt::Display for Directory { } } -impl Table for Directory { - fn table(&self) -> (String, String) { - let first_col = match (&self.icon, &self.name) { +impl From<&Directory> for Table { + fn from(value: &Directory) -> Self { + let first_col = match (&value.icon, &value.name) { (Some(icon), Some(name)) => format!("{} {}", icon, name), (Some(icon), None) => icon.clone(), (None, Some(name)) => name.clone(), (None, None) => "No name".to_string(), }; - (first_col, self.path.display().to_string()) + Self::from((first_col, value.path.display().to_string())) } } diff --git a/src/init.rs b/src/init.rs index 95e1c5d..6dcfa02 100644 --- a/src/init.rs +++ b/src/init.rs @@ -1,7 +1,8 @@ use crate::helpers::get_config_dir; -use std::fs; -use std::io; -use std::path::{Path, PathBuf}; +use std::{ + fs, io, + path::{Path, PathBuf}, +}; fn create_config_dir() -> io::Result { let config_dir = get_config_dir(); diff --git a/src/main.rs b/src/main.rs index ce4c2b4..a810b9d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,6 +4,7 @@ mod directories; mod helpers; mod init; mod macros; +mod projects; mod templates; mod tmux; mod widgets; @@ -17,5 +18,6 @@ fn main() { cli::Commands::Init => commands::init::init_handler(), cli::Commands::Directory(args) => commands::directory::directory_handler(args), cli::Commands::Template(args) => commands::template::template_handler(args), + cli::Commands::Project(args) => commands::project::project_handler(args), } } diff --git a/src/projects.rs b/src/projects.rs new file mode 100644 index 0000000..a1cae11 --- /dev/null +++ b/src/projects.rs @@ -0,0 +1,152 @@ +use crate::{ + exit, + helpers::{get_config_dir, Exit}, + templates::{parse_template_config, Window}, + widgets::table::Table, +}; +use serde::Deserialize; +use std::fs; + +#[derive(Debug, PartialEq, Eq)] +pub struct Project { + pub name: String, + pub setup: ProjectSetup, +} + +#[derive(Debug, Deserialize, PartialEq, Eq)] +#[serde(untagged)] +pub enum ProjectSetup { + Template(String), + Windows(Vec), +} + +impl From for Table { + fn from(value: ProjectSetup) -> Self { + let (template_name, windows) = match value { + ProjectSetup::Template(template_name) => { + let all_templates = parse_template_config(); + let template = all_templates + .into_iter() + .find(|t| t.name == template_name) + .unwrap_or_else(|| exit!(1, "Template {} could not be found", template_name)); + + (Some(template_name), template.windows) + } + ProjectSetup::Windows(windows) => (None, windows), + }; + + let mut rows = Self::new(vec![( + "Template".to_string(), + template_name.unwrap_or("None".to_string()), + )]) + .rows; + let windows = Self::from_iter(windows).rows; + rows.extend(windows); + + Self::new(rows) + } +} + +impl<'de> Deserialize<'de> for Project { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + #[derive(Deserialize)] + struct RawProject { + name: String, + template: Option, + windows: Option>, + } + + let raw = RawProject::deserialize(deserializer)?; + + let setup = if let Some(template) = raw.template { + ProjectSetup::Template(template) + } else if let Some(windows) = raw.windows { + ProjectSetup::Windows(windows) + } else { + return Err(serde::de::Error::custom( + "Expected either template or windows", + )); + }; + + Ok(Project { + name: raw.name, + setup, + }) + } +} + +pub fn parse_project_config() -> Vec { + let projects_content = + fs::read_dir(get_config_dir().join("projects/")).exit(1, "Can't read template config"); + + let projects_raw: Vec<_> = projects_content + .filter_map(|x| x.ok()) + .filter(|x| x.path().is_file()) + .filter_map(|x| fs::read_to_string(x.path()).ok()) + .collect(); + + projects_raw + .iter() + .filter_map(|x| serde_yaml::from_str::(x).ok()) + .collect() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parser() { + let project = serde_yaml::from_str::( + "name: OsmApp + +root_dir: ~/GitHub/osmapp/ +windows: + - name:  Neovim + panes: + - nvim + - name: Server + panes: + - yarn run dev", + ) + .unwrap(); + + assert_eq!( + project, + Project { + name: "OsmApp".to_string(), + setup: ProjectSetup::Windows(vec![ + Window { + name: Some(" Neovim".to_string()), + panes: vec!["nvim".to_string()], + layout: None, + }, + Window { + name: Some("Server".to_string()), + panes: vec!["yarn run dev".to_string()], + layout: None, + } + ]) + } + ); + + let project = serde_yaml::from_str::( + "name: Dlool + +root_dir: ~/SoftwareDevelopment/web/Dlool/dlool_frontend_v2/ +template: Svelte", + ) + .unwrap(); + + assert_eq!( + project, + Project { + name: "Dlool".to_string(), + setup: ProjectSetup::Template("Svelte".to_string()) + } + ); + } +} diff --git a/src/templates.rs b/src/templates.rs index cfe1b31..c1fe85a 100644 --- a/src/templates.rs +++ b/src/templates.rs @@ -13,18 +13,27 @@ pub struct Template { pub windows: Vec, } -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Debug, PartialEq, Eq, Clone)] pub struct Window { pub name: Option, pub layout: Option, pub panes: Vec, } -impl Table for Window { - fn table(&self) -> (String, String) { - let name = self.name.clone().unwrap_or("No name".to_string()); +// TODO: Merge From<&Window> and From +impl From<&Window> for Table { + fn from(value: &Window) -> Self { + let name = value.name.clone().unwrap_or("No name".to_string()); + + Self::from((name, format!("{} Panes", value.panes.len()))) + } +} + +impl From for Table { + fn from(value: Window) -> Self { + let name = value.name.clone().unwrap_or("No name".to_string()); - (name, format!("{} Panes", self.panes.len())) + Self::from((name, format!("{} Panes", value.panes.len()))) } } diff --git a/src/widgets/table.rs b/src/widgets/table.rs index 5c3cab2..254912f 100644 --- a/src/widgets/table.rs +++ b/src/widgets/table.rs @@ -1,39 +1,113 @@ -use std::fmt; - -pub trait Table { - fn table(&self) -> (T, U); -} - -pub fn fmt_table(rows: Vec>) -> String { - let (keys, values): (Vec<_>, Vec<_>) = rows - .iter() - .map(|row| (format!("{}", row.table().0), format!("{}", row.table().1))) - .unzip(); - - let key_width = keys.iter().map(|k| k.len()).max().unwrap_or(0); - let val_width = values.iter().map(|v| v.len()).max().unwrap_or(0); - - let formatted_rows: Vec = keys - .iter() - .zip(values.iter()) - .map(|(key, value)| { - format!( - "│ {: { + pub rows: Vec<(T, U)>, +} + +impl Table +where + T: fmt::Display, + U: fmt::Display, +{ + pub fn new(rows: Vec<(T, U)>) -> Self { + Self { rows } + } +} + +impl Iterator for Table +where + T: fmt::Display, + U: fmt::Display, +{ + type Item = (T, U); + + fn next(&mut self) -> Option { + if self.rows.is_empty() { + None + } else { + Some(self.rows.remove(0)) + } + } +} + +impl FromIterator for Table +where + T: fmt::Display + Clone, + U: fmt::Display + Clone, + I: Into>, +{ + fn from_iter(iter: Iter) -> Self + where + Iter: IntoIterator, + { + let mut merged_rows = Vec::new(); + for item in iter { + let table = item.into(); + merged_rows.extend(table.rows); + } + Table::new(merged_rows) + } +} + +impl From<(T, U)> for Table { + fn from(value: (T, U)) -> Self { + Self { rows: vec![value] } + } +} + +impl fmt::Display for Table +where + T: fmt::Display, + U: fmt::Display, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let (keys, values): (Vec<_>, Vec<_>) = self + .rows + .iter() + .map(|row| (row.0.to_string(), row.1.to_string())) + .unzip(); + let key_width = keys.iter().map(|k| k.len()).max().unwrap_or(0); + let val_width = values.iter().map(|v| v.len()).max().unwrap_or(0); + + let formatted_rows: Vec = keys + .iter() + .zip(values.iter()) + .map(|(key, value)| { + format!( + "│ {} │ {} │", + pad_str(key, ' ', key_width), + pad_str(value, ' ', val_width) + ) + }) + .collect(); + + let top_border = format!("┌─{}─┬─{}─┐", "─".repeat(key_width), "─".repeat(val_width),); + let bottom_border = format!("└─{}─┴─{}─┘", "─".repeat(key_width), "─".repeat(val_width),); + + write!( + f, + "{}\n{}\n{}", + top_border, + formatted_rows.join("\n"), + bottom_border, + ) + } +} + +fn pad_str>(string: T, padder: char, len: usize) -> String { + let string = string.into(); + let extra_len = len - string.len(); + let pad_str: String = iter::repeat(padder).take(extra_len).collect(); + format!("{}{}", string, pad_str) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_pad_str() { + assert_eq!(pad_str("hi", ' ', 5), "hi ".to_string()); + } }