1use crate::{
3 ORIGINAL_TERMIOS,
4 buffer::{Buffer, Chars, GapBuffer},
5 config::{ColorScheme, Config},
6 config_handle, die,
7 dot::Range,
8 editor::{Click, MiniBufferState},
9 input::Event,
10 key::{Input, MouseButton, MouseEvent},
11 restore_terminal_state,
12 syntax::{LineIter, RangeToken},
13 term::{
14 CurShape, Cursor, RESET_STYLE, Style, Styles, clear_screen, enable_alternate_screen,
15 enable_bracketed_paste, enable_mouse_support, enable_raw_mode, get_termios, get_termsize,
16 register_signal_handler, win_size_changed,
17 },
18 ui::{
19 Layout, StateChange, UserInterface,
20 layout::{Column, Scratch, Window},
21 },
22 ziplist,
23};
24use std::{
25 char,
26 cmp::Ordering,
27 collections::HashMap,
28 fmt::Write as _,
29 io::{self, BufWriter, Read, StdoutLock, Write, stdin, stdout},
30 iter::{Peekable, repeat_n},
31 panic,
32 sync::{Arc, RwLock, mpsc::Sender},
33 thread::{JoinHandle, spawn},
34 time::Instant,
35};
36use tracing::debug;
37use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
38
39const MIN_COLS: usize = 20;
41const MIN_ROWS: usize = 5;
42
43const H_STR: &str = "─";
44const V_STR: &str = "│";
45const TR_STR: &str = "├";
46const TL_STR: &str = "┤";
47const X_STR: &str = "┼";
48
49pub type Tui = GenericTui<StdoutLock<'static>>;
50
51#[derive(Debug)]
52pub struct GenericTui<W: Write> {
53 stdout: BufWriter<W>,
54 config: Arc<RwLock<Config>>,
55 status_message: String,
56 last_status: Instant,
57 mb_last_frame: bool,
58 frame: Frame,
59}
60
61impl Default for Tui {
62 fn default() -> Self {
63 Self::new(Default::default())
64 }
65}
66
67impl<W: Write> Drop for GenericTui<W> {
68 fn drop(&mut self) {
69 restore_terminal_state(&mut self.stdout);
70 }
71}
72
73impl Tui {
74 pub fn new(config: Arc<RwLock<Config>>) -> Self {
75 Self::new_with_stdout_handle(config, stdout().lock())
76 }
77}
78
79impl<W: Write> GenericTui<W> {
80 pub fn new_with_stdout_handle(config: Arc<RwLock<Config>>, stdout: W) -> Self {
81 Self {
82 stdout: BufWriter::new(stdout),
83 config,
84 status_message: String::new(),
85 last_status: Instant::now(),
86 mb_last_frame: false,
87 frame: Frame::new(),
88 }
89 }
90
91 pub fn set_size(&mut self, rows: usize, cols: usize) {
92 self.frame.screen_rows = rows;
93 self.frame.screen_cols = cols;
94 }
95
96 fn render(
97 &mut self,
98 mode_name: &str,
99 layout: &Layout,
100 n_running: usize,
101 pending_keys: &[Input],
102 held_click: Option<&Click>,
103 mb: Option<MiniBufferState<'_>>,
104 ) {
105 let conf = config_handle!(self);
106 let (cs, status_timeout, tabstop, max_mb_lines) = (
107 &conf.colorscheme,
108 conf.status_timeout,
109 conf.tabstop,
110 conf.minibuffer_lines,
111 );
112
113 let w_minibuffer = mb.is_some();
115 let mb = mb.unwrap_or_default();
116 let active_buffer = layout.active_buffer();
117
118 let mb_has_lines = mb.b.map(|b| !b.is_empty()).unwrap_or_default();
119 let offset = if mb_has_lines {
120 mb.bottom - mb.top + 1
121 } else if !w_minibuffer && layout.scratch.is_visible {
122 max_mb_lines
123 } else {
124 0
125 };
126
127 let effective_screen_rows = self.frame.screen_rows.saturating_sub(offset);
131
132 let load_exec_range = match held_click {
133 Some(Click::Text { btn, selection, .. })
134 if *btn == MouseButton::Right || *btn == MouseButton::Middle =>
135 {
136 Some((*btn == MouseButton::Right, *selection))
137 }
138 _ => None,
139 };
140
141 let (load_exec_range, scratch_load_exec_range) = if layout.scratch.is_focused {
142 (None, load_exec_range)
143 } else {
144 (load_exec_range, None)
145 };
146
147 self.frame
148 .render_windows(layout, load_exec_range, effective_screen_rows, tabstop, cs);
149 self.frame
150 .render_status_bar(cs, mode_name, n_running, active_buffer);
151
152 self.frame.show_mb = w_minibuffer || layout.scratch.is_visible;
153 self.frame.show_msg_bar = !w_minibuffer;
154
155 if w_minibuffer {
156 self.frame.render_minibuffer_state(&mb, tabstop, cs);
157 } else if layout.scratch.is_visible {
158 self.frame
159 .render_scratch(&layout.scratch, scratch_load_exec_range, tabstop, cs);
160 };
161
162 if self.frame.show_msg_bar {
163 self.frame.render_message_bar(
164 cs,
165 pending_keys,
166 status_timeout,
167 self.status_message.clone(),
168 self.last_status,
169 );
170 };
171
172 let (cur_x, cur_y) = if w_minibuffer {
173 (mb.cx, self.frame.screen_rows + mb.n_visible_lines + 1)
174 } else {
175 layout.ui_xy()
176 };
177
178 self.frame.cur_x = cur_x;
179 self.frame.cur_y = cur_y;
180 }
181}
182
183impl<W: Write> UserInterface for GenericTui<W> {
184 fn init(&mut self, tx: Sender<Event>) -> (usize, usize) {
185 let original_termios = get_termios();
186 enable_raw_mode(original_termios);
187 _ = ORIGINAL_TERMIOS.set(original_termios);
188
189 panic::set_hook(Box::new(|panic_info| {
190 let mut stdout = stdout();
191 restore_terminal_state(&mut stdout);
192 _ = stdout.flush();
193
194 let bt = std::backtrace::Backtrace::force_capture();
196
197 std::thread::sleep(std::time::Duration::from_millis(300));
202 eprintln!("Fatal error:\n{panic_info}\n{bt}");
203 _ = std::fs::write("/tmp/ad.panic", format!("{panic_info}\n{bt}"));
204 }));
205
206 enable_mouse_support(&mut self.stdout);
207 enable_alternate_screen(&mut self.stdout);
208 enable_bracketed_paste(&mut self.stdout);
209
210 unsafe { register_signal_handler() };
212
213 let (screen_rows, screen_cols) = get_termsize();
214 self.frame.screen_rows = screen_rows;
215 self.frame.screen_cols = screen_cols;
216
217 spawn_input_thread(tx);
218
219 (screen_rows, screen_cols)
220 }
221
222 fn shutdown(&mut self) {
223 clear_screen(&mut self.stdout);
224 }
225
226 fn state_change(&mut self, change: StateChange) {
227 match change {
228 StateChange::ConfigUpdated => self.frame.style_cache.clear(),
229 StateChange::StatusMessage { msg } => {
230 self.status_message = msg;
231 self.last_status = Instant::now();
232 }
233 }
234 }
235
236 fn refresh(
237 &mut self,
238 mode_name: &str,
239 layout: &mut Layout,
240 n_running: usize,
241 pending_keys: &[Input],
242 held_click: Option<&Click>,
243 mb: Option<MiniBufferState<'_>>,
244 ) {
245 self.frame.screen_rows = layout.screen_rows;
246 self.frame.screen_cols = layout.screen_cols;
247 self.frame.show_msg_bar = mb.is_none();
248 let mb_this_frame = mb.is_some();
249
250 if self.frame.screen_cols < MIN_COLS || self.frame.screen_rows < MIN_ROWS {
251 return;
252 }
253
254 let need_render = layout.changed_since_last_render()
259 || mb_this_frame
260 || self.mb_last_frame | held_click.is_some();
261
262 if need_render {
263 layout.update_visible_ts_state();
264 self.render(mode_name, layout, n_running, pending_keys, held_click, mb);
265 if let Err(e) = self.frame.write(&mut self.stdout) {
266 die!("Unable to refresh screen: {e}");
267 }
268 } else if self.frame.show_msg_bar {
269 let conf = config_handle!(self);
271 let (cs, status_timeout) = (&conf.colorscheme, conf.status_timeout);
272 self.frame.render_message_bar(
273 cs,
274 pending_keys,
275 status_timeout,
276 self.status_message.clone(),
277 self.last_status,
278 );
279 if let Err(e) = self.frame.write_msg_bar(&mut self.stdout) {
280 die!("Unable to refresh screen: {e}");
281 }
282 }
283
284 if let Err(e) = self.stdout.flush() {
285 die!("Unable to refresh screen: {e}");
286 }
287
288 self.mb_last_frame = mb_this_frame;
289 }
290
291 fn set_cursor_shape(&mut self, cur_shape: CurShape) {
292 if let Err(e) = self.stdout.write_all(cur_shape.to_string().as_bytes()) {
293 die!("Unable to write to stdout: {e}");
296 };
297 }
298}
299
300#[derive(Debug, Default)]
301pub struct Frame {
302 win_lines: String,
303 status_bar: String,
304 mb_lines: String,
305 show_mb: bool,
306 msg_bar: String,
307 show_msg_bar: bool,
308 screen_rows: usize,
309 screen_cols: usize,
310 cur_x: usize,
311 cur_y: usize,
312 style_cache: HashMap<String, String>,
315}
316
317impl Frame {
318 fn new() -> Self {
319 let win_lines_cap = 128 * 1024;
320 let bar_cap = 8 * 1024;
321
322 Self {
323 win_lines: String::with_capacity(win_lines_cap),
324 mb_lines: String::with_capacity(bar_cap),
325 status_bar: String::with_capacity(bar_cap),
326 msg_bar: String::with_capacity(bar_cap),
327 ..Default::default()
328 }
329 }
330
331 fn write(&self, w: &mut impl Write) -> io::Result<()> {
332 write!(w, "{}{}", Cursor::Hide, Cursor::ToStart)?;
333 w.write_all(self.win_lines.as_bytes())?;
334 w.write_all(self.status_bar.as_bytes())?;
335 if self.show_mb {
336 w.write_all(self.mb_lines.as_bytes())?;
337 }
338 if self.show_msg_bar {
339 w.write_all(self.msg_bar.as_bytes())?;
340 }
341
342 write!(
343 w,
344 "{}{}",
345 Cursor::To(self.cur_x + 1, self.cur_y + 1),
346 Cursor::Show
347 )
348 }
349
350 fn write_msg_bar(&self, w: &mut impl Write) -> io::Result<()> {
351 write!(w, "{}{}", Cursor::Hide, Cursor::To(1, self.screen_rows + 2))?;
352 w.write_all(self.msg_bar.as_bytes())?;
353 write!(
354 w,
355 "{}{}",
356 Cursor::To(self.cur_x + 1, self.cur_y + 1),
357 Cursor::Show
358 )
359 }
360
361 fn render_windows(
362 &mut self,
363 layout: &Layout,
364 load_exec_range: Option<(bool, Range)>,
365 screen_rows: usize,
366 tabstop: usize,
367 cs: &ColorScheme,
368 ) {
369 self.win_lines.clear();
370
371 let mut col_renderers: Vec<_> = layout
372 .cols
373 .iter()
374 .map(|(is_focus, col)| {
375 let rng = if is_focus { load_exec_range } else { None };
376 ColRenderer::new(col, layout, rng, screen_rows, tabstop, cs)
377 })
378 .collect();
379
380 let n_cols = col_renderers.len();
381 'outer: loop {
382 let mut remaining;
383 let mut prev_col = None;
384
385 for (i, cr) in col_renderers.iter_mut().enumerate() {
386 (remaining, prev_col) =
387 cr.render_next_line(&mut self.win_lines, prev_col, &mut self.style_cache);
388 if i == n_cols - 1 && !remaining {
389 _ = write!(&mut self.win_lines, "{}\r\n", Cursor::ClearRight);
390 break 'outer;
391 }
392 }
393
394 _ = write!(&mut self.win_lines, "{}\r\n", Cursor::ClearRight);
395 }
396 }
397
398 fn render_status_bar(
399 &mut self,
400 cs: &ColorScheme,
401 mode_name: &str,
402 n_running: usize,
403 b: &Buffer,
404 ) {
405 self.status_bar.clear();
406
407 let lstatus = format!(
408 "{} {} - {} lines {}{}",
409 mode_name,
410 b.display_name(),
411 b.len_lines(),
412 if b.dirty { "[+]" } else { "" },
413 if !b.has_trailing_newline() {
414 "[noeol]"
415 } else {
416 ""
417 }
418 );
419 let rstatus = format!(
420 "{}{}",
421 if n_running == 0 {
422 String::new()
423 } else {
424 format!("[{n_running} running] ")
425 },
426 b.dot.addr(b)
427 );
428 let width = self
429 .screen_cols
430 .saturating_sub(UnicodeWidthStr::width(lstatus.as_str()));
431
432 _ = write!(
433 &mut self.status_bar,
434 "{}{}{lstatus}{rstatus:>width$}{}\r\n",
435 Style::Bg(cs.bar_bg),
436 Style::Fg(cs.fg),
437 Style::Reset
438 );
439 }
440
441 fn render_message_bar(
443 &mut self,
444 cs: &ColorScheme,
445 pending_keys: &[Input],
446 status_timeout: u64,
447 mut msg: String,
448 last_status: Instant,
449 ) {
450 self.msg_bar.clear();
451 self.msg_bar.push_str(&Cursor::ClearRight.to_string());
452 msg.truncate(self.screen_cols.saturating_sub(10));
453
454 let pending = render_pending(pending_keys);
455 let delta = (Instant::now() - last_status).as_secs();
456
457 if !msg.is_empty() && delta < status_timeout {
458 let width = self
459 .screen_cols
460 .saturating_sub(msg.len())
461 .saturating_sub(10);
462 _ = write!(
463 &mut self.msg_bar,
464 "{}{}{msg}{pending:>width$} ",
465 Style::Fg(cs.fg),
466 Style::Bg(cs.bg)
467 );
468 } else {
469 let width = self.screen_cols.saturating_sub(10);
470 _ = write!(
471 &mut self.msg_bar,
472 "{}{}{pending:>width$} ",
473 Style::Fg(cs.fg),
474 Style::Bg(cs.bg)
475 );
476 }
477 }
478
479 fn render_minibuffer_state(
480 &mut self,
481 mb: &MiniBufferState<'_>,
482 tabstop: usize,
483 cs: &ColorScheme,
484 ) {
485 self.mb_lines.clear();
486
487 if let Some(b) = mb.b {
488 for i in mb.top..=mb.bottom {
489 let slice = b.line(i).unwrap();
490 let bg = if i == mb.selected_line_idx {
491 cs.minibuffer_hl
492 } else {
493 cs.bg
494 };
495
496 let mut cols = 0;
497 let mut chars = slice.chars().peekable();
498 _ = write!(
499 &mut self.mb_lines,
500 "{}",
501 Styles {
502 fg: Some(cs.fg),
503 bg: Some(bg),
504 ..Default::default()
505 }
506 );
507
508 render_chars(
509 &mut chars,
510 None,
511 self.screen_cols,
512 tabstop,
513 &mut cols,
514 &mut self.mb_lines,
515 );
516
517 if cols < self.screen_cols {
518 self.mb_lines.push_str(&Style::Bg(bg).to_string());
519 }
520
521 let width = self.screen_cols;
522 _ = write!(&mut self.mb_lines, "{:>width$}\r\n", Cursor::ClearRight);
523 }
524 }
525
526 _ = write!(
527 &mut self.mb_lines,
528 "{}{}{}{}{}",
529 Style::Fg(cs.fg),
530 Style::Bg(cs.bg),
531 mb.prompt,
532 mb.input,
533 Cursor::ClearRight
534 );
535 }
536
537 fn render_scratch(
538 &mut self,
539 scratch: &Scratch,
540 load_exec_range: Option<(bool, Range)>,
541 tabstop: usize,
542 cs: &ColorScheme,
543 ) {
544 self.mb_lines.clear();
545 let b = scratch.b.buffer();
546 let (w_lnum, _) = b.sign_col_dims();
547 let rng = if scratch.is_focused {
548 load_exec_range
549 } else {
550 None
551 };
552
553 let mut wr = WinRenderer {
554 y: 0,
555 w_lnum,
556 n_cols: self.screen_cols,
557 tabstop,
558 it: b.iter_tokenized_lines_from(scratch.w.view.row_off, rng),
559 gb: &b.txt,
560 w: &scratch.w,
561 cs,
562 };
563
564 while wr.render_next_line(&mut self.mb_lines, None, &mut self.style_cache) {}
565 }
566}
567
568#[derive(Clone, Copy)]
569enum PrevCol {
570 Buffer,
571 Hline,
572}
573
574struct ColRenderer<'a> {
575 inner: ziplist::Iter<'a, Window>,
576 current: Option<WinRenderer<'a>>,
577 layout: &'a Layout,
578 cs: &'a ColorScheme,
579 load_exec_range: Option<(bool, Range)>,
580 screen_rows: usize,
581 tabstop: usize,
582 n_cols: usize,
583 row: usize,
584}
585
586impl<'a> ColRenderer<'a> {
587 fn new(
588 col: &'a Column,
589 layout: &'a Layout,
590 load_exec_range: Option<(bool, Range)>,
591 screen_rows: usize,
592 tabstop: usize,
593 cs: &'a ColorScheme,
594 ) -> Self {
595 ColRenderer {
596 inner: col.wins.iter(),
597 current: None,
598 layout,
599 cs,
600 load_exec_range,
601 screen_rows,
602 tabstop,
603 n_cols: col.n_cols,
604 row: 0,
605 }
606 }
607
608 fn next_window(&mut self) -> Option<WinRenderer<'a>> {
609 let (is_focus, w) = self.inner.next()?;
610 let b = self
611 .layout
612 .buffer_with_id(w.view.bufid)
613 .expect("valid buffer id");
614
615 let (w_lnum, _) = b.sign_col_dims();
616 let rng = if is_focus { self.load_exec_range } else { None };
617 let it = b.iter_tokenized_lines_from(w.view.row_off, rng);
618
619 Some(WinRenderer {
620 y: 0,
621 w_lnum,
622 n_cols: self.n_cols,
623 tabstop: self.tabstop,
624 it,
625 gb: &b.txt,
626 w,
627 cs: self.cs,
628 })
629 }
630
631 fn render_next_line(
635 &mut self,
636 buf: &mut String,
637 prev_col: Option<PrevCol>,
638 style_cache: &mut HashMap<String, String>,
639 ) -> (bool, Option<PrevCol>) {
640 if self.current.is_none() {
641 self.current = match self.next_window() {
642 Some(w) => Some(w),
643 None => return (false, None),
644 }
645 }
646
647 let lines_remaining =
648 self.current
649 .as_mut()
650 .unwrap()
651 .render_next_line(buf, prev_col, style_cache);
652 self.row += 1;
653
654 let this_col = if lines_remaining {
655 Some(PrevCol::Buffer)
656 } else {
657 self.current = None;
658 let left_edge = match prev_col {
659 Some(PrevCol::Buffer) => TR_STR,
660 Some(PrevCol::Hline) => X_STR,
661 None => "",
662 };
663
664 _ = write!(
665 buf,
666 "{}{}{left_edge}{}",
667 Style::Fg(self.cs.minibuffer_hl),
668 Style::Bg(self.cs.bg),
669 H_STR.repeat(self.n_cols)
670 );
671 Some(PrevCol::Hline)
672 };
673
674 (self.row < self.screen_rows, this_col)
675 }
676}
677
678struct WinRenderer<'a> {
679 y: usize,
680 w_lnum: usize,
681 n_cols: usize,
682 tabstop: usize,
683 it: LineIter<'a>,
684 gb: &'a GapBuffer,
685 w: &'a Window,
686 cs: &'a ColorScheme,
687}
688
689impl<'a> WinRenderer<'a> {
690 fn render_next_line(
691 &mut self,
692 buf: &mut String,
693 prev_col: Option<PrevCol>,
694 style_cache: &mut HashMap<String, String>,
695 ) -> bool {
696 if self.y >= self.w.n_rows {
697 return false;
698 }
699
700 let file_row = self.y + self.w.view.row_off;
701 self.y += 1;
702
703 if let Some(pc) = prev_col {
704 let left_edge = match pc {
705 PrevCol::Buffer => V_STR,
706 PrevCol::Hline => TL_STR,
707 };
708
709 _ = write!(
710 buf,
711 "{}{}{left_edge}",
712 Style::Fg(self.cs.minibuffer_hl),
713 Style::Bg(self.cs.bg)
714 );
715 }
716
717 match self.it.next() {
718 None => {
719 _ = write!(
720 buf,
721 "{}{}~ {V_STR:>width$}{}",
722 Style::Fg(self.cs.signcol_fg),
723 Style::Bg(self.cs.bg),
724 Style::Fg(self.cs.fg),
725 width = self.w_lnum
726 );
727 let padding = self.n_cols.saturating_sub(self.w_lnum).saturating_sub(2);
728 buf.push_str(&" ".repeat(padding));
729 }
730
731 Some(it) => {
732 let padding = self.w_lnum + 2;
734
735 _ = write!(
736 buf,
737 "{}{} {:>width$}{V_STR}",
738 Style::Fg(self.cs.signcol_fg),
739 Style::Bg(self.cs.bg),
740 file_row + 1,
741 width = self.w_lnum
742 );
743
744 render_line(
745 self.gb,
746 it,
747 self.w.view.col_off,
748 self.n_cols.saturating_sub(padding),
749 self.tabstop,
750 self.cs,
751 style_cache,
752 buf,
753 );
754 }
755 };
756
757 true
758 }
759}
760
761fn render_pending(keys: &[Input]) -> String {
762 let mut s = String::new();
763 for k in keys {
764 match k {
765 Input::Char(c) if c.is_ascii_whitespace() => s.push_str(&format!("<{:x}>", *c as u8)),
766 Input::Char(c) => s.push(*c),
767 Input::Ctrl(c) => {
768 s.push('^');
769 s.push(*c);
770 }
771 Input::Alt(c) => {
772 s.push('^');
773 s.push('[');
774 s.push(*c);
775 }
776 Input::CtrlAlt(c) => {
777 s.push('^');
778 s.push('[');
779 s.push('^');
780 s.push(*c);
781 }
782
783 _ => (),
784 }
785 }
786
787 if s.len() > 10 {
788 s = s.split_off(s.len() - 10);
789 }
790
791 s
792}
793
794#[inline]
795fn skip_token_chars(
796 chars: &mut Peekable<Chars<'_>>,
797 tabstop: usize,
798 to_skip: &mut usize,
799) -> Option<usize> {
800 for ch in chars.by_ref() {
801 let w = if ch == '\t' {
802 tabstop
803 } else {
804 UnicodeWidthChar::width(ch).unwrap_or(1)
805 };
806
807 match (*to_skip).cmp(&w) {
808 Ordering::Less => {
809 let spaces = Some(w - *to_skip);
810 *to_skip = 0;
811 return spaces;
812 }
813
814 Ordering::Equal => {
815 *to_skip = 0;
816 break;
817 }
818
819 Ordering::Greater => *to_skip -= w,
820 }
821 }
822
823 None
824}
825
826fn render_chars(
827 chars: &mut Peekable<Chars<'_>>,
828 spaces: Option<usize>,
829 max_cols: usize,
830 tabstop: usize,
831 cols: &mut usize,
832 buf: &mut String,
833) {
834 if let Some(n) = spaces {
835 buf.extend(repeat_n(' ', n));
836 *cols = n;
837 }
838
839 for ch in chars {
840 if ch == '\n' {
841 break;
842 }
843
844 let (w, ch) = if ch == '\t' {
845 (tabstop, ch)
846 } else {
847 match UnicodeWidthChar::width(ch) {
848 Some(0) | None => (1, char::REPLACEMENT_CHARACTER),
849 Some(n) => (n, ch),
850 }
851 };
852
853 if *cols + w <= max_cols {
854 if ch == '\t' {
855 buf.extend(repeat_n(' ', tabstop));
859 } else {
860 buf.push(ch);
861 }
862 *cols += w;
863 } else {
864 break;
865 }
866 }
867
868 buf.push_str(RESET_STYLE);
869}
870
871#[allow(clippy::too_many_arguments)]
872fn render_line<'a>(
873 gb: &'a GapBuffer,
874 it: impl Iterator<Item = RangeToken<'a>>,
875 col_off: usize,
876 max_cols: usize,
877 tabstop: usize,
878 cs: &ColorScheme,
879 style_cache: &mut HashMap<String, String>,
880 buf: &mut String,
881) {
882 let mut to_skip = col_off;
883 let mut cols = 0;
884
885 for tk in it {
886 let slice = tk.as_slice(gb);
887 let mut chars = slice.chars().peekable();
888 let spaces = if to_skip > 0 {
889 let spaces = skip_token_chars(&mut chars, tabstop, &mut to_skip);
890 if to_skip > 0 || (chars.peek().is_none() && spaces.is_none()) {
891 continue;
892 }
893 spaces
894 } else {
895 None
896 };
897
898 let style_str = match style_cache.get(tk.tag) {
907 Some(s) => s,
908 None => {
909 let s = cs.styles_for(tk.tag).to_string();
910 style_cache.insert(tk.tag.to_string(), s);
911 style_cache.get(tk.tag).unwrap()
912 }
913 };
914
915 buf.push_str(style_str);
916 render_chars(&mut chars, spaces, max_cols, tabstop, &mut cols, buf);
917
918 if cols == max_cols {
919 break;
920 }
921 }
922
923 if cols < max_cols {
924 buf.push_str(&Style::Bg(cs.bg).to_string());
925 buf.extend(repeat_n(' ', max_cols - cols));
926 }
927}
928
929#[derive(Debug)]
930enum RawInput {
931 Input(Input),
932 BPasteStart,
936}
937
938impl From<Input> for RawInput {
939 fn from(value: Input) -> Self {
940 Self::Input(value)
941 }
942}
943
944fn spawn_input_thread(tx: Sender<Event>) -> JoinHandle<()> {
947 spawn(move || {
948 let mut stdin = stdin().lock();
949
950 loop {
951 match try_read_input(&mut stdin) {
952 Some(RawInput::Input(i)) => {
953 _ = tx.send(Event::Input(i));
954 continue;
955 }
956
957 Some(RawInput::BPasteStart) => {
958 let mut s = String::new();
959 let mut buf = Vec::with_capacity(6);
960
961 while let Some(c) = try_read_char(&mut stdin) {
962 match (c, buf.as_slice()) {
963 ('\x1b', [])
964 | ('[', ['\x1b'])
965 | ('2', ['\x1b', '['])
966 | ('0', ['\x1b', '[', '2'])
967 | ('1', ['\x1b', '[', '2', '0']) => buf.push(c),
968
969 ('~', ['\x1b', '[', '2', '0', '1']) => {
970 _ = tx.send(Event::BracketedPaste(s));
971 break;
972 }
973
974 (c, _) => {
975 s.extend(buf.drain(..));
976 s.push(c);
977 }
978 }
979 }
980 }
981
982 None => (),
983 }
984
985 if win_size_changed() {
988 let (rows, cols) = get_termsize();
989 _ = tx.send(Event::WinsizeChanged { rows, cols });
990 }
991 }
992 })
993}
994
995fn try_read_char(stdin: &mut impl Read) -> Option<char> {
996 let mut buf: [u8; 4] = [0; 4];
997
998 for i in 0..4 {
999 if stdin.read_exact(&mut buf[i..i + 1]).is_err() {
1000 return if i == 0 {
1001 None
1002 } else {
1003 Some(char::REPLACEMENT_CHARACTER)
1004 };
1005 }
1006
1007 match std::str::from_utf8(&buf[0..i + 1]) {
1008 Ok(s) => return s.chars().next(),
1009 Err(e) if e.error_len().is_some() => return Some(char::REPLACEMENT_CHARACTER),
1010 Err(_) => (),
1011 }
1012 }
1013
1014 Some(char::REPLACEMENT_CHARACTER)
1016}
1017
1018fn try_read_input(stdin: &mut impl Read) -> Option<RawInput> {
1019 let c = try_read_char(stdin)?;
1020
1021 match Input::from_char(c) {
1023 Input::Esc => (),
1024 i => return Some(i.into()),
1025 }
1026
1027 let c2 = match try_read_char(stdin) {
1028 Some(c2) => c2,
1029 None => return Some(Input::Esc.into()),
1030 };
1031 let c3 = match try_read_char(stdin) {
1032 Some(c3) => c3,
1033 None => return Some(Input::try_from_seq2(c, c2).unwrap_or(Input::Esc).into()),
1034 };
1035
1036 if let Some(i) = Input::try_from_seq2(c2, c3) {
1037 return Some(i.into());
1038 }
1039
1040 if c2 == '[' && c3.is_ascii_digit() {
1042 let mut digits = vec![c3];
1043 loop {
1044 match try_read_char(stdin)? {
1045 c if c.is_ascii_digit() => digits.push(c),
1046 '~' => break,
1047 c => {
1048 debug!("unknown CSIC sequence: ^[[{}{c}", String::from_iter(digits));
1049 return None;
1050 }
1051 }
1052 }
1053
1054 return match digits.as_slice() {
1056 ['1' | '7'] => Some(Input::Home.into()),
1057 ['4' | '8'] => Some(Input::End.into()),
1058 ['3'] => Some(Input::Del.into()),
1059 ['5'] => Some(Input::PageUp.into()),
1060 ['6'] => Some(Input::PageDown.into()),
1061 ['2', '0', '0'] => Some(RawInput::BPasteStart),
1062 _ => None,
1064 };
1065 }
1066
1067 if c2 == '[' && c3 == '<' {
1070 let mut buf = Vec::new();
1071 let m;
1072
1073 loop {
1074 match try_read_char(stdin) {
1075 Some(c @ 'm' | c @ 'M') => {
1076 m = c;
1077 break;
1078 }
1079 Some(c) => buf.push(c as u8),
1080 None => return None,
1081 };
1082 }
1083 let s = String::from_utf8(buf).unwrap();
1084 let nums: Vec<usize> = s.split(';').map(|s| s.parse::<usize>().unwrap()).collect();
1085 let (b, x, y) = (nums[0], nums[1], nums[2]);
1086
1087 return MouseEvent::try_from_raw(b, x, y, m).map(|i| RawInput::Input(Input::Mouse(i)));
1088 }
1089
1090 Some(Input::Esc.into())
1091}
1092
1093#[cfg(test)]
1094mod tests {
1095 use super::*;
1096 use crate::syntax::{ByteRange, TK_DEFAULT};
1097 use simple_test_case::test_case;
1098 use std::{char::REPLACEMENT_CHARACTER, io};
1099
1100 #[test_case("a".as_bytes(), &['a']; "single ascii character")]
1101 #[test_case(&[240, 159, 146, 150], &['💖']; "single utf8 character")]
1102 #[test_case(&[165, 159, 146, 150], &[REPLACEMENT_CHARACTER; 4]; "invalid utf8 with non-ascii prefix")]
1103 #[test_case(&[65, 159, 146, 150], &['A', REPLACEMENT_CHARACTER, REPLACEMENT_CHARACTER, REPLACEMENT_CHARACTER]; "invalid utf8 with ascii prefix")]
1104 #[test]
1105 fn try_read_char_works(bytes: &[u8], expected: &[char]) {
1106 let mut r = io::Cursor::new(bytes);
1107 let mut chars = Vec::new();
1108
1109 while let Some(ch) = try_read_char(&mut r) {
1110 chars.push(ch);
1111 }
1112
1113 assert_eq!(&chars, expected);
1114 }
1115
1116 fn rt(tag: &'static str, from: usize, to: usize) -> RangeToken<'static> {
1117 RangeToken {
1118 tag,
1119 r: ByteRange { from, to },
1120 }
1121 }
1122
1123 #[test]
1126 fn render_chars_correctly_handles_bidi_markers() {
1127 let line = GapBuffer::from("foobarbaz");
1128 let expected = format!("�foo�bar�baz�{RESET_STYLE}");
1129
1130 let max_cols = line.chars().count();
1131 let mut chars = line.chars().peekable();
1132 let mut buf = String::with_capacity(line.len());
1133 let mut cols = 0;
1134
1135 render_chars(&mut chars, None, max_cols, 4, &mut cols, &mut buf);
1136
1137 assert_eq!(buf, expected);
1138 }
1139
1140 #[test_case(0, 14, "foo\tbar baz", "!foo$| $!bar$| $!baz$# "; "full line padded to max cols")]
1143 #[test_case(0, 12, "foo\tbar baz", "!foo$| $!bar$| $!baz$"; "full line")]
1144 #[test_case(1, 11, "foo\tbar baz", "!oo$| $!bar$| $!baz$"; "skipping first character")]
1145 #[test_case(3, 9, "foo\tbar baz", "| $!bar$| $!baz$"; "skipping first token")]
1146 #[test_case(4, 8, "foo\tbar baz", "| $!bar$| $!baz$"; "skipping part way through a tab")]
1147 #[test_case(0, 10, "世\t界 foo", "!世$| $!界$| $!foo$"; "unicode full line")]
1148 #[test_case(0, 12, "世\t界 foo", "!世$| $!界$| $!foo$# "; "unicode full line padded to max cols")]
1149 #[test_case(1, 9, "世\t界 foo", "! $| $!界$| $!foo$"; "unicode skipping first column of multibyte char")]
1153 #[test]
1154 fn render_line_correctly_skips_tokens(
1155 col_off: usize,
1156 max_cols: usize,
1157 s: &str,
1158 expected_template: &str,
1159 ) {
1160 let gb = GapBuffer::from(s);
1161 let range_tokens = vec![
1162 rt("a", 0, 3),
1163 rt(TK_DEFAULT, 3, 4),
1164 rt("a", 4, 7),
1165 rt(TK_DEFAULT, 7, 8),
1166 rt("a", 8, 11),
1167 ];
1168
1169 let cs = ColorScheme::default();
1170 let mut style_cache: HashMap<String, String> = [
1171 ("a".to_owned(), "!".to_owned()),
1172 (TK_DEFAULT.to_owned(), "|".to_owned()),
1173 ]
1174 .into_iter()
1175 .collect();
1176
1177 let mut s = String::new();
1178 render_line(
1179 &gb,
1180 range_tokens.into_iter(),
1181 col_off,
1182 max_cols,
1183 2,
1184 &cs,
1185 &mut style_cache,
1186 &mut s,
1187 );
1188
1189 let expected = expected_template
1190 .replace("$", RESET_STYLE)
1191 .replace("#", &Style::Bg(cs.bg).to_string());
1192
1193 assert_eq!(s, expected);
1194 }
1195
1196 #[test]
1198 fn minibuffer_lines_with_multibyte_chars_dont_panic() {
1199 let s = " 56 | Fastställa att under samtliga öppetdagar den här veckan så finns det alltid minst en";
1200 let b = Buffer::new_virtual(0, "test", s, Default::default());
1201
1202 let mb = MiniBufferState {
1203 cx: 0,
1204 n_visible_lines: 10,
1205 selected_line_idx: 0,
1206 prompt: "> ",
1207 input: Default::default(),
1208 b: Some(&b),
1209 top: 0,
1210 bottom: 0,
1211 };
1212
1213 let mut frame = Frame::new();
1214 frame.screen_cols = 91;
1215
1216 frame.render_minibuffer_state(&mb, 4, &Default::default());
1221 }
1222}