Skip to content

Commit e811dd1

Browse files
committed
Fix list on terminals that don't disable line wrapping
1 parent f22700a commit e811dd1

File tree

2 files changed

+149
-70
lines changed

2 files changed

+149
-70
lines changed

src/list/state.rs

Lines changed: 51 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use std::{
1313
use crate::{
1414
app_state::AppState,
1515
exercise::Exercise,
16-
term::{progress_bar, terminal_file_link},
16+
term::{progress_bar, terminal_file_link, CountedWrite, MaxLenWriter},
1717
MAX_EXERCISE_NAME_LEN,
1818
};
1919

@@ -28,14 +28,6 @@ fn next_ln(stdout: &mut StdoutLock) -> io::Result<()> {
2828
Ok(())
2929
}
3030

31-
// Avoids having the last written char as the last displayed one when the
32-
// written width is higher than the terminal width.
33-
// Happens on the Gnome terminal for example.
34-
fn next_ln_overwrite(stdout: &mut StdoutLock) -> io::Result<()> {
35-
stdout.write_all(b" ")?;
36-
next_ln(stdout)
37-
}
38-
3931
#[derive(Copy, Clone, PartialEq, Eq)]
4032
pub enum Filter {
4133
Done,
@@ -164,40 +156,44 @@ impl<'a> ListState<'a> {
164156
.skip(self.row_offset)
165157
.take(self.max_n_rows_to_display)
166158
{
159+
let mut writer = MaxLenWriter::new(stdout, self.term_width as usize);
160+
167161
if self.selected_row == Some(self.row_offset + n_displayed_rows) {
168-
stdout.queue(SetBackgroundColor(Color::Rgb {
162+
writer.stdout.queue(SetBackgroundColor(Color::Rgb {
169163
r: 40,
170164
g: 40,
171165
b: 40,
172166
}))?;
173-
stdout.write_all("🦀".as_bytes())?;
167+
// The crab emoji has the width of two ascii chars.
168+
writer.add_to_len(2);
169+
writer.stdout.write_all("🦀".as_bytes())?;
174170
} else {
175-
stdout.write_all(b" ")?;
171+
writer.write_ascii(b" ")?;
176172
}
177173

178174
if exercise_ind == current_exercise_ind {
179-
stdout.queue(SetForegroundColor(Color::Red))?;
180-
stdout.write_all(b">>>>>>> ")?;
175+
writer.stdout.queue(SetForegroundColor(Color::Red))?;
176+
writer.write_ascii(b">>>>>>> ")?;
181177
} else {
182-
stdout.write_all(b" ")?;
178+
writer.write_ascii(b" ")?;
183179
}
184180

185181
if exercise.done {
186-
stdout.queue(SetForegroundColor(Color::Green))?;
187-
stdout.write_all(b"DONE ")?;
182+
writer.stdout.queue(SetForegroundColor(Color::Green))?;
183+
writer.write_ascii(b"DONE ")?;
188184
} else {
189-
stdout.queue(SetForegroundColor(Color::Yellow))?;
190-
stdout.write_all(b"PENDING ")?;
185+
writer.stdout.queue(SetForegroundColor(Color::Yellow))?;
186+
writer.write_ascii(b"PENDING ")?;
191187
}
192188

193-
stdout.queue(SetForegroundColor(Color::Reset))?;
189+
writer.stdout.queue(SetForegroundColor(Color::Reset))?;
194190

195-
stdout.write_all(exercise.name.as_bytes())?;
196-
stdout.write_all(&SPACE[..self.name_col_width + 2 - exercise.name.len()])?;
191+
writer.write_str(exercise.name)?;
192+
writer.write_ascii(&SPACE[..self.name_col_width + 2 - exercise.name.len()])?;
197193

198-
terminal_file_link(stdout, exercise.path, Color::Blue)?;
194+
terminal_file_link(&mut writer, exercise.path, Color::Blue)?;
199195

200-
next_ln_overwrite(stdout)?;
196+
next_ln(stdout)?;
201197
stdout.queue(ResetColor)?;
202198
n_displayed_rows += 1;
203199
}
@@ -213,10 +209,11 @@ impl<'a> ListState<'a> {
213209
stdout.queue(BeginSynchronizedUpdate)?.queue(MoveTo(0, 0))?;
214210

215211
// Header
216-
stdout.write_all(b" Current State Name")?;
217-
stdout.write_all(&SPACE[..self.name_col_width - 2])?;
218-
stdout.write_all(b"Path")?;
219-
next_ln_overwrite(stdout)?;
212+
let mut writer = MaxLenWriter::new(stdout, self.term_width as usize);
213+
writer.write_ascii(b" Current State Name")?;
214+
writer.write_ascii(&SPACE[..self.name_col_width - 2])?;
215+
writer.write_ascii(b"Path")?;
216+
next_ln(stdout)?;
220217

221218
// Rows
222219
let iter = self.app_state.exercises().iter().enumerate();
@@ -237,7 +234,7 @@ impl<'a> ListState<'a> {
237234
next_ln(stdout)?;
238235

239236
progress_bar(
240-
stdout,
237+
&mut MaxLenWriter::new(stdout, self.term_width as usize),
241238
self.app_state.n_done(),
242239
self.app_state.exercises().len() as u16,
243240
self.term_width,
@@ -247,59 +244,55 @@ impl<'a> ListState<'a> {
247244
stdout.write_all(&self.separator_line)?;
248245
next_ln(stdout)?;
249246

247+
let mut writer = MaxLenWriter::new(stdout, self.term_width as usize);
250248
if self.message.is_empty() {
251249
// Help footer message
252250
if self.selected_row.is_some() {
253-
stdout.write_all(
254-
"↓/j ↑/k home/g end/G | <c>ontinue at | <r>eset exercise".as_bytes(),
255-
)?;
251+
writer.write_str("↓/j ↑/k home/g end/G | <c>ontinue at | <r>eset exercise")?;
256252
if self.narrow_term {
257-
next_ln_overwrite(stdout)?;
258-
stdout.write_all(b"filter ")?;
253+
next_ln(stdout)?;
254+
writer = MaxLenWriter::new(stdout, self.term_width as usize);
255+
256+
writer.write_ascii(b"filter ")?;
259257
} else {
260-
stdout.write_all(b" | filter ")?;
258+
writer.write_ascii(b" | filter ")?;
261259
}
262260
} else {
263261
// Nothing selected (and nothing shown), so only display filter and quit.
264-
stdout.write_all(b"filter ")?;
262+
writer.write_ascii(b"filter ")?;
265263
}
266264

267265
match self.filter {
268266
Filter::Done => {
269-
stdout
267+
writer
268+
.stdout
270269
.queue(SetForegroundColor(Color::Magenta))?
271270
.queue(SetAttribute(Attribute::Underlined))?;
272-
stdout.write_all(b"<d>one")?;
273-
stdout.queue(ResetColor)?;
274-
stdout.write_all(b"/<p>ending")?;
271+
writer.write_ascii(b"<d>one")?;
272+
writer.stdout.queue(ResetColor)?;
273+
writer.write_ascii(b"/<p>ending")?;
275274
}
276275
Filter::Pending => {
277-
stdout.write_all(b"<d>one/")?;
278-
stdout
276+
writer.write_ascii(b"<d>one/")?;
277+
writer
278+
.stdout
279279
.queue(SetForegroundColor(Color::Magenta))?
280280
.queue(SetAttribute(Attribute::Underlined))?;
281-
stdout.write_all(b"<p>ending")?;
282-
stdout.queue(ResetColor)?;
281+
writer.write_ascii(b"<p>ending")?;
282+
writer.stdout.queue(ResetColor)?;
283283
}
284-
Filter::None => stdout.write_all(b"<d>one/<p>ending")?,
284+
Filter::None => writer.write_ascii(b"<d>one/<p>ending")?,
285285
}
286286

287-
stdout.write_all(b" | <q>uit list")?;
288-
289-
if self.narrow_term {
290-
next_ln_overwrite(stdout)?;
291-
} else {
292-
next_ln(stdout)?;
293-
}
287+
writer.write_ascii(b" | <q>uit list")?;
294288
} else {
295-
stdout.queue(SetForegroundColor(Color::Magenta))?;
296-
stdout.write_all(self.message.as_bytes())?;
289+
writer.stdout.queue(SetForegroundColor(Color::Magenta))?;
290+
writer.write_str(&self.message)?;
297291
stdout.queue(ResetColor)?;
298-
next_ln_overwrite(stdout)?;
299-
if self.narrow_term {
300-
next_ln(stdout)?;
301-
}
292+
next_ln(stdout)?;
302293
}
294+
295+
next_ln(stdout)?;
303296
}
304297

305298
stdout.queue(EndSynchronizedUpdate)?.flush()

src/term.rs

Lines changed: 98 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,83 @@ thread_local! {
1515
static VS_CODE: Cell<bool> = Cell::new(env::var_os("TERM_PROGRAM").is_some_and(|v| v == "vscode"));
1616
}
1717

18+
pub struct MaxLenWriter<'a, 'b> {
19+
pub stdout: &'a mut StdoutLock<'b>,
20+
len: usize,
21+
max_len: usize,
22+
}
23+
24+
impl<'a, 'b> MaxLenWriter<'a, 'b> {
25+
#[inline]
26+
pub fn new(stdout: &'a mut StdoutLock<'b>, max_len: usize) -> Self {
27+
Self {
28+
stdout,
29+
len: 0,
30+
max_len,
31+
}
32+
}
33+
34+
// Additional is for emojis that take more space.
35+
#[inline]
36+
pub fn add_to_len(&mut self, additional: usize) {
37+
self.len += additional;
38+
}
39+
}
40+
41+
pub trait CountedWrite<'a> {
42+
fn write_ascii(&mut self, ascii: &[u8]) -> io::Result<()>;
43+
fn write_str(&mut self, unicode: &str) -> io::Result<()>;
44+
fn stdout(&mut self) -> &mut StdoutLock<'a>;
45+
}
46+
47+
impl<'a, 'b> CountedWrite<'b> for MaxLenWriter<'a, 'b> {
48+
fn write_ascii(&mut self, ascii: &[u8]) -> io::Result<()> {
49+
let n = ascii.len().min(self.max_len.saturating_sub(self.len));
50+
self.stdout.write_all(&ascii[..n])?;
51+
self.len += n;
52+
Ok(())
53+
}
54+
55+
fn write_str(&mut self, unicode: &str) -> io::Result<()> {
56+
if let Some((ind, c)) = unicode
57+
.char_indices()
58+
.take(self.max_len.saturating_sub(self.len))
59+
.last()
60+
{
61+
self.stdout
62+
.write_all(&unicode.as_bytes()[..ind + c.len_utf8()])?;
63+
self.len += ind + 1;
64+
}
65+
66+
Ok(())
67+
}
68+
69+
#[inline]
70+
fn stdout(&mut self) -> &mut StdoutLock<'b> {
71+
self.stdout
72+
}
73+
}
74+
75+
impl<'a> CountedWrite<'a> for StdoutLock<'a> {
76+
#[inline]
77+
fn write_ascii(&mut self, ascii: &[u8]) -> io::Result<()> {
78+
self.write_all(ascii)
79+
}
80+
81+
#[inline]
82+
fn write_str(&mut self, unicode: &str) -> io::Result<()> {
83+
self.write_all(unicode.as_bytes())
84+
}
85+
86+
#[inline]
87+
fn stdout(&mut self) -> &mut StdoutLock<'a> {
88+
self
89+
}
90+
}
91+
1892
/// Terminal progress bar to be used when not using Ratataui.
19-
pub fn progress_bar(
20-
stdout: &mut StdoutLock,
93+
pub fn progress_bar<'a>(
94+
writer: &mut impl CountedWrite<'a>,
2195
progress: u16,
2296
total: u16,
2397
line_width: u16,
@@ -32,9 +106,13 @@ pub fn progress_bar(
32106
const MIN_LINE_WIDTH: u16 = WRAPPER_WIDTH + 4;
33107

34108
if line_width < MIN_LINE_WIDTH {
35-
return write!(stdout, "Progress: {progress}/{total} exercises");
109+
writer.write_ascii(b"Progress: ")?;
110+
// Integers are in ASCII.
111+
writer.write_ascii(format!("{progress}/{total}").as_bytes())?;
112+
return writer.write_ascii(b" exercises");
36113
}
37114

115+
let stdout = writer.stdout();
38116
stdout.write_all(PREFIX)?;
39117

40118
let width = line_width - WRAPPER_WIDTH;
@@ -77,16 +155,20 @@ pub fn press_enter_prompt(stdout: &mut StdoutLock) -> io::Result<()> {
77155
Ok(())
78156
}
79157

80-
pub fn terminal_file_link(stdout: &mut StdoutLock, path: &str, color: Color) -> io::Result<()> {
158+
pub fn terminal_file_link<'a>(
159+
writer: &mut impl CountedWrite<'a>,
160+
path: &str,
161+
color: Color,
162+
) -> io::Result<()> {
81163
// VS Code shows its own links. This also avoids some issues, especially on Windows.
82164
if VS_CODE.get() {
83-
return stdout.write_all(path.as_bytes());
165+
return writer.write_str(path);
84166
}
85167

86168
let canonical_path = fs::canonicalize(path).ok();
87169

88170
let Some(canonical_path) = canonical_path.as_deref().and_then(|p| p.to_str()) else {
89-
return stdout.write_all(path.as_bytes());
171+
return writer.write_str(path);
90172
};
91173

92174
// Windows itself can't handle its verbatim paths.
@@ -97,14 +179,18 @@ pub fn terminal_file_link(stdout: &mut StdoutLock, path: &str, color: Color) ->
97179
canonical_path
98180
};
99181

100-
stdout
182+
writer
183+
.stdout()
101184
.queue(SetForegroundColor(color))?
102185
.queue(SetAttribute(Attribute::Underlined))?;
103-
write!(
104-
stdout,
105-
"\x1b]8;;file://{canonical_path}\x1b\\{path}\x1b]8;;\x1b\\",
106-
)?;
107-
stdout
186+
writer.stdout().write_all(b"\x1b]8;;file://")?;
187+
writer.stdout().write_all(canonical_path.as_bytes())?;
188+
writer.stdout().write_all(b"\x1b\\")?;
189+
// Only this part is visible.
190+
writer.write_str(path)?;
191+
writer.stdout().write_all(b"\x1b]8;;\x1b\\")?;
192+
writer
193+
.stdout()
108194
.queue(SetForegroundColor(Color::Reset))?
109195
.queue(SetAttribute(Attribute::NoUnderline))?;
110196

0 commit comments

Comments
 (0)