ad_editor/ui/
tui.rs

1//! A terminal UI for ad
2use crate::{
3    buffer::{Buffer, Chars, GapBuffer},
4    config::ColorScheme,
5    config_handle, die,
6    dot::Range,
7    editor::{Click, MiniBufferState},
8    input::Event,
9    key::{Input, MouseButton, MouseEvent},
10    restore_terminal_state,
11    term::{
12        clear_screen, enable_alternate_screen, enable_mouse_support, enable_raw_mode, get_termios,
13        get_termsize, register_signal_handler, win_size_changed, CurShape, Cursor, Style, Styles,
14        RESET_STYLE,
15    },
16    ts::{LineIter, RangeToken},
17    ui::{
18        layout::{Column, Window},
19        Layout, StateChange, UserInterface,
20    },
21    ziplist, ORIGINAL_TERMIOS, VERSION,
22};
23use std::{
24    cell::RefCell,
25    char,
26    cmp::{min, Ordering},
27    collections::HashMap,
28    io::{stdin, stdout, Read, Stdout, Write},
29    iter::{repeat_n, Peekable},
30    panic,
31    rc::Rc,
32    sync::mpsc::Sender,
33    thread::{spawn, JoinHandle},
34    time::Instant,
35};
36use unicode_width::UnicodeWidthChar;
37
38// const HLINE: &str = "β€”"; // em dash
39const HLINE: &str = "-";
40const VLINE: &str = "β”‚";
41const TSTR: &str = "β”œ";
42const XSTR: &str = "β”Ό";
43
44fn box_draw_str(s: &str, cs: &ColorScheme) -> String {
45    format!("{}{}{s}", Style::Fg(cs.minibuffer_hl), Style::Bg(cs.bg))
46}
47
48#[derive(Debug)]
49pub struct Tui {
50    stdout: Stdout,
51    screen_rows: usize,
52    screen_cols: usize,
53    status_message: String,
54    last_status: Instant,
55    // Box elements for rendering window borders
56    vstr: String,
57    xstr: String,
58    tstr: String,
59    hvh: String,
60    vh: String,
61    // Cache of the ANSI escape code strings required for each fully qualified tree-sitter
62    // highlighting tag. See render_line for details on how the cache is used.
63    style_cache: Rc<RefCell<HashMap<String, String>>>,
64}
65
66impl Default for Tui {
67    fn default() -> Self {
68        Self::new()
69    }
70}
71
72impl Drop for Tui {
73    fn drop(&mut self) {
74        restore_terminal_state(&mut self.stdout);
75    }
76}
77
78impl Tui {
79    pub fn new() -> Self {
80        let mut tui = Self {
81            stdout: stdout(),
82            screen_rows: 0,
83            screen_cols: 0,
84            status_message: String::new(),
85            last_status: Instant::now(),
86            vstr: String::new(),
87            tstr: String::new(),
88            xstr: String::new(),
89            hvh: String::new(),
90            vh: String::new(),
91            style_cache: Default::default(),
92        };
93        tui.update_cached_elements();
94
95        tui
96    }
97
98    fn update_cached_elements(&mut self) {
99        let cs = &config_handle!().colorscheme;
100        let vstr = box_draw_str(VLINE, cs);
101        let hstr = box_draw_str(HLINE, cs);
102        self.tstr = box_draw_str(TSTR, cs);
103        self.xstr = box_draw_str(XSTR, cs);
104        self.hvh = format!("{hstr}{vstr}{hstr}");
105        self.vh = format!("{vstr}{hstr}");
106        self.vstr = vstr;
107        self.style_cache.borrow_mut().clear();
108    }
109
110    fn render_banner(&self, screen_rows: usize, cs: &ColorScheme) -> Vec<String> {
111        let mut lines = Vec::with_capacity(screen_rows);
112        let (w_lnum, w_sgncol) = (1, 3);
113        let y_banner = self.screen_rows / 3;
114
115        let banner_line = |mut banner: String| {
116            let mut buf = String::new();
117            banner.truncate(self.screen_cols - w_sgncol);
118            let padding = (self.screen_cols - w_sgncol - banner.len()) / 2;
119            buf.push_str(&" ".repeat(padding));
120            buf.push_str(&banner);
121
122            buf
123        };
124
125        for y in 0..screen_rows {
126            let mut line = format!(
127                "{}{}~ {VLINE:>width$}{}",
128                Style::Fg(cs.signcol_fg),
129                Style::Bg(cs.bg),
130                Style::Fg(cs.fg),
131                width = w_lnum
132            );
133
134            if y == y_banner && y < screen_rows {
135                line.push_str(&banner_line(format!("ad editor :: version {VERSION}")));
136            } else if y == y_banner + 1 && y + 1 < screen_rows {
137                line.push_str(&banner_line("type :help to view help".to_string()));
138            }
139            line.push_str(&format!("{}\r\n", Cursor::ClearRight));
140            lines.push(line);
141        }
142
143        lines
144    }
145
146    fn render_status_bar(&self, cs: &ColorScheme, mode_name: &str, b: &Buffer) -> String {
147        let lstatus = format!(
148            "{} {} - {} lines {}",
149            mode_name,
150            b.display_name(),
151            b.len_lines(),
152            if b.dirty { "[+]" } else { "" }
153        );
154        let rstatus = b.dot.addr(b);
155        let width = self.screen_cols - lstatus.len();
156
157        format!(
158            "{}{}{lstatus}{rstatus:>width$}{}\r\n",
159            Style::Bg(cs.bar_bg),
160            Style::Fg(cs.fg),
161            Style::Reset
162        )
163    }
164
165    // current prompt and pending chars
166    fn render_message_bar(
167        &self,
168        cs: &ColorScheme,
169        pending_keys: &[Input],
170        status_timeout: u64,
171    ) -> String {
172        let mut buf = String::new();
173        buf.push_str(&Cursor::ClearRight.to_string());
174
175        let mut msg = self.status_message.clone();
176        msg.truncate(self.screen_cols.saturating_sub(10));
177
178        let pending = render_pending(pending_keys);
179        let delta = (Instant::now() - self.last_status).as_secs();
180
181        if !msg.is_empty() && delta < status_timeout {
182            let width = self.screen_cols - msg.len() - 10;
183            buf.push_str(&format!(
184                "{}{}{msg}{pending:>width$}          ",
185                Style::Fg(cs.fg),
186                Style::Bg(cs.bg)
187            ));
188        } else {
189            let width = self.screen_cols - 10;
190            buf.push_str(&format!(
191                "{}{}{pending:>width$}          ",
192                Style::Fg(cs.fg),
193                Style::Bg(cs.bg)
194            ));
195        }
196
197        buf
198    }
199
200    fn render_minibuffer_state(
201        &self,
202        mb: &MiniBufferState<'_>,
203        tabstop: usize,
204        cs: &ColorScheme,
205    ) -> Vec<String> {
206        let mut lines = Vec::new();
207
208        if let Some(b) = mb.b {
209            for i in mb.top..=mb.bottom {
210                let slice = b.line(i).unwrap();
211                let bg = if i == mb.selected_line_idx {
212                    cs.minibuffer_hl
213                } else {
214                    cs.bg
215                };
216
217                let mut cols = 0;
218                let mut chars = slice.chars().peekable();
219                let mut rline = Styles {
220                    fg: Some(cs.fg),
221                    bg: Some(bg),
222                    ..Default::default()
223                }
224                .to_string();
225
226                render_chars(
227                    &mut chars,
228                    None,
229                    self.screen_cols,
230                    tabstop,
231                    &mut cols,
232                    &mut rline,
233                );
234
235                if i == mb.selected_line_idx && cols < self.screen_cols {
236                    rline.push_str(&Style::Bg(cs.minibuffer_hl).to_string());
237                }
238
239                let len = min(self.screen_cols, rline.len());
240                let width = self.screen_cols;
241                lines.push(format!(
242                    "{:<width$}{}\r\n",
243                    &rline[0..len],
244                    Cursor::ClearRight
245                ));
246            }
247        }
248
249        lines.push(format!(
250            "{}{}{}{}{}",
251            Style::Fg(cs.fg),
252            Style::Bg(cs.bg),
253            mb.prompt,
254            mb.input,
255            Cursor::ClearRight
256        ));
257
258        lines
259    }
260}
261
262impl UserInterface for Tui {
263    fn init(&mut self, tx: Sender<Event>) -> (usize, usize) {
264        let original_termios = get_termios();
265        enable_raw_mode(original_termios);
266        _ = ORIGINAL_TERMIOS.set(original_termios);
267
268        panic::set_hook(Box::new(|panic_info| {
269            let mut stdout = stdout();
270            restore_terminal_state(&mut stdout);
271            _ = stdout.flush();
272
273            // Restoring the terminal state to move us off of the alternate screen
274            // can race with our attempt to print the panic info so given that we
275            // are already in a fatal situation, sleeping briefly to ensure that
276            // the cause of the panic is visible before we exit isn't _too_ bad.
277            std::thread::sleep(std::time::Duration::from_millis(300));
278            eprintln!("Fatal error:\n{panic_info}");
279            _ = std::fs::write("/tmp/ad.panic", format!("{panic_info}"));
280        }));
281
282        enable_mouse_support(&mut self.stdout);
283        enable_alternate_screen(&mut self.stdout);
284
285        // SAFETY: we only register our signal handler once
286        unsafe { register_signal_handler() };
287
288        let (screen_rows, screen_cols) = get_termsize();
289        self.screen_rows = screen_rows;
290        self.screen_cols = screen_cols;
291
292        spawn_input_thread(tx);
293
294        (screen_rows, screen_cols)
295    }
296
297    fn shutdown(&mut self) {
298        clear_screen(&mut self.stdout);
299    }
300
301    fn state_change(&mut self, change: StateChange) {
302        match change {
303            StateChange::ConfigUpdated => self.update_cached_elements(),
304            StateChange::StatusMessage { msg } => {
305                self.status_message = msg;
306                self.last_status = Instant::now();
307            }
308        }
309    }
310
311    fn refresh(
312        &mut self,
313        mode_name: &str,
314        layout: &Layout,
315        pending_keys: &[Input],
316        held_click: Option<&Click>,
317        mb: Option<MiniBufferState<'_>>,
318    ) {
319        self.screen_rows = layout.screen_rows;
320        self.screen_cols = layout.screen_cols;
321        let w_minibuffer = mb.is_some();
322        let mb = mb.unwrap_or_default();
323        let active_buffer = layout.active_buffer();
324        let mb_lines = mb.b.map(|b| b.len_lines()).unwrap_or_default();
325        let mb_offset = if mb_lines > 0 { 1 } else { 0 };
326
327        // This is the screen size that we have to work with for the buffer content we currently want to
328        // display. If the minibuffer is active then it take priority over anything else and we always
329        // show the status bar as the final two lines of the UI.
330        let effective_screen_rows = self.screen_rows - (mb.bottom - mb.top) - mb_offset;
331
332        let conf = config_handle!();
333        let (cs, status_timeout, tabstop) = (&conf.colorscheme, conf.status_timeout, conf.tabstop);
334
335        let load_exec_range = match held_click {
336            Some(click) if click.btn == MouseButton::Right || click.btn == MouseButton::Middle => {
337                Some((click.btn == MouseButton::Right, click.selection))
338            }
339            _ => None,
340        };
341
342        // We need space for each visible line plus the two commands to hide/show the cursor
343        let mut lines = Vec::with_capacity(self.screen_rows + 2);
344        lines.push(format!("{}{}", Cursor::Hide, Cursor::ToStart));
345
346        if layout.is_empty_scratch() {
347            lines.append(&mut self.render_banner(effective_screen_rows, cs));
348        } else {
349            lines.extend(WinsIter::new(
350                layout,
351                load_exec_range,
352                effective_screen_rows,
353                tabstop,
354                self,
355                cs,
356            ));
357        }
358
359        lines.push(self.render_status_bar(cs, mode_name, active_buffer));
360
361        if w_minibuffer {
362            lines.append(&mut self.render_minibuffer_state(&mb, tabstop, cs));
363        } else {
364            lines.push(self.render_message_bar(cs, pending_keys, status_timeout));
365        }
366
367        // Position the cursor
368        let (x, y) = if w_minibuffer {
369            (mb.cx, self.screen_rows + mb.n_visible_lines + 1)
370        } else {
371            layout.ui_xy(active_buffer)
372        };
373        lines.push(format!("{}{}", Cursor::To(x + 1, y + 1), Cursor::Show));
374
375        if let Err(e) = self.stdout.write_all(lines.join("").as_bytes()) {
376            die!("Unable to refresh screen: {e}");
377        }
378
379        if let Err(e) = self.stdout.flush() {
380            die!("Unable to refresh screen: {e}");
381        }
382    }
383
384    fn set_cursor_shape(&mut self, cur_shape: CurShape) {
385        if let Err(e) = self.stdout.write_all(cur_shape.to_string().as_bytes()) {
386            // In this situation we're probably not going to be able to do all that much
387            // but we might as well try
388            die!("Unable to write to stdout: {e}");
389        };
390    }
391}
392
393struct WinsIter<'a> {
394    col_iters: Vec<ColIter<'a>>,
395    buf: Vec<String>,
396    vstr: &'a str,
397    xstr: &'a str,
398    tstr: &'a str,
399    hvh: &'a str,
400    vh: &'a str,
401}
402
403impl<'a> WinsIter<'a> {
404    fn new(
405        layout: &'a Layout,
406        load_exec_range: Option<(bool, Range)>,
407        screen_rows: usize,
408        tabstop: usize,
409        tui: &'a Tui,
410        cs: &'a ColorScheme,
411    ) -> Self {
412        let col_iters: Vec<_> = layout
413            .cols
414            .iter()
415            .map(|(is_focus, col)| {
416                let rng = if is_focus { load_exec_range } else { None };
417                ColIter::new(
418                    col,
419                    layout,
420                    rng,
421                    screen_rows,
422                    tabstop,
423                    cs,
424                    tui.style_cache.clone(),
425                )
426            })
427            .collect();
428        let buf = Vec::with_capacity(col_iters.len());
429
430        Self {
431            col_iters,
432            buf,
433            vstr: &tui.vstr,
434            xstr: &tui.xstr,
435            tstr: &tui.tstr,
436            hvh: &tui.hvh,
437            vh: &tui.vh,
438        }
439    }
440}
441
442impl Iterator for WinsIter<'_> {
443    type Item = String;
444
445    fn next(&mut self) -> Option<Self::Item> {
446        self.buf.clear();
447
448        for it in self.col_iters.iter_mut() {
449            self.buf.push(it.next()?);
450        }
451
452        let mut buf = self.buf.join(self.vstr);
453        buf = buf.replace(self.hvh, self.xstr).replace(self.vh, self.tstr);
454        buf.push_str(&format!("{}\r\n", Cursor::ClearRight));
455
456        Some(buf)
457    }
458}
459
460struct ColIter<'a> {
461    inner: ziplist::Iter<'a, Window>,
462    current: Option<WinIter<'a>>,
463    layout: &'a Layout,
464    cs: &'a ColorScheme,
465    style_cache: Rc<RefCell<HashMap<String, String>>>,
466    load_exec_range: Option<(bool, Range)>,
467    screen_rows: usize,
468    tabstop: usize,
469    n_cols: usize,
470    yielded: usize,
471}
472
473impl<'a> ColIter<'a> {
474    fn new(
475        col: &'a Column,
476        layout: &'a Layout,
477        load_exec_range: Option<(bool, Range)>,
478        screen_rows: usize,
479        tabstop: usize,
480        cs: &'a ColorScheme,
481        style_cache: Rc<RefCell<HashMap<String, String>>>,
482    ) -> Self {
483        ColIter {
484            inner: col.wins.iter(),
485            current: None,
486            layout,
487            cs,
488            style_cache,
489            load_exec_range,
490            screen_rows,
491            tabstop,
492            n_cols: col.n_cols,
493            yielded: 0,
494        }
495    }
496}
497
498impl<'a> ColIter<'a> {
499    fn next_win_iter(&mut self) -> Option<WinIter<'a>> {
500        let (is_focus, w) = self.inner.next()?;
501        let b = self
502            .layout
503            .buffer_with_id(w.view.bufid)
504            .expect("valid buffer id");
505
506        let (w_lnum, _) = b.sign_col_dims();
507        let rng = if is_focus { self.load_exec_range } else { None };
508        let it = b.iter_tokenized_lines_from(w.view.row_off, rng);
509
510        Some(WinIter {
511            y: 0,
512            w_lnum,
513            n_cols: self.n_cols,
514            tabstop: self.tabstop,
515            it,
516            gb: &b.txt,
517            w,
518            cs: self.cs,
519            style_cache: self.style_cache.clone(),
520        })
521    }
522}
523
524impl Iterator for ColIter<'_> {
525    type Item = String;
526
527    fn next(&mut self) -> Option<Self::Item> {
528        if self.current.is_none() {
529            self.current = Some(self.next_win_iter()?);
530        }
531
532        let next_line = self.current.as_mut()?.next();
533        if self.yielded == self.screen_rows {
534            return None;
535        }
536        self.yielded += 1;
537
538        match next_line {
539            Some(line) => Some(line),
540            None => {
541                self.current = None;
542                Some(box_draw_str(&HLINE.repeat(self.n_cols), self.cs))
543            }
544        }
545    }
546}
547
548struct WinIter<'a> {
549    y: usize,
550    w_lnum: usize,
551    n_cols: usize,
552    tabstop: usize,
553    it: LineIter<'a>,
554    gb: &'a GapBuffer,
555    w: &'a Window,
556    cs: &'a ColorScheme,
557    style_cache: Rc<RefCell<HashMap<String, String>>>,
558}
559
560impl Iterator for WinIter<'_> {
561    type Item = String;
562
563    fn next(&mut self) -> Option<Self::Item> {
564        if self.y >= self.w.n_rows {
565            return None;
566        }
567        let file_row = self.y + self.w.view.row_off;
568        self.y += 1;
569
570        let next = self.it.next();
571
572        let line = match next {
573            None => {
574                let mut buf = format!(
575                    "{}{}~ {VLINE:>width$}{}",
576                    Style::Fg(self.cs.signcol_fg),
577                    Style::Bg(self.cs.bg),
578                    Style::Fg(self.cs.fg),
579                    width = self.w_lnum
580                );
581                let padding = self.n_cols - self.w_lnum - 2;
582                buf.push_str(&" ".repeat(padding));
583
584                buf
585            }
586
587            Some(it) => {
588                // +2 for the leading space and vline chars
589                let padding = self.w_lnum + 2;
590
591                format!(
592                    "{}{} {:>width$}{VLINE}{}",
593                    Style::Fg(self.cs.signcol_fg),
594                    Style::Bg(self.cs.bg),
595                    file_row + 1,
596                    render_line(
597                        self.gb,
598                        it,
599                        self.w.view.col_off,
600                        self.n_cols - padding,
601                        self.tabstop,
602                        self.cs,
603                        &mut self.style_cache
604                    ),
605                    width = self.w_lnum
606                )
607            }
608        };
609
610        Some(line)
611    }
612}
613
614fn render_pending(keys: &[Input]) -> String {
615    let mut s = String::new();
616    for k in keys {
617        match k {
618            Input::Char(c) if c.is_ascii_whitespace() => s.push_str(&format!("<{:x}>", *c as u8)),
619            Input::Char(c) => s.push(*c),
620            Input::Ctrl(c) => {
621                s.push('^');
622                s.push(*c);
623            }
624            Input::Alt(c) => {
625                s.push('^');
626                s.push('[');
627                s.push(*c);
628            }
629            Input::CtrlAlt(c) => {
630                s.push('^');
631                s.push('[');
632                s.push('^');
633                s.push(*c);
634            }
635
636            _ => (),
637        }
638    }
639
640    if s.len() > 10 {
641        s = s.split_off(s.len() - 10);
642    }
643
644    s
645}
646
647#[inline]
648fn skip_token_chars(
649    chars: &mut Peekable<Chars<'_>>,
650    tabstop: usize,
651    to_skip: &mut usize,
652) -> Option<usize> {
653    for ch in chars.by_ref() {
654        let w = if ch == '\t' {
655            tabstop
656        } else {
657            UnicodeWidthChar::width(ch).unwrap_or(1)
658        };
659
660        match (*to_skip).cmp(&w) {
661            Ordering::Less => {
662                let spaces = Some(w - *to_skip);
663                *to_skip = 0;
664                return spaces;
665            }
666
667            Ordering::Equal => {
668                *to_skip = 0;
669                break;
670            }
671
672            Ordering::Greater => *to_skip -= w,
673        }
674    }
675
676    None
677}
678
679fn render_chars(
680    chars: &mut Peekable<Chars<'_>>,
681    spaces: Option<usize>,
682    max_cols: usize,
683    tabstop: usize,
684    cols: &mut usize,
685    buf: &mut String,
686) {
687    if let Some(n) = spaces {
688        buf.extend(repeat_n(' ', n));
689        *cols = n;
690    }
691
692    for ch in chars {
693        if ch == '\n' {
694            break;
695        }
696        let w = if ch == '\t' {
697            tabstop
698        } else {
699            UnicodeWidthChar::width(ch).unwrap_or(1)
700        };
701
702        if *cols + w <= max_cols {
703            if ch == '\t' {
704                // Tab is just a control character that moves the cursor rather than
705                // replacing the previous buffer content so we need to explicitly
706                // insert spaces instead.
707                buf.extend(repeat_n(' ', tabstop));
708            } else {
709                buf.push(ch);
710            }
711            *cols += w;
712        } else {
713            break;
714        }
715    }
716
717    buf.push_str(RESET_STYLE);
718}
719
720fn render_line<'a>(
721    gb: &'a GapBuffer,
722    it: impl Iterator<Item = RangeToken<'a>>,
723    col_off: usize,
724    max_cols: usize,
725    tabstop: usize,
726    cs: &ColorScheme,
727    style_cache: &mut Rc<RefCell<HashMap<String, String>>>,
728) -> String {
729    let mut buf = String::new();
730    let mut to_skip = col_off;
731    let mut cols = 0;
732
733    for tk in it {
734        let slice = tk.as_slice(gb);
735        let mut chars = slice.chars().peekable();
736        let spaces = if to_skip > 0 {
737            let spaces = skip_token_chars(&mut chars, tabstop, &mut to_skip);
738            if to_skip > 0 || (chars.peek().is_none() && spaces.is_none()) {
739                continue;
740            }
741            spaces
742        } else {
743            None
744        };
745
746        // In the common case we have the styles for each tag already cached, so we take the hit on
747        // allocating the cache key and looking it up a second time when we insert into the cache.
748        // This allows us to avoid having to allocate the key on every lookup in order to make use
749        // of the entry API.
750        //
751        // The cache styles are also stored against the original tag rather so they can be looked
752        // up directly each time they are used, rather than need to to traverse the fallback path
753        // as done in ColorScheme::styles_for.
754        //
755        // We always assume that it safe to borrow the style_cache mutably at this point as we are
756        // only expecting this function to be called as part of a render pass where the clones of
757        // the style_cache Rc are held in different iterators that are processed sequentially in a
758        // single thread.
759        let mut guard = style_cache.borrow_mut();
760        let style_str = match guard.get(tk.tag) {
761            Some(s) => s,
762            None => {
763                let s = cs.styles_for(tk.tag).to_string();
764                guard.insert(tk.tag.to_string(), s);
765                guard.get(tk.tag).unwrap()
766            }
767        };
768
769        buf.push_str(style_str);
770        render_chars(&mut chars, spaces, max_cols, tabstop, &mut cols, &mut buf);
771
772        if cols == max_cols {
773            break;
774        }
775    }
776
777    if cols < max_cols {
778        buf.push_str(&Style::Bg(cs.bg).to_string());
779        buf.extend(repeat_n(' ', max_cols - cols));
780    }
781
782    buf
783}
784
785/// Spawn a thread to read from stdin and process user input to send Events to
786/// the main editor event loop.
787fn spawn_input_thread(tx: Sender<Event>) -> JoinHandle<()> {
788    let mut stdin = stdin();
789
790    spawn(move || loop {
791        if let Some(key) = try_read_input(&mut stdin) {
792            _ = tx.send(Event::Input(key));
793        } else if win_size_changed() {
794            let (rows, cols) = get_termsize();
795            _ = tx.send(Event::WinsizeChanged { rows, cols });
796        }
797    })
798}
799
800fn try_read_char(stdin: &mut impl Read) -> Option<char> {
801    let mut buf: [u8; 4] = [0; 4];
802
803    for i in 0..4 {
804        if stdin.read_exact(&mut buf[i..i + 1]).is_err() {
805            return if i == 0 {
806                None
807            } else {
808                Some(char::REPLACEMENT_CHARACTER)
809            };
810        }
811
812        match std::str::from_utf8(&buf[0..i + 1]) {
813            Ok(s) => return s.chars().next(),
814            Err(e) if e.error_len().is_some() => return Some(char::REPLACEMENT_CHARACTER),
815            Err(_) => (),
816        }
817    }
818
819    // utf8 requires at most 4 bytes so at this point we have invalid data
820    Some(char::REPLACEMENT_CHARACTER)
821}
822
823fn try_read_input(stdin: &mut impl Read) -> Option<Input> {
824    let c = try_read_char(stdin)?;
825
826    // Normal key press
827    match Input::from_char(c) {
828        Input::Esc => (),
829        key => return Some(key),
830    }
831
832    let c2 = match try_read_char(stdin) {
833        Some(c2) => c2,
834        None => return Some(Input::Esc),
835    };
836    let c3 = match try_read_char(stdin) {
837        Some(c3) => c3,
838        None => return Some(Input::try_from_seq2(c, c2).unwrap_or(Input::Esc)),
839    };
840
841    if let Some(key) = Input::try_from_seq2(c2, c3) {
842        return Some(key);
843    }
844
845    if c2 == '[' && c3.is_ascii_digit() {
846        if let Some('~') = try_read_char(stdin) {
847            if let Some(key) = Input::try_from_bracket_tilde(c3) {
848                return Some(key);
849            }
850        }
851    }
852
853    // xterm mouse encoding: "^[< Cb;Cx;Cy(;) (M or m) "
854    if c2 == '[' && c3 == '<' {
855        let mut buf = Vec::new();
856        let m;
857
858        loop {
859            match try_read_char(stdin) {
860                Some(c @ 'm' | c @ 'M') => {
861                    m = c;
862                    break;
863                }
864                Some(c) => buf.push(c as u8),
865                None => return None,
866            };
867        }
868        let s = String::from_utf8(buf).unwrap();
869        let nums: Vec<usize> = s.split(';').map(|s| s.parse::<usize>().unwrap()).collect();
870        let (b, x, y) = (nums[0], nums[1], nums[2]);
871
872        return MouseEvent::try_from_raw(b, x, y, m).map(Input::Mouse);
873    }
874
875    Some(Input::Esc)
876}
877
878#[cfg(test)]
879mod tests {
880    use super::*;
881    use crate::ts::{ByteRange, TK_DEFAULT};
882    use simple_test_case::test_case;
883    use std::{char::REPLACEMENT_CHARACTER, io};
884
885    #[test_case("a".as_bytes(), &['a']; "single ascii character")]
886    #[test_case(&[240, 159, 146, 150], &['πŸ’–']; "single utf8 character")]
887    #[test_case(&[165, 159, 146, 150], &[REPLACEMENT_CHARACTER; 4]; "invalid utf8 with non-ascii prefix")]
888    #[test_case(&[65, 159, 146, 150], &['A', REPLACEMENT_CHARACTER, REPLACEMENT_CHARACTER, REPLACEMENT_CHARACTER]; "invalid utf8 with ascii prefix")]
889    #[test]
890    fn try_read_char_works(bytes: &[u8], expected: &[char]) {
891        let mut r = io::Cursor::new(bytes);
892        let mut chars = Vec::new();
893
894        while let Some(ch) = try_read_char(&mut r) {
895            chars.push(ch);
896        }
897
898        assert_eq!(&chars, expected);
899    }
900
901    fn rt(tag: &'static str, from: usize, to: usize) -> RangeToken<'static> {
902        RangeToken {
903            tag,
904            r: ByteRange { from, to },
905        }
906    }
907
908    // The !| characters here are the dummy style strings in the style_cache
909    // The $# characters are replaced with RESET_STYLE and bg color respectively
910    #[test_case(0, 14, "foo\tbar baz", "!foo$|  $!bar$| $!baz$#  "; "full line padded to max cols")]
911    #[test_case(0, 12, "foo\tbar baz", "!foo$|  $!bar$| $!baz$"; "full line")]
912    #[test_case(1, 11, "foo\tbar baz", "!oo$|  $!bar$| $!baz$"; "skipping first character")]
913    #[test_case(3, 9, "foo\tbar baz", "|  $!bar$| $!baz$"; "skipping first token")]
914    #[test_case(4, 8, "foo\tbar baz", "| $!bar$| $!baz$"; "skipping part way through a tab")]
915    #[test_case(0, 10, "δΈ–\tη•Œ foo", "!δΈ–$|  $!η•Œ$| $!foo$"; "unicode full line")]
916    #[test_case(0, 12, "δΈ–\tη•Œ foo", "!δΈ–$|  $!η•Œ$| $!foo$#  "; "unicode full line padded to max cols")]
917    // In the case where we skip part way through a unicode character we still apply the tag
918    // styling to the spaces we insert to pad to the correct offset rather than replacing it
919    // with default styling instead
920    #[test_case(1, 9, "δΈ–\tη•Œ foo", "! $|  $!η•Œ$| $!foo$"; "unicode skipping first column of multibyte char")]
921    #[test]
922    fn render_line_correctly_skips_tokens(
923        col_off: usize,
924        max_cols: usize,
925        s: &str,
926        expected_template: &str,
927    ) {
928        let gb = GapBuffer::from(s);
929        let range_tokens = vec![
930            rt("a", 0, 3),
931            rt(TK_DEFAULT, 3, 4),
932            rt("a", 4, 7),
933            rt(TK_DEFAULT, 7, 8),
934            rt("a", 8, 11),
935        ];
936
937        let cs = ColorScheme::default();
938        let style_cache: HashMap<String, String> = [
939            ("a".to_owned(), "!".to_owned()),
940            (TK_DEFAULT.to_owned(), "|".to_owned()),
941        ]
942        .into_iter()
943        .collect();
944
945        let s = render_line(
946            &gb,
947            range_tokens.into_iter(),
948            col_off,
949            max_cols,
950            2,
951            &cs,
952            &mut Rc::new(RefCell::new(style_cache)),
953        );
954
955        let expected = expected_template
956            .replace("$", RESET_STYLE)
957            .replace("#", &Style::Bg(cs.bg).to_string());
958
959        assert_eq!(s, expected);
960    }
961}