Implement list subcommand

This commit is contained in:
Filip Tibell 2023-01-25 14:42:10 -05:00
parent 8b7990b9a9
commit 92781a521c
No known key found for this signature in database
6 changed files with 189 additions and 1 deletions

View file

@ -1,3 +1,5 @@
--> A walkthrough of all of the basic Lune features.
print("Hello, lune! 🌙") print("Hello, lune! 🌙")
--[==[ --[==[

View file

@ -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/), 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). 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 ## `0.1.2` - January 24th, 2023
### Added ### Added

View file

@ -5,7 +5,11 @@ use clap::{CommandFactory, Parser};
use lune::Lune; 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_SELENE_FILE_NAME: &str = "lune.yml";
const LUNE_LUAU_FILE_NAME: &str = "luneTypes.d.luau"; const LUNE_LUAU_FILE_NAME: &str = "luneTypes.d.luau";
@ -21,6 +25,10 @@ pub struct Cli {
script_path: Option<String>, script_path: Option<String>,
/// Arguments to pass to the file as vararg (...) /// Arguments to pass to the file as vararg (...)
script_args: Vec<String>, script_args: Vec<String>,
/// 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 /// Pass this flag to download the Selene type
/// definitions file to the current directory /// definitions file to the current directory
#[clap(long)] #[clap(long)]
@ -69,7 +77,34 @@ impl Cli {
} }
} }
pub fn list() -> Self {
Self {
list: true,
..Default::default()
}
}
pub async fn run(self) -> Result<ExitCode> { pub async fn run(self) -> Result<ExitCode> {
// 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 // Download definition files, if wanted
let download_types_requested = self.download_selene_types || self.download_luau_types; let download_types_requested = self.download_selene_types || self.download_luau_types;
if download_types_requested { if download_types_requested {
@ -159,6 +194,14 @@ mod tests {
} }
} }
#[test]
fn list() -> Result<()> {
smol::block_on(async {
Cli::list().run().await?;
Ok(())
})
}
#[test] #[test]
fn download_selene_types() -> Result<()> { fn download_selene_types() -> Result<()> {
smol::block_on(async { smol::block_on(async {

View file

@ -2,6 +2,8 @@ use std::path::{PathBuf, MAIN_SEPARATOR};
use anyhow::{bail, Result}; use anyhow::{bail, Result};
const LUNE_COMMENT_PREFIX: &str = "-->";
pub fn find_luau_file_path(path: &str) -> Option<PathBuf> { pub fn find_luau_file_path(path: &str) -> Option<PathBuf> {
let file_path = PathBuf::from(path); let file_path = PathBuf::from(path);
if let Some(ext) = file_path.extension() { if let Some(ext) = file_path.extension() {
@ -38,3 +40,27 @@ pub fn find_parse_file_path(path: &str) -> Result<PathBuf> {
bail!("Invalid file path: '{}'", path) bail!("Invalid file path: '{}'", path)
} }
} }
pub fn parse_lune_description_from_file(contents: &str) -> Option<String> {
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)
}
}

110
src/cli/utils/listing.rs Normal file
View file

@ -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<Vec<(String, String)>> {
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(())
}

View file

@ -1,2 +1,3 @@
pub mod files; pub mod files;
pub mod github; pub mod github;
pub mod listing;