zellij_server/panes/
terminal_pane.rs

1use crate::output::{CharacterChunk, SixelImageChunk};
2use crate::panes::sixel::SixelImageStore;
3use crate::panes::LinkHandler;
4use crate::panes::{
5    grid::Grid,
6    terminal_character::{render_first_run_banner, TerminalCharacter, EMPTY_TERMINAL_CHARACTER},
7};
8use crate::pty::VteBytes;
9use crate::tab::{AdjustedInput, Pane};
10use crate::ClientId;
11use std::cell::RefCell;
12use std::collections::{HashMap, HashSet};
13use std::fmt::Debug;
14use std::rc::Rc;
15use std::time::{self, Instant};
16use vte;
17use zellij_utils::input::command::RunCommand;
18use zellij_utils::input::mouse::{MouseEvent, MouseEventType};
19use zellij_utils::pane_size::Offset;
20use zellij_utils::{
21    data::{
22        BareKey, InputMode, KeyWithModifier, Palette, PaletteColor, PaneId as ZellijUtilsPaneId,
23        Style, Styling,
24    },
25    errors::prelude::*,
26    input::layout::Run,
27    pane_size::PaneGeom,
28    pane_size::SizeInPixels,
29    position::Position,
30    shared::make_terminal_title,
31};
32
33use crate::ui::pane_boundaries_frame::{FrameParams, PaneFrame};
34
35pub const SELECTION_SCROLL_INTERVAL_MS: u64 = 10;
36
37// Some keys in different formats but are used in the code
38const LEFT_ARROW: &[u8] = &[27, 91, 68];
39const RIGHT_ARROW: &[u8] = &[27, 91, 67];
40const UP_ARROW: &[u8] = &[27, 91, 65];
41const DOWN_ARROW: &[u8] = &[27, 91, 66];
42const HOME_KEY: &[u8] = &[27, 91, 72];
43const END_KEY: &[u8] = &[27, 91, 70];
44pub const BRACKETED_PASTE_BEGIN: &[u8] = &[27, 91, 50, 48, 48, 126];
45pub const BRACKETED_PASTE_END: &[u8] = &[27, 91, 50, 48, 49, 126];
46const ENTER_NEWLINE: &[u8] = &[10];
47const ESC: &[u8] = &[27];
48const ENTER_CARRIAGE_RETURN: &[u8] = &[13];
49const SPACE: &[u8] = &[32];
50const CTRL_C: &[u8] = &[3]; // TODO: check this to be sure it fits all types of CTRL_C (with mac, etc)
51const TERMINATING_STRING: &str = "\0";
52const DELETE_KEY: &str = "\u{007F}";
53const BACKSPACE_KEY: &str = "\u{0008}";
54
55/// The ansi encoding of some keys
56#[derive(Debug, Clone, PartialEq, Eq, Hash)]
57enum AnsiEncoding {
58    Left,
59    Right,
60    Up,
61    Down,
62    Home,
63    End,
64}
65
66impl AnsiEncoding {
67    /// Returns the ANSI representation of the entries.
68    /// NOTE: There is an ANSI escape code (27) at the beginning of the string,
69    ///       some editors will not show this
70    pub fn as_bytes(&self) -> &[u8] {
71        match self {
72            Self::Left => "OD".as_bytes(),
73            Self::Right => "OC".as_bytes(),
74            Self::Up => "OA".as_bytes(),
75            Self::Down => "OB".as_bytes(),
76            Self::Home => &[27, 79, 72], // ESC O H
77            Self::End => &[27, 79, 70],  // ESC O F
78        }
79    }
80
81    pub fn as_vec_bytes(&self) -> Vec<u8> {
82        self.as_bytes().to_vec()
83    }
84}
85
86#[derive(PartialEq, Eq, Ord, PartialOrd, Hash, Clone, Copy, Debug)]
87pub enum PaneId {
88    Terminal(u32),
89    Plugin(u32), // FIXME: Drop the trait object, make this a wrapper for the struct?
90}
91
92// because crate architecture and reasons...
93impl From<ZellijUtilsPaneId> for PaneId {
94    fn from(zellij_utils_pane_id: ZellijUtilsPaneId) -> Self {
95        match zellij_utils_pane_id {
96            ZellijUtilsPaneId::Terminal(id) => PaneId::Terminal(id),
97            ZellijUtilsPaneId::Plugin(id) => PaneId::Plugin(id),
98        }
99    }
100}
101
102impl Into<ZellijUtilsPaneId> for PaneId {
103    fn into(self) -> ZellijUtilsPaneId {
104        match self {
105            PaneId::Terminal(id) => ZellijUtilsPaneId::Terminal(id),
106            PaneId::Plugin(id) => ZellijUtilsPaneId::Plugin(id),
107        }
108    }
109}
110
111type IsFirstRun = bool;
112
113// FIXME: This should hold an os_api handle so that terminal panes can set their own size via FD in
114// their `reflow_lines()` method. Drop a Box<dyn ServerOsApi> in here somewhere.
115#[allow(clippy::too_many_arguments)]
116pub struct TerminalPane {
117    pub grid: Grid,
118    pub pid: u32,
119    pub selectable: bool,
120    pub geom: PaneGeom,
121    pub geom_override: Option<PaneGeom>,
122    pub active_at: Instant,
123    pub style: Style,
124    vte_parser: vte::Parser,
125    selection_scrolled_at: time::Instant,
126    content_offset: Offset,
127    pane_title: String,
128    pane_name: String,
129    prev_pane_name: String,
130    frame: HashMap<ClientId, PaneFrame>,
131    borderless: bool,
132    exclude_from_sync: bool,
133    fake_cursor_locations: HashSet<(usize, usize)>, // (x, y) - these hold a record of previous fake cursors which we need to clear on render
134    search_term: String,
135    is_held: Option<(Option<i32>, IsFirstRun, RunCommand)>, // a "held" pane means that its command has either exited and the pane is waiting for a
136    // possible user instruction to be re-run, or that the command has not yet been run
137    banner: Option<String>, // a banner to be rendered inside this TerminalPane, used for panes
138    // held on startup and can possibly be used to display some errors
139    pane_frame_color_override: Option<(PaletteColor, Option<String>)>,
140    invoked_with: Option<Run>,
141    #[allow(dead_code)]
142    arrow_fonts: bool,
143}
144
145impl Pane for TerminalPane {
146    fn x(&self) -> usize {
147        self.get_x()
148    }
149    fn y(&self) -> usize {
150        self.get_y()
151    }
152    fn rows(&self) -> usize {
153        self.get_rows()
154    }
155    fn cols(&self) -> usize {
156        self.get_columns()
157    }
158    fn get_content_x(&self) -> usize {
159        self.get_x() + self.content_offset.left
160    }
161    fn get_content_y(&self) -> usize {
162        self.get_y() + self.content_offset.top
163    }
164    fn get_content_columns(&self) -> usize {
165        // content columns might differ from the pane's columns if the pane has a frame
166        // in that case they would be 2 less
167        self.get_columns()
168            .saturating_sub(self.content_offset.left + self.content_offset.right)
169    }
170    fn get_content_rows(&self) -> usize {
171        // content rows might differ from the pane's rows if the pane has a frame
172        // in that case they would be 2 less
173        self.get_rows()
174            .saturating_sub(self.content_offset.top + self.content_offset.bottom)
175    }
176    fn reset_size_and_position_override(&mut self) {
177        self.geom_override = None;
178        self.reflow_lines();
179    }
180    fn set_geom(&mut self, position_and_size: PaneGeom) {
181        let is_pinned = self.geom.is_pinned;
182        self.geom = position_and_size;
183        self.geom.is_pinned = is_pinned;
184        self.reflow_lines();
185        self.render_full_viewport();
186    }
187    fn set_geom_override(&mut self, pane_geom: PaneGeom) {
188        self.geom_override = Some(pane_geom);
189        self.reflow_lines();
190    }
191    fn handle_pty_bytes(&mut self, bytes: VteBytes) {
192        self.set_should_render(true);
193        for &byte in &bytes {
194            self.vte_parser.advance(&mut self.grid, byte);
195        }
196    }
197    fn cursor_coordinates(&self) -> Option<(usize, usize)> {
198        // (x, y)
199        if self.get_content_rows() < 1 || self.get_content_columns() < 1 {
200            // do not render cursor if there's no room for it
201            return None;
202        }
203        let Offset { top, left, .. } = self.content_offset;
204        self.grid
205            .cursor_coordinates()
206            .map(|(x, y)| (x + left, y + top))
207    }
208    fn is_mid_frame(&self) -> bool {
209        self.grid.is_mid_frame()
210    }
211    fn adjust_input_to_terminal(
212        &mut self,
213        key_with_modifier: &Option<KeyWithModifier>,
214        raw_input_bytes: Vec<u8>,
215        raw_input_bytes_are_kitty: bool,
216        client_id: Option<ClientId>,
217    ) -> Option<AdjustedInput> {
218        // there are some cases in which the terminal state means that input sent to it
219        // needs to be adjusted.
220        // here we match against those cases - if need be, we adjust the input and if not
221        // we send back the original input
222
223        self.reset_selection(client_id);
224        if !self.grid.bracketed_paste_mode {
225            // Zellij itself operates in bracketed paste mode, so the terminal sends these
226            // instructions (bracketed paste start and bracketed paste end respectively)
227            // when pasting input. We only need to make sure not to send them to terminal
228            // panes who do not work in this mode
229            match raw_input_bytes.as_slice() {
230                BRACKETED_PASTE_BEGIN | BRACKETED_PASTE_END => {
231                    return Some(AdjustedInput::WriteBytesToTerminal(vec![]))
232                },
233                _ => {},
234            }
235        }
236
237        if self.is_held.is_some() {
238            if key_with_modifier
239                .as_ref()
240                .map(|k| k.is_key_without_modifier(BareKey::Enter))
241                .unwrap_or(false)
242            {
243                self.handle_held_run()
244            } else if key_with_modifier
245                .as_ref()
246                .map(|k| k.is_key_without_modifier(BareKey::Esc))
247                .unwrap_or(false)
248            {
249                self.handle_held_drop_to_shell()
250            } else if key_with_modifier
251                .as_ref()
252                .map(|k| k.is_key_with_ctrl_modifier(BareKey::Char('c')))
253                .unwrap_or(false)
254            {
255                Some(AdjustedInput::CloseThisPane)
256            } else {
257                match raw_input_bytes.as_slice() {
258                    ENTER_CARRIAGE_RETURN | ENTER_NEWLINE | SPACE => self.handle_held_run(),
259                    ESC => self.handle_held_drop_to_shell(),
260                    CTRL_C => Some(AdjustedInput::CloseThisPane),
261                    _ => None,
262                }
263            }
264        } else {
265            if self.grid.supports_kitty_keyboard_protocol {
266                self.adjust_input_to_terminal_with_kitty_keyboard_protocol(
267                    key_with_modifier,
268                    raw_input_bytes,
269                    raw_input_bytes_are_kitty,
270                )
271            } else {
272                self.adjust_input_to_terminal_without_kitty_keyboard_protocol(
273                    key_with_modifier,
274                    raw_input_bytes,
275                    raw_input_bytes_are_kitty,
276                )
277            }
278        }
279    }
280    fn position_and_size(&self) -> PaneGeom {
281        self.geom
282    }
283    fn current_geom(&self) -> PaneGeom {
284        self.geom_override.unwrap_or(self.geom)
285    }
286    fn geom_override(&self) -> Option<PaneGeom> {
287        self.geom_override
288    }
289    fn should_render(&self) -> bool {
290        self.grid.should_render
291    }
292    fn set_should_render(&mut self, should_render: bool) {
293        self.grid.should_render = should_render;
294    }
295    fn render_full_viewport(&mut self) {
296        // this marks the pane for a full re-render, rather than just rendering the
297        // diff as it usually does with the OutputBuffer
298        self.frame.clear();
299        self.grid.render_full_viewport();
300    }
301    fn selectable(&self) -> bool {
302        self.selectable
303    }
304    fn set_selectable(&mut self, selectable: bool) {
305        self.selectable = selectable;
306    }
307    fn render(
308        &mut self,
309        _client_id: Option<ClientId>,
310    ) -> Result<Option<(Vec<CharacterChunk>, Option<String>, Vec<SixelImageChunk>)>> {
311        if self.should_render() {
312            let content_x = self.get_content_x();
313            let content_y = self.get_content_y();
314            let rows = self.get_content_rows();
315            let columns = self.get_content_columns();
316            if rows < 1 || columns < 1 {
317                return Ok(None);
318            }
319            match self.grid.render(content_x, content_y, &self.style) {
320                Ok(rendered_assets) => {
321                    self.set_should_render(false);
322                    return Ok(rendered_assets);
323                },
324                e => return e,
325            }
326        } else {
327            Ok(None)
328        }
329    }
330    fn render_frame(
331        &mut self,
332        client_id: ClientId,
333        frame_params: FrameParams,
334        input_mode: InputMode,
335    ) -> Result<Option<(Vec<CharacterChunk>, Option<String>)>> {
336        let err_context = || format!("failed to render frame for client {client_id}");
337        // TODO: remove the cursor stuff from here
338        let pane_title = if let Some(text_color_override) = self
339            .pane_frame_color_override
340            .as_ref()
341            .and_then(|(_color, text)| text.as_ref())
342        {
343            text_color_override.into()
344        } else if self.pane_name.is_empty()
345            && input_mode == InputMode::RenamePane
346            && frame_params.is_main_client
347        {
348            String::from("Enter name...")
349        } else if input_mode == InputMode::EnterSearch
350            && frame_params.is_main_client
351            && self.search_term.is_empty()
352        {
353            String::from("Enter search...")
354        } else if (input_mode == InputMode::EnterSearch || input_mode == InputMode::Search)
355            && !self.search_term.is_empty()
356        {
357            let mut modifier_text = String::new();
358            if self.grid.search_results.has_modifiers_set() {
359                let mut modifiers = Vec::new();
360                modifier_text.push_str(" [");
361                if self.grid.search_results.case_insensitive {
362                    modifiers.push("c")
363                }
364                if self.grid.search_results.whole_word_only {
365                    modifiers.push("o")
366                }
367                if self.grid.search_results.wrap_search {
368                    modifiers.push("w")
369                }
370                modifier_text.push_str(&modifiers.join(", "));
371                modifier_text.push(']');
372            }
373            format!("SEARCHING: {}{}", self.search_term, modifier_text)
374        } else if self.pane_name.is_empty() {
375            self.grid
376                .title
377                .clone()
378                .unwrap_or_else(|| self.pane_title.clone())
379        } else {
380            self.pane_name.clone()
381        };
382
383        let frame_geom = self.current_geom();
384        let is_pinned = frame_geom.is_pinned;
385        let mut frame = PaneFrame::new(
386            frame_geom.into(),
387            self.grid.scrollback_position_and_length(),
388            pane_title,
389            frame_params,
390        )
391        .is_pinned(is_pinned);
392        if let Some((exit_status, is_first_run, _run_command)) = &self.is_held {
393            if *is_first_run {
394                frame.indicate_first_run();
395            } else {
396                frame.add_exit_status(exit_status.as_ref().copied());
397            }
398        }
399        if let Some((frame_color_override, _text)) = self.pane_frame_color_override.as_ref() {
400            frame.override_color(*frame_color_override);
401        }
402
403        let res = match self.frame.get(&client_id) {
404            // TODO: use and_then or something?
405            Some(last_frame) => {
406                if &frame != last_frame {
407                    if !self.borderless {
408                        let frame_output = frame.render().with_context(err_context)?;
409                        self.frame.insert(client_id, frame);
410                        Some(frame_output)
411                    } else {
412                        None
413                    }
414                } else {
415                    None
416                }
417            },
418            None => {
419                if !self.borderless {
420                    let frame_output = frame.render().with_context(err_context)?;
421                    self.frame.insert(client_id, frame);
422                    Some(frame_output)
423                } else {
424                    None
425                }
426            },
427        };
428        Ok(res)
429    }
430    fn render_fake_cursor(
431        &mut self,
432        cursor_color: PaletteColor,
433        text_color: PaletteColor,
434    ) -> Option<String> {
435        let mut vte_output = None;
436        if let Some((cursor_x, cursor_y)) = self.cursor_coordinates() {
437            let mut character_under_cursor = self
438                .grid
439                .get_character_under_cursor()
440                .unwrap_or(EMPTY_TERMINAL_CHARACTER);
441            character_under_cursor.styles.update(|styles| {
442                styles.background = Some(cursor_color.into());
443                styles.foreground = Some(text_color.into());
444            });
445            // we keep track of these so that we can clear them up later (see render function)
446            self.fake_cursor_locations.insert((cursor_y, cursor_x));
447            let mut fake_cursor = format!(
448                "\u{1b}[{};{}H\u{1b}[m{}",           // goto row column and clear styles
449                self.get_content_y() + cursor_y + 1, // + 1 because goto is 1 indexed
450                self.get_content_x() + cursor_x + 1,
451                &character_under_cursor.styles,
452            );
453            fake_cursor.push(character_under_cursor.character);
454            vte_output = Some(fake_cursor);
455        }
456        vte_output
457    }
458    fn render_terminal_title(&mut self, input_mode: InputMode) -> String {
459        let pane_title = if self.pane_name.is_empty() && input_mode == InputMode::RenamePane {
460            "Enter name..."
461        } else if self.pane_name.is_empty() {
462            self.grid.title.as_deref().unwrap_or("")
463        } else {
464            &self.pane_name
465        };
466        make_terminal_title(pane_title)
467    }
468    fn update_name(&mut self, name: &str) {
469        match name {
470            TERMINATING_STRING => {
471                self.pane_name = String::new();
472            },
473            DELETE_KEY | BACKSPACE_KEY => {
474                self.pane_name.pop();
475            },
476            c => {
477                self.pane_name.push_str(c);
478            },
479        }
480        self.set_should_render(true);
481    }
482    fn pid(&self) -> PaneId {
483        PaneId::Terminal(self.pid)
484    }
485    fn reduce_height(&mut self, percent: f64) {
486        if let Some(p) = self.geom.rows.as_percent() {
487            self.geom.rows.set_percent(p - percent);
488            self.set_should_render(true);
489        }
490    }
491    fn increase_height(&mut self, percent: f64) {
492        if let Some(p) = self.geom.rows.as_percent() {
493            self.geom.rows.set_percent(p + percent);
494            self.set_should_render(true);
495        }
496    }
497    fn reduce_width(&mut self, percent: f64) {
498        if let Some(p) = self.geom.cols.as_percent() {
499            self.geom.cols.set_percent(p - percent);
500            self.set_should_render(true);
501        }
502    }
503    fn increase_width(&mut self, percent: f64) {
504        if let Some(p) = self.geom.cols.as_percent() {
505            self.geom.cols.set_percent(p + percent);
506            self.set_should_render(true);
507        }
508    }
509    fn push_down(&mut self, count: usize) {
510        self.geom.y += count;
511        self.reflow_lines();
512    }
513    fn push_right(&mut self, count: usize) {
514        self.geom.x += count;
515        self.reflow_lines();
516    }
517    fn pull_left(&mut self, count: usize) {
518        self.geom.x -= count;
519        self.reflow_lines();
520    }
521    fn pull_up(&mut self, count: usize) {
522        self.geom.y -= count;
523        self.reflow_lines();
524    }
525    fn dump_screen(&self, full: bool, _client_id: Option<ClientId>) -> String {
526        self.grid.dump_screen(full)
527    }
528    fn clear_screen(&mut self) {
529        self.grid.clear_screen()
530    }
531    fn scroll_up(&mut self, count: usize, _client_id: ClientId) {
532        self.grid.move_viewport_up(count);
533        self.set_should_render(true);
534    }
535    fn scroll_down(&mut self, count: usize, _client_id: ClientId) {
536        self.grid.move_viewport_down(count);
537        self.set_should_render(true);
538    }
539    fn clear_scroll(&mut self) {
540        self.grid.reset_viewport();
541        self.set_should_render(true);
542    }
543    fn is_scrolled(&self) -> bool {
544        self.grid.is_scrolled
545    }
546
547    fn active_at(&self) -> Instant {
548        self.active_at
549    }
550
551    fn set_active_at(&mut self, time: Instant) {
552        self.active_at = time;
553    }
554    fn cursor_shape_csi(&self) -> String {
555        self.grid.cursor_shape().get_csi_str().to_string()
556    }
557    fn drain_messages_to_pty(&mut self) -> Vec<Vec<u8>> {
558        self.grid.pending_messages_to_pty.drain(..).collect()
559    }
560
561    fn drain_clipboard_update(&mut self) -> Option<String> {
562        self.grid.pending_clipboard_update.take()
563    }
564
565    fn start_selection(&mut self, start: &Position, _client_id: ClientId) {
566        self.grid.start_selection(start);
567        self.set_should_render(true);
568    }
569
570    fn update_selection(&mut self, to: &Position, _client_id: ClientId) {
571        let should_scroll = self.selection_scrolled_at.elapsed()
572            >= time::Duration::from_millis(SELECTION_SCROLL_INTERVAL_MS);
573        let cursor_at_the_bottom = to.line.0 < 0 && should_scroll;
574        let cursor_at_the_top = to.line.0 as usize >= self.grid.height && should_scroll;
575        let cursor_in_the_middle = to.line.0 >= 0 && (to.line.0 as usize) < self.grid.height;
576
577        // TODO: check how far up/down mouse is relative to pane, to increase scroll lines?
578        if cursor_at_the_bottom {
579            self.grid.scroll_up_one_line();
580            self.selection_scrolled_at = time::Instant::now();
581            self.set_should_render(true);
582        } else if cursor_at_the_top {
583            self.grid.scroll_down_one_line();
584            self.selection_scrolled_at = time::Instant::now();
585            self.set_should_render(true);
586        } else if cursor_in_the_middle {
587            // here we'll only render if the selection was updated, and that'll be handled by the
588            // grid
589            self.grid.update_selection(to);
590        }
591    }
592
593    fn end_selection(&mut self, end: &Position, _client_id: ClientId) {
594        self.grid.end_selection(end);
595        self.set_should_render(true);
596    }
597
598    fn reset_selection(&mut self, _client_id: Option<ClientId>) {
599        self.grid.reset_selection();
600    }
601
602    fn get_selected_text(&self, _client_id: ClientId) -> Option<String> {
603        self.grid.get_selected_text()
604    }
605
606    fn set_frame(&mut self, _frame: bool) {
607        self.frame.clear();
608    }
609
610    fn set_content_offset(&mut self, offset: Offset) {
611        self.content_offset = offset;
612        self.reflow_lines();
613    }
614
615    fn get_content_offset(&self) -> Offset {
616        self.content_offset
617    }
618
619    fn store_pane_name(&mut self) {
620        if self.pane_name != self.prev_pane_name {
621            self.prev_pane_name = self.pane_name.clone()
622        }
623    }
624    fn load_pane_name(&mut self) {
625        if self.pane_name != self.prev_pane_name {
626            self.pane_name = self.prev_pane_name.clone()
627        }
628    }
629
630    fn set_borderless(&mut self, borderless: bool) {
631        self.borderless = borderless;
632    }
633    fn borderless(&self) -> bool {
634        self.borderless
635    }
636
637    fn set_exclude_from_sync(&mut self, exclude_from_sync: bool) {
638        self.exclude_from_sync = exclude_from_sync;
639    }
640
641    fn exclude_from_sync(&self) -> bool {
642        self.exclude_from_sync
643    }
644
645    fn mouse_event(&self, event: &MouseEvent, _client_id: ClientId) -> Option<String> {
646        self.grid.mouse_event_signal(event)
647    }
648
649    fn mouse_left_click(&self, position: &Position, is_held: bool) -> Option<String> {
650        self.grid.mouse_left_click_signal(position, is_held)
651    }
652    fn mouse_left_click_release(&self, position: &Position) -> Option<String> {
653        self.grid.mouse_left_click_release_signal(position)
654    }
655    fn mouse_right_click(&self, position: &Position, is_held: bool) -> Option<String> {
656        self.grid.mouse_right_click_signal(position, is_held)
657    }
658    fn mouse_right_click_release(&self, position: &Position) -> Option<String> {
659        self.grid.mouse_right_click_release_signal(position)
660    }
661    fn mouse_middle_click(&self, position: &Position, is_held: bool) -> Option<String> {
662        self.grid.mouse_middle_click_signal(position, is_held)
663    }
664    fn mouse_middle_click_release(&self, position: &Position) -> Option<String> {
665        self.grid.mouse_middle_click_release_signal(position)
666    }
667    fn mouse_scroll_up(&self, position: &Position) -> Option<String> {
668        self.grid.mouse_scroll_up_signal(position)
669    }
670    fn mouse_scroll_down(&self, position: &Position) -> Option<String> {
671        self.grid.mouse_scroll_down_signal(position)
672    }
673    fn focus_event(&self) -> Option<String> {
674        self.grid.focus_event()
675    }
676    fn unfocus_event(&self) -> Option<String> {
677        self.grid.unfocus_event()
678    }
679    fn get_line_number(&self) -> Option<usize> {
680        // + 1 because the absolute position in the scrollback is 0 indexed and this should be 1 indexed
681        Some(self.grid.absolute_position_in_scrollback() + 1)
682    }
683
684    fn update_search_term(&mut self, needle: &str) {
685        match needle {
686            TERMINATING_STRING => {
687                self.search_term = String::new();
688            },
689            DELETE_KEY | BACKSPACE_KEY => {
690                self.search_term.pop();
691            },
692            c => {
693                self.search_term.push_str(c);
694            },
695        }
696        self.grid.clear_search();
697        if !self.search_term.is_empty() {
698            self.grid.set_search_string(&self.search_term);
699        }
700        self.set_should_render(true);
701    }
702    fn search_down(&mut self) {
703        if self.search_term.is_empty() {
704            return; // No-op
705        }
706        self.grid.search_down();
707        self.set_should_render(true);
708    }
709    fn search_up(&mut self) {
710        if self.search_term.is_empty() {
711            return; // No-op
712        }
713        self.grid.search_up();
714        self.set_should_render(true);
715    }
716    fn toggle_search_case_sensitivity(&mut self) {
717        self.grid.toggle_search_case_sensitivity();
718        self.set_should_render(true);
719    }
720    fn toggle_search_whole_words(&mut self) {
721        self.grid.toggle_search_whole_words();
722        self.set_should_render(true);
723    }
724    fn toggle_search_wrap(&mut self) {
725        self.grid.toggle_search_wrap();
726    }
727    fn clear_search(&mut self) {
728        self.grid.clear_search();
729        self.search_term.clear();
730    }
731    fn is_alternate_mode_active(&self) -> bool {
732        self.grid.is_alternate_mode_active()
733    }
734    fn hold(&mut self, exit_status: Option<i32>, is_first_run: bool, run_command: RunCommand) {
735        self.invoked_with = Some(Run::Command(run_command.clone()));
736        self.is_held = Some((exit_status, is_first_run, run_command));
737        if is_first_run {
738            self.render_first_run_banner();
739        }
740        self.set_should_render(true);
741    }
742    fn add_red_pane_frame_color_override(&mut self, error_text: Option<String>) {
743        self.pane_frame_color_override = Some((self.style.colors.exit_code_error.base, error_text));
744    }
745    fn add_highlight_pane_frame_color_override(
746        &mut self,
747        text: Option<String>,
748        _client_id: Option<ClientId>,
749    ) {
750        // TODO: if we have a client_id, we should only highlight the frame for this client
751        self.pane_frame_color_override = Some((self.style.colors.frame_highlight.base, text));
752    }
753    fn clear_pane_frame_color_override(&mut self, _client_id: Option<ClientId>) {
754        // TODO: if we have a client_id, we should only clear the highlight for this client
755        self.pane_frame_color_override = None;
756    }
757    fn frame_color_override(&self) -> Option<PaletteColor> {
758        self.pane_frame_color_override
759            .as_ref()
760            .map(|(color, _text)| *color)
761    }
762    fn invoked_with(&self) -> &Option<Run> {
763        &self.invoked_with
764    }
765    fn set_title(&mut self, title: String) {
766        self.pane_title = title;
767    }
768    fn current_title(&self) -> String {
769        if self.pane_name.is_empty() {
770            self.grid
771                .title
772                .as_deref()
773                .unwrap_or(&self.pane_title)
774                .into()
775        } else {
776            self.pane_name.to_owned()
777        }
778    }
779    fn custom_title(&self) -> Option<String> {
780        if self.pane_name.is_empty() {
781            None
782        } else {
783            Some(self.pane_name.clone())
784        }
785    }
786    fn exit_status(&self) -> Option<i32> {
787        self.is_held
788            .as_ref()
789            .and_then(|(exit_status, _, _)| *exit_status)
790    }
791    fn is_held(&self) -> bool {
792        self.is_held.is_some()
793    }
794    fn exited(&self) -> bool {
795        match self.is_held {
796            Some((_, is_first_run, _)) => !is_first_run,
797            None => false,
798        }
799    }
800    fn rename(&mut self, buf: Vec<u8>) {
801        self.pane_name = String::from_utf8_lossy(&buf).to_string();
802        self.set_should_render(true);
803    }
804    fn serialize(&self, scrollback_lines_to_serialize: Option<usize>) -> Option<String> {
805        self.grid.serialize(scrollback_lines_to_serialize)
806    }
807    fn rerun(&mut self) -> Option<RunCommand> {
808        // if this is a command pane that has exited or is waiting to be rerun, will return its
809        // RunCommand, otherwise it is safe to assume this is not the right sort of pane or that it
810        // is not in the right sort of state
811        self.is_held.take().map(|(_, _, run_command)| {
812            self.is_held = None;
813            self.grid.reset_terminal_state();
814            self.set_should_render(true);
815            self.remove_banner();
816            run_command.clone()
817        })
818    }
819    fn update_theme(&mut self, theme: Styling) {
820        self.style.colors = theme.clone();
821        self.grid.update_theme(theme);
822        if self.banner.is_some() {
823            // we do this so that the banner will be updated with the new theme colors
824            self.render_first_run_banner();
825        }
826    }
827    fn update_arrow_fonts(&mut self, should_support_arrow_fonts: bool) {
828        self.arrow_fonts = should_support_arrow_fonts;
829        self.grid.update_arrow_fonts(should_support_arrow_fonts);
830    }
831    fn update_rounded_corners(&mut self, rounded_corners: bool) {
832        self.style.rounded_corners = rounded_corners;
833        self.frame.clear();
834    }
835    fn drain_fake_cursors(&mut self) -> Option<HashSet<(usize, usize)>> {
836        if !self.fake_cursor_locations.is_empty() {
837            for (y, _x) in &self.fake_cursor_locations {
838                // we do this because once these fake_cursor_locations
839                // have been drained, we have to make sure to render the line
840                // they appeared on so that whatever clears their location
841                // won't leave a hole
842                self.grid.update_line_for_rendering(*y);
843            }
844            Some(self.fake_cursor_locations.drain().collect())
845        } else {
846            None
847        }
848    }
849    fn toggle_pinned(&mut self) {
850        self.geom.is_pinned = !self.geom.is_pinned;
851    }
852    fn set_pinned(&mut self, should_be_pinned: bool) {
853        self.geom.is_pinned = should_be_pinned;
854    }
855    fn intercept_left_mouse_click(&mut self, position: &Position, client_id: ClientId) -> bool {
856        if self.position_is_on_frame(position) {
857            let relative_position = self.relative_position(position);
858            if let Some(client_frame) = self.frame.get_mut(&client_id) {
859                if client_frame.clicked_on_pinned(relative_position) {
860                    self.toggle_pinned();
861                    return true;
862                }
863            }
864        }
865        false
866    }
867    fn intercept_mouse_event_on_frame(&mut self, event: &MouseEvent, client_id: ClientId) -> bool {
868        if self.position_is_on_frame(&event.position) {
869            let relative_position = self.relative_position(&event.position);
870            if let MouseEventType::Press = event.event_type {
871                if let Some(client_frame) = self.frame.get_mut(&client_id) {
872                    if client_frame.clicked_on_pinned(relative_position) {
873                        self.toggle_pinned();
874                        return true;
875                    }
876                }
877            }
878        }
879        false
880    }
881    fn reset_logical_position(&mut self) {
882        self.geom.logical_position = None;
883    }
884}
885
886impl TerminalPane {
887    #[allow(clippy::too_many_arguments)]
888    pub fn new(
889        pid: u32,
890        position_and_size: PaneGeom,
891        style: Style,
892        pane_index: usize,
893        pane_name: String,
894        link_handler: Rc<RefCell<LinkHandler>>,
895        character_cell_size: Rc<RefCell<Option<SizeInPixels>>>,
896        sixel_image_store: Rc<RefCell<SixelImageStore>>,
897        terminal_emulator_colors: Rc<RefCell<Palette>>,
898        terminal_emulator_color_codes: Rc<RefCell<HashMap<usize, String>>>,
899        initial_pane_title: Option<String>,
900        invoked_with: Option<Run>,
901        debug: bool,
902        arrow_fonts: bool,
903        styled_underlines: bool,
904        explicitly_disable_keyboard_protocol: bool,
905    ) -> TerminalPane {
906        let initial_pane_title =
907            initial_pane_title.unwrap_or_else(|| format!("Pane #{}", pane_index));
908        let grid = Grid::new(
909            position_and_size.rows.as_usize(),
910            position_and_size.cols.as_usize(),
911            terminal_emulator_colors,
912            terminal_emulator_color_codes,
913            link_handler,
914            character_cell_size,
915            sixel_image_store,
916            style.clone(),
917            debug,
918            arrow_fonts,
919            styled_underlines,
920            explicitly_disable_keyboard_protocol,
921        );
922        TerminalPane {
923            frame: HashMap::new(),
924            content_offset: Offset::default(),
925            pid,
926            grid,
927            selectable: true,
928            geom: position_and_size,
929            geom_override: None,
930            vte_parser: vte::Parser::new(),
931            active_at: Instant::now(),
932            style,
933            selection_scrolled_at: time::Instant::now(),
934            pane_title: initial_pane_title,
935            pane_name: pane_name.clone(),
936            prev_pane_name: pane_name,
937            borderless: false,
938            exclude_from_sync: false,
939            fake_cursor_locations: HashSet::new(),
940            search_term: String::new(),
941            is_held: None,
942            banner: None,
943            pane_frame_color_override: None,
944            invoked_with,
945            arrow_fonts,
946        }
947    }
948    pub fn get_x(&self) -> usize {
949        match self.geom_override {
950            Some(position_and_size_override) => position_and_size_override.x,
951            None => self.geom.x,
952        }
953    }
954    pub fn get_y(&self) -> usize {
955        match self.geom_override {
956            Some(position_and_size_override) => position_and_size_override.y,
957            None => self.geom.y,
958        }
959    }
960    pub fn get_columns(&self) -> usize {
961        match self.geom_override {
962            Some(position_and_size_override) => position_and_size_override.cols.as_usize(),
963            None => self.geom.cols.as_usize(),
964        }
965    }
966    pub fn get_rows(&self) -> usize {
967        match self.geom_override {
968            Some(position_and_size_override) => position_and_size_override.rows.as_usize(),
969            None => self.geom.rows.as_usize(),
970        }
971    }
972    fn reflow_lines(&mut self) {
973        let rows = self.get_content_rows();
974        let cols = self.get_content_columns();
975        self.grid.force_change_size(rows, cols);
976        if self.banner.is_some() {
977            self.grid.reset_terminal_state();
978            self.render_first_run_banner();
979        }
980        self.set_should_render(true);
981    }
982    pub fn read_buffer_as_lines(&self) -> Vec<Vec<TerminalCharacter>> {
983        self.grid.as_character_lines()
984    }
985    pub fn cursor_coordinates(&self) -> Option<(usize, usize)> {
986        // (x, y)
987        if self.get_content_rows() < 1 || self.get_content_columns() < 1 {
988            // do not render cursor if there's no room for it
989            return None;
990        }
991        self.grid.cursor_coordinates()
992    }
993    fn render_first_run_banner(&mut self) {
994        let columns = self.get_content_columns();
995        let rows = self.get_content_rows();
996        let banner = match &self.is_held {
997            Some((_exit_status, _is_first_run, run_command)) => {
998                render_first_run_banner(columns, rows, &self.style, Some(run_command))
999            },
1000            None => render_first_run_banner(columns, rows, &self.style, None),
1001        };
1002        self.banner = Some(banner.clone());
1003        self.handle_pty_bytes(banner.as_bytes().to_vec());
1004    }
1005    fn remove_banner(&mut self) {
1006        if self.banner.is_some() {
1007            self.grid.reset_terminal_state();
1008            self.set_should_render(true);
1009            self.banner = None;
1010        }
1011    }
1012    fn adjust_input_to_terminal_with_kitty_keyboard_protocol(
1013        &self,
1014        key: &Option<KeyWithModifier>,
1015        raw_input_bytes: Vec<u8>,
1016        raw_input_bytes_are_kitty: bool,
1017    ) -> Option<AdjustedInput> {
1018        if raw_input_bytes_are_kitty || key.is_none() {
1019            Some(AdjustedInput::WriteBytesToTerminal(raw_input_bytes))
1020        } else {
1021            // here what happens is that the host terminal is operating in non "kitty keys" mode, but
1022            // this terminal pane *is* operating in "kitty keys" mode - so we need to serialize the "non kitty"
1023            // key to a "kitty key"
1024            key.as_ref()
1025                .and_then(|k| k.serialize_kitty())
1026                .map(|s| AdjustedInput::WriteBytesToTerminal(s.as_bytes().to_vec()))
1027        }
1028    }
1029    fn adjust_input_to_terminal_without_kitty_keyboard_protocol(
1030        &self,
1031        key: &Option<KeyWithModifier>,
1032        raw_input_bytes: Vec<u8>,
1033        raw_input_bytes_are_kitty: bool,
1034    ) -> Option<AdjustedInput> {
1035        if self.grid.new_line_mode {
1036            let key_is_enter = raw_input_bytes.as_slice() == &[13]
1037                || key
1038                    .as_ref()
1039                    .map(|k| k.is_key_without_modifier(BareKey::Enter))
1040                    .unwrap_or(false);
1041            if key_is_enter {
1042                // LNM - carriage return is followed by linefeed
1043                return Some(AdjustedInput::WriteBytesToTerminal(
1044                    "\u{0d}\u{0a}".as_bytes().to_vec(),
1045                ));
1046            };
1047        }
1048        if self.grid.cursor_key_mode {
1049            let key_is_left_arrow = raw_input_bytes.as_slice() == LEFT_ARROW
1050                || key
1051                    .as_ref()
1052                    .map(|k| k.is_key_without_modifier(BareKey::Left))
1053                    .unwrap_or(false);
1054            let key_is_right_arrow = raw_input_bytes.as_slice() == RIGHT_ARROW
1055                || key
1056                    .as_ref()
1057                    .map(|k| k.is_key_without_modifier(BareKey::Right))
1058                    .unwrap_or(false);
1059            let key_is_up_arrow = raw_input_bytes.as_slice() == UP_ARROW
1060                || key
1061                    .as_ref()
1062                    .map(|k| k.is_key_without_modifier(BareKey::Up))
1063                    .unwrap_or(false);
1064            let key_is_down_arrow = raw_input_bytes.as_slice() == DOWN_ARROW
1065                || key
1066                    .as_ref()
1067                    .map(|k| k.is_key_without_modifier(BareKey::Down))
1068                    .unwrap_or(false);
1069            let key_is_home_key = raw_input_bytes.as_slice() == HOME_KEY
1070                || key
1071                    .as_ref()
1072                    .map(|k| k.is_key_without_modifier(BareKey::Home))
1073                    .unwrap_or(false);
1074            let key_is_end_key = raw_input_bytes.as_slice() == END_KEY
1075                || key
1076                    .as_ref()
1077                    .map(|k| k.is_key_without_modifier(BareKey::End))
1078                    .unwrap_or(false);
1079            if key_is_left_arrow {
1080                return Some(AdjustedInput::WriteBytesToTerminal(
1081                    AnsiEncoding::Left.as_vec_bytes(),
1082                ));
1083            } else if key_is_right_arrow {
1084                return Some(AdjustedInput::WriteBytesToTerminal(
1085                    AnsiEncoding::Right.as_vec_bytes(),
1086                ));
1087            } else if key_is_up_arrow {
1088                return Some(AdjustedInput::WriteBytesToTerminal(
1089                    AnsiEncoding::Up.as_vec_bytes(),
1090                ));
1091            } else if key_is_down_arrow {
1092                return Some(AdjustedInput::WriteBytesToTerminal(
1093                    AnsiEncoding::Down.as_vec_bytes(),
1094                ));
1095            } else if key_is_home_key {
1096                return Some(AdjustedInput::WriteBytesToTerminal(
1097                    AnsiEncoding::Home.as_vec_bytes(),
1098                ));
1099            } else if key_is_end_key {
1100                return Some(AdjustedInput::WriteBytesToTerminal(
1101                    AnsiEncoding::End.as_vec_bytes(),
1102                ));
1103            }
1104        }
1105        if raw_input_bytes_are_kitty {
1106            // here what happens is that the host terminal is operating in "kitty keys" mode, but
1107            // this terminal pane is not - so we need to serialize the kitty key to "non kitty" if
1108            // possible - if not possible (eg. with multiple modifiers), we'll return a None here
1109            // and write nothing to the terminal pane
1110            key.as_ref()
1111                .and_then(|k| k.serialize_non_kitty())
1112                .map(|s| AdjustedInput::WriteBytesToTerminal(s.as_bytes().to_vec()))
1113        } else {
1114            Some(AdjustedInput::WriteBytesToTerminal(raw_input_bytes))
1115        }
1116    }
1117    fn handle_held_run(&mut self) -> Option<AdjustedInput> {
1118        self.is_held.take().map(|(_, _, run_command)| {
1119            self.is_held = None;
1120            self.grid.reset_terminal_state();
1121            self.set_should_render(true);
1122            self.remove_banner();
1123            AdjustedInput::ReRunCommandInThisPane(run_command.clone())
1124        })
1125    }
1126    fn handle_held_drop_to_shell(&mut self) -> Option<AdjustedInput> {
1127        self.is_held.take().map(|(_, _, run_command)| {
1128            // Drop to shell in the same working directory as the command was run
1129            let working_dir = run_command.cwd.clone();
1130            self.is_held = None;
1131            self.grid.reset_terminal_state();
1132            self.set_should_render(true);
1133            self.remove_banner();
1134            AdjustedInput::DropToShellInThisPane { working_dir }
1135        })
1136    }
1137}
1138
1139#[cfg(test)]
1140#[path = "./unit/terminal_pane_tests.rs"]
1141mod grid_tests;
1142
1143#[cfg(test)]
1144#[path = "./unit/search_in_pane_tests.rs"]
1145mod search_tests;