mirror of
https://git.aramjonghu.dev/AramJonghu/rustlings.git
synced 2026-09-02 00:13:30 +02:00
Start list without Ratatui
This commit is contained in:
+1
-1
@@ -1,5 +1,5 @@
|
||||
use anyhow::Result;
|
||||
use ratatui::crossterm::style::{style, StyledContent, Stylize};
|
||||
use crossterm::style::{style, StyledContent, Stylize};
|
||||
use std::{
|
||||
fmt::{self, Display, Formatter},
|
||||
io::Write,
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
use anyhow::{bail, Context, Result};
|
||||
use ratatui::crossterm::style::Stylize;
|
||||
use crossterm::style::Stylize;
|
||||
use serde::Deserialize;
|
||||
use std::{
|
||||
env::set_current_dir,
|
||||
|
||||
+75
-67
@@ -1,95 +1,101 @@
|
||||
use anyhow::{Context, Result};
|
||||
use ratatui::{
|
||||
backend::CrosstermBackend,
|
||||
crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
QueueableCommand,
|
||||
use crossterm::{
|
||||
cursor,
|
||||
event::{
|
||||
self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind, MouseEventKind,
|
||||
},
|
||||
Terminal,
|
||||
terminal::{
|
||||
disable_raw_mode, enable_raw_mode, DisableLineWrap, EnableLineWrap, EnterAlternateScreen,
|
||||
LeaveAlternateScreen,
|
||||
},
|
||||
QueueableCommand,
|
||||
};
|
||||
use std::io::{self, StdoutLock, Write};
|
||||
|
||||
use crate::app_state::AppState;
|
||||
|
||||
use self::state::{Filter, UiState};
|
||||
use self::state::{Filter, ListState};
|
||||
|
||||
mod state;
|
||||
|
||||
fn handle_list(app_state: &mut AppState, stdout: &mut StdoutLock) -> Result<()> {
|
||||
let mut terminal = Terminal::new(CrosstermBackend::new(stdout))?;
|
||||
terminal.clear()?;
|
||||
let mut list_state = ListState::new(app_state, stdout)?;
|
||||
|
||||
let mut ui_state = UiState::new(app_state);
|
||||
loop {
|
||||
match event::read().context("Failed to read terminal event")? {
|
||||
Event::Key(key) => {
|
||||
match key.kind {
|
||||
KeyEventKind::Release => continue,
|
||||
KeyEventKind::Press | KeyEventKind::Repeat => (),
|
||||
}
|
||||
|
||||
'outer: loop {
|
||||
terminal.try_draw(|frame| ui_state.draw(frame).map_err(io::Error::other))?;
|
||||
list_state.message.clear();
|
||||
|
||||
let key = loop {
|
||||
match event::read().context("Failed to read terminal event")? {
|
||||
Event::Key(key) => match key.kind {
|
||||
KeyEventKind::Press | KeyEventKind::Repeat => break key,
|
||||
KeyEventKind::Release => (),
|
||||
},
|
||||
// Redraw
|
||||
Event::Resize(_, _) => continue 'outer,
|
||||
// Ignore
|
||||
Event::FocusGained | Event::FocusLost | Event::Mouse(_) | Event::Paste(_) => (),
|
||||
match key.code {
|
||||
KeyCode::Char('q') => return Ok(()),
|
||||
KeyCode::Down | KeyCode::Char('j') => list_state.select_next(),
|
||||
KeyCode::Up | KeyCode::Char('k') => list_state.select_previous(),
|
||||
KeyCode::Home | KeyCode::Char('g') => list_state.select_first(),
|
||||
KeyCode::End | KeyCode::Char('G') => list_state.select_last(),
|
||||
KeyCode::Char('d') => {
|
||||
let message = if list_state.filter() == Filter::Done {
|
||||
list_state.set_filter(Filter::None);
|
||||
"Disabled filter DONE"
|
||||
} else {
|
||||
list_state.set_filter(Filter::Done);
|
||||
"Enabled filter DONE │ Press d again to disable the filter"
|
||||
};
|
||||
|
||||
list_state.message.push_str(message);
|
||||
}
|
||||
KeyCode::Char('p') => {
|
||||
let message = if list_state.filter() == Filter::Pending {
|
||||
list_state.set_filter(Filter::None);
|
||||
"Disabled filter PENDING"
|
||||
} else {
|
||||
list_state.set_filter(Filter::Pending);
|
||||
"Enabled filter PENDING │ Press p again to disable the filter"
|
||||
};
|
||||
|
||||
list_state.message.push_str(message);
|
||||
}
|
||||
KeyCode::Char('r') => {
|
||||
list_state.reset_selected()?;
|
||||
}
|
||||
KeyCode::Char('c') => {
|
||||
return list_state.selected_to_current_exercise();
|
||||
}
|
||||
// Redraw to remove the message.
|
||||
KeyCode::Esc => (),
|
||||
_ => continue,
|
||||
}
|
||||
|
||||
list_state.redraw(stdout)?;
|
||||
}
|
||||
};
|
||||
Event::Mouse(event) => {
|
||||
match event.kind {
|
||||
MouseEventKind::ScrollDown => list_state.select_next(),
|
||||
MouseEventKind::ScrollUp => list_state.select_previous(),
|
||||
_ => continue,
|
||||
}
|
||||
|
||||
ui_state.message.clear();
|
||||
|
||||
match key.code {
|
||||
KeyCode::Char('q') => break,
|
||||
KeyCode::Down | KeyCode::Char('j') => ui_state.select_next(),
|
||||
KeyCode::Up | KeyCode::Char('k') => ui_state.select_previous(),
|
||||
KeyCode::Home | KeyCode::Char('g') => ui_state.select_first(),
|
||||
KeyCode::End | KeyCode::Char('G') => ui_state.select_last(),
|
||||
KeyCode::Char('d') => {
|
||||
let message = if ui_state.filter == Filter::Done {
|
||||
ui_state.filter = Filter::None;
|
||||
"Disabled filter DONE"
|
||||
} else {
|
||||
ui_state.filter = Filter::Done;
|
||||
"Enabled filter DONE │ Press d again to disable the filter"
|
||||
};
|
||||
|
||||
ui_state = ui_state.with_updated_rows();
|
||||
ui_state.message.push_str(message);
|
||||
list_state.redraw(stdout)?;
|
||||
}
|
||||
KeyCode::Char('p') => {
|
||||
let message = if ui_state.filter == Filter::Pending {
|
||||
ui_state.filter = Filter::None;
|
||||
"Disabled filter PENDING"
|
||||
} else {
|
||||
ui_state.filter = Filter::Pending;
|
||||
"Enabled filter PENDING │ Press p again to disable the filter"
|
||||
};
|
||||
|
||||
ui_state = ui_state.with_updated_rows();
|
||||
ui_state.message.push_str(message);
|
||||
}
|
||||
KeyCode::Char('r') => {
|
||||
ui_state = ui_state.with_reset_selected()?;
|
||||
}
|
||||
KeyCode::Char('c') => {
|
||||
ui_state.selected_to_current_exercise()?;
|
||||
break;
|
||||
}
|
||||
_ => (),
|
||||
// Redraw
|
||||
Event::Resize(_, _) => list_state.redraw(stdout)?,
|
||||
// Ignore
|
||||
Event::FocusGained | Event::FocusLost => (),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn list(app_state: &mut AppState) -> Result<()> {
|
||||
let mut stdout = io::stdout().lock();
|
||||
stdout
|
||||
.queue(EnterAlternateScreen)?
|
||||
.queue(EnableMouseCapture)?
|
||||
.flush()?;
|
||||
.queue(cursor::Hide)?
|
||||
.queue(DisableLineWrap)?
|
||||
.queue(EnableMouseCapture)?;
|
||||
enable_raw_mode()?;
|
||||
|
||||
let res = handle_list(app_state, &mut stdout);
|
||||
@@ -97,6 +103,8 @@ pub fn list(app_state: &mut AppState) -> Result<()> {
|
||||
// Restore the terminal even if we got an error.
|
||||
stdout
|
||||
.queue(LeaveAlternateScreen)?
|
||||
.queue(cursor::Show)?
|
||||
.queue(EnableLineWrap)?
|
||||
.queue(DisableMouseCapture)?
|
||||
.flush()?;
|
||||
disable_raw_mode()?;
|
||||
|
||||
+175
-186
@@ -1,14 +1,19 @@
|
||||
use anyhow::{Context, Result};
|
||||
use ratatui::{
|
||||
layout::{Constraint, Rect},
|
||||
style::{Style, Stylize},
|
||||
text::{Span, Text},
|
||||
widgets::{Block, Borders, HighlightSpacing, Paragraph, Row, Table, TableState},
|
||||
Frame,
|
||||
use crossterm::{
|
||||
cursor::{MoveDown, MoveTo},
|
||||
style::{Color, ResetColor, SetForegroundColor},
|
||||
terminal::{self, BeginSynchronizedUpdate, EndSynchronizedUpdate},
|
||||
QueueableCommand,
|
||||
};
|
||||
use std::{
|
||||
fmt::Write as _,
|
||||
io::{self, StdoutLock, Write as _},
|
||||
};
|
||||
use std::{fmt::Write, mem};
|
||||
|
||||
use crate::{app_state::AppState, progress_bar::progress_bar_ratatui};
|
||||
use crate::{app_state::AppState, term::clear_terminal, MAX_EXERCISE_NAME_LEN};
|
||||
|
||||
// +1 for padding.
|
||||
const SPACE: &[u8] = &[b' '; MAX_EXERCISE_NAME_LEN + 1];
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
pub enum Filter {
|
||||
@@ -17,230 +22,213 @@ pub enum Filter {
|
||||
None,
|
||||
}
|
||||
|
||||
pub struct UiState<'a> {
|
||||
pub table: Table<'static>,
|
||||
pub struct ListState<'a> {
|
||||
pub message: String,
|
||||
pub filter: Filter,
|
||||
filter: Filter,
|
||||
app_state: &'a mut AppState,
|
||||
table_state: TableState,
|
||||
n_rows: usize,
|
||||
n_rows_with_filter: usize,
|
||||
name_col_width: usize,
|
||||
offset: usize,
|
||||
selected: Option<usize>,
|
||||
}
|
||||
|
||||
impl<'a> UiState<'a> {
|
||||
pub fn with_updated_rows(mut self) -> Self {
|
||||
let current_exercise_ind = self.app_state.current_exercise_ind();
|
||||
|
||||
self.n_rows = 0;
|
||||
let rows = self
|
||||
.app_state
|
||||
.exercises()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(ind, exercise)| {
|
||||
let exercise_state = if exercise.done {
|
||||
if self.filter == Filter::Pending {
|
||||
return None;
|
||||
}
|
||||
|
||||
"DONE".green()
|
||||
} else {
|
||||
if self.filter == Filter::Done {
|
||||
return None;
|
||||
}
|
||||
|
||||
"PENDING".yellow()
|
||||
};
|
||||
|
||||
self.n_rows += 1;
|
||||
|
||||
let next = if ind == current_exercise_ind {
|
||||
">>>>".bold().red()
|
||||
} else {
|
||||
Span::default()
|
||||
};
|
||||
|
||||
Some(Row::new([
|
||||
next,
|
||||
exercise_state,
|
||||
Span::raw(exercise.name),
|
||||
Span::raw(exercise.path),
|
||||
]))
|
||||
});
|
||||
|
||||
self.table = self.table.rows(rows);
|
||||
|
||||
if self.n_rows == 0 {
|
||||
self.table_state.select(None);
|
||||
} else {
|
||||
self.table_state.select(Some(
|
||||
self.table_state
|
||||
.selected()
|
||||
.map_or(0, |selected| selected.min(self.n_rows - 1)),
|
||||
));
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub fn new(app_state: &'a mut AppState) -> Self {
|
||||
let header = Row::new(["Next", "State", "Name", "Path"]);
|
||||
|
||||
let max_name_len = app_state
|
||||
impl<'a> ListState<'a> {
|
||||
pub fn new(app_state: &'a mut AppState, stdout: &mut StdoutLock) -> io::Result<Self> {
|
||||
let name_col_width = app_state
|
||||
.exercises()
|
||||
.iter()
|
||||
.map(|exercise| exercise.name.len())
|
||||
.max()
|
||||
.unwrap_or(4) as u16;
|
||||
.map_or(4, |max| max.max(4));
|
||||
|
||||
let widths = [
|
||||
Constraint::Length(4),
|
||||
Constraint::Length(7),
|
||||
Constraint::Length(max_name_len),
|
||||
Constraint::Fill(1),
|
||||
];
|
||||
|
||||
let table = Table::default()
|
||||
.widths(widths)
|
||||
.header(header)
|
||||
.column_spacing(2)
|
||||
.highlight_spacing(HighlightSpacing::Always)
|
||||
.highlight_style(Style::new().bg(ratatui::style::Color::Rgb(50, 50, 50)))
|
||||
.highlight_symbol("🦀")
|
||||
.block(Block::default().borders(Borders::BOTTOM));
|
||||
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 table_state = TableState::default()
|
||||
.with_offset(selected.saturating_sub(10))
|
||||
.with_selected(Some(selected));
|
||||
let n_rows_with_filter = app_state.exercises().len();
|
||||
|
||||
let filter = Filter::None;
|
||||
let n_rows = app_state.exercises().len();
|
||||
|
||||
let slf = Self {
|
||||
table,
|
||||
let mut slf = Self {
|
||||
message: String::with_capacity(128),
|
||||
filter,
|
||||
filter: Filter::None,
|
||||
app_state,
|
||||
table_state,
|
||||
n_rows,
|
||||
n_rows_with_filter,
|
||||
name_col_width,
|
||||
offset: selected.saturating_sub(10),
|
||||
selected: Some(selected),
|
||||
};
|
||||
|
||||
slf.with_updated_rows()
|
||||
slf.redraw(stdout)?;
|
||||
|
||||
Ok(slf)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn filter(&self) -> Filter {
|
||||
self.filter
|
||||
}
|
||||
|
||||
pub fn set_filter(&mut self, filter: Filter) {
|
||||
self.filter = filter;
|
||||
self.n_rows_with_filter = match filter {
|
||||
Filter::Done => self
|
||||
.app_state
|
||||
.exercises()
|
||||
.iter()
|
||||
.filter(|exercise| !exercise.done)
|
||||
.count(),
|
||||
Filter::Pending => self
|
||||
.app_state
|
||||
.exercises()
|
||||
.iter()
|
||||
.filter(|exercise| exercise.done)
|
||||
.count(),
|
||||
Filter::None => self.app_state.exercises().len(),
|
||||
};
|
||||
|
||||
if self.n_rows_with_filter == 0 {
|
||||
self.selected = None;
|
||||
} else {
|
||||
self.selected = Some(
|
||||
self.selected
|
||||
.map_or(0, |selected| selected.min(self.n_rows_with_filter - 1)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select_next(&mut self) {
|
||||
if self.n_rows > 0 {
|
||||
let next = self
|
||||
.table_state
|
||||
.selected()
|
||||
.map_or(0, |selected| (selected + 1).min(self.n_rows - 1));
|
||||
self.table_state.select(Some(next));
|
||||
if self.n_rows_with_filter > 0 {
|
||||
let next = self.selected.map_or(0, |selected| {
|
||||
(selected + 1).min(self.n_rows_with_filter - 1)
|
||||
});
|
||||
self.selected = Some(next);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select_previous(&mut self) {
|
||||
if self.n_rows > 0 {
|
||||
if self.n_rows_with_filter > 0 {
|
||||
let previous = self
|
||||
.table_state
|
||||
.selected()
|
||||
.selected
|
||||
.map_or(0, |selected| selected.saturating_sub(1));
|
||||
self.table_state.select(Some(previous));
|
||||
self.selected = Some(previous);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select_first(&mut self) {
|
||||
if self.n_rows > 0 {
|
||||
self.table_state.select(Some(0));
|
||||
if self.n_rows_with_filter > 0 {
|
||||
self.selected = Some(0);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select_last(&mut self) {
|
||||
if self.n_rows > 0 {
|
||||
self.table_state.select(Some(self.n_rows - 1));
|
||||
if self.n_rows_with_filter > 0 {
|
||||
self.selected = Some(self.n_rows_with_filter - 1);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn draw(&mut self, frame: &mut Frame) -> Result<()> {
|
||||
let area = frame.area();
|
||||
let narrow = area.width < 95;
|
||||
pub fn redraw(&mut self, stdout: &mut StdoutLock) -> io::Result<()> {
|
||||
stdout.queue(BeginSynchronizedUpdate)?;
|
||||
stdout.queue(MoveTo(0, 1))?;
|
||||
let (width, height) = terminal::size()?;
|
||||
let narrow = width < 95;
|
||||
let narrow_u16 = u16::from(narrow);
|
||||
let table_height = area.height - 3 - narrow_u16;
|
||||
let max_n_rows_to_display = height.saturating_sub(narrow_u16 + 4);
|
||||
|
||||
frame.render_stateful_widget(
|
||||
&self.table,
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: area.width,
|
||||
height: table_height,
|
||||
},
|
||||
&mut self.table_state,
|
||||
);
|
||||
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 as usize);
|
||||
|
||||
frame.render_widget(
|
||||
Paragraph::new(progress_bar_ratatui(
|
||||
self.app_state.n_done(),
|
||||
self.app_state.exercises().len() as u16,
|
||||
area.width,
|
||||
)?)
|
||||
.block(Block::default().borders(Borders::BOTTOM)),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: table_height,
|
||||
width: area.width,
|
||||
height: 2,
|
||||
},
|
||||
);
|
||||
|
||||
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 "));
|
||||
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 {
|
||||
spans.push(Span::raw(" filter "));
|
||||
stdout.write_all(b" ")?;
|
||||
}
|
||||
|
||||
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")),
|
||||
if ind == current_exercise_ind {
|
||||
stdout.queue(SetForegroundColor(Color::Red))?;
|
||||
stdout.write_all(b">>>>>>> ")?;
|
||||
} else {
|
||||
stdout.write_all(b" ")?;
|
||||
}
|
||||
|
||||
spans.push(Span::raw(" │ <q>uit list"));
|
||||
text.push_line(spans);
|
||||
text
|
||||
} else {
|
||||
Text::from(self.message.as_str().light_blue())
|
||||
};
|
||||
frame.render_widget(
|
||||
message,
|
||||
Rect {
|
||||
x: 0,
|
||||
y: table_height + 2,
|
||||
width: area.width,
|
||||
height: 1 + narrow_u16,
|
||||
},
|
||||
);
|
||||
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 with_reset_selected(mut self) -> Result<Self> {
|
||||
let Some(selected) = self.table_state.selected() else {
|
||||
return Ok(self);
|
||||
pub fn reset_selected(&mut self) -> Result<()> {
|
||||
let Some(selected) = self.selected else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let ind = self
|
||||
@@ -259,11 +247,12 @@ impl<'a> UiState<'a> {
|
||||
let exercise_path = self.app_state.reset_exercise_by_ind(ind)?;
|
||||
write!(self.message, "The exercise {exercise_path} has been reset")?;
|
||||
|
||||
Ok(self.with_updated_rows())
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn selected_to_current_exercise(&mut self) -> Result<()> {
|
||||
let Some(selected) = self.table_state.selected() else {
|
||||
let Some(selected) = self.selected else {
|
||||
// TODO: Don't exit list
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ mod terminal_link;
|
||||
mod watch;
|
||||
|
||||
const CURRENT_FORMAT_VERSION: u8 = 1;
|
||||
const MAX_EXERCISE_NAME_LEN: usize = 32;
|
||||
|
||||
/// Rustlings is a collection of small exercises to get you used to writing and reading Rust code
|
||||
#[derive(Parser)]
|
||||
|
||||
+23
-70
@@ -1,100 +1,53 @@
|
||||
use anyhow::{bail, Result};
|
||||
use ratatui::text::{Line, Span};
|
||||
use std::fmt::Write;
|
||||
use std::io::{self, StdoutLock, Write};
|
||||
|
||||
const PREFIX: &str = "Progress: [";
|
||||
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;
|
||||
|
||||
const PROGRESS_EXCEEDS_MAX_ERR: &str =
|
||||
"The progress of the progress bar is higher than the maximum";
|
||||
|
||||
/// Terminal progress bar to be used when not using Ratataui.
|
||||
pub fn progress_bar(progress: u16, total: u16, line_width: u16) -> Result<String> {
|
||||
use ratatui::crossterm::style::Stylize;
|
||||
|
||||
if progress > total {
|
||||
bail!(PROGRESS_EXCEEDS_MAX_ERR);
|
||||
}
|
||||
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 Ok(format!("Progress: {progress}/{total} exercises"));
|
||||
return write!(stdout, "Progress: {progress}/{total} exercises");
|
||||
}
|
||||
|
||||
let mut line = String::with_capacity(usize::from(line_width));
|
||||
line.push_str(PREFIX);
|
||||
stdout.write_all(PREFIX)?;
|
||||
|
||||
let width = line_width - WRAPPER_WIDTH;
|
||||
let filled = (width * progress) / total;
|
||||
|
||||
let mut green_part = String::with_capacity(usize::from(filled + 1));
|
||||
stdout.queue(SetForegroundColor(Color::Green))?;
|
||||
for _ in 0..filled {
|
||||
green_part.push('#');
|
||||
stdout.write_all(b"#")?;
|
||||
}
|
||||
|
||||
if filled < width {
|
||||
green_part.push('>');
|
||||
stdout.write_all(b">")?;
|
||||
}
|
||||
write!(line, "{}", green_part.green()).unwrap();
|
||||
|
||||
let width_minus_filled = width - filled;
|
||||
if width_minus_filled > 1 {
|
||||
let red_part_width = width_minus_filled - 1;
|
||||
let mut red_part = String::with_capacity(usize::from(red_part_width));
|
||||
stdout.queue(SetForegroundColor(Color::Red))?;
|
||||
for _ in 0..red_part_width {
|
||||
red_part.push('-');
|
||||
stdout.write_all(b"-")?;
|
||||
}
|
||||
write!(line, "{}", red_part.red()).unwrap();
|
||||
}
|
||||
|
||||
writeln!(line, "] {progress:>3}/{total} exercises").unwrap();
|
||||
|
||||
Ok(line)
|
||||
}
|
||||
|
||||
/// Progress bar to be used with Ratataui.
|
||||
// Not using Ratatui's Gauge widget to keep the progress bar consistent.
|
||||
pub fn progress_bar_ratatui(progress: u16, total: u16, line_width: u16) -> Result<Line<'static>> {
|
||||
use ratatui::style::Stylize;
|
||||
|
||||
if progress > total {
|
||||
bail!(PROGRESS_EXCEEDS_MAX_ERR);
|
||||
}
|
||||
|
||||
if line_width < MIN_LINE_WIDTH {
|
||||
return Ok(Line::raw(format!("Progress: {progress}/{total} exercises")));
|
||||
}
|
||||
|
||||
let mut spans = Vec::with_capacity(4);
|
||||
spans.push(Span::raw(PREFIX));
|
||||
|
||||
let width = line_width - WRAPPER_WIDTH;
|
||||
let filled = (width * progress) / total;
|
||||
|
||||
let mut green_part = String::with_capacity(usize::from(filled + 1));
|
||||
for _ in 0..filled {
|
||||
green_part.push('#');
|
||||
}
|
||||
|
||||
if filled < width {
|
||||
green_part.push('>');
|
||||
}
|
||||
spans.push(green_part.green());
|
||||
|
||||
let width_minus_filled = width - filled;
|
||||
if width_minus_filled > 1 {
|
||||
let red_part_width = width_minus_filled - 1;
|
||||
let mut red_part = String::with_capacity(usize::from(red_part_width));
|
||||
for _ in 0..red_part_width {
|
||||
red_part.push('-');
|
||||
}
|
||||
spans.push(red_part.red());
|
||||
}
|
||||
|
||||
spans.push(Span::raw(format!("] {progress:>3}/{total} exercises")));
|
||||
|
||||
Ok(Line::from(spans))
|
||||
stdout.queue(ResetColor)?;
|
||||
write!(stdout, "] {progress:>3}/{total} exercises")
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
use anyhow::{bail, Result};
|
||||
use ratatui::crossterm::style::{style, Stylize};
|
||||
use crossterm::style::{style, Stylize};
|
||||
use std::io::{self, Write};
|
||||
|
||||
use crate::{
|
||||
|
||||
+11
-1
@@ -1,7 +1,17 @@
|
||||
use std::io::{self, BufRead, StdoutLock, Write};
|
||||
|
||||
use crossterm::{
|
||||
cursor::MoveTo,
|
||||
terminal::{Clear, ClearType},
|
||||
QueueableCommand,
|
||||
};
|
||||
|
||||
pub fn clear_terminal(stdout: &mut StdoutLock) -> io::Result<()> {
|
||||
stdout.write_all(b"\x1b[H\x1b[2J\x1b[3J")
|
||||
stdout
|
||||
.queue(MoveTo(0, 0))?
|
||||
.queue(Clear(ClearType::All))?
|
||||
.queue(Clear(ClearType::Purge))
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
pub fn press_enter_prompt(stdout: &mut StdoutLock) -> io::Result<()> {
|
||||
|
||||
+8
-6
@@ -1,5 +1,5 @@
|
||||
use anyhow::Result;
|
||||
use ratatui::crossterm::{
|
||||
use crossterm::{
|
||||
style::{style, Stylize},
|
||||
terminal,
|
||||
};
|
||||
@@ -76,7 +76,8 @@ impl<'a> WatchState<'a> {
|
||||
self.done_status = DoneStatus::Pending;
|
||||
}
|
||||
|
||||
self.render()
|
||||
self.render()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn handle_file_change(&mut self, exercise_ind: usize) -> Result<()> {
|
||||
@@ -120,7 +121,7 @@ impl<'a> WatchState<'a> {
|
||||
self.writer.flush()
|
||||
}
|
||||
|
||||
pub fn render(&mut self) -> Result<()> {
|
||||
pub fn render(&mut self) -> io::Result<()> {
|
||||
// Prevent having the first line shifted if clearing wasn't successful.
|
||||
self.writer.write_all(b"\n")?;
|
||||
clear_terminal(&mut self.writer)?;
|
||||
@@ -155,14 +156,15 @@ impl<'a> WatchState<'a> {
|
||||
}
|
||||
|
||||
let line_width = terminal::size()?.0;
|
||||
let progress_bar = progress_bar(
|
||||
progress_bar(
|
||||
&mut self.writer,
|
||||
self.app_state.n_done(),
|
||||
self.app_state.exercises().len() as u16,
|
||||
line_width,
|
||||
)?;
|
||||
writeln!(
|
||||
self.writer,
|
||||
"{progress_bar}Current exercise: {}",
|
||||
"\nCurrent exercise: {}",
|
||||
self.app_state.current_exercise().terminal_link(),
|
||||
)?;
|
||||
|
||||
@@ -171,7 +173,7 @@ impl<'a> WatchState<'a> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn show_hint(&mut self) -> Result<()> {
|
||||
pub fn show_hint(&mut self) -> io::Result<()> {
|
||||
self.show_hint = true;
|
||||
self.render()
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use ratatui::crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers};
|
||||
use crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers};
|
||||
use std::sync::mpsc::Sender;
|
||||
|
||||
use super::WatchEvent;
|
||||
@@ -78,7 +78,7 @@ pub fn terminal_event_handler(tx: Sender<WatchEvent>, manual_run: bool) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
Event::FocusGained | Event::FocusLost | Event::Mouse(_) | Event::Paste(_) => continue,
|
||||
Event::FocusGained | Event::FocusLost | Event::Mouse(_) => continue,
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user