1
0
mirror of https://github.com/rust-lang/rustlings.git synced 2024-11-08 09:09:17 +01:00

Almost done with list display

This commit is contained in:
mo8it 2024-08-24 17:17:56 +02:00
parent 4e12725616
commit b779c43126
6 changed files with 260 additions and 213 deletions

@ -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")
}

@ -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,
}; };