• 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

7.39
/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
        if let Some(print_line) = line.print_line() {
×
233
            if !line.is_utf8() || print_line.trim().is_empty() {
×
234
                self.print_line(print_line);
×
235
            } else {
×
236
                let mut count = 0;
×
237
                let cur_line = self.history.len();
×
238
                for l in wrap_line(print_line, self.width as usize) {
×
239
                    self.print_line(l);
×
240
                    count += 1;
×
241
                }
×
242
                if self.scroll_data.scroll_lock && count > self.height {
×
243
                    self.scroll_to(cur_line).ok();
×
244
                }
×
245
            }
246
        }
×
247
    }
×
248

249
    fn print_prompt(&mut self, prompt: &Line) {
×
250
        //debug!("UI: {:?}", prompt);
251
        self.mud_prompt = prompt.clone();
×
252
        self.redraw_prompt();
×
253
    }
×
254

255
    fn print_prompt_input(&mut self, input: &str, pos: usize) {
×
256
        // Sanity check: pos is a character index
NEW
257
        debug_assert!(pos <= input.chars().count());
×
258

259
        self.prompt_input = input.to_string();
×
260
        self.prompt_input_pos = pos;
×
261

262
        // Calculate display width up to cursor position
NEW
263
        let chars_before_cursor: String = input.chars().take(pos).collect();
×
NEW
264
        let mut cursor_display_pos = chars_before_cursor.as_str().display_width();
×
265

266
        let mut input = input;
×
267
        let width = self.width as usize;
×
268

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

281
        // Truncate input if it's still too wide for the display
NEW
282
        if input.display_width() >= width {
×
NEW
283
            let (byte_idx, _) = input.byte_index_at_display_width(width);
×
NEW
284
            input = input.split_at(byte_idx).0;
×
285
        }
×
286

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

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

322
    fn reset(&mut self) -> Result<()> {
×
323
        write!(self.screen, "{}{}", termion::clear::All, ResetScrollRegion)?;
×
324
        Ok(())
×
325
    }
×
326

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

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

373
    fn scroll_down(&mut self) -> Result<()> {
×
374
        self.scroll_data.clamp(&self.history);
×
375
        if self.scroll_data.active {
×
376
            let output_range = self.scroll_range() as i32;
×
377
            let max_start_index: i32 = self.history.inner.len() as i32 - output_range;
×
378
            let new_start_index = self.scroll_data.pos + 5;
×
379
            if new_start_index >= max_start_index as usize {
×
380
                self.reset_scroll()?;
×
381
            } else {
382
                self.scroll_data.pos = new_start_index;
×
383
                self.draw_scroll()?;
×
384
            }
385
        }
×
386
        Ok(())
×
387
    }
×
388

389
    fn scroll_lock(&mut self, lock: bool) -> Result<()> {
×
390
        self.scroll_data.lock(lock)
×
391
    }
×
392

393
    fn scroll_to(&mut self, row: usize) -> Result<()> {
×
394
        self.scroll_data.clamp(&self.history);
×
395
        if self.history.len() > self.scroll_range() as usize {
×
396
            let max_start_index = self.history.inner.len() as i32 - self.scroll_range() as i32;
×
397
            if max_start_index > 0 && row < max_start_index as usize {
×
398
                self.init_scroll()?;
×
399
                self.scroll_data.pos = row;
×
400
                self.draw_scroll()?;
×
401
            } else {
402
                self.reset_scroll()?;
×
403
            }
404
        }
×
405
        Ok(())
×
406
    }
×
407

408
    fn scroll_top(&mut self) -> Result<()> {
×
409
        if self.history.inner.len() as u16 >= self.output_line {
×
410
            self.init_scroll()?;
×
411
            self.scroll_data.pos = 0;
×
412
            self.draw_scroll()?;
×
413
        }
×
414
        Ok(())
×
415
    }
×
416

417
    fn scroll_up(&mut self) -> Result<()> {
×
418
        self.scroll_data.clamp(&self.history);
×
419
        let output_range: usize = self.scroll_range() as usize;
×
420
        if self.history.inner.len() > output_range {
×
421
            if !self.scroll_data.active {
×
422
                self.init_scroll()?;
×
423
                self.scroll_data.pos = self.history.inner.len() - output_range;
×
424
            }
×
425
            self.scroll_data.pos -= self.scroll_data.pos.min(5);
×
426
            self.draw_scroll()?;
×
427
        }
×
428
        Ok(())
×
429
    }
×
430

431
    fn find_up(&mut self, pattern: &Regex) -> Result<()> {
×
432
        self.scroll_data.clamp(&self.history);
×
433
        let pos = if self.scroll_data.active {
×
434
            self.scroll_data.pos
×
435
        } else if self.history.len() > self.scroll_range() as usize {
×
436
            self.history.len() - self.scroll_range() as usize
×
437
        } else {
438
            self.history.len()
×
439
        };
440
        if let Some(line) = self.history.find_backward(pattern, pos) {
×
441
            self.scroll_data.hilite = Some(pattern.clone());
×
442
            self.scroll_to(0.max(line))?;
×
443
        }
×
444
        Ok(())
×
445
    }
×
446

447
    fn find_down(&mut self, pattern: &Regex) -> Result<()> {
×
448
        self.scroll_data.clamp(&self.history);
×
449
        if self.scroll_data.active {
×
450
            if let Some(line) = self
×
451
                .history
×
452
                .find_forward(pattern, self.history.len().min(self.scroll_data.pos + 1))
×
453
            {
454
                self.scroll_data.hilite = Some(pattern.clone());
×
455
                self.scroll_to(line.min(self.history.len() - 1))?;
×
456
            }
×
457
        }
×
458
        Ok(())
×
459
    }
×
460

461
    fn set_host(&mut self, host: &str, port: u16) -> Result<()> {
×
462
        self.connection = if !host.is_empty() {
×
463
            Some(format!("{host}:{port}"))
×
464
        } else {
465
            None
×
466
        };
467
        self.redraw_top_bar()
×
468
    }
×
469

470
    fn add_tag(&mut self, tag: &str) -> Result<()> {
×
471
        self.tags.insert(tag.to_string());
×
472
        self.redraw_top_bar()
×
473
    }
×
474

475
    fn remove_tag(&mut self, tag: &str) -> Result<()> {
×
476
        self.tags.remove(tag);
×
477
        self.redraw_top_bar()
×
478
    }
×
479

480
    fn clear_tags(&mut self) -> Result<()> {
×
481
        self.tags.clear();
×
482
        self.redraw_top_bar()
×
483
    }
×
484

485
    fn set_status_area_height(&mut self, height: u16) -> Result<()> {
×
486
        let height = StatusArea::clamp_height(height) as u16;
×
487
        self.status_area
×
488
            .set_height(height, self.height - height - PROMPT_HEIGHT);
×
489
        self.setup()?;
×
490
        let input_str = self.prompt_input.as_str().to_owned();
×
491
        self.print_prompt_input(&input_str, self.prompt_input_pos);
×
492
        Ok(())
×
493
    }
×
494

495
    fn set_status_line(&mut self, line: usize, info: String) -> Result<()> {
×
496
        self.status_area.set_status_line(line, info);
×
497
        self.status_area.redraw_line(&mut self.screen, line)?;
×
498
        write!(self.screen, "{}", self.goto_prompt())?;
×
499
        Ok(())
×
500
    }
×
501

502
    fn flush(&mut self) {
×
503
        self.screen.flush().unwrap();
×
504
    }
×
505

506
    fn width(&self) -> u16 {
×
507
        self.width
×
508
    }
×
509

510
    fn height(&self) -> u16 {
×
511
        self.height
×
512
    }
×
513

514
    fn destroy(mut self: Box<Self>) -> Result<(Box<dyn Write>, History)> {
×
515
        self.reset()?;
×
516
        Ok((self.screen, self.history))
×
517
    }
×
518
}
519

520
impl SplitScreen {
521
    pub fn new(screen: Box<dyn Write>, history: History) -> Result<Self> {
×
522
        let (width, height) = termion::terminal_size()?;
×
523

524
        let output_start_line = 2;
×
525
        let status_area_height = 1;
×
526
        let output_line = height - status_area_height - 2;
×
527
        let mud_prompt_line = height - status_area_height - 1;
×
528
        let prompt_line = height;
×
529

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

532
        Ok(Self {
×
533
            screen,
×
534
            width,
×
535
            height,
×
536
            output_start_line,
×
537
            output_line,
×
538
            mud_prompt_line,
×
539
            mud_prompt: Line::from(""),
×
540
            status_area,
×
541
            prompt_line,
×
542
            cursor_prompt_pos: 1,
×
543
            history,
×
544
            scroll_data: ScrollData::new(),
×
545
            connection: None,
×
546
            tags: HashSet::new(),
×
547
            prompt_input: String::new(),
×
548
            prompt_input_pos: 0,
×
549
        })
×
550
    }
×
551

552
    fn print_line(&mut self, line: &str) {
×
553
        self.history.append(line);
×
554
        if self.scroll_data.not_scrolled_or_split() {
×
555
            write!(
×
556
                self.screen,
×
557
                "{}\r\n{}{}",
×
558
                termion::cursor::Goto(1, self.output_line),
×
559
                &line,
×
560
                self.goto_prompt(),
×
561
            )
×
562
            .unwrap();
×
563
        }
×
564
    }
×
565

566
    fn clear_prompt(&mut self) {
×
567
        write!(
×
568
            self.screen,
×
569
            "{}{}{}",
570
            termion::cursor::Goto(1, self.mud_prompt_line),
×
571
            termion::clear::CurrentLine,
572
            self.goto_prompt(),
×
573
        )
574
        .unwrap();
×
575
    }
×
576

577
    fn redraw_prompt(&mut self) {
×
578
        let prompt_line = self.mud_prompt.print_line().unwrap_or("");
×
579
        if self.scroll_data.not_scrolled_or_split() {
×
580
            write!(
×
581
                self.screen,
×
582
                "{}{}{}{}",
×
583
                termion::cursor::Goto(1, self.mud_prompt_line),
×
584
                termion::clear::CurrentLine,
×
585
                prompt_line,
×
586
                self.goto_prompt(),
×
587
            )
×
588
            .unwrap();
×
589
        }
×
590
    }
×
591

592
    fn redraw_top_bar(&mut self) -> Result<()> {
×
593
        if self.output_start_line > 1 {
×
594
            write!(
×
595
                self.screen,
×
596
                "{}{}{}",
597
                termion::cursor::Goto(1, 1),
598
                termion::clear::CurrentLine,
599
                Fg(color::Green),
600
            )?;
×
601
            let host = if let Some(connection) = &self.connection {
×
602
                format!("═ {connection} ")
×
603
            } else {
604
                "".to_string()
×
605
            };
606
            let mut tags = self
×
607
                .tags
×
608
                .iter()
×
609
                .map(|s| format!("[{s}]"))
×
610
                .collect::<Vec<String>>();
×
611
            tags.sort();
×
612
            let tags = tags.join("");
×
613
            let mut output = format!("{host}{tags}");
×
614
            if !output.is_empty() {
×
615
                output.push(' ');
×
616
            }
×
617
            write!(self.screen, "{:═<1$}", output, self.width as usize)?; // Print separator
×
618
            write!(self.screen, "{}{}", Fg(color::Reset), self.goto_prompt(),)?;
×
619
        }
×
620
        Ok(())
×
621
    }
×
622

623
    fn redraw_status_area(&mut self) -> Result<()> {
×
624
        self.status_area.set_width(self.width);
×
625
        self.status_area.update_pos(self.mud_prompt_line + 1);
×
626
        self.status_area.redraw(&mut self.screen)?;
×
627
        write!(self.screen, "{}", self.goto_prompt(),)?;
×
628
        Ok(())
×
629
    }
×
630

631
    fn goto_prompt(&self) -> String {
×
632
        format!(
×
633
            "{}",
634
            termion::cursor::Goto(self.cursor_prompt_pos, self.prompt_line),
×
635
        )
636
    }
×
637

638
    fn init_scroll(&mut self) -> Result<()> {
×
639
        self.scroll_data.active = true;
×
640
        if self.scroll_range() < self.output_range() {
×
641
            self.scroll_data.split = true;
×
642
            let scroll_range = self.scroll_range();
×
643
            write!(self.screen, "{ResetScrollRegion}")?;
×
644
            write!(
×
645
                self.screen,
×
646
                "{}{}",
647
                ScrollRegion(scroll_range + 3, self.output_line),
×
648
                DisableOriginMode
649
            )?;
×
650
            write!(
×
651
                self.screen,
×
652
                "{}{}{:━<4$}{}",
653
                cursor::Goto(1, scroll_range + self.output_start_line),
×
654
                color::Fg(color::Green),
655
                "━ (scroll) ",
656
                color::Fg(color::Reset),
657
                self.width as usize
×
658
            )?;
×
659
        } else {
660
            self.status_area.set_scroll_marker(true);
×
661
            self.status_area.redraw_line(&mut self.screen, 0)?;
×
662
            self.clear_prompt();
×
663
        }
664
        Ok(())
×
665
    }
×
666

667
    fn draw_scroll(&mut self) -> Result<()> {
×
668
        let output_range = self.scroll_range();
×
669
        for i in 0..output_range {
×
670
            let index = self.scroll_data.pos + i as usize;
×
671
            if index >= self.history.inner.len() {
×
672
                // History has been trimmed during scrolling
673
                // TODO: It should be possible to lock history during render perhaps?
674
                // The lock would prevent the drain function until scrolls is done.
675
                break;
×
676
            }
×
677
            let line_no = self.output_start_line + i;
×
678
            let mut line = self.history.inner[index].clone();
×
679
            if let Some(pattern) = &self.scroll_data.hilite {
×
680
                line = pattern
×
681
                    .replace_all(
×
682
                        &line,
×
683
                        format!(
×
684
                            "{}{}$0{}{}",
×
685
                            Fg(color::LightWhite),
×
686
                            Bg(color::Blue),
×
687
                            Bg(color::Reset),
×
688
                            Fg(color::Reset)
×
689
                        ),
×
690
                    )
×
691
                    .to_string();
×
692
            }
×
693
            write!(
×
694
                self.screen,
×
695
                "{}{}{}",
696
                termion::cursor::Goto(1, line_no),
×
697
                termion::clear::CurrentLine,
698
                line,
699
            )?;
×
700
        }
701
        Ok(())
×
702
    }
×
703

704
    fn scroll_range(&self) -> u16 {
×
705
        if self.scroll_data.allow_split && self.height > SCROLL_LIVE_BUFFER_SIZE * 2 {
×
706
            self.output_line - self.output_start_line - SCROLL_LIVE_BUFFER_SIZE + 1
×
707
        } else {
708
            self.output_range()
×
709
        }
710
    }
×
711

712
    fn output_range(&self) -> u16 {
×
713
        self.output_line - self.output_start_line + 1
×
714
    }
×
715
}
716

717
#[cfg(test)]
718
mod screen_test {
719
    use super::*;
720

721
    #[test]
722
    fn test_append_history() {
1✔
723
        let line = "a nice line\n\nwith a blank line\nand lines\nc\ntest\n";
1✔
724

725
        let mut history = History::new();
1✔
726
        history.append(line);
1✔
727
        assert_eq!(
1✔
728
            history.inner,
729
            vec![
1✔
730
                "a nice line",
731
                "",
1✔
732
                "with a blank line",
1✔
733
                "and lines",
1✔
734
                "c",
1✔
735
                "test",
1✔
736
            ]
737
        );
738
    }
1✔
739

740
    #[test]
741
    fn test_search_history() {
1✔
742
        let line = "a nice line\n\nwith a blank line\nand lines\nc\ntest\n";
1✔
743

744
        let mut history = History::new();
1✔
745
        history.append(line);
1✔
746
        let re = crate::model::Regex::new("and lines", None).unwrap();
1✔
747
        assert_eq!(history.find_forward(&re, 0), Some(3));
1✔
748
        assert_eq!(history.find_forward(&re, 4), None);
1✔
749
        assert_eq!(history.find_backward(&re, 4), Some(3));
1✔
750
        assert_eq!(history.find_backward(&re, 2), None);
1✔
751
    }
1✔
752

753
    #[test]
754
    fn test_drain_history() {
1✔
755
        let mut history = History::new();
1✔
756
        history.capacity = 20;
1✔
757
        history.drain_length = 10;
1✔
758
        assert!(history.is_empty());
1✔
759
        for _ in 0..19 {
19✔
760
            history.append("test");
19✔
761
        }
19✔
762
        assert_eq!(history.len(), 19);
1✔
763
        history.append("test");
1✔
764
        assert_eq!(history.len(), 10);
1✔
765
        for _ in 0..9 {
9✔
766
            history.append("test");
9✔
767
        }
9✔
768
        assert_eq!(history.len(), 19);
1✔
769
        history.append("test");
1✔
770
        assert_eq!(history.len(), 10);
1✔
771
    }
1✔
772
}
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