mirror of
https://github.com/notohh/rustlings.git
synced 2024-12-17 22:58:08 -05:00
Fix list on terminals that don't disable line wrapping
This commit is contained in:
parent
f22700a4ec
commit
e811dd15b5
2 changed files with 149 additions and 70 deletions
|
@ -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 {
|
|
||||||
next_ln(stdout)?;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
stdout.queue(SetForegroundColor(Color::Magenta))?;
|
writer.stdout.queue(SetForegroundColor(Color::Magenta))?;
|
||||||
stdout.write_all(self.message.as_bytes())?;
|
writer.write_str(&self.message)?;
|
||||||
stdout.queue(ResetColor)?;
|
stdout.queue(ResetColor)?;
|
||||||
next_ln_overwrite(stdout)?;
|
next_ln(stdout)?;
|
||||||
if self.narrow_term {
|
|
||||||
next_ln(stdout)?;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
next_ln(stdout)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
stdout.queue(EndSynchronizedUpdate)?.flush()
|
stdout.queue(EndSynchronizedUpdate)?.flush()
|
||||||
|
|
110
src/term.rs
110
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"));
|
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))?;
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue