rustlings/src/term.rs

275 lines
7.6 KiB
Rust
Raw Normal View History

2024-08-24 01:14:12 +03:00
use crossterm::{
cursor::MoveTo,
2024-10-14 00:28:17 +03:00
style::{Attribute, Color, ResetColor, SetAttribute, SetForegroundColor},
2024-08-24 01:14:12 +03:00
terminal::{Clear, ClearType},
2024-08-26 00:53:50 +03:00
Command, QueueableCommand,
2024-08-24 01:14:12 +03:00
};
2024-09-04 03:19:45 +03:00
use std::{
fmt, fs,
io::{self, BufRead, StdoutLock, Write},
};
2024-08-24 01:14:12 +03:00
2024-10-14 00:28:17 +03:00
use crate::app_state::ExerciseCheckProgress;
pub struct MaxLenWriter<'a, 'b> {
pub stdout: &'a mut StdoutLock<'b>,
len: usize,
max_len: usize,
}
impl<'a, 'b> MaxLenWriter<'a, 'b> {
#[inline]
pub fn new(stdout: &'a mut StdoutLock<'b>, max_len: usize) -> Self {
Self {
stdout,
len: 0,
max_len,
}
}
// Additional is for emojis that take more space.
#[inline]
pub fn add_to_len(&mut self, additional: usize) {
self.len += additional;
}
}
pub trait CountedWrite<'a> {
fn write_ascii(&mut self, ascii: &[u8]) -> io::Result<()>;
fn write_str(&mut self, unicode: &str) -> io::Result<()>;
fn stdout(&mut self) -> &mut StdoutLock<'a>;
}
impl<'a, 'b> CountedWrite<'b> for MaxLenWriter<'a, 'b> {
fn write_ascii(&mut self, ascii: &[u8]) -> io::Result<()> {
let n = ascii.len().min(self.max_len.saturating_sub(self.len));
2024-08-26 05:41:26 +03:00
if n > 0 {
self.stdout.write_all(&ascii[..n])?;
self.len += n;
}
Ok(())
}
fn write_str(&mut self, unicode: &str) -> io::Result<()> {
if let Some((ind, c)) = unicode
.char_indices()
.take(self.max_len.saturating_sub(self.len))
.last()
{
self.stdout
.write_all(&unicode.as_bytes()[..ind + c.len_utf8()])?;
self.len += ind + 1;
}
Ok(())
}
#[inline]
fn stdout(&mut self) -> &mut StdoutLock<'b> {
self.stdout
}
}
impl<'a> CountedWrite<'a> for StdoutLock<'a> {
#[inline]
fn write_ascii(&mut self, ascii: &[u8]) -> io::Result<()> {
self.write_all(ascii)
}
#[inline]
fn write_str(&mut self, unicode: &str) -> io::Result<()> {
self.write_all(unicode.as_bytes())
}
#[inline]
fn stdout(&mut self) -> &mut StdoutLock<'a> {
self
}
}
2024-10-14 02:06:11 +03:00
pub struct ExercisesCheckProgressVisualizer<'a, 'b> {
stdout: &'a mut StdoutLock<'b>,
n_cols: usize,
}
impl<'a, 'b> ExercisesCheckProgressVisualizer<'a, 'b> {
pub fn build(stdout: &'a mut StdoutLock<'b>, term_width: u16) -> io::Result<Self> {
clear_terminal(stdout)?;
stdout.write_all("Checking all exercises…\n".as_bytes())?;
// Legend
stdout.write_all(b"Color of exercise number: ")?;
stdout.queue(SetForegroundColor(Color::Blue))?;
stdout.write_all(b"Checking")?;
stdout.queue(ResetColor)?;
stdout.write_all(b" - ")?;
stdout.queue(SetForegroundColor(Color::Green))?;
stdout.write_all(b"Done")?;
stdout.queue(ResetColor)?;
stdout.write_all(b" - ")?;
stdout.queue(SetForegroundColor(Color::Red))?;
stdout.write_all(b"Pending")?;
stdout.queue(ResetColor)?;
stdout.write_all(b"\n")?;
// Exercise numbers with up to 3 digits.
// +1 because the last column doesn't end with a whitespace.
let n_cols = usize::from(term_width + 1) / 4;
Ok(Self { stdout, n_cols })
}
pub fn update(&mut self, progresses: &[ExerciseCheckProgress]) -> io::Result<()> {
self.stdout.queue(MoveTo(0, 2))?;
let mut exercise_num = 1;
for exercise_progress in progresses {
match exercise_progress {
ExerciseCheckProgress::None => (),
ExerciseCheckProgress::Checking => {
self.stdout.queue(SetForegroundColor(Color::Blue))?;
}
ExerciseCheckProgress::Done => {
self.stdout.queue(SetForegroundColor(Color::Green))?;
}
ExerciseCheckProgress::Pending => {
self.stdout.queue(SetForegroundColor(Color::Red))?;
}
}
write!(self.stdout, "{exercise_num:<3}")?;
self.stdout.queue(ResetColor)?;
if exercise_num != progresses.len() {
if exercise_num % self.n_cols == 0 {
self.stdout.write_all(b"\n")?;
} else {
self.stdout.write_all(b" ")?;
}
exercise_num += 1;
}
}
self.stdout.flush()
}
}
pub fn progress_bar<'a>(
writer: &mut impl CountedWrite<'a>,
2024-08-24 18:17:56 +03:00
progress: u16,
total: u16,
2024-10-10 20:43:35 +03:00
term_width: u16,
2024-08-24 18:17:56 +03:00
) -> io::Result<()> {
debug_assert!(total < 1000);
2024-10-14 00:28:17 +03:00
debug_assert!(progress <= total);
2024-08-24 18:17:56 +03:00
const PREFIX: &[u8] = b"Progress: [";
const PREFIX_WIDTH: u16 = PREFIX.len() as u16;
const POSTFIX_WIDTH: u16 = "] xxx/xxx".len() as u16;
2024-08-24 18:17:56 +03:00
const WRAPPER_WIDTH: u16 = PREFIX_WIDTH + POSTFIX_WIDTH;
2024-10-14 00:28:17 +03:00
const MIN_LINE_WIDTH: u16 = WRAPPER_WIDTH + 4;
2024-08-24 18:17:56 +03:00
2024-10-14 00:28:17 +03:00
if term_width < MIN_LINE_WIDTH {
writer.write_ascii(b"Progress: ")?;
// Integers are in ASCII.
2024-10-14 00:28:17 +03:00
return writer.write_ascii(format!("{progress}/{total}").as_bytes());
2024-08-24 18:17:56 +03:00
}
let stdout = writer.stdout();
2024-08-24 18:17:56 +03:00
stdout.write_all(PREFIX)?;
2024-10-10 20:43:35 +03:00
let width = term_width - WRAPPER_WIDTH;
2024-10-14 00:28:17 +03:00
let filled = (width * progress) / total;
2024-10-14 00:28:17 +03:00
stdout.queue(SetForegroundColor(Color::Green))?;
for _ in 0..filled {
2024-08-24 18:17:56 +03:00
stdout.write_all(b"#")?;
}
2024-10-14 00:28:17 +03:00
if filled < width {
2024-08-24 18:17:56 +03:00
stdout.write_all(b">")?;
}
2024-10-14 00:28:17 +03:00
let width_minus_filled = width - filled;
2024-08-24 18:17:56 +03:00
if width_minus_filled > 1 {
let red_part_width = width_minus_filled - 1;
stdout.queue(SetForegroundColor(Color::Red))?;
for _ in 0..red_part_width {
stdout.write_all(b"-")?;
}
}
stdout.queue(SetForegroundColor(Color::Reset))?;
2024-10-14 00:28:17 +03:00
write!(stdout, "] {progress:>3}/{total}")
}
2024-08-08 03:45:18 +03:00
pub fn clear_terminal(stdout: &mut StdoutLock) -> io::Result<()> {
2024-08-24 01:14:12 +03:00
stdout
.queue(MoveTo(0, 0))?
.queue(Clear(ClearType::All))?
.queue(Clear(ClearType::Purge))
.map(|_| ())
2024-08-08 03:45:18 +03:00
}
pub fn press_enter_prompt(stdout: &mut StdoutLock) -> io::Result<()> {
stdout.flush()?;
io::stdin().lock().read_until(b'\n', &mut Vec::new())?;
2024-08-26 23:03:09 +03:00
stdout.write_all(b"\n")
2024-08-08 03:45:18 +03:00
}
2024-08-26 00:53:50 +03:00
2024-09-04 03:19:45 +03:00
/// Canonicalize, convert to string and remove verbatim part on Windows.
pub fn canonicalize(path: &str) -> Option<String> {
fs::canonicalize(path)
.ok()?
.into_os_string()
.into_string()
.ok()
.map(|mut path| {
// Windows itself can't handle its verbatim paths.
if cfg!(windows) && path.as_bytes().starts_with(br"\\?\") {
path.drain(..4);
}
path
})
}
pub fn terminal_file_link<'a>(
writer: &mut impl CountedWrite<'a>,
path: &str,
2024-09-04 03:19:45 +03:00
canonical_path: &str,
color: Color,
) -> io::Result<()> {
writer
.stdout()
2024-08-26 00:53:50 +03:00
.queue(SetForegroundColor(color))?
.queue(SetAttribute(Attribute::Underlined))?;
writer.stdout().write_all(b"\x1b]8;;file://")?;
writer.stdout().write_all(canonical_path.as_bytes())?;
writer.stdout().write_all(b"\x1b\\")?;
// Only this part is visible.
writer.write_str(path)?;
writer.stdout().write_all(b"\x1b]8;;\x1b\\")?;
writer
.stdout()
.queue(SetForegroundColor(Color::Reset))?
.queue(SetAttribute(Attribute::NoUnderline))?;
2024-08-26 00:53:50 +03:00
Ok(())
}
pub fn write_ansi(output: &mut Vec<u8>, command: impl Command) {
struct FmtWriter<'a>(&'a mut Vec<u8>);
impl fmt::Write for FmtWriter<'_> {
fn write_str(&mut self, s: &str) -> fmt::Result {
self.0.extend_from_slice(s.as_bytes());
Ok(())
}
}
let _ = command.write_ansi(&mut FmtWriter(output));
}