• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Blightmud / Blightmud / 21584282226

02 Feb 2026 09:20AM UTC coverage: 74.321% (+0.1%) from 74.186%
21584282226

push

github

web-flow
Adds multicolumn character support to input prompt (#1337)

Fixes: #1320

44 of 81 new or added lines in 3 files covered. (54.32%)

1 existing line in 1 file now uncovered.

6836 of 9198 relevant lines covered (74.32%)

336.69 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/ui/reader_screen.rs
1
use std::io::Write;
2

3
use anyhow::Result;
4
use termion::{
5
    clear,
6
    cursor::{self, Goto},
7
};
8

9
use crate::{
10
    model::{Line, Regex},
11
    ui::{
12
        printable_chars::PrintableCharsIterator, DisableOriginMode, ResetScrollRegion, ScrollRegion,
13
    },
14
};
15

16
use super::{
17
    history::History, scroll_data::ScrollData, user_interface::TerminalSizeError, wrap_line,
18
    UserInterface,
19
};
20

21
pub struct ReaderScreen {
22
    screen: Box<dyn Write>,
23
    history: History,
24
    scroll_data: ScrollData,
25
    output_line: u16,
26
    prompt_line: u16,
27
    width: u16,
28
    height: u16,
29
    prompt_input: Option<(String, usize)>,
30
}
31

32
impl ReaderScreen {
33
    pub fn new(screen: Box<dyn Write>, history: History) -> Result<Self> {
×
34
        let (width, height) = termion::terminal_size()?;
×
35
        let output_line = height - 1;
×
36
        let prompt_line = height;
×
37
        let scroll_data = ScrollData::new();
×
38
        Ok(Self {
×
39
            screen,
×
40
            history,
×
41
            scroll_data,
×
42
            output_line,
×
43
            prompt_line,
×
44
            width,
×
45
            height,
×
46
            prompt_input: None,
×
47
        })
×
48
    }
×
49

50
    #[inline]
51
    fn print(&mut self, line: &str, new_line: bool) {
×
52
        self.history.append(line);
×
53
        if !self.scroll_data.active {
×
54
            write!(
×
55
                self.screen,
×
56
                "{}{}{}{}",
57
                Goto(1, self.height - 1),
×
58
                if new_line { "\n" } else { "" },
×
59
                line,
60
                Goto(1, self.height)
×
61
            )
62
            .unwrap();
×
63
        }
×
64
    }
×
65

66
    #[inline]
67
    fn print_line(&mut self, line: &Line) {
×
68
        if let Some(print_line) = &line.print_line() {
×
69
            self.history.append(print_line);
×
70
            if !self.scroll_data.active {
×
71
                writeln!(
×
72
                    self.screen,
×
73
                    "{}\n{}{}",
×
74
                    Goto(1, self.height - 1),
×
75
                    print_line,
×
76
                    Goto(1, self.height)
×
77
                )
×
78
                .unwrap();
×
79
            }
×
80
        }
×
81
    }
×
82

83
    #[inline]
84
    fn print_wrapped_prompt_input(&mut self, line: &str, pos: usize) {
×
85
        let mut input = line;
×
86
        let width = self.width as usize;
×
87

88
        // Calculate display width up to cursor position (pos is character index)
NEW
89
        let chars_before_cursor: String = line.chars().take(pos).collect();
×
NEW
90
        let mut cursor_display_pos = chars_before_cursor.as_str().display_width();
×
91

92
        // Scroll the view when cursor goes past the visible width
NEW
93
        while input.display_width() >= width && cursor_display_pos >= width {
×
NEW
94
            let (byte_idx, skipped_width) = input.byte_index_at_display_width(width);
×
NEW
95
            if byte_idx < input.len() {
×
NEW
96
                input = input.split_at(byte_idx).1;
×
NEW
97
                cursor_display_pos -= skipped_width;
×
98
            } else {
×
99
                input = "";
×
NEW
100
                cursor_display_pos = 0;
×
101
            }
×
102
        }
103

104
        // Truncate input if it's still too wide for the display
NEW
105
        if input.display_width() >= width {
×
NEW
106
            let (byte_idx, _) = input.byte_index_at_display_width(width);
×
NEW
107
            input = input.split_at(byte_idx).0;
×
108
        }
×
109

110
        write!(
×
111
            self.screen,
×
112
            "{}{}{}{}",
113
            Goto(1, self.prompt_line),
×
114
            clear::CurrentLine,
115
            input,
NEW
116
            Goto(cursor_display_pos as u16 + 1, self.prompt_line)
×
117
        )
118
        .unwrap();
×
119
    }
×
120

121
    #[inline]
122
    fn print_prompt_input_suffix(&mut self, line: &str, start: usize, end: usize) {
×
123
        write!(
×
124
            self.screen,
×
125
            "{}{}{}",
126
            Goto(start as u16 + 1, self.prompt_line),
×
127
            line,
128
            Goto(end as u16 + 1, self.prompt_line)
×
129
        )
130
        .unwrap();
×
131
    }
×
132

133
    #[inline]
134
    fn trim_prompt_input(&mut self, pos: usize) {
×
135
        write!(
×
136
            self.screen,
×
137
            "{}{}",
138
            Goto(pos as u16 + 1, self.prompt_line),
×
139
            clear::AfterCursor,
140
        )
141
        .unwrap();
×
142
    }
×
143

144
    fn draw_scroll(&mut self) -> Result<()> {
×
145
        for i in 0..self.height - 1 {
×
146
            let index = self.scroll_data.pos + i as usize;
×
147
            let line = self.history.inner[index].clone();
×
148
            write!(
×
149
                self.screen,
×
150
                "{}{}{}{}",
151
                termion::cursor::Goto(1, i + 1),
×
152
                termion::clear::CurrentLine,
153
                line,
154
                cursor::Goto(1, self.prompt_line),
×
155
            )?;
×
156
        }
157
        Ok(())
×
158
    }
×
159
}
160

161
impl UserInterface for ReaderScreen {
162
    fn setup(&mut self) -> Result<()> {
×
163
        self.reset()?;
×
164
        let (width, height) = termion::terminal_size()?;
×
165
        if width > 0 && height > 0 {
×
166
            self.output_line = height - 1;
×
167
            self.prompt_line = height;
×
168
            self.width = width;
×
169
            self.height = height;
×
170
            write!(
×
171
                self.screen,
×
172
                "{}{}{}",
173
                ScrollRegion(1, self.output_line),
×
174
                DisableOriginMode,
175
                cursor::Goto(1, self.prompt_line),
×
176
            )?;
×
177
            self.reset_scroll()?;
×
178
            self.screen.flush()?;
×
179
            Ok(())
×
180
        } else {
181
            Err(TerminalSizeError.into())
×
182
        }
183
    }
×
184

185
    fn print_error(&mut self, output: &str) {
×
186
        self.print_line(&Line::from(format!("ERROR: {output}")));
×
187
    }
×
188

189
    fn print_info(&mut self, output: &str) {
×
190
        self.print_line(&Line::from(format!("INFO: {output}")));
×
191
    }
×
192

193
    fn print_output(&mut self, line: &Line) {
×
194
        if line.flags.separate_receives {
×
195
            if let Some(print_line) = line.print_line() {
×
196
                self.history.remove_last_if_prefix(print_line);
×
197
            }
×
198
        }
×
199
        if let Some(print_line) = line.print_line() {
×
200
            if !line.is_utf8() || print_line.trim().is_empty() {
×
201
                self.print(print_line, !line.flags.separate_receives);
×
202
            } else {
×
203
                let mut new_line = !line.flags.separate_receives;
×
204
                let mut count = 0;
×
205
                let cur_line = self.history.len();
×
206
                for l in wrap_line(print_line, self.width as usize) {
×
207
                    self.print(l, new_line);
×
208
                    new_line = true;
×
209
                    count += 1;
×
210
                }
×
211
                if self.scroll_data.scroll_lock && count > self.output_line {
×
212
                    self.scroll_to(cur_line).ok();
×
213
                }
×
214
            }
215
        }
×
216
    }
×
217

218
    fn print_prompt(&mut self, prompt: &Line) {
×
219
        if !prompt.is_empty() {
×
220
            self.print_line(prompt);
×
221
        }
×
222
    }
×
223

224
    // This is fancy logic to make 'tdsr' less noisy
225
    fn print_prompt_input(&mut self, input: &str, pos: usize) {
×
226
        // Reader screens only operate on printable input characters (no term control sequences, e.g. ANSI colour).
227
        let sanitized_input = input.printable_chars().collect::<String>();
×
228
        let input = sanitized_input.as_str();
×
229
        let width = self.width as usize;
×
230

231
        // Calculate display width up to cursor position (pos is character index)
NEW
232
        let chars_before_cursor: String = input.chars().take(pos).collect();
×
NEW
233
        let mut display_pos = chars_before_cursor.as_str().display_width();
×
234

NEW
235
        if let Some((existing, orig_display_pos)) = &self.prompt_input {
×
NEW
236
            if (width - 1..width + 1).contains(&display_pos) {
×
237
                // Fall back to default behaviour when the prompt wraps
×
238
                self.print_wrapped_prompt_input(input, pos);
×
239
            } else {
×
NEW
240
                let mut orig = *orig_display_pos;
×
NEW
241
                while display_pos >= width {
×
NEW
242
                    display_pos -= width;
×
243
                    if orig >= width {
×
244
                        orig -= width;
×
245
                    }
×
246
                }
247
                if input.starts_with(existing) {
×
NEW
248
                    let suffix = input[existing.len()..].to_owned();
×
NEW
249
                    self.print_prompt_input_suffix(&suffix, orig, display_pos);
×
250
                } else if existing.starts_with(input) {
×
NEW
251
                    self.trim_prompt_input(display_pos);
×
252
                } else {
×
253
                    self.print_wrapped_prompt_input(input, pos);
×
254
                }
×
255
            }
256
        } else {
×
257
            self.print_wrapped_prompt_input(input, pos);
×
258
        }
×
NEW
259
        self.prompt_input = Some((input.to_string(), display_pos));
×
260
    }
×
261

262
    fn print_send(&mut self, send: &Line) {
×
263
        if self.scroll_data.active && send.flags.source != Some("script".to_string()) {
×
264
            self.reset_scroll().ok();
×
265
        }
×
266
        if let Some(print_line) = send.print_line() {
×
267
            self.history.append(print_line);
×
268
        }
×
269
    }
×
270

271
    fn reset(&mut self) -> Result<()> {
×
272
        write!(self.screen, "{}{}", termion::clear::All, ResetScrollRegion)?;
×
273
        Ok(())
×
274
    }
×
275

276
    fn reset_scroll(&mut self) -> Result<()> {
×
277
        self.scroll_data.reset(&self.history)?;
×
278
        let output_range = self.output_line;
×
279
        let output_start_index = self.history.inner.len() as i32 - output_range as i32;
×
280
        if output_start_index >= 0 {
×
281
            let output_start_index = output_start_index as usize;
×
282
            for i in 0..output_range {
×
283
                let index = output_start_index + i as usize;
×
284
                write!(
×
285
                    self.screen,
×
286
                    "{}{}{}{}",
287
                    cursor::Goto(1, 1 + i),
×
288
                    clear::AfterCursor,
289
                    self.history.inner[index],
×
290
                    cursor::Goto(1, self.prompt_line),
×
291
                )?;
×
292
            }
293
        } else {
294
            for line in &self.history.inner {
×
295
                write!(
×
296
                    self.screen,
×
297
                    "{}\n{}{}{}",
298
                    Goto(1, self.output_line),
×
299
                    clear::AfterCursor,
300
                    line,
301
                    cursor::Goto(1, self.prompt_line),
×
302
                )?;
×
303
            }
304
        }
305
        Ok(())
×
306
    }
×
307

308
    fn scroll_down(&mut self) -> Result<()> {
×
309
        self.scroll_data.clamp(&self.history);
×
310
        if self.scroll_data.active {
×
311
            let output_range = self.output_line as i32;
×
312
            let max_start_index = self.history.inner.len() as i32 - output_range;
×
313
            let new_start_index = self.scroll_data.pos + 5;
×
314
            if new_start_index >= max_start_index as usize {
×
315
                self.reset_scroll()?;
×
316
            } else {
317
                self.scroll_data.pos = new_start_index;
×
318
                self.draw_scroll()?;
×
319
            }
320
        }
×
321
        Ok(())
×
322
    }
×
323

324
    fn scroll_lock(&mut self, lock: bool) -> Result<()> {
×
325
        self.scroll_data.lock(lock)
×
326
    }
×
327

328
    fn scroll_to(&mut self, row: usize) -> Result<()> {
×
329
        self.scroll_data.clamp(&self.history);
×
330
        if self.history.len() > self.output_line as usize {
×
331
            let max_start_index = self.history.inner.len() as i32 - self.output_line as i32;
×
332
            if max_start_index > 0 && row < max_start_index as usize {
×
333
                self.scroll_data.active = true;
×
334
                self.scroll_data.pos = row;
×
335
                self.draw_scroll()?;
×
336
            } else {
337
                self.reset_scroll()?;
×
338
            }
339
        }
×
340
        Ok(())
×
341
    }
×
342

343
    fn scroll_top(&mut self) -> Result<()> {
×
344
        if self.history.inner.len() as u16 >= self.output_line {
×
345
            self.scroll_data.active = true;
×
346
            self.scroll_data.pos = 0;
×
347
            self.draw_scroll()?;
×
348
        }
×
349
        Ok(())
×
350
    }
×
351

352
    fn scroll_up(&mut self) -> Result<()> {
×
353
        self.scroll_data.clamp(&self.history);
×
354
        let output_range = self.output_line as usize;
×
355
        if self.history.inner.len() > output_range {
×
356
            if !self.scroll_data.active {
×
357
                self.scroll_data.active = true;
×
358
                self.scroll_data.pos = self.history.inner.len() - output_range;
×
359
            }
×
360
            self.scroll_data.pos -= self.scroll_data.pos.min(5);
×
361
            self.draw_scroll()?;
×
362
        }
×
363
        Ok(())
×
364
    }
×
365

366
    fn find_up(&mut self, pattern: &Regex) -> Result<()> {
×
367
        self.scroll_data.clamp(&self.history);
×
368
        let scroll_range = self.output_line as usize;
×
369
        let pos = if self.scroll_data.active {
×
370
            self.scroll_data.pos
×
371
        } else if self.history.len() > scroll_range {
×
372
            self.history.len() - scroll_range
×
373
        } else {
374
            self.history.len()
×
375
        };
376
        if let Some(line) = self.history.find_backward(pattern, pos) {
×
377
            self.scroll_data.hilite = Some(pattern.clone());
×
378
            self.scroll_to(0.max(line))?;
×
379
        }
×
380
        Ok(())
×
381
    }
×
382

383
    fn find_down(&mut self, pattern: &Regex) -> Result<()> {
×
384
        self.scroll_data.clamp(&self.history);
×
385
        if self.scroll_data.active {
×
386
            if let Some(line) = self
×
387
                .history
×
388
                .find_forward(pattern, self.history.len().min(self.scroll_data.pos + 1))
×
389
            {
390
                self.scroll_data.hilite = Some(pattern.clone());
×
391
                self.scroll_to(line.min(self.history.len() - 1))?;
×
392
            }
×
393
        }
×
394
        Ok(())
×
395
    }
×
396

397
    fn set_host(&mut self, _host: &str, _port: u16) -> Result<()> {
×
398
        Ok(())
×
399
    }
×
400

401
    fn add_tag(&mut self, _: &str) -> Result<()> {
×
402
        Ok(())
×
403
    }
×
404

405
    fn remove_tag(&mut self, _: &str) -> Result<()> {
×
406
        Ok(())
×
407
    }
×
408

409
    fn clear_tags(&mut self) -> Result<()> {
×
410
        Ok(())
×
411
    }
×
412

413
    fn set_status_area_height(&mut self, _height: u16) -> Result<()> {
×
414
        Ok(())
×
415
    }
×
416

417
    fn set_status_line(&mut self, _line: usize, _info: String) -> Result<()> {
×
418
        Ok(())
×
419
    }
×
420

421
    fn flush(&mut self) {
×
422
        self.screen.flush().unwrap();
×
423
    }
×
424

425
    fn width(&self) -> u16 {
×
426
        self.width
×
427
    }
×
428

429
    fn height(&self) -> u16 {
×
430
        self.height
×
431
    }
×
432

433
    fn destroy(mut self: Box<Self>) -> Result<(Box<dyn Write>, super::history::History)> {
×
434
        self.reset()?;
×
435
        Ok((self.screen, self.history))
×
436
    }
×
437
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc