summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormo8it <mo8it@proton.me>2024-08-26 04:29:58 +0200
committermo8it <mo8it@proton.me>2024-08-26 04:29:58 +0200
commite811dd15b56d839b0e43e51eeaea1a2a700c0ebb (patch)
tree512ba0253516af4fd15540880f0816e96ff81279
parentf22700a4eca613f1b3cbbd6f8b3bd4fc37569039 (diff)
Fix list on terminals that don't disable line wrapping
-rw-r--r--src/list/state.rs109
-rw-r--r--src/term.rs110
2 files changed, 149 insertions, 70 deletions
diff --git a/src/list/state.rs b/src/list/state.rs
index cc56346..3876884 100644
--- a/src/list/state.rs
+++ b/src/list/state.rs
@@ -13,7 +13,7 @@ use std::{
use crate::{
app_state::AppState,
exercise::Exercise,
- term::{progress_bar, terminal_file_link},
+ term::{progress_bar, terminal_file_link, CountedWrite, MaxLenWriter},
MAX_EXERCISE_NAME_LEN,
};
@@ -28,14 +28,6 @@ fn next_ln(stdout: &mut StdoutLock) -> io::Result<()> {
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,
@@ -164,40 +156,44 @@ impl<'a> ListState<'a> {
.skip(self.row_offset)
.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) {
- stdout.queue(SetBackgroundColor(Color::Rgb {
+ writer.stdout.queue(SetBackgroundColor(Color::Rgb {
r: 40,
g: 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 {
- stdout.write_all(b" ")?;
+ writer.write_ascii(b" ")?;
}
if exercise_ind == current_exercise_ind {
- stdout.queue(SetForegroundColor(Color::Red))?;
- stdout.write_all(b">>>>>>> ")?;
+ writer.stdout.queue(SetForegroundColor(Color::Red))?;
+ writer.write_ascii(b">>>>>>> ")?;
} else {
- stdout.write_all(b" ")?;
+ writer.write_ascii(b" ")?;
}
if exercise.done {
- stdout.queue(SetForegroundColor(Color::Green))?;
- stdout.write_all(b"DONE ")?;
+ writer.stdout.queue(SetForegroundColor(Color::Green))?;
+ writer.write_ascii(b"DONE ")?;
} else {
- stdout.queue(SetForegroundColor(Color::Yellow))?;
- stdout.write_all(b"PENDING ")?;
+ writer.stdout.queue(SetForegroundColor(Color::Yellow))?;
+ writer.write_ascii(b"PENDING ")?;
}
- stdout.queue(SetForegroundColor(Color::Reset))?;
+ writer.stdout.queue(SetForegroundColor(Color::Reset))?;
- stdout.write_all(exercise.name.as_bytes())?;
- stdout.write_all(&SPACE[..self.name_col_width + 2 - exercise.name.len()])?;
+ writer.write_str(exercise.name)?;
+ 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)?;
n_displayed_rows += 1;
}
@@ -213,10 +209,11 @@ impl<'a> ListState<'a> {
stdout.queue(BeginSynchronizedUpdate)?.queue(MoveTo(0, 0))?;
// Header
- stdout.write_all(b" Current State Name")?;
- stdout.write_all(&SPACE[..self.name_col_width - 2])?;
- stdout.write_all(b"Path")?;
- next_ln_overwrite(stdout)?;
+ let mut writer = MaxLenWriter::new(stdout, self.term_width as usize);
+ writer.write_ascii(b" Current State Name")?;
+ writer.write_ascii(&SPACE[..self.name_col_width - 2])?;
+ writer.write_ascii(b"Path")?;
+ next_ln(stdout)?;
// Rows
let iter = self.app_state.exercises().iter().enumerate();
@@ -237,7 +234,7 @@ impl<'a> ListState<'a> {
next_ln(stdout)?;
progress_bar(
- stdout,
+ &mut MaxLenWriter::new(stdout, self.term_width as usize),
self.app_state.n_done(),
self.app_state.exercises().len() as u16,
self.term_width,
@@ -247,59 +244,55 @@ impl<'a> ListState<'a> {
stdout.write_all(&self.separator_line)?;
next_ln(stdout)?;
+ let mut writer = MaxLenWriter::new(stdout, self.term_width as usize);
if self.message.is_empty() {
// Help footer message
if self.selected_row.is_some() {
- stdout.write_all(
- "↓/j ↑/k home/g end/G | <c>ontinue at | <r>eset exercise".as_bytes(),
- )?;
+ writer.write_str("↓/j ↑/k home/g end/G | <c>ontinue at | <r>eset exercise")?;
if self.narrow_term {
- next_ln_overwrite(stdout)?;
- stdout.write_all(b"filter ")?;
+ next_ln(stdout)?;
+ writer = MaxLenWriter::new(stdout, self.term_width as usize);
+
+ writer.write_ascii(b"filter ")?;
} else {
- stdout.write_all(b" | filter ")?;
+ writer.write_ascii(b" | filter ")?;
}
} else {
// Nothing selected (and nothing shown), so only display filter and quit.
- stdout.write_all(b"filter ")?;
+ writer.write_ascii(b"filter ")?;
}
match self.filter {
Filter::Done => {
- stdout
+ writer
+ .stdout
.queue(SetForegroundColor(Color::Magenta))?
.queue(SetAttribute(Attribute::Underlined))?;
- stdout.write_all(b"<d>one")?;
- stdout.queue(ResetColor)?;
- stdout.write_all(b"/<p>ending")?;
+ writer.write_ascii(b"<d>one")?;
+ writer.stdout.queue(ResetColor)?;
+ writer.write_ascii(b"/<p>ending")?;
}
Filter::Pending => {
- stdout.write_all(b"<d>one/")?;
- stdout
+ writer.write_ascii(b"<d>one/")?;
+ writer
+ .stdout
.queue(SetForegroundColor(Color::Magenta))?
.queue(SetAttribute(Attribute::Underlined))?;
- stdout.write_all(b"<p>ending")?;
- stdout.queue(ResetColor)?;
+ writer.write_ascii(b"<p>ending")?;
+ 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")?;
-
- if self.narrow_term {
- next_ln_overwrite(stdout)?;
- } else {
- next_ln(stdout)?;
- }
+ writer.write_ascii(b" | <q>uit list")?;
} else {
- stdout.queue(SetForegroundColor(Color::Magenta))?;
- stdout.write_all(self.message.as_bytes())?;
+ writer.stdout.queue(SetForegroundColor(Color::Magenta))?;
+ writer.write_str(&self.message)?;
stdout.queue(ResetColor)?;
- next_ln_overwrite(stdout)?;
- if self.narrow_term {
- next_ln(stdout)?;
- }
+ next_ln(stdout)?;
}
+
+ next_ln(stdout)?;
}
stdout.queue(EndSynchronizedUpdate)?.flush()
diff --git a/src/term.rs b/src/term.rs
index 9242919..51fcad1 100644
--- a/src/term.rs
+++ b/src/term.rs
@@ -15,9 +15,83 @@ thread_local! {
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.
-pub fn progress_bar(
- stdout: &mut StdoutLock,
+pub fn progress_bar<'a>(
+ writer: &mut impl CountedWrite<'a>,
progress: u16,
total: u16,
line_width: u16,
@@ -32,9 +106,13 @@ pub fn progress_bar(
const MIN_LINE_WIDTH: u16 = WRAPPER_WIDTH + 4;
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)?;
let width = line_width - WRAPPER_WIDTH;
@@ -77,16 +155,20 @@ pub fn press_enter_prompt(stdout: &mut StdoutLock) -> io::Result<()> {
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.
if VS_CODE.get() {
- return stdout.write_all(path.as_bytes());
+ return writer.write_str(path);
}
let canonical_path = fs::canonicalize(path).ok();
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.
@@ -97,14 +179,18 @@ pub fn terminal_file_link(stdout: &mut StdoutLock, path: &str, color: Color) ->
canonical_path
};
- stdout
+ writer
+ .stdout()
.queue(SetForegroundColor(color))?
.queue(SetAttribute(Attribute::Underlined))?;
- write!(
- stdout,
- "\x1b]8;;file://{canonical_path}\x1b\\{path}\x1b]8;;\x1b\\",
- )?;
- stdout
+ writer.stdout().write_all(b"\x1b]8;;file://")?;
+ writer.stdout().write_all(canonical_path.as_bytes())?;
+ writer.stdout().write_all(b"\x1b\\")?;
+ // Only this part is visible.
+ writer.write_str(path)?;
+ writer.stdout().write_all(b"\x1b]8;;\x1b\\")?;
+ writer
+ .stdout()
.queue(SetForegroundColor(Color::Reset))?
.queue(SetAttribute(Attribute::NoUnderline))?;