summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authormo8it <mo8it@proton.me>2024-08-25 19:24:12 +0200
committermo8it <mo8it@proton.me>2024-08-25 19:24:12 +0200
commit5f4875e2bae07d3c8ce6505abbc67bbe447b7aa6 (patch)
tree0620de6aa9a684b4c19268a94795ea6741f9d5c8 /src
parentfd2a8c01cb35fcff4a5358cce9473ff91272c790 (diff)
Almost done with list
Diffstat (limited to 'src')
-rw-r--r--src/list.rs8
-rw-r--r--src/list/state.rs230
2 files changed, 135 insertions, 103 deletions
diff --git a/src/list.rs b/src/list.rs
index e360182..a8e5225 100644
--- a/src/list.rs
+++ b/src/list.rs
@@ -59,9 +59,7 @@ fn handle_list(app_state: &mut AppState, stdout: &mut StdoutLock) -> Result<()>
list_state.message.push_str(message);
}
- KeyCode::Char('r') => {
- list_state.reset_selected()?;
- }
+ KeyCode::Char('r') => list_state.reset_selected()?,
KeyCode::Char('c') => {
if list_state.selected_to_current_exercise()? {
return Ok(());
@@ -77,9 +75,7 @@ fn handle_list(app_state: &mut AppState, stdout: &mut StdoutLock) -> Result<()>
MouseEventKind::ScrollUp => list_state.select_previous(),
_ => continue,
},
- Event::Resize(width, height) => {
- list_state.set_term_size(width, height);
- }
+ Event::Resize(width, height) => list_state.set_term_size(width, height),
// Ignore
Event::FocusGained | Event::FocusLost => continue,
}
diff --git a/src/list/state.rs b/src/list/state.rs
index cbca1d9..b8fdfcb 100644
--- a/src/list/state.rs
+++ b/src/list/state.rs
@@ -15,20 +15,21 @@ use crate::{app_state::AppState, exercise::Exercise, term::progress_bar, MAX_EXE
// +1 for padding.
const SPACE: &[u8] = &[b' '; MAX_EXERCISE_NAME_LEN + 1];
-fn next_ln<const CLEAR_LAST_CHAR: bool>(stdout: &mut StdoutLock) -> io::Result<()> {
- 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" ")?;
- }
-
+fn next_ln(stdout: &mut StdoutLock) -> io::Result<()> {
stdout
.queue(Clear(ClearType::UntilNewLine))?
.queue(MoveToNextLine(1))?;
Ok(())
}
+// 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.
+fn next_ln_overwrite(stdout: &mut StdoutLock) -> io::Result<()> {
+ stdout.write_all(b" ")?;
+ next_ln(stdout)
+}
+
#[derive(Copy, Clone, PartialEq, Eq)]
pub enum Filter {
Done,
@@ -37,65 +38,111 @@ pub enum Filter {
}
pub struct ListState<'a> {
+ /// Footer message to be displayed if not empty.
pub message: String,
- filter: Filter,
app_state: &'a mut AppState,
- n_rows_with_filter: usize,
name_col_width: usize,
- offset: usize,
- selected: Option<usize>,
+ filter: Filter,
+ n_rows_with_filter: usize,
+ /// Selected row out of the displayed ones.
+ selected_row: Option<usize>,
term_width: u16,
term_height: u16,
- separator: Vec<u8>,
+ separator_line: Vec<u8>,
+ narrow_term: bool,
+ show_footer: bool,
+ max_n_rows_to_display: usize,
+ scroll_padding: usize,
+ row_offset: usize,
}
impl<'a> ListState<'a> {
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_title_len = 4;
let name_col_width = app_state
.exercises()
.iter()
.map(|exercise| exercise.name.len())
.max()
- .map_or(4, |max| max.max(4));
+ .map_or(name_col_title_len, |max| max.max(name_col_title_len));
+ let filter = Filter::None;
let n_rows_with_filter = app_state.exercises().len();
- let selected = app_state.current_exercise_ind();
+ let selected = Some(app_state.current_exercise_ind());
let mut slf = Self {
message: String::with_capacity(128),
- filter: Filter::None,
app_state,
- n_rows_with_filter,
name_col_width,
- offset: selected.saturating_sub(10),
- selected: Some(selected),
- term_width,
- term_height,
- separator: "─".as_bytes().repeat(term_width as usize),
+ filter,
+ n_rows_with_filter,
+ selected_row: selected,
+ // Set by `set_term_size`
+ term_width: 0,
+ term_height: 0,
+ separator_line: Vec::new(),
+ narrow_term: false,
+ show_footer: true,
+ max_n_rows_to_display: 0,
+ scroll_padding: 0,
+ // Updated by `draw`
+ row_offset: 0,
};
+ let (width, height) = terminal::size()?;
+ slf.set_term_size(width, height);
slf.draw(stdout)?;
Ok(slf)
}
+ pub fn set_term_size(&mut self, width: u16, height: u16) {
+ self.term_width = width;
+ self.term_height = height;
+
+ self.separator_line = "─".as_bytes().repeat(width as usize);
+
+ self.narrow_term = width < 95 && self.selected_row.is_some();
+ self.show_footer = height > 6;
+ self.max_n_rows_to_display =
+ (height - 1 - u16::from(self.show_footer) * (4 + u16::from(self.narrow_term))) as usize;
+ self.scroll_padding = (self.max_n_rows_to_display / 4).min(5);
+ }
+
+ fn update_offset(&mut self) {
+ let Some(selected) = self.selected_row else {
+ return;
+ };
+
+ let min_offset = (selected + self.scroll_padding)
+ .saturating_sub(self.max_n_rows_to_display.saturating_sub(1));
+ let max_offset = selected.saturating_sub(self.scroll_padding);
+ let global_max_offset = self
+ .n_rows_with_filter
+ .saturating_sub(self.max_n_rows_to_display);
+
+ self.row_offset = self
+ .row_offset
+ .max(min_offset)
+ .min(max_offset)
+ .min(global_max_offset);
+ }
+
fn draw_rows(
&self,
stdout: &mut StdoutLock,
- max_n_rows_to_display: usize,
filtered_exercises: impl Iterator<Item = (usize, &'a Exercise)>,
) -> io::Result<usize> {
let current_exercise_ind = self.app_state.current_exercise_ind();
let mut n_displayed_rows = 0;
for (exercise_ind, exercise) in filtered_exercises
- .skip(self.offset)
- .take(max_n_rows_to_display)
+ .skip(self.row_offset)
+ .take(self.max_n_rows_to_display)
{
- if self.selected == Some(n_displayed_rows) {
+ if self.selected_row == Some(self.row_offset + n_displayed_rows) {
stdout.queue(SetBackgroundColor(Color::Rgb {
r: 50,
g: 50,
@@ -128,7 +175,7 @@ impl<'a> ListState<'a> {
stdout.write_all(exercise.path.as_bytes())?;
- next_ln::<true>(stdout)?;
+ next_ln_overwrite(stdout)?;
stdout.queue(ResetColor)?;
n_displayed_rows += 1;
}
@@ -147,36 +194,27 @@ impl<'a> ListState<'a> {
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)?;
+ next_ln_overwrite(stdout)?;
- let narrow = self.term_width < 95;
- 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;
+ self.update_offset();
// Rows
let iter = self.app_state.exercises().iter().enumerate();
let n_displayed_rows = match self.filter {
- Filter::Done => self.draw_rows(
- stdout,
- max_n_rows_to_display,
- iter.filter(|(_, exercise)| exercise.done),
- )?,
- Filter::Pending => self.draw_rows(
- stdout,
- max_n_rows_to_display,
- iter.filter(|(_, exercise)| !exercise.done),
- )?,
- Filter::None => self.draw_rows(stdout, max_n_rows_to_display, iter)?,
+ Filter::Done => self.draw_rows(stdout, iter.filter(|(_, exercise)| exercise.done))?,
+ Filter::Pending => {
+ self.draw_rows(stdout, iter.filter(|(_, exercise)| !exercise.done))?
+ }
+ Filter::None => self.draw_rows(stdout, iter)?,
};
- for _ in 0..max_n_rows_to_display - n_displayed_rows {
- next_ln::<false>(stdout)?;
+ for _ in 0..self.max_n_rows_to_display - n_displayed_rows {
+ next_ln(stdout)?;
}
- if show_footer {
- stdout.write_all(&self.separator)?;
- next_ln::<false>(stdout)?;
+ if self.show_footer {
+ stdout.write_all(&self.separator_line)?;
+ next_ln(stdout)?;
progress_bar(
stdout,
@@ -184,21 +222,25 @@ impl<'a> ListState<'a> {
self.app_state.exercises().len() as u16,
self.term_width,
)?;
- next_ln::<false>(stdout)?;
+ next_ln(stdout)?;
- stdout.write_all(&self.separator)?;
- next_ln::<false>(stdout)?;
+ stdout.write_all(&self.separator_line)?;
+ next_ln(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 ")?;
+ if self.selected_row.is_some() {
+ stdout.write_all(
+ "↓/j ↑/k home/g end/G | <c>ontinue at | <r>eset exercise".as_bytes(),
+ )?;
+ if self.narrow_term {
+ next_ln_overwrite(stdout)?;
+ stdout.write_all(b"filter ")?;
+ } else {
+ stdout.write_all(b" | filter ")?;
+ }
} else {
- stdout.write_all(b" filter ")?;
+ stdout.write_all(b"filter ")?;
}
match self.filter {
@@ -220,19 +262,19 @@ impl<'a> ListState<'a> {
}
Filter::None => stdout.write_all(b"<d>one/<p>ending")?,
}
- stdout.write_all(" │ <q>uit list".as_bytes())?;
- if narrow {
- next_ln::<true>(stdout)?;
+ stdout.write_all(b" | <q>uit list")?;
+ if self.narrow_term {
+ next_ln_overwrite(stdout)?;
} else {
- next_ln::<false>(stdout)?;
+ next_ln(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)?;
+ next_ln_overwrite(stdout)?;
+ if self.narrow_term {
+ next_ln(stdout)?;
}
}
}
@@ -240,20 +282,8 @@ impl<'a> ListState<'a> {
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]
- 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 {
+ fn update_rows(&mut self) {
+ self.n_rows_with_filter = match self.filter {
Filter::Done => self
.app_state
.exercises()
@@ -270,42 +300,46 @@ impl<'a> ListState<'a> {
};
if self.n_rows_with_filter == 0 {
- self.selected = None;
+ self.selected_row = None;
} else {
- self.selected = Some(
- self.selected
+ self.selected_row = Some(
+ self.selected_row
.map_or(0, |selected| selected.min(self.n_rows_with_filter - 1)),
);
}
}
+ #[inline]
+ pub fn filter(&self) -> Filter {
+ self.filter
+ }
+
+ pub fn set_filter(&mut self, filter: Filter) {
+ self.filter = filter;
+ self.update_rows();
+ }
+
pub fn select_next(&mut self) {
- 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);
+ if let Some(selected) = self.selected_row {
+ self.selected_row = Some((selected + 1).min(self.n_rows_with_filter - 1));
}
}
pub fn select_previous(&mut self) {
- if self.n_rows_with_filter > 0 {
- let previous = self
- .selected
- .map_or(0, |selected| selected.saturating_sub(1));
- self.selected = Some(previous);
+ if let Some(selected) = self.selected_row {
+ self.selected_row = Some(selected.saturating_sub(1));
}
}
pub fn select_first(&mut self) {
if self.n_rows_with_filter > 0 {
- self.selected = Some(0);
+ self.selected_row = Some(0);
}
}
pub fn select_last(&mut self) {
if self.n_rows_with_filter > 0 {
- self.selected = Some(self.n_rows_with_filter - 1);
+ self.selected_row = Some(self.n_rows_with_filter - 1);
}
}
@@ -334,13 +368,14 @@ impl<'a> ListState<'a> {
}
pub fn reset_selected(&mut self) -> Result<()> {
- let Some(selected) = self.selected else {
+ let Some(selected) = self.selected_row else {
self.message.push_str("Nothing selected to reset!");
return Ok(());
};
let exercise_ind = self.selected_to_exercise_ind(selected)?;
let exercise_path = self.app_state.reset_exercise_by_ind(exercise_ind)?;
+ self.update_rows();
write!(self.message, "The exercise {exercise_path} has been reset")?;
Ok(())
@@ -348,13 +383,14 @@ impl<'a> ListState<'a> {
// Return `true` if there was something to select.
pub fn selected_to_current_exercise(&mut self) -> Result<bool> {
- let Some(selected) = self.selected else {
+ let Some(selected) = self.selected_row else {
self.message.push_str("Nothing selected to continue at!");
return Ok(false);
};
let exercise_ind = self.selected_to_exercise_ind(selected)?;
self.app_state.set_current_exercise_ind(exercise_ind)?;
+
Ok(true)
}
}