mirror of
https://github.com/notohh/rustlings.git
synced 2024-12-17 22:58:08 -05:00
Almost done with list display
This commit is contained in:
parent
4e12725616
commit
b779c43126
6 changed files with 260 additions and 213 deletions
35
src/list.rs
35
src/list.rs
|
@ -38,15 +38,15 @@ fn handle_list(app_state: &mut AppState, stdout: &mut StdoutLock) -> Result<()>
|
||||||
KeyCode::Home | KeyCode::Char('g') => list_state.select_first(),
|
KeyCode::Home | KeyCode::Char('g') => list_state.select_first(),
|
||||||
KeyCode::End | KeyCode::Char('G') => list_state.select_last(),
|
KeyCode::End | KeyCode::Char('G') => list_state.select_last(),
|
||||||
KeyCode::Char('d') => {
|
KeyCode::Char('d') => {
|
||||||
let message = if list_state.filter() == Filter::Done {
|
if list_state.filter() == Filter::Done {
|
||||||
list_state.set_filter(Filter::None);
|
list_state.set_filter(Filter::None);
|
||||||
"Disabled filter DONE"
|
list_state.message.push_str("Disabled filter DONE");
|
||||||
} else {
|
} else {
|
||||||
list_state.set_filter(Filter::Done);
|
list_state.set_filter(Filter::Done);
|
||||||
"Enabled filter DONE │ Press d again to disable the filter"
|
list_state.message.push_str(
|
||||||
};
|
"Enabled filter DONE │ Press d again to disable the filter",
|
||||||
|
);
|
||||||
list_state.message.push_str(message);
|
}
|
||||||
}
|
}
|
||||||
KeyCode::Char('p') => {
|
KeyCode::Char('p') => {
|
||||||
let message = if list_state.filter() == Filter::Pending {
|
let message = if list_state.filter() == Filter::Pending {
|
||||||
|
@ -71,23 +71,20 @@ fn handle_list(app_state: &mut AppState, stdout: &mut StdoutLock) -> Result<()>
|
||||||
KeyCode::Esc => (),
|
KeyCode::Esc => (),
|
||||||
_ => continue,
|
_ => continue,
|
||||||
}
|
}
|
||||||
|
|
||||||
list_state.redraw(stdout)?;
|
|
||||||
}
|
}
|
||||||
Event::Mouse(event) => {
|
Event::Mouse(event) => match event.kind {
|
||||||
match event.kind {
|
MouseEventKind::ScrollDown => list_state.select_next(),
|
||||||
MouseEventKind::ScrollDown => list_state.select_next(),
|
MouseEventKind::ScrollUp => list_state.select_previous(),
|
||||||
MouseEventKind::ScrollUp => list_state.select_previous(),
|
_ => continue,
|
||||||
_ => continue,
|
},
|
||||||
}
|
Event::Resize(width, height) => {
|
||||||
|
list_state.set_term_size(width, height);
|
||||||
list_state.redraw(stdout)?;
|
|
||||||
}
|
}
|
||||||
// Redraw
|
|
||||||
Event::Resize(_, _) => list_state.redraw(stdout)?,
|
|
||||||
// Ignore
|
// Ignore
|
||||||
Event::FocusGained | Event::FocusLost => (),
|
Event::FocusGained | Event::FocusLost => continue,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
list_state.redraw(stdout)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,19 +1,30 @@
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use crossterm::{
|
use crossterm::{
|
||||||
cursor::{MoveDown, MoveTo},
|
cursor::{MoveTo, MoveToNextLine},
|
||||||
style::{Color, ResetColor, SetForegroundColor},
|
style::{Attribute, Color, ResetColor, SetAttribute, SetForegroundColor},
|
||||||
terminal::{self, BeginSynchronizedUpdate, EndSynchronizedUpdate},
|
terminal::{self, BeginSynchronizedUpdate, Clear, ClearType, EndSynchronizedUpdate},
|
||||||
QueueableCommand,
|
QueueableCommand,
|
||||||
};
|
};
|
||||||
use std::{
|
use std::{
|
||||||
fmt::Write as _,
|
fmt::Write as _,
|
||||||
io::{self, StdoutLock, Write as _},
|
io::{self, StdoutLock, Write},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{app_state::AppState, term::clear_terminal, MAX_EXERCISE_NAME_LEN};
|
use crate::{app_state::AppState, term::progress_bar, MAX_EXERCISE_NAME_LEN};
|
||||||
|
|
||||||
// +1 for padding.
|
fn next_ln<const CLEAR_LAST_CHAR: bool>(stdout: &mut StdoutLock) -> io::Result<()> {
|
||||||
const SPACE: &[u8] = &[b' '; MAX_EXERCISE_NAME_LEN + 1];
|
if CLEAR_LAST_CHAR {
|
||||||
|
// Avoids having the last written char as the last displayed one when
|
||||||
|
// the written width is higher than the terminal width.
|
||||||
|
// Happens on the Gnome terminal for example.
|
||||||
|
stdout.write_all(b" ")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
stdout
|
||||||
|
.queue(Clear(ClearType::UntilNewLine))?
|
||||||
|
.queue(MoveToNextLine(1))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||||
pub enum Filter {
|
pub enum Filter {
|
||||||
|
@ -30,10 +41,16 @@ pub struct ListState<'a> {
|
||||||
name_col_width: usize,
|
name_col_width: usize,
|
||||||
offset: usize,
|
offset: usize,
|
||||||
selected: Option<usize>,
|
selected: Option<usize>,
|
||||||
|
term_width: u16,
|
||||||
|
term_height: u16,
|
||||||
|
separator: Vec<u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> ListState<'a> {
|
impl<'a> ListState<'a> {
|
||||||
pub fn new(app_state: &'a mut AppState, stdout: &mut StdoutLock) -> io::Result<Self> {
|
pub fn new(app_state: &'a mut AppState, stdout: &mut StdoutLock) -> io::Result<Self> {
|
||||||
|
let (term_width, term_height) = terminal::size()?;
|
||||||
|
stdout.queue(Clear(ClearType::All))?;
|
||||||
|
|
||||||
let name_col_width = app_state
|
let name_col_width = app_state
|
||||||
.exercises()
|
.exercises()
|
||||||
.iter()
|
.iter()
|
||||||
|
@ -41,13 +58,8 @@ impl<'a> ListState<'a> {
|
||||||
.max()
|
.max()
|
||||||
.map_or(4, |max| max.max(4));
|
.map_or(4, |max| max.max(4));
|
||||||
|
|
||||||
clear_terminal(stdout)?;
|
|
||||||
stdout.write_all(b" Current State Name ")?;
|
|
||||||
stdout.write_all(&SPACE[..name_col_width - 4])?;
|
|
||||||
stdout.write_all(b"Path\r\n")?;
|
|
||||||
|
|
||||||
let selected = app_state.current_exercise_ind();
|
|
||||||
let n_rows_with_filter = app_state.exercises().len();
|
let n_rows_with_filter = app_state.exercises().len();
|
||||||
|
let selected = app_state.current_exercise_ind();
|
||||||
|
|
||||||
let mut slf = Self {
|
let mut slf = Self {
|
||||||
message: String::with_capacity(128),
|
message: String::with_capacity(128),
|
||||||
|
@ -57,6 +69,9 @@ impl<'a> ListState<'a> {
|
||||||
name_col_width,
|
name_col_width,
|
||||||
offset: selected.saturating_sub(10),
|
offset: selected.saturating_sub(10),
|
||||||
selected: Some(selected),
|
selected: Some(selected),
|
||||||
|
term_width,
|
||||||
|
term_height,
|
||||||
|
separator: "─".as_bytes().repeat(term_width as usize),
|
||||||
};
|
};
|
||||||
|
|
||||||
slf.redraw(stdout)?;
|
slf.redraw(stdout)?;
|
||||||
|
@ -64,6 +79,145 @@ impl<'a> ListState<'a> {
|
||||||
Ok(slf)
|
Ok(slf)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn redraw(&mut self, stdout: &mut StdoutLock) -> io::Result<()> {
|
||||||
|
if self.term_height == 0 {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
stdout.queue(BeginSynchronizedUpdate)?.queue(MoveTo(0, 0))?;
|
||||||
|
|
||||||
|
// +1 for padding.
|
||||||
|
const SPACE: &[u8] = &[b' '; MAX_EXERCISE_NAME_LEN + 1];
|
||||||
|
stdout.write_all(b" Current State Name")?;
|
||||||
|
stdout.write_all(&SPACE[..self.name_col_width - 2])?;
|
||||||
|
stdout.write_all(b"Path")?;
|
||||||
|
next_ln::<true>(stdout)?;
|
||||||
|
|
||||||
|
let narrow = self.term_width < 96;
|
||||||
|
let show_footer = self.term_height > 6;
|
||||||
|
let max_n_rows_to_display =
|
||||||
|
(self.term_height - 1 - u16::from(show_footer) * (4 + u16::from(narrow))) as usize;
|
||||||
|
|
||||||
|
let displayed_exercises = self
|
||||||
|
.app_state
|
||||||
|
.exercises()
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter(|(_, exercise)| match self.filter {
|
||||||
|
Filter::Done => exercise.done,
|
||||||
|
Filter::Pending => !exercise.done,
|
||||||
|
Filter::None => true,
|
||||||
|
})
|
||||||
|
.skip(self.offset)
|
||||||
|
.take(max_n_rows_to_display);
|
||||||
|
|
||||||
|
let current_exercise_ind = self.app_state.current_exercise_ind();
|
||||||
|
let mut n_displayed_rows = 0;
|
||||||
|
for (exercise_ind, exercise) in displayed_exercises {
|
||||||
|
if self.selected == Some(n_displayed_rows) {
|
||||||
|
stdout.write_all("🦀".as_bytes())?;
|
||||||
|
} else {
|
||||||
|
stdout.write_all(b" ")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if exercise_ind == current_exercise_ind {
|
||||||
|
stdout.queue(SetForegroundColor(Color::Red))?;
|
||||||
|
stdout.write_all(b">>>>>>> ")?;
|
||||||
|
} else {
|
||||||
|
stdout.write_all(b" ")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if exercise.done {
|
||||||
|
stdout.queue(SetForegroundColor(Color::Yellow))?;
|
||||||
|
stdout.write_all(b"DONE ")?;
|
||||||
|
} else {
|
||||||
|
stdout.queue(SetForegroundColor(Color::Green))?;
|
||||||
|
stdout.write_all(b"PENDING ")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
stdout.queue(ResetColor)?;
|
||||||
|
|
||||||
|
stdout.write_all(exercise.name.as_bytes())?;
|
||||||
|
stdout.write_all(&SPACE[..self.name_col_width + 2 - exercise.name.len()])?;
|
||||||
|
|
||||||
|
stdout.write_all(exercise.path.as_bytes())?;
|
||||||
|
|
||||||
|
next_ln::<true>(stdout)?;
|
||||||
|
n_displayed_rows += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
for _ in 0..max_n_rows_to_display - n_displayed_rows {
|
||||||
|
next_ln::<false>(stdout)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if show_footer {
|
||||||
|
stdout.write_all(&self.separator)?;
|
||||||
|
next_ln::<false>(stdout)?;
|
||||||
|
|
||||||
|
progress_bar(
|
||||||
|
stdout,
|
||||||
|
self.app_state.n_done(),
|
||||||
|
self.app_state.exercises().len() as u16,
|
||||||
|
self.term_width,
|
||||||
|
)?;
|
||||||
|
next_ln::<false>(stdout)?;
|
||||||
|
|
||||||
|
stdout.write_all(&self.separator)?;
|
||||||
|
next_ln::<false>(stdout)?;
|
||||||
|
|
||||||
|
if self.message.is_empty() {
|
||||||
|
// Help footer.
|
||||||
|
stdout.write_all(
|
||||||
|
"↓/j ↑/k home/g end/G │ <c>ontinue at │ <r>eset exercise │".as_bytes(),
|
||||||
|
)?;
|
||||||
|
if narrow {
|
||||||
|
next_ln::<true>(stdout)?;
|
||||||
|
stdout.write_all(b"filter ")?;
|
||||||
|
} else {
|
||||||
|
stdout.write_all(b" filter ")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
match self.filter {
|
||||||
|
Filter::Done => {
|
||||||
|
stdout
|
||||||
|
.queue(SetForegroundColor(Color::Magenta))?
|
||||||
|
.queue(SetAttribute(Attribute::Underlined))?;
|
||||||
|
stdout.write_all(b"<d>one")?;
|
||||||
|
stdout.queue(ResetColor)?;
|
||||||
|
stdout.write_all(b"/<p>ending")?;
|
||||||
|
}
|
||||||
|
Filter::Pending => {
|
||||||
|
stdout.write_all(b"<d>one/")?;
|
||||||
|
stdout
|
||||||
|
.queue(SetForegroundColor(Color::Magenta))?
|
||||||
|
.queue(SetAttribute(Attribute::Underlined))?;
|
||||||
|
stdout.write_all(b"<p>ending")?;
|
||||||
|
stdout.queue(ResetColor)?;
|
||||||
|
}
|
||||||
|
Filter::None => stdout.write_all(b"<d>one/<p>ending")?,
|
||||||
|
}
|
||||||
|
stdout.write_all(" │ <q>uit list".as_bytes())?;
|
||||||
|
next_ln::<true>(stdout)?;
|
||||||
|
} else {
|
||||||
|
stdout.queue(SetForegroundColor(Color::Magenta))?;
|
||||||
|
stdout.write_all(self.message.as_bytes())?;
|
||||||
|
stdout.queue(ResetColor)?;
|
||||||
|
next_ln::<true>(stdout)?;
|
||||||
|
if narrow {
|
||||||
|
next_ln::<false>(stdout)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stdout.queue(EndSynchronizedUpdate)?.flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_term_size(&mut self, width: u16, height: u16) {
|
||||||
|
self.term_width = width;
|
||||||
|
self.term_height = height;
|
||||||
|
self.separator = "─".as_bytes().repeat(width as usize);
|
||||||
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn filter(&self) -> Filter {
|
pub fn filter(&self) -> Filter {
|
||||||
self.filter
|
self.filter
|
||||||
|
@ -76,13 +230,13 @@ impl<'a> ListState<'a> {
|
||||||
.app_state
|
.app_state
|
||||||
.exercises()
|
.exercises()
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|exercise| !exercise.done)
|
.filter(|exercise| exercise.done)
|
||||||
.count(),
|
.count(),
|
||||||
Filter::Pending => self
|
Filter::Pending => self
|
||||||
.app_state
|
.app_state
|
||||||
.exercises()
|
.exercises()
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|exercise| exercise.done)
|
.filter(|exercise| !exercise.done)
|
||||||
.count(),
|
.count(),
|
||||||
Filter::None => self.app_state.exercises().len(),
|
Filter::None => self.app_state.exercises().len(),
|
||||||
};
|
};
|
||||||
|
@ -127,124 +281,38 @@ impl<'a> ListState<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn redraw(&mut self, stdout: &mut StdoutLock) -> io::Result<()> {
|
fn selected_to_exercise_ind(&self, selected: usize) -> Result<usize> {
|
||||||
stdout.queue(BeginSynchronizedUpdate)?;
|
match self.filter {
|
||||||
stdout.queue(MoveTo(0, 1))?;
|
Filter::Done => self
|
||||||
let (width, height) = terminal::size()?;
|
.app_state
|
||||||
let narrow = width < 95;
|
.exercises()
|
||||||
let narrow_u16 = u16::from(narrow);
|
.iter()
|
||||||
let max_n_rows_to_display = height.saturating_sub(narrow_u16 + 4);
|
.enumerate()
|
||||||
|
.filter(|(_, exercise)| exercise.done)
|
||||||
let displayed_exercises = self
|
.nth(selected)
|
||||||
.app_state
|
.context("Invalid selection index")
|
||||||
.exercises()
|
.map(|(ind, _)| ind),
|
||||||
.iter()
|
Filter::Pending => self
|
||||||
.enumerate()
|
.app_state
|
||||||
.filter(|(_, exercise)| match self.filter {
|
.exercises()
|
||||||
Filter::Done => exercise.done,
|
.iter()
|
||||||
Filter::Pending => !exercise.done,
|
.enumerate()
|
||||||
Filter::None => true,
|
.filter(|(_, exercise)| !exercise.done)
|
||||||
})
|
.nth(selected)
|
||||||
.skip(self.offset)
|
.context("Invalid selection index")
|
||||||
.take(max_n_rows_to_display as usize);
|
.map(|(ind, _)| ind),
|
||||||
|
Filter::None => Ok(selected),
|
||||||
let mut n_displayed_rows: u16 = 0;
|
|
||||||
let current_exercise_ind = self.app_state.current_exercise_ind();
|
|
||||||
for (ind, exercise) in displayed_exercises {
|
|
||||||
if self.selected == Some(n_displayed_rows as usize) {
|
|
||||||
write!(stdout, "🦀")?;
|
|
||||||
} else {
|
|
||||||
stdout.write_all(b" ")?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ind == current_exercise_ind {
|
|
||||||
stdout.queue(SetForegroundColor(Color::Red))?;
|
|
||||||
stdout.write_all(b">>>>>>> ")?;
|
|
||||||
} else {
|
|
||||||
stdout.write_all(b" ")?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if exercise.done {
|
|
||||||
stdout.queue(SetForegroundColor(Color::Yellow))?;
|
|
||||||
stdout.write_all(b"DONE ")?;
|
|
||||||
} else {
|
|
||||||
stdout.queue(SetForegroundColor(Color::Green))?;
|
|
||||||
stdout.write_all(b"PENDING ")?;
|
|
||||||
}
|
|
||||||
|
|
||||||
stdout.queue(ResetColor)?;
|
|
||||||
|
|
||||||
stdout.write_all(exercise.name.as_bytes())?;
|
|
||||||
stdout.write_all(&SPACE[..self.name_col_width + 2 - exercise.name.len()])?;
|
|
||||||
|
|
||||||
stdout.write_all(exercise.path.as_bytes())?;
|
|
||||||
stdout.write_all(b"\r\n")?;
|
|
||||||
|
|
||||||
n_displayed_rows += 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
stdout.queue(MoveDown(max_n_rows_to_display - n_displayed_rows))?;
|
|
||||||
|
|
||||||
// TODO
|
|
||||||
// let message = if self.message.is_empty() {
|
|
||||||
// // Help footer.
|
|
||||||
// let mut text = Text::default();
|
|
||||||
// let mut spans = Vec::with_capacity(4);
|
|
||||||
// spans.push(Span::raw(
|
|
||||||
// "↓/j ↑/k home/g end/G │ <c>ontinue at │ <r>eset exercise │",
|
|
||||||
// ));
|
|
||||||
|
|
||||||
// if narrow {
|
|
||||||
// text.push_line(mem::take(&mut spans));
|
|
||||||
// spans.push(Span::raw("filter "));
|
|
||||||
// } else {
|
|
||||||
// spans.push(Span::raw(" filter "));
|
|
||||||
// }
|
|
||||||
|
|
||||||
// match self.filter {
|
|
||||||
// Filter::Done => {
|
|
||||||
// spans.push("<d>one".underlined().magenta());
|
|
||||||
// spans.push(Span::raw("/<p>ending"));
|
|
||||||
// }
|
|
||||||
// Filter::Pending => {
|
|
||||||
// spans.push(Span::raw("<d>one/"));
|
|
||||||
// spans.push("<p>ending".underlined().magenta());
|
|
||||||
// }
|
|
||||||
// Filter::None => spans.push(Span::raw("<d>one/<p>ending")),
|
|
||||||
// }
|
|
||||||
|
|
||||||
// spans.push(Span::raw(" │ <q>uit list"));
|
|
||||||
// text.push_line(spans);
|
|
||||||
// text
|
|
||||||
// } else {
|
|
||||||
// Text::from(self.message.as_str().light_blue())
|
|
||||||
// };
|
|
||||||
|
|
||||||
stdout.queue(EndSynchronizedUpdate)?;
|
|
||||||
stdout.flush()?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn reset_selected(&mut self) -> Result<()> {
|
pub fn reset_selected(&mut self) -> Result<()> {
|
||||||
let Some(selected) = self.selected else {
|
let Some(selected) = self.selected else {
|
||||||
|
self.message.push_str("Nothing selected to reset!");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
|
|
||||||
let ind = self
|
let exercise_ind = self.selected_to_exercise_ind(selected)?;
|
||||||
.app_state
|
let exercise_path = self.app_state.reset_exercise_by_ind(exercise_ind)?;
|
||||||
.exercises()
|
|
||||||
.iter()
|
|
||||||
.enumerate()
|
|
||||||
.filter_map(|(ind, exercise)| match self.filter {
|
|
||||||
Filter::Done => exercise.done.then_some(ind),
|
|
||||||
Filter::Pending => (!exercise.done).then_some(ind),
|
|
||||||
Filter::None => Some(ind),
|
|
||||||
})
|
|
||||||
.nth(selected)
|
|
||||||
.context("Invalid selection index")?;
|
|
||||||
|
|
||||||
let exercise_path = self.app_state.reset_exercise_by_ind(ind)?;
|
|
||||||
write!(self.message, "The exercise {exercise_path} has been reset")?;
|
write!(self.message, "The exercise {exercise_path} has been reset")?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -257,20 +325,8 @@ impl<'a> ListState<'a> {
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
let (ind, _) = self
|
let exercise_ind = self.selected_to_exercise_ind(selected)?;
|
||||||
.app_state
|
self.app_state.set_current_exercise_ind(exercise_ind)?;
|
||||||
.exercises()
|
|
||||||
.iter()
|
|
||||||
.enumerate()
|
|
||||||
.filter(|(_, exercise)| match self.filter {
|
|
||||||
Filter::Done => exercise.done,
|
|
||||||
Filter::Pending => !exercise.done,
|
|
||||||
Filter::None => true,
|
|
||||||
})
|
|
||||||
.nth(selected)
|
|
||||||
.context("Invalid selection index")?;
|
|
||||||
|
|
||||||
self.app_state.set_current_exercise_ind(ind)?;
|
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,6 @@ mod exercise;
|
||||||
mod info_file;
|
mod info_file;
|
||||||
mod init;
|
mod init;
|
||||||
mod list;
|
mod list;
|
||||||
mod progress_bar;
|
|
||||||
mod run;
|
mod run;
|
||||||
mod term;
|
mod term;
|
||||||
mod terminal_link;
|
mod terminal_link;
|
||||||
|
|
|
@ -1,53 +0,0 @@
|
||||||
use std::io::{self, StdoutLock, Write};
|
|
||||||
|
|
||||||
use crossterm::{
|
|
||||||
style::{Color, ResetColor, SetForegroundColor},
|
|
||||||
QueueableCommand,
|
|
||||||
};
|
|
||||||
|
|
||||||
const PREFIX: &[u8] = b"Progress: [";
|
|
||||||
const PREFIX_WIDTH: u16 = PREFIX.len() as u16;
|
|
||||||
// Leaving the last char empty (_) for `total` > 99.
|
|
||||||
const POSTFIX_WIDTH: u16 = "] xxx/xx exercises_".len() as u16;
|
|
||||||
const WRAPPER_WIDTH: u16 = PREFIX_WIDTH + POSTFIX_WIDTH;
|
|
||||||
const MIN_LINE_WIDTH: u16 = WRAPPER_WIDTH + 4;
|
|
||||||
|
|
||||||
/// Terminal progress bar to be used when not using Ratataui.
|
|
||||||
pub fn progress_bar(
|
|
||||||
stdout: &mut StdoutLock,
|
|
||||||
progress: u16,
|
|
||||||
total: u16,
|
|
||||||
line_width: u16,
|
|
||||||
) -> io::Result<()> {
|
|
||||||
debug_assert!(progress <= total);
|
|
||||||
|
|
||||||
if line_width < MIN_LINE_WIDTH {
|
|
||||||
return write!(stdout, "Progress: {progress}/{total} exercises");
|
|
||||||
}
|
|
||||||
|
|
||||||
stdout.write_all(PREFIX)?;
|
|
||||||
|
|
||||||
let width = line_width - WRAPPER_WIDTH;
|
|
||||||
let filled = (width * progress) / total;
|
|
||||||
|
|
||||||
stdout.queue(SetForegroundColor(Color::Green))?;
|
|
||||||
for _ in 0..filled {
|
|
||||||
stdout.write_all(b"#")?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if filled < width {
|
|
||||||
stdout.write_all(b">")?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let width_minus_filled = width - filled;
|
|
||||||
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(ResetColor)?;
|
|
||||||
write!(stdout, "] {progress:>3}/{total} exercises")
|
|
||||||
}
|
|
48
src/term.rs
48
src/term.rs
|
@ -2,10 +2,58 @@ use std::io::{self, BufRead, StdoutLock, Write};
|
||||||
|
|
||||||
use crossterm::{
|
use crossterm::{
|
||||||
cursor::MoveTo,
|
cursor::MoveTo,
|
||||||
|
style::{Color, ResetColor, SetForegroundColor},
|
||||||
terminal::{Clear, ClearType},
|
terminal::{Clear, ClearType},
|
||||||
QueueableCommand,
|
QueueableCommand,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Terminal progress bar to be used when not using Ratataui.
|
||||||
|
pub fn progress_bar(
|
||||||
|
stdout: &mut StdoutLock,
|
||||||
|
progress: u16,
|
||||||
|
total: u16,
|
||||||
|
line_width: u16,
|
||||||
|
) -> io::Result<()> {
|
||||||
|
debug_assert!(progress <= total);
|
||||||
|
|
||||||
|
const PREFIX: &[u8] = b"Progress: [";
|
||||||
|
const PREFIX_WIDTH: u16 = PREFIX.len() as u16;
|
||||||
|
// Leaving the last char empty (_) for `total` > 99.
|
||||||
|
const POSTFIX_WIDTH: u16 = "] xxx/xx exercises_".len() as u16;
|
||||||
|
const WRAPPER_WIDTH: u16 = PREFIX_WIDTH + POSTFIX_WIDTH;
|
||||||
|
const MIN_LINE_WIDTH: u16 = WRAPPER_WIDTH + 4;
|
||||||
|
|
||||||
|
if line_width < MIN_LINE_WIDTH {
|
||||||
|
return write!(stdout, "Progress: {progress}/{total} exercises");
|
||||||
|
}
|
||||||
|
|
||||||
|
stdout.write_all(PREFIX)?;
|
||||||
|
|
||||||
|
let width = line_width - WRAPPER_WIDTH;
|
||||||
|
let filled = (width * progress) / total;
|
||||||
|
|
||||||
|
stdout.queue(SetForegroundColor(Color::Green))?;
|
||||||
|
for _ in 0..filled {
|
||||||
|
stdout.write_all(b"#")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if filled < width {
|
||||||
|
stdout.write_all(b">")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let width_minus_filled = width - filled;
|
||||||
|
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(ResetColor)?;
|
||||||
|
write!(stdout, "] {progress:>3}/{total} exercises")
|
||||||
|
}
|
||||||
|
|
||||||
pub fn clear_terminal(stdout: &mut StdoutLock) -> io::Result<()> {
|
pub fn clear_terminal(stdout: &mut StdoutLock) -> io::Result<()> {
|
||||||
stdout
|
stdout
|
||||||
.queue(MoveTo(0, 0))?
|
.queue(MoveTo(0, 0))?
|
||||||
|
|
|
@ -9,7 +9,7 @@ use crate::{
|
||||||
app_state::{AppState, ExercisesProgress},
|
app_state::{AppState, ExercisesProgress},
|
||||||
clear_terminal,
|
clear_terminal,
|
||||||
exercise::{RunnableExercise, OUTPUT_CAPACITY},
|
exercise::{RunnableExercise, OUTPUT_CAPACITY},
|
||||||
progress_bar::progress_bar,
|
term::progress_bar,
|
||||||
terminal_link::TerminalFileLink,
|
terminal_link::TerminalFileLink,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue