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

Blightmud / Blightmud / 21593863486

02 Feb 2026 02:24PM UTC coverage: 79.391% (-0.1%) from 79.502%
21593863486

push

github

web-flow
Filters ED sequences (Erase in display) from server (#1340)

ED sequences don't respect scroll regions and will mess up blightmuds UI.
To tackle this we now filter them from output and apply their effect
only to the output view.

112 of 160 new or added lines in 6 files covered. (70.0%)

1 existing line in 1 file now uncovered.

8625 of 10864 relevant lines covered (79.39%)

354.25 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)
89
        let chars_before_cursor: String = line.chars().take(pos).collect();
×
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
93
        while input.display_width() >= width && cursor_display_pos >= width {
×
94
            let (byte_idx, skipped_width) = input.byte_index_at_display_width(width);
×
95
            if byte_idx < input.len() {
×
96
                input = input.split_at(byte_idx).1;
×
97
                cursor_display_pos -= skipped_width;
×
98
            } else {
×
99
                input = "";
×
100
                cursor_display_pos = 0;
×
101
            }
×
102
        }
103

104
        // Truncate input if it's still too wide for the display
105
        if input.display_width() >= width {
×
106
            let (byte_idx, _) = input.byte_index_at_display_width(width);
×
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,
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
        // Handle screen clear request from server
NEW
195
        if line.flags.screen_clear {
×
NEW
196
            self.clear_output_area().ok();
×
NEW
197
        }
×
198
        if line.flags.separate_receives {
×
199
            if let Some(print_line) = line.print_line() {
×
200
                self.history.remove_last_if_prefix(print_line);
×
201
            }
×
202
        }
×
203
        if let Some(print_line) = line.print_line() {
×
204
            if !line.is_utf8() || print_line.trim().is_empty() {
×
205
                self.print(print_line, !line.flags.separate_receives);
×
206
            } else {
×
207
                let mut new_line = !line.flags.separate_receives;
×
208
                let mut count = 0;
×
209
                let cur_line = self.history.len();
×
210
                for l in wrap_line(print_line, self.width as usize) {
×
211
                    self.print(l, new_line);
×
212
                    new_line = true;
×
213
                    count += 1;
×
214
                }
×
215
                if self.scroll_data.scroll_lock && count > self.output_line {
×
216
                    self.scroll_to(cur_line).ok();
×
217
                }
×
218
            }
219
        }
×
220
    }
×
221

222
    fn print_prompt(&mut self, prompt: &Line) {
×
223
        if !prompt.is_empty() {
×
224
            self.print_line(prompt);
×
225
        }
×
226
    }
×
227

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

235
        // Calculate display width up to cursor position (pos is character index)
236
        let chars_before_cursor: String = input.chars().take(pos).collect();
×
237
        let mut display_pos = chars_before_cursor.as_str().display_width();
×
238

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

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

275
    fn reset(&mut self) -> Result<()> {
×
276
        write!(self.screen, "{}{}", termion::clear::All, ResetScrollRegion)?;
×
277
        Ok(())
×
278
    }
×
279

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

NEW
312
    fn clear_output_area(&mut self) -> Result<()> {
×
313
        // Clear all lines in the output area
NEW
314
        for line_no in 1..=self.output_line {
×
NEW
315
            write!(
×
NEW
316
                self.screen,
×
317
                "{}{}",
NEW
318
                cursor::Goto(1, line_no),
×
319
                clear::CurrentLine,
NEW
320
            )?;
×
321
        }
322
        // Clear the history buffer
NEW
323
        self.history.clear();
×
324
        // Reset scroll state
NEW
325
        self.scroll_data.reset(&self.history)?;
×
326
        // Reposition cursor
NEW
327
        write!(self.screen, "{}", cursor::Goto(1, self.prompt_line))?;
×
NEW
328
        Ok(())
×
NEW
329
    }
×
330

331
    fn scroll_down(&mut self) -> Result<()> {
×
332
        self.scroll_data.clamp(&self.history);
×
333
        if self.scroll_data.active {
×
334
            let output_range = self.output_line as i32;
×
335
            let max_start_index = self.history.inner.len() as i32 - output_range;
×
336
            let new_start_index = self.scroll_data.pos + 5;
×
337
            if new_start_index >= max_start_index as usize {
×
338
                self.reset_scroll()?;
×
339
            } else {
340
                self.scroll_data.pos = new_start_index;
×
341
                self.draw_scroll()?;
×
342
            }
343
        }
×
344
        Ok(())
×
345
    }
×
346

347
    fn scroll_lock(&mut self, lock: bool) -> Result<()> {
×
348
        self.scroll_data.lock(lock)
×
349
    }
×
350

351
    fn scroll_to(&mut self, row: usize) -> Result<()> {
×
352
        self.scroll_data.clamp(&self.history);
×
353
        if self.history.len() > self.output_line as usize {
×
354
            let max_start_index = self.history.inner.len() as i32 - self.output_line as i32;
×
355
            if max_start_index > 0 && row < max_start_index as usize {
×
356
                self.scroll_data.active = true;
×
357
                self.scroll_data.pos = row;
×
358
                self.draw_scroll()?;
×
359
            } else {
360
                self.reset_scroll()?;
×
361
            }
362
        }
×
363
        Ok(())
×
364
    }
×
365

366
    fn scroll_top(&mut self) -> Result<()> {
×
367
        if self.history.inner.len() as u16 >= self.output_line {
×
368
            self.scroll_data.active = true;
×
369
            self.scroll_data.pos = 0;
×
370
            self.draw_scroll()?;
×
371
        }
×
372
        Ok(())
×
373
    }
×
374

375
    fn scroll_up(&mut self) -> Result<()> {
×
376
        self.scroll_data.clamp(&self.history);
×
377
        let output_range = self.output_line as usize;
×
378
        if self.history.inner.len() > output_range {
×
379
            if !self.scroll_data.active {
×
380
                self.scroll_data.active = true;
×
381
                self.scroll_data.pos = self.history.inner.len() - output_range;
×
382
            }
×
383
            self.scroll_data.pos -= self.scroll_data.pos.min(5);
×
384
            self.draw_scroll()?;
×
385
        }
×
386
        Ok(())
×
387
    }
×
388

389
    fn find_up(&mut self, pattern: &Regex) -> Result<()> {
×
390
        self.scroll_data.clamp(&self.history);
×
391
        let scroll_range = self.output_line as usize;
×
392
        let pos = if self.scroll_data.active {
×
393
            self.scroll_data.pos
×
394
        } else if self.history.len() > scroll_range {
×
395
            self.history.len() - scroll_range
×
396
        } else {
397
            self.history.len()
×
398
        };
399
        if let Some(line) = self.history.find_backward(pattern, pos) {
×
400
            self.scroll_data.hilite = Some(pattern.clone());
×
401
            self.scroll_to(0.max(line))?;
×
402
        }
×
403
        Ok(())
×
404
    }
×
405

406
    fn find_down(&mut self, pattern: &Regex) -> Result<()> {
×
407
        self.scroll_data.clamp(&self.history);
×
408
        if self.scroll_data.active {
×
409
            if let Some(line) = self
×
410
                .history
×
411
                .find_forward(pattern, self.history.len().min(self.scroll_data.pos + 1))
×
412
            {
413
                self.scroll_data.hilite = Some(pattern.clone());
×
414
                self.scroll_to(line.min(self.history.len() - 1))?;
×
415
            }
×
416
        }
×
417
        Ok(())
×
418
    }
×
419

420
    fn set_host(&mut self, _host: &str, _port: u16) -> Result<()> {
×
421
        Ok(())
×
422
    }
×
423

424
    fn add_tag(&mut self, _: &str) -> Result<()> {
×
425
        Ok(())
×
426
    }
×
427

428
    fn remove_tag(&mut self, _: &str) -> Result<()> {
×
429
        Ok(())
×
430
    }
×
431

432
    fn clear_tags(&mut self) -> Result<()> {
×
433
        Ok(())
×
434
    }
×
435

436
    fn set_status_area_height(&mut self, _height: u16) -> Result<()> {
×
437
        Ok(())
×
438
    }
×
439

440
    fn set_status_line(&mut self, _line: usize, _info: String) -> Result<()> {
×
441
        Ok(())
×
442
    }
×
443

444
    fn flush(&mut self) {
×
445
        self.screen.flush().unwrap();
×
446
    }
×
447

448
    fn width(&self) -> u16 {
×
449
        self.width
×
450
    }
×
451

452
    fn height(&self) -> u16 {
×
453
        self.height
×
454
    }
×
455

456
    fn destroy(mut self: Box<Self>) -> Result<(Box<dyn Write>, super::history::History)> {
×
457
        self.reset()?;
×
458
        Ok((self.screen, self.history))
×
459
    }
×
460
}
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