mirror of
https://github.com/rust-lang/rustlings.git
synced 2024-12-26 00:00:03 +03:00
Clean up as a preparation for the TUI
This commit is contained in:
parent
9ea744a710
commit
34375b2ebf
44
src/main.rs
44
src/main.rs
|
@ -7,10 +7,9 @@ use clap::{Parser, Subcommand};
|
||||||
use console::Emoji;
|
use console::Emoji;
|
||||||
use notify_debouncer_mini::notify::RecursiveMode;
|
use notify_debouncer_mini::notify::RecursiveMode;
|
||||||
use notify_debouncer_mini::{new_debouncer, DebouncedEventKind};
|
use notify_debouncer_mini::{new_debouncer, DebouncedEventKind};
|
||||||
use shlex::Shlex;
|
|
||||||
use std::io::{BufRead, Write};
|
use std::io::{BufRead, Write};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::process::{exit, Command};
|
use std::process::exit;
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use std::sync::mpsc::{channel, RecvTimeoutError};
|
use std::sync::mpsc::{channel, RecvTimeoutError};
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
@ -31,9 +30,6 @@ mod verify;
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(version)]
|
#[command(version)]
|
||||||
struct Args {
|
struct Args {
|
||||||
/// Show outputs from the test exercises
|
|
||||||
#[arg(long)]
|
|
||||||
nocapture: bool,
|
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
command: Option<Subcommands>,
|
command: Option<Subcommands>,
|
||||||
}
|
}
|
||||||
|
@ -45,11 +41,7 @@ enum Subcommands {
|
||||||
/// Verify all exercises according to the recommended order
|
/// Verify all exercises according to the recommended order
|
||||||
Verify,
|
Verify,
|
||||||
/// Rerun `verify` when files were edited
|
/// Rerun `verify` when files were edited
|
||||||
Watch {
|
Watch,
|
||||||
/// Show hints on success
|
|
||||||
#[arg(long)]
|
|
||||||
success_hints: bool,
|
|
||||||
},
|
|
||||||
/// Run/Test a single exercise
|
/// Run/Test a single exercise
|
||||||
Run {
|
Run {
|
||||||
/// The name of the exercise
|
/// The name of the exercise
|
||||||
|
@ -117,7 +109,6 @@ If you are just starting with Rustlings, run the command `rustlings init` to ini
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
let verbose = args.nocapture;
|
|
||||||
let command = args.command.unwrap_or_else(|| {
|
let command = args.command.unwrap_or_else(|| {
|
||||||
println!("{DEFAULT_OUT}\n");
|
println!("{DEFAULT_OUT}\n");
|
||||||
exit(0);
|
exit(0);
|
||||||
|
@ -203,7 +194,7 @@ If you are just starting with Rustlings, run the command `rustlings init` to ini
|
||||||
|
|
||||||
Subcommands::Run { name } => {
|
Subcommands::Run { name } => {
|
||||||
let exercise = find_exercise(&name, &exercises)?;
|
let exercise = find_exercise(&name, &exercises)?;
|
||||||
run(exercise, verbose).unwrap_or_else(|_| exit(1));
|
run(exercise).unwrap_or_else(|_| exit(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
Subcommands::Reset { name } => {
|
Subcommands::Reset { name } => {
|
||||||
|
@ -219,12 +210,12 @@ If you are just starting with Rustlings, run the command `rustlings init` to ini
|
||||||
println!("{}", exercise.hint);
|
println!("{}", exercise.hint);
|
||||||
}
|
}
|
||||||
|
|
||||||
Subcommands::Verify => match verify(&exercises, (0, exercises.len()), verbose, false)? {
|
Subcommands::Verify => match verify(&exercises, (0, exercises.len()))? {
|
||||||
VerifyState::AllExercisesDone => println!("All exercises done!"),
|
VerifyState::AllExercisesDone => println!("All exercises done!"),
|
||||||
VerifyState::Failed(exercise) => bail!("Exercise {exercise} failed"),
|
VerifyState::Failed(exercise) => bail!("Exercise {exercise} failed"),
|
||||||
},
|
},
|
||||||
|
|
||||||
Subcommands::Watch { success_hints } => match watch(&exercises, verbose, success_hints) {
|
Subcommands::Watch => match watch(&exercises) {
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
println!("Error: Could not watch your progress. Error message was {e:?}.");
|
println!("Error: Could not watch your progress. Error message was {e:?}.");
|
||||||
println!("Most likely you've run out of disk space or your 'inotify limit' has been reached.");
|
println!("Most likely you've run out of disk space or your 'inotify limit' has been reached.");
|
||||||
|
@ -277,17 +268,6 @@ fn spawn_watch_shell(
|
||||||
println!("Bye!");
|
println!("Bye!");
|
||||||
} else if input == "help" {
|
} else if input == "help" {
|
||||||
println!("{WATCH_MODE_HELP_MESSAGE}");
|
println!("{WATCH_MODE_HELP_MESSAGE}");
|
||||||
} else if let Some(cmd) = input.strip_prefix('!') {
|
|
||||||
let mut parts = Shlex::new(cmd);
|
|
||||||
|
|
||||||
let Some(program) = parts.next() else {
|
|
||||||
println!("no command provided");
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Err(e) = Command::new(program).args(parts).status() {
|
|
||||||
println!("failed to execute command `{cmd}`: {e}");
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
println!("unknown command: {input}\n{WATCH_MODE_HELP_MESSAGE}");
|
println!("unknown command: {input}\n{WATCH_MODE_HELP_MESSAGE}");
|
||||||
}
|
}
|
||||||
|
@ -319,7 +299,7 @@ enum WatchStatus {
|
||||||
Unfinished,
|
Unfinished,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn watch(exercises: &[Exercise], verbose: bool, success_hints: bool) -> Result<WatchStatus> {
|
fn watch(exercises: &[Exercise]) -> Result<WatchStatus> {
|
||||||
/* Clears the terminal with an ANSI escape code.
|
/* Clears the terminal with an ANSI escape code.
|
||||||
Works in UNIX and newer Windows terminals. */
|
Works in UNIX and newer Windows terminals. */
|
||||||
fn clear_screen() {
|
fn clear_screen() {
|
||||||
|
@ -336,11 +316,10 @@ fn watch(exercises: &[Exercise], verbose: bool, success_hints: bool) -> Result<W
|
||||||
|
|
||||||
clear_screen();
|
clear_screen();
|
||||||
|
|
||||||
let failed_exercise_hint =
|
let failed_exercise_hint = match verify(exercises, (0, exercises.len()))? {
|
||||||
match verify(exercises, (0, exercises.len()), verbose, success_hints)? {
|
VerifyState::AllExercisesDone => return Ok(WatchStatus::Finished),
|
||||||
VerifyState::AllExercisesDone => return Ok(WatchStatus::Finished),
|
VerifyState::Failed(exercise) => Arc::new(Mutex::new(Some(exercise.hint.clone()))),
|
||||||
VerifyState::Failed(exercise) => Arc::new(Mutex::new(Some(exercise.hint.clone()))),
|
};
|
||||||
};
|
|
||||||
|
|
||||||
spawn_watch_shell(Arc::clone(&failed_exercise_hint), Arc::clone(&should_quit));
|
spawn_watch_shell(Arc::clone(&failed_exercise_hint), Arc::clone(&should_quit));
|
||||||
|
|
||||||
|
@ -364,8 +343,6 @@ fn watch(exercises: &[Exercise], verbose: bool, success_hints: bool) -> Result<W
|
||||||
match verify(
|
match verify(
|
||||||
pending_exercises.iter().copied(),
|
pending_exercises.iter().copied(),
|
||||||
(num_done, exercises.len()),
|
(num_done, exercises.len()),
|
||||||
verbose,
|
|
||||||
success_hints,
|
|
||||||
)? {
|
)? {
|
||||||
VerifyState::AllExercisesDone => return Ok(WatchStatus::Finished),
|
VerifyState::AllExercisesDone => return Ok(WatchStatus::Finished),
|
||||||
VerifyState::Failed(exercise) => {
|
VerifyState::Failed(exercise) => {
|
||||||
|
@ -429,7 +406,6 @@ const WATCH_MODE_HELP_MESSAGE: &str = "Commands available to you in watch mode:
|
||||||
hint - prints the current exercise's hint
|
hint - prints the current exercise's hint
|
||||||
clear - clears the screen
|
clear - clears the screen
|
||||||
quit - quits watch mode
|
quit - quits watch mode
|
||||||
!<cmd> - executes a command, like `!rustc --explain E0381`
|
|
||||||
help - displays this help message
|
help - displays this help message
|
||||||
|
|
||||||
Watch mode automatically re-evaluates the current exercise
|
Watch mode automatically re-evaluates the current exercise
|
||||||
|
|
40
src/run.rs
40
src/run.rs
|
@ -1,39 +1,27 @@
|
||||||
use anyhow::{bail, Result};
|
use anyhow::Result;
|
||||||
use std::io::{stdout, Write};
|
use std::io::{stdout, Write};
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use crate::exercise::{Exercise, Mode};
|
use crate::exercise::Exercise;
|
||||||
use crate::verify::test;
|
|
||||||
use indicatif::ProgressBar;
|
|
||||||
|
|
||||||
// Invoke the rust compiler on the path of the given exercise,
|
// Invoke the rust compiler on the path of the given exercise,
|
||||||
// and run the ensuing binary.
|
// and run the ensuing binary.
|
||||||
// The verbose argument helps determine whether or not to show
|
// The verbose argument helps determine whether or not to show
|
||||||
// the output from the test harnesses (if the mode of the exercise is test)
|
// the output from the test harnesses (if the mode of the exercise is test)
|
||||||
pub fn run(exercise: &Exercise, verbose: bool) -> Result<()> {
|
pub fn run(exercise: &Exercise) -> Result<()> {
|
||||||
match exercise.mode {
|
|
||||||
Mode::Test => test(exercise, verbose),
|
|
||||||
Mode::Compile | Mode::Clippy => compile_and_run(exercise),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compile and run an exercise.
|
|
||||||
// This is strictly for non-test binaries, so output is displayed
|
|
||||||
fn compile_and_run(exercise: &Exercise) -> Result<()> {
|
|
||||||
let progress_bar = ProgressBar::new_spinner();
|
|
||||||
progress_bar.set_message(format!("Running {exercise}..."));
|
|
||||||
progress_bar.enable_steady_tick(Duration::from_millis(100));
|
|
||||||
|
|
||||||
let output = exercise.run()?;
|
let output = exercise.run()?;
|
||||||
progress_bar.finish_and_clear();
|
|
||||||
|
|
||||||
stdout().write_all(&output.stdout)?;
|
{
|
||||||
if !output.status.success() {
|
let mut stdout = stdout().lock();
|
||||||
stdout().write_all(&output.stderr)?;
|
stdout.write_all(&output.stdout)?;
|
||||||
warn!("Ran {} with errors", exercise);
|
stdout.write_all(&output.stderr)?;
|
||||||
bail!("TODO");
|
stdout.flush()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if output.status.success() {
|
||||||
|
success!("Successfully ran {}", exercise);
|
||||||
|
} else {
|
||||||
|
warn!("Ran {} with errors", exercise);
|
||||||
}
|
}
|
||||||
|
|
||||||
success!("Successfully ran {}", exercise);
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
249
src/verify.rs
249
src/verify.rs
|
@ -1,12 +1,6 @@
|
||||||
use anyhow::{bail, Result};
|
use anyhow::Result;
|
||||||
use console::style;
|
use console::style;
|
||||||
use indicatif::{ProgressBar, ProgressStyle};
|
use std::io::{stdout, Write};
|
||||||
use std::{
|
|
||||||
env,
|
|
||||||
io::{stdout, Write},
|
|
||||||
process::Output,
|
|
||||||
time::Duration,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::exercise::{Exercise, Mode, State};
|
use crate::exercise::{Exercise, Mode, State};
|
||||||
|
|
||||||
|
@ -23,201 +17,64 @@ pub enum VerifyState<'a> {
|
||||||
pub fn verify<'a>(
|
pub fn verify<'a>(
|
||||||
pending_exercises: impl IntoIterator<Item = &'a Exercise>,
|
pending_exercises: impl IntoIterator<Item = &'a Exercise>,
|
||||||
progress: (usize, usize),
|
progress: (usize, usize),
|
||||||
verbose: bool,
|
|
||||||
success_hints: bool,
|
|
||||||
) -> Result<VerifyState<'a>> {
|
) -> Result<VerifyState<'a>> {
|
||||||
let (num_done, total) = progress;
|
let (mut num_done, total) = progress;
|
||||||
let bar = ProgressBar::new(total as u64);
|
println!(
|
||||||
let mut percentage = num_done as f32 / total as f32 * 100.0;
|
"Progress: {num_done}/{total} ({:.1}%)\n",
|
||||||
bar.set_style(
|
num_done as f32 / total as f32 * 100.0,
|
||||||
ProgressStyle::default_bar()
|
|
||||||
.template("Progress: [{bar:60.green/red}] {pos}/{len} {msg}")
|
|
||||||
.expect("Progressbar template should be valid!")
|
|
||||||
.progress_chars("#>-"),
|
|
||||||
);
|
);
|
||||||
bar.set_position(num_done as u64);
|
|
||||||
bar.set_message(format!("({percentage:.1} %)"));
|
|
||||||
|
|
||||||
for exercise in pending_exercises {
|
for exercise in pending_exercises {
|
||||||
let compile_result = match exercise.mode {
|
let output = exercise.run()?;
|
||||||
Mode::Test => compile_and_test(exercise, RunMode::Interactive, verbose, success_hints)?,
|
|
||||||
Mode::Compile => compile_and_run_interactively(exercise, success_hints)?,
|
{
|
||||||
Mode::Clippy => compile_only(exercise, success_hints)?,
|
let mut stdout = stdout().lock();
|
||||||
};
|
stdout.write_all(&output.stdout)?;
|
||||||
if !compile_result {
|
stdout.write_all(&output.stderr)?;
|
||||||
|
stdout.flush()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
return Ok(VerifyState::Failed(exercise));
|
return Ok(VerifyState::Failed(exercise));
|
||||||
}
|
}
|
||||||
percentage += 100.0 / total as f32;
|
|
||||||
bar.inc(1);
|
|
||||||
bar.set_message(format!("({percentage:.1} %)"));
|
|
||||||
}
|
|
||||||
|
|
||||||
bar.finish();
|
println!();
|
||||||
println!("You completed all exercises!");
|
match exercise.mode {
|
||||||
|
Mode::Compile => success!("Successfully ran {}!", exercise),
|
||||||
|
Mode::Test => success!("Successfully tested {}!", exercise),
|
||||||
|
Mode::Clippy => success!("Successfully checked {}!", exercise),
|
||||||
|
}
|
||||||
|
|
||||||
|
if let State::Pending(context) = exercise.state()? {
|
||||||
|
println!(
|
||||||
|
"\nYou can keep working on this exercise,
|
||||||
|
or jump into the next one by removing the {} comment:\n",
|
||||||
|
style("`I AM NOT DONE`").bold()
|
||||||
|
);
|
||||||
|
|
||||||
|
for context_line in context {
|
||||||
|
let formatted_line = if context_line.important {
|
||||||
|
format!("{}", style(context_line.line).bold())
|
||||||
|
} else {
|
||||||
|
context_line.line
|
||||||
|
};
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"{:>2} {} {}",
|
||||||
|
style(context_line.number).blue().bold(),
|
||||||
|
style("|").blue(),
|
||||||
|
formatted_line,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Ok(VerifyState::Failed(exercise));
|
||||||
|
}
|
||||||
|
|
||||||
|
num_done += 1;
|
||||||
|
println!(
|
||||||
|
"Progress: {num_done}/{total} ({:.1}%)\n",
|
||||||
|
num_done as f32 / total as f32 * 100.0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Ok(VerifyState::AllExercisesDone)
|
Ok(VerifyState::AllExercisesDone)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(PartialEq, Eq)]
|
|
||||||
enum RunMode {
|
|
||||||
Interactive,
|
|
||||||
NonInteractive,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compile and run the resulting test harness of the given Exercise
|
|
||||||
pub fn test(exercise: &Exercise, verbose: bool) -> Result<()> {
|
|
||||||
compile_and_test(exercise, RunMode::NonInteractive, verbose, false)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Invoke the rust compiler without running the resulting binary
|
|
||||||
fn compile_only(exercise: &Exercise, success_hints: bool) -> Result<bool> {
|
|
||||||
let progress_bar = ProgressBar::new_spinner();
|
|
||||||
progress_bar.set_message(format!("Compiling {exercise}..."));
|
|
||||||
progress_bar.enable_steady_tick(Duration::from_millis(100));
|
|
||||||
|
|
||||||
let _ = exercise.run()?;
|
|
||||||
progress_bar.finish_and_clear();
|
|
||||||
|
|
||||||
prompt_for_completion(exercise, None, success_hints)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compile the given Exercise and run the resulting binary in an interactive mode
|
|
||||||
fn compile_and_run_interactively(exercise: &Exercise, success_hints: bool) -> Result<bool> {
|
|
||||||
let progress_bar = ProgressBar::new_spinner();
|
|
||||||
progress_bar.set_message(format!("Running {exercise}..."));
|
|
||||||
progress_bar.enable_steady_tick(Duration::from_millis(100));
|
|
||||||
|
|
||||||
let output = exercise.run()?;
|
|
||||||
progress_bar.finish_and_clear();
|
|
||||||
|
|
||||||
if !output.status.success() {
|
|
||||||
warn!("Ran {} with errors", exercise);
|
|
||||||
{
|
|
||||||
let mut stdout = stdout().lock();
|
|
||||||
stdout.write_all(&output.stdout)?;
|
|
||||||
stdout.write_all(&output.stderr)?;
|
|
||||||
stdout.flush()?;
|
|
||||||
}
|
|
||||||
bail!("TODO");
|
|
||||||
}
|
|
||||||
|
|
||||||
prompt_for_completion(exercise, Some(output), success_hints)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compile the given Exercise as a test harness and display
|
|
||||||
// the output if verbose is set to true
|
|
||||||
fn compile_and_test(
|
|
||||||
exercise: &Exercise,
|
|
||||||
run_mode: RunMode,
|
|
||||||
verbose: bool,
|
|
||||||
success_hints: bool,
|
|
||||||
) -> Result<bool> {
|
|
||||||
let progress_bar = ProgressBar::new_spinner();
|
|
||||||
progress_bar.set_message(format!("Testing {exercise}..."));
|
|
||||||
progress_bar.enable_steady_tick(Duration::from_millis(100));
|
|
||||||
|
|
||||||
let output = exercise.run()?;
|
|
||||||
progress_bar.finish_and_clear();
|
|
||||||
|
|
||||||
if !output.status.success() {
|
|
||||||
warn!(
|
|
||||||
"Testing of {} failed! Please try again. Here's the output:",
|
|
||||||
exercise
|
|
||||||
);
|
|
||||||
{
|
|
||||||
let mut stdout = stdout().lock();
|
|
||||||
stdout.write_all(&output.stdout)?;
|
|
||||||
stdout.write_all(&output.stderr)?;
|
|
||||||
stdout.flush()?;
|
|
||||||
}
|
|
||||||
bail!("TODO");
|
|
||||||
}
|
|
||||||
|
|
||||||
if verbose {
|
|
||||||
stdout().write_all(&output.stdout)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if run_mode == RunMode::Interactive {
|
|
||||||
prompt_for_completion(exercise, None, success_hints)
|
|
||||||
} else {
|
|
||||||
Ok(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn prompt_for_completion(
|
|
||||||
exercise: &Exercise,
|
|
||||||
prompt_output: Option<Output>,
|
|
||||||
success_hints: bool,
|
|
||||||
) -> Result<bool> {
|
|
||||||
let context = match exercise.state()? {
|
|
||||||
State::Done => return Ok(true),
|
|
||||||
State::Pending(context) => context,
|
|
||||||
};
|
|
||||||
match exercise.mode {
|
|
||||||
Mode::Compile => success!("Successfully ran {}!", exercise),
|
|
||||||
Mode::Test => success!("Successfully tested {}!", exercise),
|
|
||||||
Mode::Clippy => success!("Successfully compiled {}!", exercise),
|
|
||||||
}
|
|
||||||
|
|
||||||
let no_emoji = env::var("NO_EMOJI").is_ok();
|
|
||||||
|
|
||||||
let clippy_success_msg = if no_emoji {
|
|
||||||
"The code is compiling, and Clippy is happy!"
|
|
||||||
} else {
|
|
||||||
"The code is compiling, and 📎 Clippy 📎 is happy!"
|
|
||||||
};
|
|
||||||
|
|
||||||
let success_msg = match exercise.mode {
|
|
||||||
Mode::Compile => "The code is compiling!",
|
|
||||||
Mode::Test => "The code is compiling, and the tests pass!",
|
|
||||||
Mode::Clippy => clippy_success_msg,
|
|
||||||
};
|
|
||||||
|
|
||||||
if no_emoji {
|
|
||||||
println!("\n~*~ {success_msg} ~*~\n");
|
|
||||||
} else {
|
|
||||||
println!("\n🎉 🎉 {success_msg} 🎉 🎉\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(output) = prompt_output {
|
|
||||||
let separator = separator();
|
|
||||||
println!("Output:\n{separator}");
|
|
||||||
stdout().write_all(&output.stdout).unwrap();
|
|
||||||
println!("\n{separator}\n");
|
|
||||||
}
|
|
||||||
if success_hints {
|
|
||||||
println!(
|
|
||||||
"Hints:\n{separator}\n{}\n{separator}\n",
|
|
||||||
exercise.hint,
|
|
||||||
separator = separator(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("You can keep working on this exercise,");
|
|
||||||
println!(
|
|
||||||
"or jump into the next one by removing the {} comment:",
|
|
||||||
style("`I AM NOT DONE`").bold()
|
|
||||||
);
|
|
||||||
println!();
|
|
||||||
for context_line in context {
|
|
||||||
let formatted_line = if context_line.important {
|
|
||||||
format!("{}", style(context_line.line).bold())
|
|
||||||
} else {
|
|
||||||
context_line.line
|
|
||||||
};
|
|
||||||
|
|
||||||
println!(
|
|
||||||
"{:>2} {} {}",
|
|
||||||
style(context_line.number).blue().bold(),
|
|
||||||
style("|").blue(),
|
|
||||||
formatted_line,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn separator() -> console::StyledObject<&'static str> {
|
|
||||||
style("====================").bold()
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in a new issue