From 92781a521c9139510b372276466ef17c5e995740 Mon Sep 17 00:00:00 2001 From: Filip Tibell Date: Wed, 25 Jan 2023 14:42:10 -0500 Subject: [PATCH] Implement list subcommand --- .lune/hello_lune.luau | 2 + CHANGELOG.md | 6 +++ src/cli/cli.rs | 45 +++++++++++++++- src/cli/utils/files.rs | 26 +++++++++ src/cli/utils/listing.rs | 110 +++++++++++++++++++++++++++++++++++++++ src/cli/utils/mod.rs | 1 + 6 files changed, 189 insertions(+), 1 deletion(-) create mode 100644 src/cli/utils/listing.rs diff --git a/.lune/hello_lune.luau b/.lune/hello_lune.luau index bfe87e3..63707d7 100644 --- a/.lune/hello_lune.luau +++ b/.lune/hello_lune.luau @@ -1,3 +1,5 @@ +--> A walkthrough of all of the basic Lune features. + print("Hello, lune! 🌙") --[==[ diff --git a/CHANGELOG.md b/CHANGELOG.md index 2289491..315105e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ 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). +## Unreleased + +### Added + +- Added a `--list` subcommand to list scripts found in the `lune` or `.lune` directory. + ## `0.1.2` - January 24th, 2023 ### Added diff --git a/src/cli/cli.rs b/src/cli/cli.rs index 98fa9ee..a1359f9 100644 --- a/src/cli/cli.rs +++ b/src/cli/cli.rs @@ -5,7 +5,11 @@ use clap::{CommandFactory, Parser}; use lune::Lune; -use crate::utils::{files::find_parse_file_path, github::Client as GithubClient}; +use crate::utils::{ + files::find_parse_file_path, + github::Client as GithubClient, + listing::{find_lune_scripts, print_lune_scripts, sort_lune_scripts}, +}; const LUNE_SELENE_FILE_NAME: &str = "lune.yml"; const LUNE_LUAU_FILE_NAME: &str = "luneTypes.d.luau"; @@ -21,6 +25,10 @@ pub struct Cli { script_path: Option, /// Arguments to pass to the file as vararg (...) script_args: Vec, + /// Pass this flag to list scripts inside of + /// nearby `lune` and / or `.lune` directories + #[clap(long, short = 'l')] + list: bool, /// Pass this flag to download the Selene type /// definitions file to the current directory #[clap(long)] @@ -69,7 +77,34 @@ impl Cli { } } + pub fn list() -> Self { + Self { + list: true, + ..Default::default() + } + } + pub async fn run(self) -> Result { + // List files in `lune` and `.lune` directories, if wanted + // This will also exit early and not run anything else + if self.list { + match find_lune_scripts().await { + Ok(scripts) => { + let sorted = sort_lune_scripts(scripts); + if sorted.is_empty() { + println!("No scripts found."); + } else { + print!("Available scripts:"); + print_lune_scripts(sorted)?; + } + return Ok(ExitCode::SUCCESS); + } + Err(e) => { + eprintln!("{e}"); + return Ok(ExitCode::FAILURE); + } + } + } // Download definition files, if wanted let download_types_requested = self.download_selene_types || self.download_luau_types; if download_types_requested { @@ -159,6 +194,14 @@ mod tests { } } + #[test] + fn list() -> Result<()> { + smol::block_on(async { + Cli::list().run().await?; + Ok(()) + }) + } + #[test] fn download_selene_types() -> Result<()> { smol::block_on(async { diff --git a/src/cli/utils/files.rs b/src/cli/utils/files.rs index c315eb0..863e0b7 100644 --- a/src/cli/utils/files.rs +++ b/src/cli/utils/files.rs @@ -2,6 +2,8 @@ use std::path::{PathBuf, MAIN_SEPARATOR}; use anyhow::{bail, Result}; +const LUNE_COMMENT_PREFIX: &str = "-->"; + pub fn find_luau_file_path(path: &str) -> Option { let file_path = PathBuf::from(path); if let Some(ext) = file_path.extension() { @@ -38,3 +40,27 @@ pub fn find_parse_file_path(path: &str) -> Result { bail!("Invalid file path: '{}'", path) } } + +pub fn parse_lune_description_from_file(contents: &str) -> Option { + let mut comment_lines = Vec::new(); + for line in contents.lines() { + if let Some(stripped) = line.strip_prefix(LUNE_COMMENT_PREFIX) { + comment_lines.push(stripped); + } else { + break; + } + } + if comment_lines.is_empty() { + None + } else { + let shortest_indent = comment_lines.iter().fold(usize::MAX, |acc, line| { + let first_alphanumeric = line.find(char::is_alphanumeric).unwrap(); + acc.min(first_alphanumeric) + }); + let unindented_lines = comment_lines + .iter() + .map(|line| &line[shortest_indent..]) + .collect(); + Some(unindented_lines) + } +} diff --git a/src/cli/utils/listing.rs b/src/cli/utils/listing.rs new file mode 100644 index 0000000..6cbcbff --- /dev/null +++ b/src/cli/utils/listing.rs @@ -0,0 +1,110 @@ +use std::{cmp::Ordering, fmt::Write as _}; + +use anyhow::{bail, Result}; +use smol::{fs, io, prelude::*}; + +use super::files::parse_lune_description_from_file; + +// TODO: Use some crate for this instead +pub const COLOR_RESET: &str = if cfg!(test) { "" } else { "\x1B[0m" }; +pub const COLOR_BLUE: &str = if cfg!(test) { "" } else { "\x1B[34m" }; + +pub const STYLE_RESET: &str = if cfg!(test) { "" } else { "\x1B[22m" }; +pub const STYLE_DIM: &str = if cfg!(test) { "" } else { "\x1B[2m" }; + +pub async fn find_lune_scripts() -> Result> { + let mut lune_dir = fs::read_dir("lune").await; + if lune_dir.is_err() { + lune_dir = fs::read_dir(".lune").await; + } + match lune_dir { + Ok(mut dir) => { + let mut files = Vec::new(); + while let Some(entry) = dir.next().await.transpose()? { + let meta = entry.metadata().await?; + if meta.is_file() { + let contents = fs::read_to_string(entry.path()).await?; + files.push((entry, meta, contents)); + } + } + let parsed: Vec<_> = files + .iter() + .map(|(entry, _, contents)| { + let file_path = entry.path().with_extension(""); + let file_name = file_path.file_name().unwrap().to_string_lossy(); + let description = parse_lune_description_from_file(contents); + (file_name.to_string(), description.unwrap_or_default()) + }) + .collect(); + Ok(parsed) + } + Err(e) if matches!(e.kind(), io::ErrorKind::NotFound) => { + bail!("No lune directory was found.") + } + Err(e) => { + bail!("Failed to read lune files!\n{e}") + } + } +} + +pub fn sort_lune_scripts(scripts: Vec<(String, String)>) -> Vec<(String, String)> { + let mut sorted = scripts; + sorted.sort_by(|left, right| { + // Prefer scripts that have a description + let left_has_desc = !left.1.is_empty(); + let right_has_desc = !right.1.is_empty(); + if left_has_desc == right_has_desc { + // If both have a description or both + // have no description, we sort by name + left.0.cmp(&right.0) + } else if left_has_desc { + Ordering::Less + } else { + Ordering::Greater + } + }); + sorted +} + +pub fn print_lune_scripts(scripts: Vec<(String, String)>) -> Result<()> { + let longest_file_name_len = scripts + .iter() + .fold(0, |acc, (file_name, _)| acc.max(file_name.len())); + let script_with_description_exists = scripts.iter().any(|(_, desc)| !desc.is_empty()); + // Pre-calculate some strings that will be used often + let prefix = format!("{STYLE_DIM}>{STYLE_RESET} "); + let separator = format!("{STYLE_DIM}-{STYLE_RESET}"); + // Write the entire output to a buffer, doing this instead of using individual + // println! calls will ensure that no output get mixed up in between these lines + let mut buffer = String::new(); + if script_with_description_exists { + for (file_name, description) in scripts { + if description.is_empty() { + write!(&mut buffer, "\n{prefix}{file_name}")?; + } else { + let mut lines = description.lines(); + let first_line = lines.next().unwrap_or_default(); + let file_spacing = " ".repeat(file_name.len()); + let line_spacing = " ".repeat(longest_file_name_len - file_name.len()); + write!( + &mut buffer, + "\n{prefix}{file_name}{line_spacing} {separator} {COLOR_BLUE}{first_line}{COLOR_RESET}" + )?; + for line in lines { + write!( + &mut buffer, + "\n{prefix}{file_spacing}{line_spacing} {COLOR_BLUE}{line}{COLOR_RESET}" + )?; + } + } + } + } else { + for (file_name, _) in scripts { + write!(&mut buffer, "\n{prefix}{file_name}")?; + } + } + // Finally, print the entire buffer out + // with an ending newline added to it + println!("{buffer}"); + Ok(()) +} diff --git a/src/cli/utils/mod.rs b/src/cli/utils/mod.rs index 557c0bd..828523e 100644 --- a/src/cli/utils/mod.rs +++ b/src/cli/utils/mod.rs @@ -1,2 +1,3 @@ pub mod files; pub mod github; +pub mod listing;