Fix list on terminals that don't disable line wrapping

This commit is contained in:
mo8it 2024-08-26 04:29:58 +02:00
parent f22700a4ec
commit e811dd15b5
2 changed files with 149 additions and 70 deletions

View file

@ -13,7 +13,7 @@ use std::{
use crate::{ use crate::{
app_state::AppState, app_state::AppState,
exercise::Exercise, exercise::Exercise,
term::{progress_bar, terminal_file_link}, term::{progress_bar, terminal_file_link, CountedWrite, MaxLenWriter},
MAX_EXERCISE_NAME_LEN, MAX_EXERCISE_NAME_LEN,
}; };
@ -28,14 +28,6 @@ fn next_ln(stdout: &mut StdoutLock) -> io::Result<()> {
Ok(()) 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)] #[derive(Copy, Clone, PartialEq, Eq)]
pub enum Filter { pub enum Filter {
Done, Done,
@ -164,40 +156,44 @@ impl<'a> ListState<'a> {
.skip(self.row_offset) .skip(self.row_offset)
.take(self.max_n_rows_to_display) .take(self.max_n_rows_to_display)
{ {
let mut writer = MaxLenWriter::new(stdout, self.term_width as usize);
if self.selected_row == Some(self.row_offset + n_displayed_rows) { if self.selected_row == Some(self.row_offset + n_displayed_rows) {
stdout.queue(SetBackgroundColor(Color::Rgb { writer.stdout.queue(SetBackgroundColor(Color::Rgb {
r: 40, r: 40,
g: 40, g: 40,
b: 40, b: 40,
}))?; }))?;
stdout.write_all("🦀".as_bytes())?; // The crab emoji has the width of two ascii chars.
writer.add_to_len(2);
writer.stdout.write_all("🦀".as_bytes())?;
} else { } else {
stdout.write_all(b" ")?; writer.write_ascii(b" ")?;
} }
if exercise_ind == current_exercise_ind { if exercise_ind == current_exercise_ind {
stdout.queue(SetForegroundColor(Color::Red))?; writer.stdout.queue(SetForegroundColor(Color::Red))?;
stdout.write_all(b">>>>>>> ")?; writer.write_ascii(b">>>>>>> ")?;
} else { } else {
stdout.write_all(b" ")?; writer.write_ascii(b" ")?;
} }
if exercise.done { if exercise.done {
stdout.queue(SetForegroundColor(Color::Green))?; writer.stdout.queue(SetForegroundColor(Color::Green))?;
stdout.write_all(b"DONE ")?; writer.write_ascii(b"DONE ")?;
} else { } else {
stdout.queue(SetForegroundColor(Color::Yellow))?; writer.stdout.queue(SetForegroundColor(Color::Yellow))?;
stdout.write_all(b"PENDING ")?; writer.write_ascii(b"PENDING ")?;
} }
stdout.queue(SetForegroundColor(Color::Reset))?; writer.stdout.queue(SetForegroundColor(Color::Reset))?;
stdout.write_all(exercise.name.as_bytes())?; writer.write_str(exercise.name)?;
stdout.write_all(&SPACE[..self.name_col_width + 2 - exercise.name.len()])?; writer.write_ascii(&SPACE[..self.name_col_width + 2 - exercise.name.len()])?;
terminal_file_link(stdout, exercise.path, Color::Blue)?; terminal_file_link(&mut writer, exercise.path, Color::Blue)?;
next_ln_overwrite(stdout)?; next_ln(stdout)?;
stdout.queue(ResetColor)?; stdout.queue(ResetColor)?;
n_displayed_rows += 1; n_displayed_rows += 1;
} }
@ -213,10 +209,11 @@ impl<'a> ListState<'a> {
stdout.queue(BeginSynchronizedUpdate)?.queue(MoveTo(0, 0))?; stdout.queue(BeginSynchronizedUpdate)?.queue(MoveTo(0, 0))?;
// Header // Header
stdout.write_all(b" Current State Name")?; let mut writer = MaxLenWriter::new(stdout, self.term_width as usize);
stdout.write_all(&SPACE[..self.name_col_width - 2])?; writer.write_ascii(b" Current State Name")?;
stdout.write_all(b"Path")?; writer.write_ascii(&SPACE[..self.name_col_width - 2])?;
next_ln_overwrite(stdout)?; writer.write_ascii(b"Path")?;
next_ln(stdout)?;
// Rows // Rows
let iter = self.app_state.exercises().iter().enumerate(); let iter = self.app_state.exercises().iter().enumerate();
@ -237,7 +234,7 @@ impl<'a> ListState<'a> {
next_ln(stdout)?; next_ln(stdout)?;
progress_bar( progress_bar(
stdout, &mut MaxLenWriter::new(stdout, self.term_width as usize),
self.app_state.n_done(), self.app_state.n_done(),
self.app_state.exercises().len() as u16, self.app_state.exercises().len() as u16,
self.term_width, self.term_width,
@ -247,59 +244,55 @@ impl<'a> ListState<'a> {
stdout.write_all(&self.separator_line)?; stdout.write_all(&self.separator_line)?;
next_ln(stdout)?; next_ln(stdout)?;
let mut writer = MaxLenWriter::new(stdout, self.term_width as usize);
if self.message.is_empty() { if self.message.is_empty() {
// Help footer message // Help footer message
if self.selected_row.is_some() { if self.selected_row.is_some() {
stdout.write_all( writer.write_str("↓/j ↑/k home/g end/G | <c>ontinue at | <r>eset exercise")?;
"↓/j ↑/k home/g end/G | <c>ontinue at | <r>eset exercise".as_bytes(),
)?;
if self.narrow_term { if self.narrow_term {
next_ln_overwrite(stdout)?; next_ln(stdout)?;
stdout.write_all(b"filter ")?; writer = MaxLenWriter::new(stdout, self.term_width as usize);
writer.write_ascii(b"filter ")?;
} else { } else {
stdout.write_all(b" | filter ")?; writer.write_ascii(b" | filter ")?;
} }
} else { } else {
// Nothing selected (and nothing shown), so only display filter and quit. // Nothing selected (and nothing shown), so only display filter and quit.
stdout.write_all(b"filter ")?; writer.write_ascii(b"filter ")?;
} }
match self.filter { match self.filter {
Filter::Done => { Filter::Done => {
stdout writer
.stdout
.queue(SetForegroundColor(Color::Magenta))? .queue(SetForegroundColor(Color::Magenta))?
.queue(SetAttribute(Attribute::Underlined))?; .queue(SetAttribute(Attribute::Underlined))?;
stdout.write_all(b"<d>one")?; writer.write_ascii(b"<d>one")?;
stdout.queue(ResetColor)?; writer.stdout.queue(ResetColor)?;
stdout.write_all(b"/<p>ending")?; writer.write_ascii(b"/<p>ending")?;
} }
Filter::Pending => { Filter::Pending => {
stdout.write_all(b"<d>one/")?; writer.write_ascii(b"<d>one/")?;
stdout writer
.stdout
.queue(SetForegroundColor(Color::Magenta))? .queue(SetForegroundColor(Color::Magenta))?
.queue(SetAttribute(Attribute::Underlined))?; .queue(SetAttribute(Attribute::Underlined))?;
stdout.write_all(b"<p>ending")?; writer.write_ascii(b"<p>ending")?;
stdout.queue(ResetColor)?; writer.stdout.queue(ResetColor)?;
} }
Filter::None => stdout.write_all(b"<d>one/<p>ending")?, Filter::None => writer.write_ascii(b"<d>one/<p>ending")?,
} }
stdout.write_all(b" | <q>uit list")?; writer.write_ascii(b" | <q>uit list")?;
if self.narrow_term {
next_ln_overwrite(stdout)?;
} else { } else {
next_ln(stdout)?; writer.stdout.queue(SetForegroundColor(Color::Magenta))?;
} writer.write_str(&self.message)?;
} else {
stdout.queue(SetForegroundColor(Color::Magenta))?;
stdout.write_all(self.message.as_bytes())?;
stdout.queue(ResetColor)?; stdout.queue(ResetColor)?;
next_ln_overwrite(stdout)?;
if self.narrow_term {
next_ln(stdout)?; next_ln(stdout)?;
} }
}
next_ln(stdout)?;
} }
stdout.queue(EndSynchronizedUpdate)?.flush() stdout.queue(EndSynchronizedUpdate)?.flush()

View file

@ -15,9 +15,83 @@ thread_local! {
static VS_CODE: Cell<bool> = Cell::new(env::var_os("TERM_PROGRAM").is_some_and(|v| v == "vscode")); static VS_CODE: Cell<bool> = Cell::new(env::var_os("TERM_PROGRAM").is_some_and(|v| v == "vscode"));
} }
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));
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
}
}
/// Terminal progress bar to be used when not using Ratataui. /// Terminal progress bar to be used when not using Ratataui.
pub fn progress_bar( pub fn progress_bar<'a>(
stdout: &mut StdoutLock, writer: &mut impl CountedWrite<'a>,
progress: u16, progress: u16,
total: u16, total: u16,
line_width: u16, line_width: u16,
@ -32,9 +106,13 @@ pub fn progress_bar(
const MIN_LINE_WIDTH: u16 = WRAPPER_WIDTH + 4; const MIN_LINE_WIDTH: u16 = WRAPPER_WIDTH + 4;
if line_width < MIN_LINE_WIDTH { if line_width < MIN_LINE_WIDTH {
return write!(stdout, "Progress: {progress}/{total} exercises"); writer.write_ascii(b"Progress: ")?;
// Integers are in ASCII.
writer.write_ascii(format!("{progress}/{total}").as_bytes())?;
return writer.write_ascii(b" exercises");
} }
let stdout = writer.stdout();
stdout.write_all(PREFIX)?; stdout.write_all(PREFIX)?;
let width = line_width - WRAPPER_WIDTH; let width = line_width - WRAPPER_WIDTH;
@ -77,16 +155,20 @@ pub fn press_enter_prompt(stdout: &mut StdoutLock) -> io::Result<()> {
Ok(()) Ok(())
} }
pub fn terminal_file_link(stdout: &mut StdoutLock, path: &str, color: Color) -> io::Result<()> { pub fn terminal_file_link<'a>(
writer: &mut impl CountedWrite<'a>,
path: &str,
color: Color,
) -> io::Result<()> {
// VS Code shows its own links. This also avoids some issues, especially on Windows. // VS Code shows its own links. This also avoids some issues, especially on Windows.
if VS_CODE.get() { if VS_CODE.get() {
return stdout.write_all(path.as_bytes()); return writer.write_str(path);
} }
let canonical_path = fs::canonicalize(path).ok(); let canonical_path = fs::canonicalize(path).ok();
let Some(canonical_path) = canonical_path.as_deref().and_then(|p| p.to_str()) else { let Some(canonical_path) = canonical_path.as_deref().and_then(|p| p.to_str()) else {
return stdout.write_all(path.as_bytes()); return writer.write_str(path);
}; };
// Windows itself can't handle its verbatim paths. // Windows itself can't handle its verbatim paths.
@ -97,14 +179,18 @@ pub fn terminal_file_link(stdout: &mut StdoutLock, path: &str, color: Color) ->
canonical_path canonical_path
}; };
stdout writer
.stdout()
.queue(SetForegroundColor(color))? .queue(SetForegroundColor(color))?
.queue(SetAttribute(Attribute::Underlined))?; .queue(SetAttribute(Attribute::Underlined))?;
write!( writer.stdout().write_all(b"\x1b]8;;file://")?;
stdout, writer.stdout().write_all(canonical_path.as_bytes())?;
"\x1b]8;;file://{canonical_path}\x1b\\{path}\x1b]8;;\x1b\\", writer.stdout().write_all(b"\x1b\\")?;
)?; // Only this part is visible.
stdout writer.write_str(path)?;
writer.stdout().write_all(b"\x1b]8;;\x1b\\")?;
writer
.stdout()
.queue(SetForegroundColor(Color::Reset))? .queue(SetForegroundColor(Color::Reset))?
.queue(SetAttribute(Attribute::NoUnderline))?; .queue(SetAttribute(Attribute::NoUnderline))?;