Skip to content

Commit 2ebc979

Browse files
sahanaxzyhansl
andauthored
feat: improve REPL interface with banner, dot-commands, and --quiet flag (#4888)
## Summary Improves the REPL experience by adding a welcome banner, dot-commands, and a `--quiet` flag. Solves #4883 ## Changes - Welcome banner with version info on startup - `.help`, `.clear`, `.load <file>` dot-commands - `--quiet` / `-q` flag to suppress the banner - Ctrl+C now shows exit hint instead of quitting - Improved error formatting (bold + red labels) - Added help descriptions for `-O` and `--optimizer-statistics` - Updated [README.md](cci:7://file:///c:/Users/Sahana/boa/cli/README.md:0:0-0:0) with new REPL Commands section ## Files Changed Modified `cli/src/main.rs` > REPL loop, banner, dot-commands, CLI flags, error formatting Modified `cli/README.md` > Updated CLI options and added REPL Commands section --------- Co-authored-by: Hans Larsen <681969+hansl@users.noreply.github.com>
1 parent 1753d97 commit 2ebc979

File tree

2 files changed

+101
-11
lines changed

2 files changed

+101
-11
lines changed

cli/README.md

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,17 +53,33 @@ Options:
5353
-a, --dump-ast [<FORMAT>] Dump the AST to stdout with the given format [possible values: debug, json, json-pretty]
5454
-t, --trace Dump the AST to stdout with the given format
5555
--vi Use vi mode in the REPL
56-
-O, --optimize
57-
--optimizer-statistics
56+
-O, --optimize Enable bytecode compiler optimizations
57+
--optimizer-statistics Print optimizer statistics (requires -O)
5858
--flowgraph [<FORMAT>] Generate instruction flowgraph. Default is Graphviz [possible values: graphviz, mermaid]
5959
--flowgraph-direction <FORMAT> Specifies the direction of the flowgraph. Default is top-top-bottom [possible values: top-to-bottom, bottom-to-top, left-to-right, right-to-left]
6060
--debug-object Inject debugging object `$boa`
6161
-m, --module Treats the input files as modules
6262
-r, --root <ROOT> Root path from where the module resolver will try to load the modules [default: .]
63+
-e, --expression <EXPR> Execute a JavaScript expression then exit
64+
-q, --quiet Suppress the welcome banner when starting the REPL
6365
-h, --help Print help (see more with '--help')
6466
-V, --version Print version
6567
```
6668

69+
## REPL Commands
70+
71+
When running the interactive REPL (`boa` with no file arguments), the following
72+
dot-commands are available:
73+
74+
| Command | Description |
75+
| -------------- | ----------------------------------- |
76+
| `.help` | Show available REPL commands |
77+
| `.exit` | Exit the REPL |
78+
| `.clear` | Clear the terminal screen |
79+
| `.load <file>` | Load and evaluate a JavaScript file |
80+
81+
You can also press `Ctrl+C` to abort the current expression, or `Ctrl+D` to exit.
82+
6783
## Features
6884

6985
Boa's CLI currently has a variety of features (as listed in `Options`).

cli/src/main.rs

Lines changed: 83 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ use std::cell::RefCell;
4141
use std::time::{Duration, Instant};
4242
use std::{
4343
fs::OpenOptions,
44-
io::{self, IsTerminal, Read},
44+
io::{self, IsTerminal, Read, Write},
4545
path::{Path, PathBuf},
4646
rc::Rc,
4747
thread,
@@ -167,6 +167,10 @@ struct Opt {
167167
/// executed prior to the expression.
168168
#[arg(long, short = 'e')]
169169
expression: Option<String>,
170+
171+
/// Suppress the welcome banner when starting the REPL.
172+
#[arg(long, short = 'q')]
173+
quiet: bool,
170174
}
171175

172176
impl Opt {
@@ -361,14 +365,18 @@ fn generate_flowgraph<R: ReadChar>(
361365

362366
#[must_use]
363367
fn uncaught_error(error: &JsError) -> String {
364-
format!("{}: {}\n", "Uncaught".red(), error.to_string().red())
368+
format!(
369+
"{} {}\n",
370+
"Uncaught Error:".red().bold(),
371+
error.to_string().red()
372+
)
365373
}
366374

367375
#[must_use]
368376
fn uncaught_job_error(error: &JsError) -> String {
369377
format!(
370-
"{}: {}\n",
371-
"Uncaught error (during job evaluation)".red(),
378+
"{} {}\n",
379+
"Uncaught Error (during job evaluation):".red().bold(),
372380
error.to_string().red()
373381
)
374382
}
@@ -515,6 +523,7 @@ fn evaluate_files(
515523
Ok(())
516524
}
517525

526+
#[expect(clippy::too_many_lines)]
518527
fn main() -> Result<()> {
519528
color_eyre::config::HookBuilder::default()
520529
.display_location_section(false)
@@ -580,12 +589,46 @@ fn main() -> Result<()> {
580589
};
581590
}
582591

592+
// Print the welcome banner unless --quiet is passed.
593+
if !args.quiet {
594+
let version = env!("CARGO_PKG_VERSION");
595+
println!("{}", format!("Welcome to Boa v{version}").bold());
596+
println!(
597+
"Type {} for more information, {} to exit.",
598+
"\".help\"".green(),
599+
"Ctrl+D".green()
600+
);
601+
println!();
602+
}
603+
583604
let handle = start_readline_thread(sender, printer.clone(), args.vi_mode);
584605

606+
// TODO: Replace the `__BOA_LOAD_FILE__` string sentinel with a `CliCommand` enum
607+
// (e.g. `Exec(String)` / `LoadFile(PathBuf)`) for type-safe cross-thread communication.
585608
let exec = executor.clone();
586609
let eval_loop = NativeAsyncJob::new(async move |context| {
587610
while let Ok(line) = receiver.recv().await {
588611
let printer_clone = printer.clone();
612+
613+
if let Some(file_path) = line.strip_prefix("__BOA_LOAD_FILE__:") {
614+
let path = Path::new(file_path);
615+
if path.exists() {
616+
let mut context = context.borrow_mut();
617+
if let Err(e) =
618+
evaluate_file(path, &args, &mut context, &loader, &printer_clone)
619+
{
620+
printer_clone.print(format!("{e}\n"));
621+
}
622+
} else {
623+
printer_clone.print(format!(
624+
"{} file '{}' not found\n",
625+
"Error:".red().bold(),
626+
file_path
627+
));
628+
}
629+
continue;
630+
}
631+
589632
// schedule a new evaluation job that can run asynchronously
590633
// with the other evaluations.
591634
let eval_script = NativeAsyncJob::new(async move |context| {
@@ -666,14 +709,45 @@ fn readline_thread_main(
666709
editor.set_helper(Some(helper::RLHelper::new(readline)));
667710

668711
loop {
669-
match editor.readline(readline) {
712+
match editor.readline(readline).map(|l| l.trim().to_string()) {
670713
Ok(line) if line == ".exit" => break,
671-
Err(ReadlineError::Interrupted | ReadlineError::Eof) => break,
714+
Err(ReadlineError::Eof) => break,
715+
Err(ReadlineError::Interrupted) => {
716+
println!("(To exit, press Ctrl+D or type .exit)");
717+
}
718+
719+
Ok(ref line) if line == ".help" => {
720+
println!("REPL Commands:");
721+
println!(" {} Show this help message", ".help".green());
722+
println!(" {} Exit the REPL", ".exit".green());
723+
println!(" {} Clear the terminal screen", ".clear".green());
724+
println!(
725+
" {} Load and evaluate a JavaScript file",
726+
".load <file>".green()
727+
);
728+
println!();
729+
println!("Press {} to abort the current expression.", "Ctrl+C".bold());
730+
println!("Press {} to exit the REPL.", "Ctrl+D".bold());
731+
}
732+
733+
Ok(ref line) if line == ".clear" => {
734+
print!("\x1B[2J\x1B[3J\x1B[1;1H");
735+
io::stdout().flush().ok();
736+
}
737+
738+
Ok(ref line) if line == ".load" || line.starts_with(".load ") => {
739+
let file = line.strip_prefix(".load").unwrap_or("").trim();
740+
if file.is_empty() {
741+
eprintln!("{}", "Usage: .load <filename>".yellow());
742+
} else {
743+
sender.send_blocking(format!("__BOA_LOAD_FILE__:{file}"))?;
744+
thread::sleep(Duration::from_millis(10));
745+
}
746+
}
672747

673748
Ok(line) => {
674-
let line = line.trim_end();
675-
editor.add_history_entry(line).map_err(io::Error::other)?;
676-
sender.send_blocking(line.to_string())?;
749+
editor.add_history_entry(&line).map_err(io::Error::other)?;
750+
sender.send_blocking(line)?;
677751
thread::sleep(Duration::from_millis(10));
678752
}
679753

0 commit comments

Comments
 (0)