1
0

Clean up the README and style the CLI

This commit is contained in:
Jeff 2024-12-18 08:49:45 -05:00
parent 46060a473d
commit ed05a981e7
3 changed files with 144 additions and 142 deletions

View File

@ -170,9 +170,9 @@ coincidence.
### Semicolons
Dust borrowed Rust's approach to semicolons and their effect on evaluation and relaxed the rules to
accomated different styles of coding. Rust, for example, isn't design for command lines or REPLs but
Dust could be well-suited to those applications. Dust needs to work in a source file or in an ad-hoc
one-liner sent to the CLI. Thus, semicolons are optional in most cases.
accomated different styles of coding. Rust, isn't designed for command lines or REPLs but Dust could
be well-suited to those applications. Dust needs to work in a source file or in an ad-hoc one-liner
sent to the CLI. Thus, semicolons are optional in most cases.
There are two things you need to know about semicolons in Dust:

View File

@ -1,6 +1,6 @@
[package]
name = "dust-cli"
description = "Tool for running and debugging Dust programs"
description = "Command line interface for the Dust programming language"
authors = ["Jeff Anderson"]
edition.workspace = true
license.workspace = true
@ -13,7 +13,13 @@ name = "dust"
path = "src/main.rs"
[dependencies]
clap = { version = "4.5.14", features = ["cargo", "color", "derive", "help", "wrap_help"] }
clap = { version = "4.5.14", features = [
"cargo",
"color",
"derive",
"help",
"wrap_help",
] }
color-print = "0.3.7"
dust-lang = { path = "../dust-lang" }
env_logger = "0.11.5"

View File

@ -3,46 +3,64 @@ use std::time::{Duration, Instant};
use std::{fs::read_to_string, path::PathBuf};
use clap::builder::StyledStr;
use clap::Args;
use clap::{
builder::{styling::AnsiColor, Styles},
crate_authors, crate_description, crate_version, ArgAction, Args, ColorChoice, Parser,
Subcommand, ValueHint,
crate_authors, crate_description, crate_version, ColorChoice, Parser, Subcommand, ValueHint,
};
use color_print::cstr;
use dust_lang::{CompileError, Compiler, DustError, DustString, Lexer, Span, Token, Vm};
use log::{Level, LevelFilter};
const HELP_TEMPLATE: &str = cstr!(
"\
<bold,bright-magenta>Dust CLI</bold,bright-magenta>
{about}
Version: {version}
Author: {author}
License: GPL-3.0
const CLI_HELP_TEMPLATE: &str = cstr!(
r#"
<bright-magenta><bold>Dust CLI
</bold>
{about}</bright-magenta>
<bold,bright-magenta>Usage</bold,bright-magenta>
Version: {version}
Author: {author}
License: GPL-3.0
🌐 Repository: git.jeffa.io/jeff/dust
<bright-magenta,bold>Usage
</bright-magenta,bold>
{tab}{usage}
<bold,bright-magenta>Options</bold,bright-magenta>
{options}
<bold,bright-magenta>Modes</bold,bright-magenta>
<bright-magenta,bold>Modes
</bright-magenta,bold>
{subcommands}
<bold,bright-magenta>Arguments</bold,bright-magenta>
{positionals}
<bright-magenta,bold>Options
</bright-magenta,bold>
{options}
"#
);
"
const MODE_HELP_TEMPLATE: &str = cstr!(
r#"
<bright-magenta><bold>Dust CLI
</bold>
{about}</bright-magenta>
Version: {version}
Author: {author}
License: GPL-3.0
🌐 Repository: git.jeffa.io/jeff/dust
<bright-magenta,bold>Usage
</bright-magenta,bold>
{tab}{usage}
<bright-magenta,bold>Options
</bright-magenta,bold>
{options}
"#
);
const STYLES: Styles = Styles::styled()
.header(AnsiColor::BrightMagenta.on_default().bold())
.usage(AnsiColor::BrightWhite.on_default().bold())
.usage(AnsiColor::BrightCyan.on_default().bold())
.literal(AnsiColor::BrightCyan.on_default())
.placeholder(AnsiColor::BrightMagenta.on_default())
.error(AnsiColor::BrightRed.on_default().bold())
@ -55,175 +73,148 @@ const STYLES: Styles = Styles::styled()
author = crate_authors!(),
about = crate_description!(),
color = ColorChoice::Auto,
disable_help_flag = true,
disable_version_flag = true,
help_template = StyledStr::from(HELP_TEMPLATE),
help_template = StyledStr::from(CLI_HELP_TEMPLATE),
styles = STYLES,
term_width = 80,
)]
struct Cli {
/// Print help information for this or the selected subcommand
#[arg(short, long, action = ArgAction::Help)]
help: bool,
/// Print version information
#[arg(short, long, action = ArgAction::Version)]
version: bool,
/// Log level, overrides the DUST_LOG environment variable
#[arg(
short,
long,
value_name = "LOG_LEVEL",
value_parser = ["info", "trace", "debug"],
)]
/// Overrides the DUST_LOG environment variable
#[arg(short, long, value_name = "LOG_LEVEL")]
log: Option<LevelFilter>,
#[command(subcommand)]
mode: Mode,
}
#[derive(Args)]
#[clap(
styles = STYLES,
)]
#[group()]
struct Input {
/// Source code to run instead of a file
#[arg(short, long, value_hint = ValueHint::Other, value_name = "SOURCE")]
#[arg(short, long, value_hint = ValueHint::Other, value_name = "INPUT")]
command: Option<String>,
/// Read source code from stdin
#[arg(long)]
stdin: bool,
#[command(subcommand)]
mode: Mode,
/// Path to a source code file
#[arg(value_hint = ValueHint::FilePath)]
file: Option<PathBuf>,
}
#[derive(Subcommand)]
#[clap(
help_template = StyledStr::from(HELP_TEMPLATE),
styles = STYLES,
)]
#[clap(subcommand_value_name = "MODE", flatten_help = true)]
enum Mode {
/// Compile and run the program (default)
#[command(short_flag = 'r')]
#[command(
short_flag = 'r',
help_template = MODE_HELP_TEMPLATE
)]
Run {
#[arg(short, long, action = ArgAction::Help)]
#[clap(help_heading = Some("Options"))]
help: bool,
/// Print the time taken for compilation and execution
#[arg(long)]
#[clap(help_heading = Some("Run Options"))]
time: bool,
/// Do not print the program's return value
#[arg(long)]
#[clap(help_heading = Some("Run Options"))]
no_output: bool,
/// Custom program name, overrides the file name
#[arg(long)]
#[clap(help_heading = Some("Run Options"))]
name: Option<DustString>,
#[command(flatten)]
input: Input,
},
/// Compile and print the bytecode disassembly
#[command(short_flag = 'd')]
#[command(
short_flag = 'd',
help_template = MODE_HELP_TEMPLATE
)]
Disassemble {
#[arg(short, long, action = ArgAction::Help)]
#[clap(help_heading = Some("Options"))]
help: bool,
/// Style disassembly output
#[arg(short, long, default_value = "true")]
#[clap(help_heading = Some("Disassemble Options"))]
style: bool,
/// Custom program name, overrides the file name
#[arg(long)]
#[clap(help_heading = Some("Disassemble Options"))]
name: Option<DustString>,
#[command(flatten)]
input: Input,
},
/// Lex the source code and print the tokens
#[command(short_flag = 't')]
#[command(
short_flag = 't',
help_template = MODE_HELP_TEMPLATE
)]
Tokenize {
#[arg(short, long, action = ArgAction::Help)]
#[clap(help_heading = Some("Options"))]
help: bool,
/// Style token output
#[arg(short, long, default_value = "true")]
#[clap(help_heading = Some("Tokenize Options"))]
style: bool,
#[command(flatten)]
input: Input,
},
}
#[derive(Args, Clone)]
#[group(required = true, multiple = false)]
struct Source {}
fn main() {
let start_time = Instant::now();
// let mut logger = env_logger::builder();
// logger.format(move |buf, record| {
// let elapsed = format!("T+{:.04}", start_time.elapsed().as_secs_f32()).dimmed();
// let level_display = match record.level() {
// Level::Info => "INFO".bold().white(),
// Level::Debug => "DEBUG".bold().blue(),
// Level::Warn => "WARN".bold().yellow(),
// Level::Error => "ERROR".bold().red(),
// Level::Trace => "TRACE".bold().purple(),
// };
// let display = format!("[{elapsed}] {level_display:5} {args}", args = record.args());
// writeln!(buf, "{display}")
// });
let Cli {
log,
command,
stdin,
mode,
file,
..
} = Cli::parse();
// if let Some(level) = log {
// logger.filter_level(level).init();
// } else {
// logger.parse_env("DUST_LOG").init();
// }
let (source, file_name) = if let Some(source) = command {
(source, None)
} else if stdin {
let mut source = String::new();
io::stdin()
.read_to_string(&mut source)
.expect("Failed to read from stdin");
(source, None)
} else {
let path = file.expect("Path is required when command is not provided");
let source = read_to_string(&path).expect("Failed to read file");
fn get_source_and_file_name(input: Input) -> (String, Option<DustString>) {
if let Some(path) = input.file {
let source = read_to_string(&path).expect("Failed to read source file");
let file_name = path
.file_name()
.and_then(|os_str| os_str.to_str())
.map(DustString::from);
(source, file_name)
};
let program_name = match &mode {
Mode::Run { name, .. } => name,
Mode::Disassemble { name, .. } => name,
Mode::Tokenize { .. } => &None,
return (source, file_name);
}
.iter()
.next()
.cloned()
.or(file_name);
if let Mode::Disassemble { style, .. } = mode {
if input.stdin {
let mut source = String::new();
io::stdin()
.read_to_string(&mut source)
.expect("Failed to read from stdin");
return (source, None);
}
let source = input.command.expect("No source code provided");
(source, None)
}
fn main() {
let start_time = Instant::now();
let mut logger = env_logger::builder();
logger.format(move |buf, record| {
let elapsed = format!("T+{:.04}", start_time.elapsed().as_secs_f32());
let level_display = match record.level() {
Level::Info => cstr!("<bright-magenta,bold>INFO<bright-magenta,bold>"),
Level::Trace => cstr!("<bright-cyan,bold>TRACE<bright-cyan,bold>"),
Level::Debug => cstr!("<bright-blue,bold>DEBUG<bright-blue,bold>"),
Level::Warn => cstr!("<bright-yellow,bold>WARN<bright-yellow,bold>"),
Level::Error => cstr!("<bright-red,bold>ERROR<bright-red,bold>"),
};
let display = format!("[{elapsed}] {level_display:5} {args}", args = record.args());
writeln!(buf, "{display}")
});
let Cli { log, mode } = Cli::parse();
if let Some(level) = log {
logger.filter_level(level).init();
} else {
logger.parse_env("DUST_LOG").init();
}
if let Mode::Disassemble { style, name, input } = mode {
let (source, file_name) = get_source_and_file_name(input);
let lexer = Lexer::new(&source);
let mut compiler = match Compiler::new(lexer) {
Ok(compiler) => compiler,
@ -243,21 +234,22 @@ fn main() {
}
}
let chunk = compiler.finish(program_name);
let chunk = compiler.finish(file_name);
let mut stdout = stdout().lock();
chunk
.disassembler(&mut stdout)
.style(style)
.source(&source)
.width(80)
.width(65)
.disassemble()
.expect("Failed to write disassembly to stdout");
return;
}
if let Mode::Tokenize { style, .. } = mode {
if let Mode::Tokenize { input, .. } = mode {
let (source, _) = get_source_and_file_name(input);
let mut lexer = Lexer::new(&source);
let mut next_token = || -> Option<(Token, Span, bool)> {
match lexer.next_token() {
@ -303,9 +295,13 @@ fn main() {
}
if let Mode::Run {
time, no_output, ..
name,
time,
no_output,
input,
} = mode
{
let (source, file_name) = get_source_and_file_name(input);
let lexer = Lexer::new(&source);
let mut compiler = match Compiler::new(lexer) {
Ok(compiler) => compiler,
@ -325,7 +321,7 @@ fn main() {
}
}
let chunk = compiler.finish(program_name);
let chunk = compiler.finish(name.or(file_name));
let compile_end = start_time.elapsed();
if time {