From 568833cd2676ce945ffaecc302925bc78772a349 Mon Sep 17 00:00:00 2001 From: Lucas Newcomb Date: Mon, 11 Mar 2024 19:23:50 -0400 Subject: [PATCH] first step on 2.0. achieved some basic functionality --- Cargo.lock | 2 +- Cargo.toml | 2 +- SCRATCH.md | 78 -------------- src/app.rs | 258 ---------------------------------------------- src/cli.rs | 23 ++--- src/cli_util.rs | 13 --- src/file_util.rs | 96 ----------------- src/main.rs | 131 ++++++++++++++++++++--- src/settings.rs | 58 ----------- src/skeleton.rs | 55 ---------- src/util.rs | 39 +++++++ tests/cli_test.rs | 163 ----------------------------- 12 files changed, 166 insertions(+), 752 deletions(-) delete mode 100644 SCRATCH.md delete mode 100644 src/app.rs delete mode 100644 src/cli_util.rs delete mode 100644 src/file_util.rs delete mode 100644 src/settings.rs delete mode 100644 src/skeleton.rs create mode 100644 src/util.rs delete mode 100644 tests/cli_test.rs diff --git a/Cargo.lock b/Cargo.lock index 732886a..a25f987 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -529,7 +529,7 @@ dependencies = [ [[package]] name = "skely" -version = "0.1.6" +version = "2.0.0" dependencies = [ "anyhow", "assert_cmd", diff --git a/Cargo.toml b/Cargo.toml index 251ce87..1050986 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "skely" -version = "0.1.6" +version = "2.0.0" authors = ["Lucas Newcomb "] description = """ A simple command line tool for using and managing skeleton projects diff --git a/SCRATCH.md b/SCRATCH.md deleted file mode 100644 index ec2eec9..0000000 --- a/SCRATCH.md +++ /dev/null @@ -1,78 +0,0 @@ -# Refernces -- [Clap cookbook git derive](https://docs.rs/clap/latest/clap/_derive/_cookbook/git_derive/index.html) -- [Clap cookbook cargo derive](https://docs.rs/clap/latest/clap/_derive/_cookbook/cargo_example_derive/index.html) -- [Copy directory recursivley function (common.rs)](https://nick.groenen.me/notes/recursively-copy-files-in-rust/) -- [Capitalize word function (common.rs)](https://nick.groenen.me/notes/capitalize-a-string-in-rust/) -- [create_dir_all documentation](https://doc.rust-lang.org/std/fs/fn.create_dir_all.html) -- [crates.io Publishing](https://doc.rust-lang.org/cargo/reference/publishing.html) -- [toml formatting](https://toml.io/en/) -- [Rust file struct](https://doc.rust-lang.org/std/fs/struct.File.html) -- [Toml crate docs](https://docs.rs/toml/latest/toml/#) -- [Command Line Application testing](https://rust-cli.github.io/book/tutorial/testing.html) -- [Test driven development](https://en.wikipedia.org/wiki/Test-driven_development) - -# TODO -- [ ] Add default skeleton text -- [ ] Individual project cfg files -- [x] Template word config -- [ ] Bash completion -- [x] Add correct error handling (get rid of the 100 std::io::Error's) -- [x] Add --touch tag for add -- [x] Choose unwrap_or_else() or Result<> -- [ ] Interactive dir created for `sk add --dir` -- [x] Proper bin instillation of release -- [x] Non vim text editors -- [x] Settings -- [x] Toml serialization for default config file -- [ ] Unit testing / Integration testing -- [ ] Add -s option to name the new directory after source directory -- [ ] Add error if --source for sk add doesn't exist -- [ ] More in depth list tests - -# Command Line Interface - -## Commands - -- List - List all configured skeletons -- Edit - Edit a skeleton -- Add - Configure new skeleton -- Add --source - Configure new skeleton from path -- New - Copy skeleton to specified directory -- Remove - Remove configured skeleton and its files - -### Usage Examples - -sk list -sk edit rust (opens vim with the rust sk file/dir) -sk add rust (todo! maybe interactive dir creator) -sk add --source rust_sk/ -sk new rust -sk remove javascript - - - - - - - - - - - - - - - - -# TOML Config Format - -```toml -# Skely config - -[config] -# Default editor is vim -editor = "vim" -``` - - - diff --git a/src/app.rs b/src/app.rs deleted file mode 100644 index a4924a1..0000000 --- a/src/app.rs +++ /dev/null @@ -1,258 +0,0 @@ -use anyhow::{anyhow, Context, Result}; -use colored::Colorize; -use itertools::Itertools; -use std::fs::{self, create_dir_all}; -use std::io::{stdout, Write}; -use std::path::PathBuf; - -use crate::cli::Commands; -use crate::cli_util::{self, is_yes}; -use crate::file_util; -use crate::settings::Settings; - -use crate::skeleton::Skeleton; - -/// Central data structure for skely -pub struct App { - pub items: Vec, - pub settings: Settings, -} - -impl App { - pub fn new() -> Self { - Self { - items: Vec::new(), - settings: Settings::new(), - } - } - - pub fn default() -> Result { - Self::create_config()?; - - let mut app: Self = Self::new(); - app.items_from_dir(Self::skeletons_path()?) - .context("Could not fetch items from skely config directory")?; - - app.settings = Settings::load(Self::config_file_path()?)?; - Settings::create_default_cfg_file(Self::config_file_path()?)?; - - Ok(app) - } - - pub fn create_config() -> Result<()> { - let path = Self::skeletons_path()?; - if !path.exists() { - create_dir_all(path).context("Could not create config directory")?; - } - - if !Self::config_file_path()?.exists() { - eprintln!("Config file (config.toml) does not exist. Creating..."); - Settings::create_default_cfg_file(Self::config_file_path()?)?; - } - Ok(()) - } - - pub fn config_path() -> Result { - match home::home_dir() { - Some(home) => { - Ok([home, ".config".into(), "sk".into()].iter().collect()) - }, - None => Err(anyhow!("Could not fetch home directory")), - } - } - - pub fn config_file_path() -> Result { - let mut file_dir = Self::config_path()?; - file_dir.push("config.toml"); - Ok(file_dir) - } - - pub fn skeletons_path() -> Result { - let mut file_dir = Self::config_path()?; - file_dir.push("skeletons"); - Ok(file_dir) - } - - pub fn items_from_dir(&mut self, path: PathBuf) -> Result<()> { - let paths = fs::read_dir(path)?; - - // for dir_entry_res in paths { - // let item_path_buf = dir_entry_res?.path(); - // self.items.push(Skeleton::from_path_buf(item_path_buf)?); - // } - // FIXME: Really bad code - self.items.extend( - paths.into_iter() - .map_ok(|dir_entry| dir_entry.path()) - .filter_map(|x| x.ok()) - .map(|path| Skeleton::from_path_buf(path)) - .filter_map(|x| x.ok()) - ); - - - Ok(()) - } - - pub fn skeleton_by_id(&self, id: &str) -> Option<&Skeleton> { - self.items.iter().find(|&item| item.id == *id.to_string()) - } - - pub fn handle_command(&self, command: Commands) -> Result<()> { - match command { - Commands::List { verbose } => self.list(verbose)?, - Commands::Edit { id } => self.edit(id)?, - Commands::Add { - name, - source, - touch, - } => self.add(name, source, touch)?, - Commands::New { id, path, name } => self.new_project(id, path, name)?, - Commands::Remove { id, no_confirm } => self.remove(id, no_confirm)?, - } - - Ok(()) - } - - pub fn list(&self, verbose: bool) -> Result<()> { - self.print_skeletons(verbose, stdout())?; - Ok(()) - } - - pub fn edit(&self, skeleton_str: String) -> Result<()> { - match self.skeleton_by_id(&skeleton_str) { - Some(skeleton) => file_util::open_editor(&skeleton.path, &self.settings.editor), - None => Err(anyhow!("Skeleton not found")), - } - } - - pub fn add(&self, id: String, source: Option, touch: bool) -> Result<()> { - let mut path: PathBuf = Self::skeletons_path()?; - path.push(&id); - - if (path.exists() && path.is_dir()) || path.with_extension("sk").exists() { - return Err(anyhow!(format!( - "Skeleton at {} already exists", - file_util::path_buf_to_string(&path)? - ))); - } - - match source { - Some(source) => { - if source.is_dir() { - file_util::copy_recursively(source, path) - .context("Failed to copy directory to skely folder")?; - } else if source.is_file() { - fs::copy(source, path.with_extension("sk"))?; - } - } - None => { - if !touch { - file_util::open_editor(&path.with_extension("sk"), &self.settings.editor) - .context("Failed to open editor")?; - } else { - file_util::touch(&path.with_extension("sk")).context("Failed to create file")?; - } - } - } - Ok(()) - } - - pub fn new_project(&self, id: String, path: Option, name: Option) -> Result<()> { - let mut path = match path { - Some(p) => p, - None => PathBuf::from(&id), - }; - - if path.exists() { - return Err(anyhow!("Target directory already exists")); - } - - match self.skeleton_by_id(&id) { - Some(skeleton) => { - if file_util::path_buf_to_string(&path)? == "." { - println!( - "This will copy all files in skeleton {id} to your current working directory." - ); - println!("Are you sure you want to do this? (y/n) "); - - let mut input: String = String::new(); - std::io::stdin().read_line(&mut input)?; - input.truncate(input.len() - 1); - - if !is_yes(&input)? { - return Ok(()); - } - } - skeleton.copy_to_dir(&mut path)?; - - // FIXME: Bad - let project_name = name.unwrap_or(path.file_name().unwrap().to_str().unwrap().to_string()); - match &self.settings.placeholder { - Some(placeholder) => { - file_util::replace_string_in_dir(&path, placeholder.to_string(), project_name)?; - } - None => () - }; - Ok(()) - }, - None => Err(anyhow!("Skeleton not found")), - - } - } - - pub fn remove(&self, id: String, no_confirm: bool) -> Result<()> { - match self.skeleton_by_id(&id) { - Some(skeleton) => { - if !no_confirm { - let mut input = String::new(); - println!( - "Are you sure you want to delete {}? (y/n) ", - file_util::path_buf_to_string(&skeleton.path)? - ); - std::io::stdin().read_line(&mut input)?; - if !cli_util::is_yes(&input)? { - return Ok(()); - } - } - match skeleton.path.is_file() { - true => fs::remove_file(&skeleton.path)?, - false => fs::remove_dir_all(&skeleton.path)?, - } - Ok(()) - }, - None => Err(anyhow!("Skeleton not found")), - } - } - - // TODO: Pass writer - pub fn print_skeletons(&self, verbose: bool, mut writer: impl Write) -> Result<()> { - for item in self.items.iter() { - if let Some(item_path) = item.path.to_str() { - let single_file_str: &str; - let id_styled; - if item.path.is_file() { - single_file_str = "Single File"; - id_styled = item.id.to_string().white(); - } else { - single_file_str = "Project"; - id_styled = item.id.to_string().blue().bold(); - }; - if !verbose { - write!(writer, "{} ", &id_styled)?; - if item == self.items.iter().last().unwrap() { - writeln!(writer)?; - } - } else if verbose { - writeln!( - writer, - " {} [{}]: {}", - &id_styled, - file_util::tilda_ize_path_str(item_path)?, - single_file_str - )?; - } - } - } - Ok(()) - } -} diff --git a/src/cli.rs b/src/cli.rs index 2a32680..a97dc15 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -15,27 +15,15 @@ pub struct Cli { #[derive(Debug, PartialEq, Subcommand)] pub enum Commands { /// Lists all configured skeletons - List { - #[arg(short, long)] - verbose: bool, - }, - /// Opens skeleton to edit - #[command(arg_required_else_help = true)] - Edit { - /// Id of skeleton to edit - id: String, - }, + List, /// Adds skeleton to configured skeletons Add { - /// Name of skeleton + /// Source to create skeleton from #[arg(required = true)] - name: String, - /// Optional source to create skeleton from - #[arg(short, long)] - source: Option, - /// Creates .sk file without opening editor + source: PathBuf, + /// Identifier of skeleton #[arg(short, long)] - touch: bool, + id: Option, }, /// Creates a new project from specified skeleton New { @@ -43,6 +31,7 @@ pub enum Commands { #[arg(required = true)] id: String, /// Desired project path + #[arg(short, long)] path: Option, /// Optional name, defaults to directory name #[arg(short, long)] diff --git a/src/cli_util.rs b/src/cli_util.rs deleted file mode 100644 index fcfeb83..0000000 --- a/src/cli_util.rs +++ /dev/null @@ -1,13 +0,0 @@ -use anyhow::{anyhow, Result}; - -pub fn is_yes(input: &str) -> Result { - let input = input.to_lowercase(); - if input == "yes" || input == "y" { - Ok(true) - } else if input == "no" || input == "n" { - Ok(false) - } else { - Err(anyhow!("Invalid user input")) - } -} - diff --git a/src/file_util.rs b/src/file_util.rs deleted file mode 100644 index 5491bfa..0000000 --- a/src/file_util.rs +++ /dev/null @@ -1,96 +0,0 @@ -use std::{fs, io::Write, path::{Path, PathBuf}, process::Command, env}; - -use anyhow::{anyhow, Context, Result}; - -pub fn path_buf_to_string(p: &PathBuf) -> Result { - match p.to_str() { - Some(s) => Ok(s.to_string()), - None => Err(anyhow!("Could not convert string to PathBuf")), - } -} - -pub fn touch(path: &PathBuf) -> Result<()> { - match fs::OpenOptions::new().create(true).write(true).open(path) { - Ok(_) => Ok(()), - Err(e) => Err(e.into()), - } -} - -pub fn tilda_ize_path_str(item: &str) -> Result { - // TODO: Validate item - let home_dir = home::home_dir().context("Could not get home directory")?; - let home_str = path_buf_to_string(&home_dir)?; - Ok(item.replace(&home_str, "~")) -} - -pub fn copy_recursively(source: impl AsRef, destination: impl AsRef) -> Result<()> { - fs::create_dir_all(&destination)?; - for entry in fs::read_dir(&source)? { - let entry = entry?; - let filetype = entry.file_type()?; - if filetype.is_dir() { - copy_recursively(entry.path(), destination.as_ref().join(entry.file_name()))?; - } else { - fs::copy(entry.path(), destination.as_ref().join(entry.file_name()))?; - } - } - - Ok(()) -} - -pub fn open_editor(arg: &PathBuf, editor: &Option) -> Result<()> { - match editor { - Some(editor) => { - let output = Command::new("which") - .arg(editor) - .output()?; - - if output.status.success() { - Command::new(editor).arg(arg).spawn()?.wait()?; - } else { - return Err(anyhow!(format!("Editor \"{}\" not found", editor))); - } - } - None => { - let editor = env::var_os("EDITOR").unwrap_or("vim".into()); - Command::new(editor) - .arg(arg) - .spawn() - .context("$EDITOR enviroment variable is set incorrectly")? - .wait()?; - } - } - Ok(()) -} - -// Spaghetti code FIX PLEASE -pub fn replace_string_in_dir(input_path: &PathBuf, from: String, to: String) -> Result<()> { - if input_path.is_file() { - replace_string_in_file(input_path, from.clone(), to.clone())?; - return Ok(()); - } - - let paths = fs::read_dir(input_path)?; - - for dir_entry in paths { - if dir_entry.as_ref().unwrap().path().is_dir() { - replace_string_in_dir(&dir_entry?.path(), from.clone(), to.clone())?; - } else { - replace_string_in_file(&dir_entry?.path(), from.clone(), to.clone())?; - } - } - - Ok(()) -} - -pub fn replace_string_in_file(path: &PathBuf, from: String, to: String) -> Result<()> { - let data = fs::read_to_string(path)?; - let new = data.replace(&from, &to); - let mut file = fs::OpenOptions::new() - .write(true) - .truncate(true) - .open(path)?; - file.write_all(new.as_bytes())?; - - Ok(()) -} diff --git a/src/main.rs b/src/main.rs index 7d5142d..50fe7b5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,19 +1,126 @@ -use anyhow::Result; -use clap::Parser; +use anyhow::{anyhow, Context, Result}; +use cli::{Cli, Commands}; +use colored::{ColoredString, Colorize}; +use util::sk_cfg_path; +use std::{fs::create_dir_all, path::PathBuf}; +use std::fs; -use crate::app::App; -use crate::cli::Cli; +use clap::{Parser, Subcommand}; + +use crate::util::{copy_recursively, path_buf_to_string}; -mod app; mod cli; -mod cli_util; -mod settings; -mod skeleton; -mod file_util; +mod util; fn main() -> Result<()> { - let app: App = App::default()?; - let args = Cli::parse(); - app.handle_command(args.command)?; + run()?; + + Ok(()) +} + +fn run() -> Result<()> { + let args = Cli::parse(); + match args.command { + Commands::Add { id, source } => add(source, id)?, + Commands::New { id, path, name } => new(id, path, name)?, + Commands::List => list()?, + Commands::Remove { id, no_confirm } => remove(id, no_confirm)?, + } + + Ok(()) +} + +fn add(source: PathBuf, id: Option) -> Result<()> { + let dest_path: PathBuf = sk_cfg_path()?.join("skeletons").join( + match id { + Some(id) => id, + None => source.file_name().unwrap().to_str().unwrap().to_string() + } + ); + + if !source.exists() { + return Err(anyhow!( + "Could not find file {:?}", + dest_path.display() + )) + } + + if dest_path.exists() { + return Err(anyhow!( + "Skeleton at {:?} already exists!", + dest_path.display() + )) + } + + if source.is_dir() { + copy_recursively(source, dest_path).context("Failed to copy source to skeletons directory")?; + } else if source.is_file() { + fs::copy(source, dest_path).context("Failed to copy source to skeletons directory")?; + } + Ok(()) } + +fn new(id: String, path: Option, name: Option) -> Result<()> { + let mut dest_path = path.unwrap_or(PathBuf::from(&id)); + let skeleton_path = sk_cfg_path()?.join("skeletons").join(&id); + + if !skeleton_path.exists() { + return Err(anyhow!("Could not find skeleton")); + } + + if skeleton_path.is_file() { + if dest_path.is_dir() { + dest_path.push(&id); + } + + if dest_path.is_file() { + return Err(anyhow!("Destination file already exists")); + } + + // FIXME: Hacky + if !dest_path.exists() && path_buf_to_string(&dest_path)?.ends_with("/") { + return Err(anyhow!("Target directory does not exist")); + } + + fs::File::create(&dest_path)?; + fs::copy(&skeleton_path, &dest_path)?; + } else if skeleton_path.is_dir() { + if dest_path.exists() && !dest_path.read_dir()?.next().is_none() { + return Err(anyhow!("Target directory already exists and is not an empty directory")); + } + + copy_recursively(&skeleton_path, &dest_path)?; + } + + Ok(()) +} + +fn list() -> Result<()> { + for entry in fs::read_dir(sk_cfg_path()?.join("skeletons"))? { + match entry { + Ok(n) => { + let path = n.path(); + // lmao nice "refactor" + let mut id: ColoredString = path.file_name().unwrap().to_str().unwrap().to_string().into(); + + if path.is_dir() { + id = id.blue().bold(); + } else { + id = id.white(); + } + + print!("{} ", &id); + }, + Err(_) => (), + } + } + + println!(); + + Ok(()) +} + +fn remove(id: String, no_confirm: bool) -> Result<()> { + unimplemented!() +} diff --git a/src/settings.rs b/src/settings.rs deleted file mode 100644 index 6f070e2..0000000 --- a/src/settings.rs +++ /dev/null @@ -1,58 +0,0 @@ -use anyhow::Result; -use serde::{Deserialize, Serialize}; -use std::fs::File; -use std::io::{Read, Write}; -use std::path::PathBuf; - -#[derive(Serialize, Deserialize, Debug)] -pub struct Settings { - pub editor: Option, - pub placeholder: Option, -} - -impl Settings { - pub fn new() -> Self { - Settings { - editor: None, - placeholder: None, - } - } - - pub fn default() -> Self { - Settings { - editor: Some("".to_string()), - placeholder: Some("PLACEHOLDER".to_string()), - } - } - - pub fn load(cfg_path: PathBuf) -> Result { - let mut cfg_file = File::open(cfg_path)?; - let mut contents = String::new(); - cfg_file.read_to_string(&mut contents)?; - let mut s: Settings = toml::from_str(&contents)?; - // Make this loop over struct fields - if let Some(editor) = &s.editor { - if editor.is_empty() { - s.editor = None; - } - } - - if let Some(placeholder) = &s.placeholder { - if placeholder.is_empty() { - s.placeholder = None; - } - } - Ok(s) - } - - pub fn create_default_cfg_file(path: PathBuf) -> Result<()> { - let settings = Settings::default(); - - let serialized = toml::to_string_pretty(&settings)?; - - let mut file = File::create(path)?; - file.write_all(serialized.as_bytes())?; - - Ok(()) - } -} diff --git a/src/skeleton.rs b/src/skeleton.rs deleted file mode 100644 index 14d319f..0000000 --- a/src/skeleton.rs +++ /dev/null @@ -1,55 +0,0 @@ -use anyhow::{anyhow, Context, Result}; -use crate::file_util; -use std::fs::create_dir_all; -use std::{fs, path::PathBuf}; - -/// Data structure for storing a skeleton project's information -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] -pub struct Skeleton { - pub id: String, - pub path: PathBuf, -} - -impl Skeleton { - // pub fn new(id: &str) -> Result { - // let mut path: PathBuf = sk_cfg_dir()?; - // let id_lower: String = id.to_lowercase(); - // path.push(format!("{id}.sk")); - // check_cfg_dir()?; - // touch(path.as_path()).context("Could not create .sk file")?; - - // Ok(Self { id: id_lower, path }) - // } - - /// Constructor for a skeleton from a specified path - pub fn from_path_buf(path: PathBuf) -> Result { - if let Some(file_name) = path.file_name() { - Ok(Self { - id: file_name.to_string_lossy().replace(".sk", ""), - path, - }) - } else { - Err(anyhow!("Could not find .sk file at path {:.?}", path)) - } - } - - - /// Copy skeleton to specified path - pub fn copy_to_dir(&self, path: &mut PathBuf) -> Result<()> { - if !path.exists() && self.path.is_dir() { - create_dir_all(&path)?; - } - - if self.path.is_file() { - if file_util::path_buf_to_string(path)? == "." { - path.push(format!("{}.sk", &self.id)); - } - fs::File::create(&path)?; - fs::copy(&self.path, &path).context("Could not copy file")?; - } else if self.path.is_dir() { - file_util::copy_recursively(&self.path, &path).context("Could not copy directory")?; - } - - Ok(()) - } -} diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..a2e3365 --- /dev/null +++ b/src/util.rs @@ -0,0 +1,39 @@ +use std::path::PathBuf; +use std::fs; +use std::path::Path; + +use anyhow::{anyhow, Result, Error}; +use home::home_dir; + +pub fn sk_cfg_path() -> Result { + match home_dir() { + Some(h) => Ok(h.join(".config").join("sk")), + None => Err(anyhow!("Could not get sk config directory")) + } +} + +pub fn path_buf_filename(path: &PathBuf) -> Result { + Ok(path.file_name().unwrap().to_str().unwrap().to_string()) +} + +pub fn path_buf_to_string(p: &PathBuf) -> Result { + match p.to_str() { + Some(s) => Ok(s.to_string()), + None => Err(anyhow!("Could not convert string to PathBuf")), + } +} + +pub fn copy_recursively(source: impl AsRef, destination: impl AsRef) -> Result<()> { + fs::create_dir_all(&destination)?; + for entry in fs::read_dir(&source)? { + let entry = entry?; + let filetype = entry.file_type()?; + if filetype.is_dir() { + copy_recursively(entry.path(), destination.as_ref().join(entry.file_name()))?; + } else { + fs::copy(entry.path(), destination.as_ref().join(entry.file_name()))?; + } + } + + Ok(()) +} diff --git a/tests/cli_test.rs b/tests/cli_test.rs deleted file mode 100644 index cc896c3..0000000 --- a/tests/cli_test.rs +++ /dev/null @@ -1,163 +0,0 @@ -use anyhow::{Result, Context}; -use assert_cmd::prelude::*; // Add methods on commands -use assert_fs::prelude::*; -use home::home_dir; -use predicates::prelude::*; // Used for writing assertions -use std::{ - fs::{read_to_string, remove_file, read_dir}, - path::{PathBuf, Path}, - process::Command, collections::HashSet, -}; // Run programs - -// - List - List all configured skeletons -// - Edit - Edit a skeleton -// - Add - Configure new skeleton -// - Add --source - Configure new skeleton from path -// - New - Copy skeleton to specified directory -// - Remove - Remove configured skeleton and its files - -mod list_tests { - use super::*; - - #[test] - fn list_basic() -> Result<()> { - let mut list_cmd = Command::cargo_bin("sk")?; - list_cmd.arg("list"); - list_cmd.assert().success(); - - Ok(()) - } - - #[test] - fn list_verbose() -> Result<()> { - let mut list_cmd = Command::cargo_bin("sk")?; - list_cmd.arg("list").arg("-v"); - list_cmd.assert().success(); - - Ok(()) - } -} - -mod edit_tests { - use super::*; - - #[test] - fn edit_file_doesnt_exist() -> Result<()> { - let skeleton_dir: PathBuf = PathBuf::from(format!( - "{}/.config/sk/skeletons", - home_dir().unwrap().display() - )); - - let filename = find_shortest_nonfilenames(&skeleton_dir)?; - - let mut cmd = Command::cargo_bin("sk")?; - - cmd.arg("edit") - .arg(filename); - cmd.assert() - .failure() - .stderr(predicate::str::contains("Error: Skeleton not found")); - - Ok(()) - } -} - -mod add_tests { - use super::*; - use anyhow::Context; - - #[test] - /// Creates empty skeleton at TEST_FILE.sk and removes it - fn add_touch() -> Result<()> { - let sk_file: PathBuf = PathBuf::from(format!( - "{}/.config/sk/skeletons/TEST_FILE.sk", - home_dir().unwrap().display() - )); - - if sk_file.exists() { - remove_file(&sk_file)?; - } - - let mut add_cmd = Command::cargo_bin("sk")?; - add_cmd.arg("add").arg("TEST_FILE").arg("-t"); - add_cmd.assert().success(); - - assert!(sk_file.exists()); - - remove_file(&sk_file)?; - - Ok(()) - } -} - -mod new_tests { - use super::*; -} - -mod remove_tests { - use super::*; -} - -// -// UTILS -// - -fn file_eq(file1: PathBuf, file2: PathBuf) -> Result { - assert!(file1.exists()); - assert!(file2.exists()); - let file1_string: String = read_to_string(file1).context("file1 error")?; - let file2_string: String = read_to_string(file2).context("file2 error")?; - - Ok(file1_string == file2_string) -} - -// I'm proud of this algorithm -fn find_shortest_nonfilenames(dir: &Path) -> Result { - let mut filenames = HashSet::new(); - - // Iterate through all entries in the directory - for entry in read_dir(dir)? { - let entry = entry?; - // If it's a file, add the file name to the set of filenames - let filename = entry.file_name().to_string_lossy().into_owned(); - filenames.insert(filename); - } - - // Iterate through all possible strings of increasing length until - // we find a string that is not a filename - for len in 1.. { - for name in generate_strings(len) { - if !filenames.contains(&name) { - return Ok(name); - } - } - } - - // We should never get here - unreachable!() -} - -fn generate_strings(length: usize) -> Vec { - let mut names = Vec::new(); - let chars = (b'!'..=b'~').map(char::from).collect::>(); - generate_strings_rec(&chars, length, &mut names, String::new()); - names -} - -fn generate_strings_rec( - chars: &[char], - length: usize, - names: &mut Vec, - current_name: String, -) { - if current_name.len() == length { - names.push(current_name); - return; - } - - for c in chars { - let mut new_prefix = current_name.clone(); - new_prefix.push(*c); - generate_strings_rec(chars, length, names, new_prefix); - } -}