• 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

7.16
/src/ui/split_screen.rs
1
use super::history::History;
2
use super::scroll_data::ScrollData;
3
use super::user_interface::TerminalSizeError;
4
use super::wrap_line;
5
use crate::io::SaveData;
6
use crate::model::{Settings, HIDE_TOPBAR};
7
use crate::{model::Line, model::Regex, ui::ansi::*, ui::printable_chars::PrintableCharsIterator};
8
use anyhow::Result;
9
use std::collections::HashSet;
10
use std::io::Write;
11
use termion::color::{self, Bg, Fg};
12
use termion::cursor;
13

14
use super::UserInterface;
15

16
const SCROLL_LIVE_BUFFER_SIZE: u16 = 10;
17
const PROMPT_HEIGHT: u16 = 1;
18
const STATUS_HEIGHT_MIN: u16 = 0;
19
const STATUS_HEIGHT_MAX: u16 = 5;
20

21
struct StatusArea {
22
    start_line: u16,
23
    width: u16,
24
    status_lines: Vec<Option<String>>,
25
    scroll_marker: bool,
26
}
27

28
impl StatusArea {
29
    fn new(height: u16, start_line: u16, width: u16) -> Self {
×
30
        let height = Self::clamp_height(height);
×
31
        Self {
×
32
            start_line,
×
33
            width,
×
34
            status_lines: vec![None; height],
×
35
            scroll_marker: false,
×
36
        }
×
37
    }
×
38

39
    fn set_scroll_marker(&mut self, value: bool) {
×
40
        self.scroll_marker = value;
×
41
    }
×
42

43
    fn clamp_height(height: u16) -> usize {
×
44
        height.clamp(STATUS_HEIGHT_MIN, STATUS_HEIGHT_MAX) as usize
×
45
    }
×
46

47
    fn clamp_index(&self, index: usize) -> usize {
×
48
        index.clamp(0, self.status_lines.len() - 1)
×
49
    }
×
50

51
    fn set_height(&mut self, height: u16, start_line: u16) {
×
52
        self.clear();
×
53
        self.status_lines.resize(Self::clamp_height(height), None);
×
54
        self.update_pos(start_line);
×
55
    }
×
56

57
    fn update_pos(&mut self, start_line: u16) {
×
58
        self.start_line = start_line;
×
59
    }
×
60

61
    fn set_width(&mut self, width: u16) {
×
62
        self.width = width;
×
63
    }
×
64

65
    fn set_status_line(&mut self, index: usize, line: String) {
×
66
        let index = self.clamp_index(index);
×
67
        if !line.trim().is_empty() {
×
68
            self.status_lines[index] = Some(line);
×
69
        } else {
×
70
            self.status_lines[index] = None;
×
71
        }
×
72
    }
×
73

74
    fn clear(&mut self) {
×
75
        self.status_lines = vec![None; self.status_lines.len()];
×
76
    }
×
77

78
    fn redraw_line(&mut self, screen: &mut impl Write, line_no: usize) -> Result<()> {
×
79
        let line_no = self.clamp_index(line_no);
×
80
        let index = self.start_line as usize + line_no;
×
81

82
        let mut info = if self.scroll_marker && line_no == 0 {
×
83
            "(more) ".to_string()
×
84
        } else {
85
            String::new()
×
86
        };
87

88
        if let Some(Some(custom_info)) = self.status_lines.get(line_no) {
×
89
            info = if info.is_empty() {
×
90
                custom_info.to_string()
×
91
            } else {
92
                format!("{info}━ {custom_info} ")
×
93
            };
94
        }
×
95

96
        if line_no == 0 || line_no == self.status_lines.len() - 1 {
×
97
            self.draw_bar(index, screen, &info)?;
×
98
        } else {
99
            self.draw_line(index, screen, &info)?;
×
100
        }
101

102
        Ok(())
×
103
    }
×
104

105
    fn redraw(&mut self, screen: &mut impl Write) -> Result<()> {
×
106
        for line in 0..self.status_lines.len() {
×
107
            self.redraw_line(screen, line)?;
×
108
        }
109
        Ok(())
×
110
    }
×
111

112
    fn draw_bar(&self, line: usize, screen: &mut impl Write, custom_info: &str) -> Result<()> {
×
113
        write!(
×
114
            screen,
×
115
            "{}{}{}",
116
            termion::cursor::Goto(1, line as u16),
×
117
            termion::clear::CurrentLine,
118
            Fg(color::Green),
119
        )?;
×
120

121
        let custom_info = if !custom_info.trim().is_empty() {
×
122
            format!(
×
123
                "━ {}{}{} ",
124
                custom_info.trim(),
×
125
                Fg(color::Reset),
126
                Fg(color::Green)
127
            )
128
        } else {
129
            "".to_string()
×
130
        };
131

132
        let info_line = Line::from(&custom_info);
×
133
        let stripped_chars = info_line.line().len() - info_line.clean_line().len();
×
134

135
        write!(
×
136
            screen,
×
137
            "{:━<1$}",
138
            &custom_info,
×
139
            self.width as usize + stripped_chars
×
140
        )?; // Print separator
×
141
        write!(screen, "{}", Fg(color::Reset))?;
×
142
        Ok(())
×
143
    }
×
144

145
    fn draw_line(&self, line: usize, screen: &mut impl Write, info: &str) -> Result<()> {
×
146
        write!(
×
147
            screen,
×
148
            "{}{}",
149
            termion::cursor::Goto(1, line as u16),
×
150
            termion::clear::CurrentLine,
151
        )?;
×
152

153
        write!(screen, "{info}")?; // Print separator
×
154
        Ok(())
×
155
    }
×
156

157
    fn height(&self) -> u16 {
×
158
        self.status_lines.len() as u16
×
159
    }
×
160
}
161

162
pub struct SplitScreen {
163
    screen: Box<dyn Write>,
164
    width: u16,
165
    height: u16,
166
    output_start_line: u16,
167
    output_line: u16,
168
    mud_prompt_line: u16,
169
    mud_prompt: Line,
170
    prompt_line: u16,
171
    status_area: StatusArea,
172
    cursor_prompt_pos: u16,
173
    history: History,
174
    scroll_data: ScrollData,
175
    connection: Option<String>,
176
    tags: HashSet<String>,
177
    prompt_input: String,
178
    prompt_input_pos: usize,
179
}
180

181
impl UserInterface for SplitScreen {
182
    fn setup(&mut self) -> Result<()> {
×
183
        self.reset()?;
×
184

185
        let settings = Settings::try_load()?;
×
186

187
        // Get params in case screen resized
188
        let (width, height) = termion::terminal_size()?;
×
189
        if width > 0 && height > 0 {
×
190
            self.width = width;
×
191
            self.height = height;
×
192
            self.output_line = height - self.status_area.height() - 2;
×
193
            self.mud_prompt_line = height - self.status_area.height() - 1;
×
194
            self.prompt_line = height;
×
195
            self.output_start_line = if settings.get(HIDE_TOPBAR)? { 1 } else { 2 };
×
196

197
            write!(
×
198
                self.screen,
×
199
                "{}{}",
200
                ScrollRegion(self.output_start_line, self.output_line),
×
201
                DisableOriginMode
202
            )
203
            .unwrap(); // Set scroll region, non origin mode
×
204
            self.redraw_top_bar()?;
×
205
            self.reset_scroll()?;
×
206
            self.redraw_status_area()?;
×
207
            self.screen.flush()?;
×
208
            write!(
×
209
                self.screen,
×
210
                "{}{}",
211
                termion::cursor::Goto(1, self.output_start_line),
×
212
                termion::cursor::Save
213
            )?;
×
214
            Ok(())
×
215
        } else {
216
            Err(TerminalSizeError.into())
×
217
        }
218
    }
×
219

220
    fn print_error(&mut self, output: &str) {
×
221
        let line = &format!("{}[!!] {}{}", Fg(color::Red), output, Fg(color::Reset));
×
222
        self.print_line(line);
×
223
    }
×
224

225
    fn print_info(&mut self, output: &str) {
×
226
        let line = &format!("[**] {output}");
×
227
        self.print_line(line);
×
228
    }
×
229

230
    fn print_output(&mut self, line: &Line) {
×
231
        //debug!("UI: {:?}", line);
232
        // Handle screen clear request from server
NEW
233
        if line.flags.screen_clear {
×
NEW
234
            self.clear_output_area().ok();
×
NEW
235
        }
×
236
        if let Some(print_line) = line.print_line() {
×
237
            if !line.is_utf8() || print_line.trim().is_empty() {
×
238
                self.print_line(print_line);
×
239
            } else {
×
240
                let mut count = 0;
×
241
                let cur_line = self.history.len();
×
242
                for l in wrap_line(print_line, self.width as usize) {
×
243
                    self.print_line(l);
×
244
                    count += 1;
×
245
                }
×
246
                if self.scroll_data.scroll_lock && count > self.height {
×
247
                    self.scroll_to(cur_line).ok();
×
248
                }
×
249
            }
250
        }
×
251
    }
×
252

253
    fn print_prompt(&mut self, prompt: &Line) {
×
254
        //debug!("UI: {:?}", prompt);
255
        self.mud_prompt = prompt.clone();
×
256
        self.redraw_prompt();
×
257
    }
×
258

259
    fn print_prompt_input(&mut self, input: &str, pos: usize) {
×
260
        // Sanity check: pos is a character index
261
        debug_assert!(pos <= input.chars().count());
×
262

263
        self.prompt_input = input.to_string();
×
264
        self.prompt_input_pos = pos;
×
265

266
        // Calculate display width up to cursor position
267
        let chars_before_cursor: String = input.chars().take(pos).collect();
×
268
        let mut cursor_display_pos = chars_before_cursor.as_str().display_width();
×
269

270
        let mut input = input;
×
271
        let width = self.width as usize;
×
272

273
        // Scroll the view when cursor goes past the visible width
274
        while input.display_width() >= width && cursor_display_pos >= width {
×
275
            let (byte_idx, skipped_width) = input.byte_index_at_display_width(width);
×
276
            if byte_idx < input.len() {
×
277
                input = input.split_at(byte_idx).1;
×
278
                cursor_display_pos -= skipped_width;
×
279
            } else {
×
280
                input = "";
×
281
                cursor_display_pos = 0;
×
282
            }
×
283
        }
284

285
        // Truncate input if it's still too wide for the display
286
        if input.display_width() >= width {
×
287
            let (byte_idx, _) = input.byte_index_at_display_width(width);
×
288
            input = input.split_at(byte_idx).0;
×
289
        }
×
290

291
        self.cursor_prompt_pos = cursor_display_pos as u16 + 1;
×
292
        write!(
×
293
            self.screen,
×
294
            "{}{}{}{}{}{}{}{}{}",
295
            termion::cursor::Save,
296
            termion::cursor::Goto(1, self.prompt_line),
×
297
            Fg(termion::color::Reset),
298
            Bg(termion::color::Reset),
299
            termion::style::Reset,
300
            termion::clear::CurrentLine,
301
            input,
302
            termion::cursor::Restore,
303
            self.goto_prompt(),
×
304
        )
305
        .unwrap();
×
306
    }
×
307

308
    fn print_send(&mut self, send: &Line) {
×
309
        if self.scroll_data.active && send.flags.source != Some("script".to_string()) {
×
310
            self.reset_scroll().ok();
×
311
        }
×
312
        if let Some(line) = send.print_line() {
×
313
            let line = &format!(
×
314
                "{}{}> {}{}",
×
315
                termion::style::Reset,
×
316
                Fg(color::LightYellow),
×
317
                line,
×
318
                Fg(color::Reset),
×
319
            );
×
320
            for line in wrap_line(line, self.width as usize) {
×
321
                self.print_line(line);
×
322
            }
×
323
        }
×
324
    }
×
325

326
    fn reset(&mut self) -> Result<()> {
×
327
        write!(self.screen, "{}{}", termion::clear::All, ResetScrollRegion)?;
×
328
        Ok(())
×
329
    }
×
330

331
    fn reset_scroll(&mut self) -> Result<()> {
×
332
        let reset_split = self.scroll_data.split;
×
333
        let reset_scroll = self.scroll_data.active;
×
334
        self.scroll_data.reset(&self.history)?;
×
335
        if reset_split {
×
336
            write!(self.screen, "{ResetScrollRegion}")?;
×
337
            write!(
×
338
                self.screen,
×
339
                "{}{}",
340
                ScrollRegion(self.output_start_line, self.output_line),
×
341
                DisableOriginMode
342
            )?;
×
343
        } else if reset_scroll {
×
344
            self.status_area.set_scroll_marker(false);
×
345
            self.status_area.redraw_line(&mut self.screen, 0)?;
×
346
        }
×
347
        self.redraw_prompt();
×
348

349
        let output_range = self.output_range();
×
350
        let output_start_index = self.history.inner.len() as i32 - output_range as i32;
×
351
        if output_start_index >= 0 {
×
352
            let output_start_index = output_start_index as usize;
×
353
            for i in 0..output_range {
×
354
                let index = output_start_index + i as usize;
×
355
                let line_no = self.output_start_line + i;
×
356
                write!(
×
357
                    self.screen,
×
358
                    "{}{}{}",
359
                    termion::cursor::Goto(1, line_no),
×
360
                    termion::clear::CurrentLine,
361
                    self.history.inner[index],
×
362
                )?;
×
363
            }
364
        } else {
365
            for line in &self.history.inner {
×
366
                write!(
×
367
                    self.screen,
×
368
                    "{}\n{}",
369
                    termion::cursor::Goto(1, self.output_line),
×
370
                    line,
371
                )?;
×
372
            }
373
        }
374
        Ok(())
×
375
    }
×
376

NEW
377
    fn clear_output_area(&mut self) -> Result<()> {
×
378
        // Clear all lines in the output scroll region
NEW
379
        for line_no in self.output_start_line..=self.output_line {
×
NEW
380
            write!(
×
NEW
381
                self.screen,
×
382
                "{}{}",
NEW
383
                termion::cursor::Goto(1, line_no),
×
384
                termion::clear::CurrentLine,
NEW
385
            )?;
×
386
        }
387
        // Clear the history buffer as well
NEW
388
        self.history.clear();
×
389
        // Reset scroll state
NEW
390
        self.scroll_data.reset(&self.history)?;
×
391
        // Reposition cursor
NEW
392
        write!(
×
NEW
393
            self.screen,
×
394
            "{}{}",
NEW
395
            termion::cursor::Goto(1, self.output_start_line),
×
NEW
396
            self.goto_prompt(),
×
NEW
397
        )?;
×
NEW
398
        Ok(())
×
NEW
399
    }
×
400

401
    fn scroll_down(&mut self) -> Result<()> {
×
402
        self.scroll_data.clamp(&self.history);
×
403
        if self.scroll_data.active {
×
404
            let output_range = self.scroll_range() as i32;
×
405
            let max_start_index: i32 = self.history.inner.len() as i32 - output_range;
×
406
            let new_start_index = self.scroll_data.pos + 5;
×
407
            if new_start_index >= max_start_index as usize {
×
408
                self.reset_scroll()?;
×
409
            } else {
410
                self.scroll_data.pos = new_start_index;
×
411
                self.draw_scroll()?;
×
412
            }
413
        }
×
414
        Ok(())
×
415
    }
×
416

417
    fn scroll_lock(&mut self, lock: bool) -> Result<()> {
×
418
        self.scroll_data.lock(lock)
×
419
    }
×
420

421
    fn scroll_to(&mut self, row: usize) -> Result<()> {
×
422
        self.scroll_data.clamp(&self.history);
×
423
        if self.history.len() > self.scroll_range() as usize {
×
424
            let max_start_index = self.history.inner.len() as i32 - self.scroll_range() as i32;
×
425
            if max_start_index > 0 && row < max_start_index as usize {
×
426
                self.init_scroll()?;
×
427
                self.scroll_data.pos = row;
×
428
                self.draw_scroll()?;
×
429
            } else {
430
                self.reset_scroll()?;
×
431
            }
432
        }
×
433
        Ok(())
×
434
    }
×
435

436
    fn scroll_top(&mut self) -> Result<()> {
×
437
        if self.history.inner.len() as u16 >= self.output_line {
×
438
            self.init_scroll()?;
×
439
            self.scroll_data.pos = 0;
×
440
            self.draw_scroll()?;
×
441
        }
×
442
        Ok(())
×
443
    }
×
444

445
    fn scroll_up(&mut self) -> Result<()> {
×
446
        self.scroll_data.clamp(&self.history);
×
447
        let output_range: usize = self.scroll_range() as usize;
×
448
        if self.history.inner.len() > output_range {
×
449
            if !self.scroll_data.active {
×
450
                self.init_scroll()?;
×
451
                self.scroll_data.pos = self.history.inner.len() - output_range;
×
452
            }
×
453
            self.scroll_data.pos -= self.scroll_data.pos.min(5);
×
454
            self.draw_scroll()?;
×
455
        }
×
456
        Ok(())
×
457
    }
×
458

459
    fn find_up(&mut self, pattern: &Regex) -> Result<()> {
×
460
        self.scroll_data.clamp(&self.history);
×
461
        let pos = if self.scroll_data.active {
×
462
            self.scroll_data.pos
×
463
        } else if self.history.len() > self.scroll_range() as usize {
×
464
            self.history.len() - self.scroll_range() as usize
×
465
        } else {
466
            self.history.len()
×
467
        };
468
        if let Some(line) = self.history.find_backward(pattern, pos) {
×
469
            self.scroll_data.hilite = Some(pattern.clone());
×
470
            self.scroll_to(0.max(line))?;
×
471
        }
×
472
        Ok(())
×
473
    }
×
474

475
    fn find_down(&mut self, pattern: &Regex) -> Result<()> {
×
476
        self.scroll_data.clamp(&self.history);
×
477
        if self.scroll_data.active {
×
478
            if let Some(line) = self
×
479
                .history
×
480
                .find_forward(pattern, self.history.len().min(self.scroll_data.pos + 1))
×
481
            {
482
                self.scroll_data.hilite = Some(pattern.clone());
×
483
                self.scroll_to(line.min(self.history.len() - 1))?;
×
484
            }
×
485
        }
×
486
        Ok(())
×
487
    }
×
488

489
    fn set_host(&mut self, host: &str, port: u16) -> Result<()> {
×
490
        self.connection = if !host.is_empty() {
×
491
            Some(format!("{host}:{port}"))
×
492
        } else {
493
            None
×
494
        };
495
        self.redraw_top_bar()
×
496
    }
×
497

498
    fn add_tag(&mut self, tag: &str) -> Result<()> {
×
499
        self.tags.insert(tag.to_string());
×
500
        self.redraw_top_bar()
×
501
    }
×
502

503
    fn remove_tag(&mut self, tag: &str) -> Result<()> {
×
504
        self.tags.remove(tag);
×
505
        self.redraw_top_bar()
×
506
    }
×
507

508
    fn clear_tags(&mut self) -> Result<()> {
×
509
        self.tags.clear();
×
510
        self.redraw_top_bar()
×
511
    }
×
512

513
    fn set_status_area_height(&mut self, height: u16) -> Result<()> {
×
514
        let height = StatusArea::clamp_height(height) as u16;
×
515
        self.status_area
×
516
            .set_height(height, self.height - height - PROMPT_HEIGHT);
×
517
        self.setup()?;
×
518
        let input_str = self.prompt_input.as_str().to_owned();
×
519
        self.print_prompt_input(&input_str, self.prompt_input_pos);
×
520
        Ok(())
×
521
    }
×
522

523
    fn set_status_line(&mut self, line: usize, info: String) -> Result<()> {
×
524
        self.status_area.set_status_line(line, info);
×
525
        self.status_area.redraw_line(&mut self.screen, line)?;
×
526
        write!(self.screen, "{}", self.goto_prompt())?;
×
527
        Ok(())
×
528
    }
×
529

530
    fn flush(&mut self) {
×
531
        self.screen.flush().unwrap();
×
532
    }
×
533

534
    fn width(&self) -> u16 {
×
535
        self.width
×
536
    }
×
537

538
    fn height(&self) -> u16 {
×
539
        self.height
×
540
    }
×
541

542
    fn destroy(mut self: Box<Self>) -> Result<(Box<dyn Write>, History)> {
×
543
        self.reset()?;
×
544
        Ok((self.screen, self.history))
×
545
    }
×
546
}
547

548
impl SplitScreen {
549
    pub fn new(screen: Box<dyn Write>, history: History) -> Result<Self> {
×
550
        let (width, height) = termion::terminal_size()?;
×
551

552
        let output_start_line = 2;
×
553
        let status_area_height = 1;
×
554
        let output_line = height - status_area_height - 2;
×
555
        let mud_prompt_line = height - status_area_height - 1;
×
556
        let prompt_line = height;
×
557

558
        let status_area = StatusArea::new(status_area_height, mud_prompt_line + 1, width);
×
559

560
        Ok(Self {
×
561
            screen,
×
562
            width,
×
563
            height,
×
564
            output_start_line,
×
565
            output_line,
×
566
            mud_prompt_line,
×
567
            mud_prompt: Line::from(""),
×
568
            status_area,
×
569
            prompt_line,
×
570
            cursor_prompt_pos: 1,
×
571
            history,
×
572
            scroll_data: ScrollData::new(),
×
573
            connection: None,
×
574
            tags: HashSet::new(),
×
575
            prompt_input: String::new(),
×
576
            prompt_input_pos: 0,
×
577
        })
×
578
    }
×
579

580
    fn print_line(&mut self, line: &str) {
×
581
        self.history.append(line);
×
582
        if self.scroll_data.not_scrolled_or_split() {
×
583
            write!(
×
584
                self.screen,
×
585
                "{}\r\n{}{}",
×
586
                termion::cursor::Goto(1, self.output_line),
×
587
                &line,
×
588
                self.goto_prompt(),
×
589
            )
×
590
            .unwrap();
×
591
        }
×
592
    }
×
593

594
    fn clear_prompt(&mut self) {
×
595
        write!(
×
596
            self.screen,
×
597
            "{}{}{}",
598
            termion::cursor::Goto(1, self.mud_prompt_line),
×
599
            termion::clear::CurrentLine,
600
            self.goto_prompt(),
×
601
        )
602
        .unwrap();
×
603
    }
×
604

605
    fn redraw_prompt(&mut self) {
×
606
        let prompt_line = self.mud_prompt.print_line().unwrap_or("");
×
607
        if self.scroll_data.not_scrolled_or_split() {
×
608
            write!(
×
609
                self.screen,
×
610
                "{}{}{}{}",
×
611
                termion::cursor::Goto(1, self.mud_prompt_line),
×
612
                termion::clear::CurrentLine,
×
613
                prompt_line,
×
614
                self.goto_prompt(),
×
615
            )
×
616
            .unwrap();
×
617
        }
×
618
    }
×
619

620
    fn redraw_top_bar(&mut self) -> Result<()> {
×
621
        if self.output_start_line > 1 {
×
622
            write!(
×
623
                self.screen,
×
624
                "{}{}{}",
625
                termion::cursor::Goto(1, 1),
626
                termion::clear::CurrentLine,
627
                Fg(color::Green),
628
            )?;
×
629
            let host = if let Some(connection) = &self.connection {
×
630
                format!("═ {connection} ")
×
631
            } else {
632
                "".to_string()
×
633
            };
634
            let mut tags = self
×
635
                .tags
×
636
                .iter()
×
637
                .map(|s| format!("[{s}]"))
×
638
                .collect::<Vec<String>>();
×
639
            tags.sort();
×
640
            let tags = tags.join("");
×
641
            let mut output = format!("{host}{tags}");
×
642
            if !output.is_empty() {
×
643
                output.push(' ');
×
644
            }
×
645
            write!(self.screen, "{:═<1$}", output, self.width as usize)?; // Print separator
×
646
            write!(self.screen, "{}{}", Fg(color::Reset), self.goto_prompt(),)?;
×
647
        }
×
648
        Ok(())
×
649
    }
×
650

651
    fn redraw_status_area(&mut self) -> Result<()> {
×
652
        self.status_area.set_width(self.width);
×
653
        self.status_area.update_pos(self.mud_prompt_line + 1);
×
654
        self.status_area.redraw(&mut self.screen)?;
×
655
        write!(self.screen, "{}", self.goto_prompt(),)?;
×
656
        Ok(())
×
657
    }
×
658

659
    fn goto_prompt(&self) -> String {
×
660
        format!(
×
661
            "{}",
662
            termion::cursor::Goto(self.cursor_prompt_pos, self.prompt_line),
×
663
        )
664
    }
×
665

666
    fn init_scroll(&mut self) -> Result<()> {
×
667
        self.scroll_data.active = true;
×
668
        if self.scroll_range() < self.output_range() {
×
669
            self.scroll_data.split = true;
×
670
            let scroll_range = self.scroll_range();
×
671
            write!(self.screen, "{ResetScrollRegion}")?;
×
672
            write!(
×
673
                self.screen,
×
674
                "{}{}",
675
                ScrollRegion(scroll_range + 3, self.output_line),
×
676
                DisableOriginMode
677
            )?;
×
678
            write!(
×
679
                self.screen,
×
680
                "{}{}{:━<4$}{}",
681
                cursor::Goto(1, scroll_range + self.output_start_line),
×
682
                color::Fg(color::Green),
683
                "━ (scroll) ",
684
                color::Fg(color::Reset),
685
                self.width as usize
×
686
            )?;
×
687
        } else {
688
            self.status_area.set_scroll_marker(true);
×
689
            self.status_area.redraw_line(&mut self.screen, 0)?;
×
690
            self.clear_prompt();
×
691
        }
692
        Ok(())
×
693
    }
×
694

695
    fn draw_scroll(&mut self) -> Result<()> {
×
696
        let output_range = self.scroll_range();
×
697
        for i in 0..output_range {
×
698
            let index = self.scroll_data.pos + i as usize;
×
699
            if index >= self.history.inner.len() {
×
700
                // History has been trimmed during scrolling
701
                // TODO: It should be possible to lock history during render perhaps?
702
                // The lock would prevent the drain function until scrolls is done.
703
                break;
×
704
            }
×
705
            let line_no = self.output_start_line + i;
×
706
            let mut line = self.history.inner[index].clone();
×
707
            if let Some(pattern) = &self.scroll_data.hilite {
×
708
                line = pattern
×
709
                    .replace_all(
×
710
                        &line,
×
711
                        format!(
×
712
                            "{}{}$0{}{}",
×
713
                            Fg(color::LightWhite),
×
714
                            Bg(color::Blue),
×
715
                            Bg(color::Reset),
×
716
                            Fg(color::Reset)
×
717
                        ),
×
718
                    )
×
719
                    .to_string();
×
720
            }
×
721
            write!(
×
722
                self.screen,
×
723
                "{}{}{}",
724
                termion::cursor::Goto(1, line_no),
×
725
                termion::clear::CurrentLine,
726
                line,
727
            )?;
×
728
        }
729
        Ok(())
×
730
    }
×
731

732
    fn scroll_range(&self) -> u16 {
×
733
        if self.scroll_data.allow_split && self.height > SCROLL_LIVE_BUFFER_SIZE * 2 {
×
734
            self.output_line - self.output_start_line - SCROLL_LIVE_BUFFER_SIZE + 1
×
735
        } else {
736
            self.output_range()
×
737
        }
738
    }
×
739

740
    fn output_range(&self) -> u16 {
×
741
        self.output_line - self.output_start_line + 1
×
742
    }
×
743
}
744

745
#[cfg(test)]
746
mod screen_test {
747
    use super::*;
748

749
    #[test]
750
    fn test_append_history() {
1✔
751
        let line = "a nice line\n\nwith a blank line\nand lines\nc\ntest\n";
1✔
752

753
        let mut history = History::new();
1✔
754
        history.append(line);
1✔
755
        assert_eq!(
1✔
756
            history.inner,
757
            vec![
1✔
758
                "a nice line",
759
                "",
1✔
760
                "with a blank line",
1✔
761
                "and lines",
1✔
762
                "c",
1✔
763
                "test",
1✔
764
            ]
765
        );
766
    }
1✔
767

768
    #[test]
769
    fn test_search_history() {
1✔
770
        let line = "a nice line\n\nwith a blank line\nand lines\nc\ntest\n";
1✔
771

772
        let mut history = History::new();
1✔
773
        history.append(line);
1✔
774
        let re = crate::model::Regex::new("and lines", None).unwrap();
1✔
775
        assert_eq!(history.find_forward(&re, 0), Some(3));
1✔
776
        assert_eq!(history.find_forward(&re, 4), None);
1✔
777
        assert_eq!(history.find_backward(&re, 4), Some(3));
1✔
778
        assert_eq!(history.find_backward(&re, 2), None);
1✔
779
    }
1✔
780

781
    #[test]
782
    fn test_drain_history() {
1✔
783
        let mut history = History::new();
1✔
784
        history.capacity = 20;
1✔
785
        history.drain_length = 10;
1✔
786
        assert!(history.is_empty());
1✔
787
        for _ in 0..19 {
19✔
788
            history.append("test");
19✔
789
        }
19✔
790
        assert_eq!(history.len(), 19);
1✔
791
        history.append("test");
1✔
792
        assert_eq!(history.len(), 10);
1✔
793
        for _ in 0..9 {
9✔
794
            history.append("test");
9✔
795
        }
9✔
796
        assert_eq!(history.len(), 19);
1✔
797
        history.append("test");
1✔
798
        assert_eq!(history.len(), 10);
1✔
799
    }
1✔
800
}
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