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

Blightmud / Blightmud / 21713156637

05 Feb 2026 01:23PM UTC coverage: 79.354% (+0.02%) from 79.335%
21713156637

push

github

web-flow
Adds a wrap indicator when prompt input is 'wrapped'. (#1347)

0 of 8 new or added lines in 1 file covered. (0.0%)

1 existing line in 1 file now uncovered.

8625 of 10869 relevant lines covered (79.35%)

354.12 hits per line

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

7.09
/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
233
        if line.flags.screen_clear {
×
234
            self.clear_output_area().ok();
×
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;
×
NEW
272
        let mut wrapped = false;
×
273

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

287
        // When wrapped, reserve 1 column for the '>' indicator
NEW
288
        let effective_width = if wrapped { width - 1 } else { width };
×
289

290
        // Truncate input if it's still too wide for the display
NEW
291
        if input.display_width() >= effective_width {
×
NEW
292
            let (byte_idx, _) = input.byte_index_at_display_width(effective_width);
×
293
            input = input.split_at(byte_idx).0;
×
294
        }
×
295

296
        // Adjust cursor position for the wrap indicator
NEW
297
        let cursor_offset = if wrapped { 2 } else { 1 };
×
NEW
298
        self.cursor_prompt_pos = cursor_display_pos as u16 + cursor_offset;
×
299

NEW
300
        let wrap_indicator = if wrapped { ">" } else { "" };
×
301
        write!(
×
302
            self.screen,
×
303
            "{}{}{}{}{}{}{}{}{}{}",
304
            termion::cursor::Save,
305
            termion::cursor::Goto(1, self.prompt_line),
×
306
            Fg(termion::color::Reset),
307
            Bg(termion::color::Reset),
308
            termion::style::Reset,
309
            termion::clear::CurrentLine,
310
            wrap_indicator,
311
            input,
312
            termion::cursor::Restore,
313
            self.goto_prompt(),
×
314
        )
315
        .unwrap();
×
316
    }
×
317

318
    fn print_send(&mut self, send: &Line) {
×
319
        if self.scroll_data.active && send.flags.source != Some("script".to_string()) {
×
320
            self.reset_scroll().ok();
×
321
        }
×
322
        if let Some(line) = send.print_line() {
×
323
            let line = &format!(
×
324
                "{}{}> {}{}",
×
325
                termion::style::Reset,
×
326
                Fg(color::LightYellow),
×
327
                line,
×
328
                Fg(color::Reset),
×
329
            );
×
330
            for line in wrap_line(line, self.width as usize) {
×
331
                self.print_line(line);
×
332
            }
×
333
        }
×
334
    }
×
335

336
    fn reset(&mut self) -> Result<()> {
×
337
        write!(self.screen, "{}{}", termion::clear::All, ResetScrollRegion)?;
×
338
        Ok(())
×
339
    }
×
340

341
    fn reset_scroll(&mut self) -> Result<()> {
×
342
        let reset_split = self.scroll_data.split;
×
343
        let reset_scroll = self.scroll_data.active;
×
344
        self.scroll_data.reset(&self.history)?;
×
345
        if reset_split {
×
346
            write!(self.screen, "{ResetScrollRegion}")?;
×
347
            write!(
×
348
                self.screen,
×
349
                "{}{}",
350
                ScrollRegion(self.output_start_line, self.output_line),
×
351
                DisableOriginMode
352
            )?;
×
353
        } else if reset_scroll {
×
354
            self.status_area.set_scroll_marker(false);
×
355
            self.status_area.redraw_line(&mut self.screen, 0)?;
×
356
        }
×
357
        self.redraw_prompt();
×
358

359
        let output_range = self.output_range();
×
360
        let output_start_index = self.history.inner.len() as i32 - output_range as i32;
×
361
        if output_start_index >= 0 {
×
362
            let output_start_index = output_start_index as usize;
×
363
            for i in 0..output_range {
×
364
                let index = output_start_index + i as usize;
×
365
                let line_no = self.output_start_line + i;
×
366
                write!(
×
367
                    self.screen,
×
368
                    "{}{}{}",
369
                    termion::cursor::Goto(1, line_no),
×
370
                    termion::clear::CurrentLine,
371
                    self.history.inner[index],
×
372
                )?;
×
373
            }
374
        } else {
375
            for line in &self.history.inner {
×
376
                write!(
×
377
                    self.screen,
×
378
                    "{}\n{}",
379
                    termion::cursor::Goto(1, self.output_line),
×
380
                    line,
381
                )?;
×
382
            }
383
        }
384
        Ok(())
×
385
    }
×
386

387
    fn clear_output_area(&mut self) -> Result<()> {
×
388
        // Clear all lines in the output scroll region
389
        for line_no in self.output_start_line..=self.output_line {
×
390
            write!(
×
391
                self.screen,
×
392
                "{}{}",
393
                termion::cursor::Goto(1, line_no),
×
394
                termion::clear::CurrentLine,
395
            )?;
×
396
        }
397
        // Clear the history buffer as well
398
        self.history.clear();
×
399
        // Reset scroll state
400
        self.scroll_data.reset(&self.history)?;
×
401
        // Reposition cursor
402
        write!(
×
403
            self.screen,
×
404
            "{}{}",
405
            termion::cursor::Goto(1, self.output_start_line),
×
406
            self.goto_prompt(),
×
407
        )?;
×
408
        Ok(())
×
409
    }
×
410

411
    fn scroll_down(&mut self) -> Result<()> {
×
412
        self.scroll_data.clamp(&self.history);
×
413
        if self.scroll_data.active {
×
414
            let output_range = self.scroll_range() as i32;
×
415
            let max_start_index: i32 = self.history.inner.len() as i32 - output_range;
×
416
            let new_start_index = self.scroll_data.pos + 5;
×
417
            if new_start_index >= max_start_index as usize {
×
418
                self.reset_scroll()?;
×
419
            } else {
420
                self.scroll_data.pos = new_start_index;
×
421
                self.draw_scroll()?;
×
422
            }
423
        }
×
424
        Ok(())
×
425
    }
×
426

427
    fn scroll_lock(&mut self, lock: bool) -> Result<()> {
×
428
        self.scroll_data.lock(lock)
×
429
    }
×
430

431
    fn scroll_to(&mut self, row: usize) -> Result<()> {
×
432
        self.scroll_data.clamp(&self.history);
×
433
        if self.history.len() > self.scroll_range() as usize {
×
434
            let max_start_index = self.history.inner.len() as i32 - self.scroll_range() as i32;
×
435
            if max_start_index > 0 && row < max_start_index as usize {
×
436
                self.init_scroll()?;
×
437
                self.scroll_data.pos = row;
×
438
                self.draw_scroll()?;
×
439
            } else {
440
                self.reset_scroll()?;
×
441
            }
442
        }
×
443
        Ok(())
×
444
    }
×
445

446
    fn scroll_top(&mut self) -> Result<()> {
×
447
        if self.history.inner.len() as u16 >= self.output_line {
×
448
            self.init_scroll()?;
×
449
            self.scroll_data.pos = 0;
×
450
            self.draw_scroll()?;
×
451
        }
×
452
        Ok(())
×
453
    }
×
454

455
    fn scroll_up(&mut self) -> Result<()> {
×
456
        self.scroll_data.clamp(&self.history);
×
457
        let output_range: usize = self.scroll_range() as usize;
×
458
        if self.history.inner.len() > output_range {
×
459
            if !self.scroll_data.active {
×
460
                self.init_scroll()?;
×
461
                self.scroll_data.pos = self.history.inner.len() - output_range;
×
462
            }
×
463
            self.scroll_data.pos -= self.scroll_data.pos.min(5);
×
464
            self.draw_scroll()?;
×
465
        }
×
466
        Ok(())
×
467
    }
×
468

469
    fn find_up(&mut self, pattern: &Regex) -> Result<()> {
×
470
        self.scroll_data.clamp(&self.history);
×
471
        let pos = if self.scroll_data.active {
×
472
            self.scroll_data.pos
×
473
        } else if self.history.len() > self.scroll_range() as usize {
×
474
            self.history.len() - self.scroll_range() as usize
×
475
        } else {
476
            self.history.len()
×
477
        };
478
        if let Some(line) = self.history.find_backward(pattern, pos) {
×
479
            self.scroll_data.hilite = Some(pattern.clone());
×
480
            self.scroll_to(0.max(line))?;
×
481
        }
×
482
        Ok(())
×
483
    }
×
484

485
    fn find_down(&mut self, pattern: &Regex) -> Result<()> {
×
486
        self.scroll_data.clamp(&self.history);
×
487
        if self.scroll_data.active {
×
488
            if let Some(line) = self
×
489
                .history
×
490
                .find_forward(pattern, self.history.len().min(self.scroll_data.pos + 1))
×
491
            {
492
                self.scroll_data.hilite = Some(pattern.clone());
×
493
                self.scroll_to(line.min(self.history.len() - 1))?;
×
494
            }
×
495
        }
×
496
        Ok(())
×
497
    }
×
498

499
    fn set_host(&mut self, host: &str, port: u16) -> Result<()> {
×
500
        self.connection = if !host.is_empty() {
×
501
            Some(format!("{host}:{port}"))
×
502
        } else {
503
            None
×
504
        };
505
        self.redraw_top_bar()
×
506
    }
×
507

508
    fn add_tag(&mut self, tag: &str) -> Result<()> {
×
509
        self.tags.insert(tag.to_string());
×
510
        self.redraw_top_bar()
×
511
    }
×
512

513
    fn remove_tag(&mut self, tag: &str) -> Result<()> {
×
514
        self.tags.remove(tag);
×
515
        self.redraw_top_bar()
×
516
    }
×
517

518
    fn clear_tags(&mut self) -> Result<()> {
×
519
        self.tags.clear();
×
520
        self.redraw_top_bar()
×
521
    }
×
522

523
    fn set_status_area_height(&mut self, height: u16) -> Result<()> {
×
524
        let height = StatusArea::clamp_height(height) as u16;
×
525
        self.status_area
×
526
            .set_height(height, self.height - height - PROMPT_HEIGHT);
×
527
        self.setup()?;
×
528
        let input_str = self.prompt_input.as_str().to_owned();
×
529
        self.print_prompt_input(&input_str, self.prompt_input_pos);
×
530
        Ok(())
×
531
    }
×
532

533
    fn set_status_line(&mut self, line: usize, info: String) -> Result<()> {
×
534
        self.status_area.set_status_line(line, info);
×
535
        self.status_area.redraw_line(&mut self.screen, line)?;
×
536
        write!(self.screen, "{}", self.goto_prompt())?;
×
537
        Ok(())
×
538
    }
×
539

540
    fn flush(&mut self) {
×
541
        self.screen.flush().unwrap();
×
542
    }
×
543

544
    fn width(&self) -> u16 {
×
545
        self.width
×
546
    }
×
547

548
    fn height(&self) -> u16 {
×
549
        self.height
×
550
    }
×
551

552
    fn destroy(mut self: Box<Self>) -> Result<(Box<dyn Write>, History)> {
×
553
        self.reset()?;
×
554
        Ok((self.screen, self.history))
×
555
    }
×
556
}
557

558
impl SplitScreen {
559
    pub fn new(screen: Box<dyn Write>, history: History) -> Result<Self> {
×
560
        let (width, height) = termion::terminal_size()?;
×
561

562
        let output_start_line = 2;
×
563
        let status_area_height = 1;
×
564
        let output_line = height - status_area_height - 2;
×
565
        let mud_prompt_line = height - status_area_height - 1;
×
566
        let prompt_line = height;
×
567

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

570
        Ok(Self {
×
571
            screen,
×
572
            width,
×
573
            height,
×
574
            output_start_line,
×
575
            output_line,
×
576
            mud_prompt_line,
×
577
            mud_prompt: Line::from(""),
×
578
            status_area,
×
579
            prompt_line,
×
580
            cursor_prompt_pos: 1,
×
581
            history,
×
582
            scroll_data: ScrollData::new(),
×
583
            connection: None,
×
584
            tags: HashSet::new(),
×
585
            prompt_input: String::new(),
×
586
            prompt_input_pos: 0,
×
587
        })
×
588
    }
×
589

590
    fn print_line(&mut self, line: &str) {
×
591
        self.history.append(line);
×
592
        if self.scroll_data.not_scrolled_or_split() {
×
593
            write!(
×
594
                self.screen,
×
595
                "{}\r\n{}{}",
×
596
                termion::cursor::Goto(1, self.output_line),
×
597
                &line,
×
598
                self.goto_prompt(),
×
599
            )
×
600
            .unwrap();
×
601
        }
×
602
    }
×
603

604
    fn clear_prompt(&mut self) {
×
605
        write!(
×
606
            self.screen,
×
607
            "{}{}{}",
608
            termion::cursor::Goto(1, self.mud_prompt_line),
×
609
            termion::clear::CurrentLine,
610
            self.goto_prompt(),
×
611
        )
612
        .unwrap();
×
613
    }
×
614

615
    fn redraw_prompt(&mut self) {
×
616
        let prompt_line = self.mud_prompt.print_line().unwrap_or("");
×
617
        if self.scroll_data.not_scrolled_or_split() {
×
618
            write!(
×
619
                self.screen,
×
620
                "{}{}{}{}",
×
621
                termion::cursor::Goto(1, self.mud_prompt_line),
×
622
                termion::clear::CurrentLine,
×
623
                prompt_line,
×
624
                self.goto_prompt(),
×
625
            )
×
626
            .unwrap();
×
627
        }
×
628
    }
×
629

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

661
    fn redraw_status_area(&mut self) -> Result<()> {
×
662
        self.status_area.set_width(self.width);
×
663
        self.status_area.update_pos(self.mud_prompt_line + 1);
×
664
        self.status_area.redraw(&mut self.screen)?;
×
665
        write!(self.screen, "{}", self.goto_prompt(),)?;
×
666
        Ok(())
×
667
    }
×
668

669
    fn goto_prompt(&self) -> String {
×
670
        format!(
×
671
            "{}",
672
            termion::cursor::Goto(self.cursor_prompt_pos, self.prompt_line),
×
673
        )
674
    }
×
675

676
    fn init_scroll(&mut self) -> Result<()> {
×
677
        self.scroll_data.active = true;
×
678
        if self.scroll_range() < self.output_range() {
×
679
            self.scroll_data.split = true;
×
680
            let scroll_range = self.scroll_range();
×
681
            write!(self.screen, "{ResetScrollRegion}")?;
×
682
            write!(
×
683
                self.screen,
×
684
                "{}{}",
685
                ScrollRegion(scroll_range + 3, self.output_line),
×
686
                DisableOriginMode
687
            )?;
×
688
            write!(
×
689
                self.screen,
×
690
                "{}{}{:━<4$}{}",
691
                cursor::Goto(1, scroll_range + self.output_start_line),
×
692
                color::Fg(color::Green),
693
                "━ (scroll) ",
694
                color::Fg(color::Reset),
695
                self.width as usize
×
696
            )?;
×
697
        } else {
698
            self.status_area.set_scroll_marker(true);
×
699
            self.status_area.redraw_line(&mut self.screen, 0)?;
×
700
            self.clear_prompt();
×
701
        }
702
        Ok(())
×
703
    }
×
704

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

742
    fn scroll_range(&self) -> u16 {
×
743
        if self.scroll_data.allow_split && self.height > SCROLL_LIVE_BUFFER_SIZE * 2 {
×
744
            self.output_line - self.output_start_line - SCROLL_LIVE_BUFFER_SIZE + 1
×
745
        } else {
746
            self.output_range()
×
747
        }
748
    }
×
749

750
    fn output_range(&self) -> u16 {
×
751
        self.output_line - self.output_start_line + 1
×
752
    }
×
753
}
754

755
#[cfg(test)]
756
mod screen_test {
757
    use super::*;
758

759
    #[test]
760
    fn test_append_history() {
1✔
761
        let line = "a nice line\n\nwith a blank line\nand lines\nc\ntest\n";
1✔
762

763
        let mut history = History::new();
1✔
764
        history.append(line);
1✔
765
        assert_eq!(
1✔
766
            history.inner,
767
            vec![
1✔
768
                "a nice line",
769
                "",
1✔
770
                "with a blank line",
1✔
771
                "and lines",
1✔
772
                "c",
1✔
773
                "test",
1✔
774
            ]
775
        );
776
    }
1✔
777

778
    #[test]
779
    fn test_search_history() {
1✔
780
        let line = "a nice line\n\nwith a blank line\nand lines\nc\ntest\n";
1✔
781

782
        let mut history = History::new();
1✔
783
        history.append(line);
1✔
784
        let re = crate::model::Regex::new("and lines", None).unwrap();
1✔
785
        assert_eq!(history.find_forward(&re, 0), Some(3));
1✔
786
        assert_eq!(history.find_forward(&re, 4), None);
1✔
787
        assert_eq!(history.find_backward(&re, 4), Some(3));
1✔
788
        assert_eq!(history.find_backward(&re, 2), None);
1✔
789
    }
1✔
790

791
    #[test]
792
    fn test_drain_history() {
1✔
793
        let mut history = History::new();
1✔
794
        history.capacity = 20;
1✔
795
        history.drain_length = 10;
1✔
796
        assert!(history.is_empty());
1✔
797
        for _ in 0..19 {
19✔
798
            history.append("test");
19✔
799
        }
19✔
800
        assert_eq!(history.len(), 19);
1✔
801
        history.append("test");
1✔
802
        assert_eq!(history.len(), 10);
1✔
803
        for _ in 0..9 {
9✔
804
            history.append("test");
9✔
805
        }
9✔
806
        assert_eq!(history.len(), 19);
1✔
807
        history.append("test");
1✔
808
        assert_eq!(history.len(), 10);
1✔
809
    }
1✔
810
}
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