Skip to main content

gpui_cn/terminal/
state.rs

1//! The terminal entity: frames in, input out.
2//!
3//! Derived from Herdr (Apache-2.0).
4
5use std::collections::HashSet;
6use std::path::{Path, PathBuf};
7use std::sync::Arc;
8
9use gpui_kit::{
10    App, ClipboardItem, Context, ElementId, EventEmitter, FocusHandle, Focusable, IntoElement,
11    KeyDownEvent, KeyUpEvent, Keystroke, KeystrokeEvent, Modifiers, MouseButton, MouseDownEvent,
12    MouseMoveEvent, MouseUpEvent, Pixels, Point, Render, ScrollEvent, SharedString, Subscription,
13    Task, Window,
14};
15
16use crate::terminal::appearance::TerminalAppearance;
17use crate::terminal::cursor::Blink;
18use crate::terminal::engine::{ExitStatus, ForegroundProcess, TerminalSnapshot, TerminalStatus};
19use crate::terminal::frame::{TerminalColors, TerminalFrame, Viewport};
20use crate::terminal::geometry::Geometry;
21use crate::terminal::input::{KeyAction, MouseAction, ScrollRequest, TerminalInput};
22use crate::terminal::keys::{self, ScrollAccumulator};
23use crate::terminal::links::{self, Link};
24use crate::terminal::selection::{self, Cell, Selection};
25use crate::terminal::source::{FrameHandle, FrameSink, FrameSource, StartOptions};
26use crate::theme::ActiveTheme as _;
27
28/// Events a host subscribes to.
29#[derive(Clone, Debug, PartialEq, Eq)]
30#[non_exhaustive]
31pub enum TerminalEvent {
32    /// The terminal took keyboard focus.
33    Focused,
34    /// The program changed the title; read it with `title`.
35    TitleChanged,
36    /// The program reported a new working directory; read it with `cwd`.
37    CwdChanged,
38    /// The program rang the bell.
39    Bell,
40    /// The program wrote to the clipboard through OSC 52.
41    ClipboardWritten,
42    /// The program asked for a desktop notification.
43    Notification {
44        /// The title, possibly empty.
45        title: SharedString,
46        /// The body.
47        body: SharedString,
48    },
49    /// The program ended.
50    Exited(ExitStatus),
51    /// The terminal could not start or the engine failed.
52    Failed(SharedString),
53    /// The font size changed through the zoom actions or
54    /// [`TerminalState::set_font_size`]; read it with `font_size`.
55    FontSizeChanged,
56    /// Another process took the foreground, such as an editor the shell
57    /// started, or the shell back at its prompt; read it with
58    /// `foreground`.
59    ForegroundChanged,
60}
61
62/// Colors and appearance a terminal starts with.
63#[derive(Clone, Debug, Default, PartialEq)]
64#[non_exhaustive]
65pub struct TerminalConfig {
66    colors: TerminalColors,
67    appearance: TerminalAppearance,
68}
69
70impl TerminalConfig {
71    /// Set the colors.
72    #[must_use]
73    pub fn with_colors(mut self, colors: TerminalColors) -> Self {
74        self.colors = colors;
75        self
76    }
77
78    /// Set the appearance.
79    #[must_use]
80    pub fn with_appearance(mut self, appearance: TerminalAppearance) -> Self {
81        self.appearance = appearance;
82        self
83    }
84
85    /// The colors.
86    pub fn colors(&self) -> &TerminalColors {
87        &self.colors
88    }
89
90    /// The appearance.
91    pub fn appearance(&self) -> &TerminalAppearance {
92        &self.appearance
93    }
94}
95
96/// A terminal: the latest frame, the program's status and everything the
97/// element needs to draw and to send input.
98#[non_exhaustive]
99pub struct TerminalState {
100    handle: Option<Box<dyn FrameHandle>>,
101    closed: bool,
102    frame: Arc<TerminalFrame>,
103    status: TerminalStatus,
104    title: SharedString,
105    cwd: Option<PathBuf>,
106    foreground: Option<ForegroundProcess>,
107    bell_count: u64,
108    clipboard_sequence: u64,
109    notification_sequence: u64,
110    at_prompt: bool,
111    colors: TerminalColors,
112    appearance: TerminalAppearance,
113    /// The size the font was zoomed to, over the appearance's.
114    font_size: Option<Pixels>,
115    focus: FocusHandle,
116    focused: bool,
117    visible: bool,
118    pub(crate) geometry: Option<Geometry>,
119    geometry_scale: Option<f32>,
120    requested: Viewport,
121    pub(crate) selection: Option<Selection>,
122    pub(crate) marked_text: Option<String>,
123    /// Rows prepared in the last frame, for the element to reuse.
124    pub(crate) row_cache: crate::terminal::element::RowCache,
125    pressed_keys: HashSet<String>,
126    right_click_passthrough: bool,
127    hovered_link: Option<(u16, Link)>,
128    scroll: ScrollAccumulator,
129    pub(crate) cursor: Blink,
130    frames: Option<Task<()>>,
131    _subscriptions: [Subscription; 3],
132}
133
134impl TerminalState {
135    /// A terminal fed by `source`, started at once.
136    pub fn new(
137        source: impl FrameSource,
138        config: TerminalConfig,
139        window: &mut Window,
140        cx: &mut Context<Self>,
141    ) -> Self {
142        let focus = cx.focus_handle();
143        let this = cx.weak_entity();
144        let mut state = Self {
145            handle: None,
146            closed: false,
147            frame: Arc::new(TerminalFrame::default()),
148            status: TerminalStatus::Starting,
149            title: SharedString::default(),
150            cwd: None,
151            foreground: None,
152            bell_count: 0,
153            clipboard_sequence: 0,
154            notification_sequence: 0,
155            at_prompt: false,
156            colors: config.colors,
157            appearance: config.appearance,
158            font_size: None,
159            focus: focus.clone(),
160            focused: false,
161            visible: true,
162            geometry: None,
163            geometry_scale: None,
164            requested: Viewport::default(),
165            selection: None,
166            marked_text: None,
167            row_cache: crate::terminal::element::RowCache::default(),
168            pressed_keys: HashSet::new(),
169            right_click_passthrough: true,
170            hovered_link: None,
171            scroll: ScrollAccumulator::default(),
172            cursor: Blink::new(),
173            frames: None,
174            _subscriptions: [
175                cx.on_focus_in(&focus, window, |state: &mut Self, _, cx| {
176                    state.focus_changed(true, cx);
177                }),
178                cx.on_focus_out(&focus, window, |state: &mut Self, _, _, cx| {
179                    state.focus_changed(false, cx);
180                }),
181                cx.intercept_keystrokes({
182                    let state = this;
183                    let focus = focus.clone();
184                    move |event, window, cx| {
185                        if !focus.is_focused(window) {
186                            return;
187                        }
188                        if let Some(state) = state.upgrade() {
189                            state.update(cx, |state, cx| state.claim_key(event, window, cx));
190                        }
191                    }
192                }),
193            ],
194        };
195        state.start(Box::new(source), cx);
196        state
197    }
198
199    /// A terminal running a local program in a pty.
200    #[cfg(not(any(target_os = "ios", target_os = "android")))]
201    pub fn local(
202        options: crate::terminal::options::LocalTerminalOptions,
203        config: TerminalConfig,
204        window: &mut Window,
205        cx: &mut Context<Self>,
206    ) -> Self {
207        let engine = crate::terminal::engine::Engine::new(
208            crate::terminal::pty::LocalPty::new(options.clone()),
209            options.engine.clone(),
210        )
211        .with_abnormal_exit_runtime(options.abnormal_exit_runtime)
212        .with_input(options.input);
213        Self::new(engine, config, window, cx)
214    }
215
216    fn start(&mut self, source: Box<dyn FrameSource>, cx: &mut Context<Self>) {
217        let (sink, wake) = FrameSink::new();
218        let options = StartOptions {
219            viewport: self.requested,
220            colors: self.colors.clone(),
221            visible: self.visible,
222            cursor: self
223                .appearance
224                .cursor_shape
225                .map(|shape| (shape, self.appearance.cursor_blink.unwrap_or(true))),
226        };
227        match source.start(sink.clone(), options) {
228            Ok(handle) => {
229                self.handle = Some(handle);
230                self.status = TerminalStatus::Starting;
231            }
232            Err(error) => {
233                self.status = TerminalStatus::Failed(error.to_string());
234                cx.emit(TerminalEvent::Failed(error.to_string().into()));
235                return;
236            }
237        }
238        self.frames = Some(cx.spawn(async move |state, cx| {
239            while wake.recv().await.is_ok() {
240                let Some(snapshot) = sink.take() else {
241                    continue;
242                };
243                if state
244                    .update(cx, |state, cx| state.apply(&snapshot, cx))
245                    .is_err()
246                {
247                    break;
248                }
249            }
250        }));
251    }
252
253    /// Install a snapshot. Public so a host driving its own frames can use
254    /// the same path as the engine.
255    pub fn apply(&mut self, snapshot: &TerminalSnapshot, cx: &mut Context<Self>) {
256        let reflowed = snapshot.frame.content_revision != self.frame.content_revision;
257        self.selection = self
258            .selection
259            .and_then(|selection| selection.refresh(&self.frame, &snapshot.frame));
260        self.frame = Arc::clone(&snapshot.frame);
261        self.at_prompt = snapshot.at_prompt;
262        if snapshot.title != self.title.as_ref() {
263            self.title = snapshot.title.clone().into();
264            cx.emit(TerminalEvent::TitleChanged);
265        }
266        if snapshot.cwd != self.cwd {
267            self.cwd.clone_from(&snapshot.cwd);
268            cx.emit(TerminalEvent::CwdChanged);
269        }
270        if snapshot.foreground != self.foreground {
271            let cwd = self.cwd().map(Path::to_path_buf);
272            self.foreground.clone_from(&snapshot.foreground);
273            cx.emit(TerminalEvent::ForegroundChanged);
274            if self.cwd().map(Path::to_path_buf) != cwd {
275                cx.emit(TerminalEvent::CwdChanged);
276            }
277        }
278        if snapshot.bell_count != self.bell_count {
279            self.bell_count = snapshot.bell_count;
280            cx.emit(TerminalEvent::Bell);
281        }
282        if snapshot.clipboard_sequence != self.clipboard_sequence {
283            self.clipboard_sequence = snapshot.clipboard_sequence;
284            if let Some(text) = &snapshot.clipboard {
285                cx.write_to_clipboard(ClipboardItem::new_string(text.to_string()));
286                cx.emit(TerminalEvent::ClipboardWritten);
287            }
288        }
289        if snapshot.notification_sequence != self.notification_sequence {
290            self.notification_sequence = snapshot.notification_sequence;
291            if let Some((title, body)) = &snapshot.notification {
292                cx.emit(TerminalEvent::Notification {
293                    title: title.to_string().into(),
294                    body: body.to_string().into(),
295                });
296            }
297        }
298        if snapshot.status != self.status {
299            self.status = snapshot.status.clone();
300            match &self.status {
301                TerminalStatus::Exited(status) => cx.emit(TerminalEvent::Exited(status.clone())),
302                TerminalStatus::Failed(error) => {
303                    cx.emit(TerminalEvent::Failed(error.clone().into()));
304                }
305                TerminalStatus::Starting | TerminalStatus::Live => {}
306            }
307            if !matches!(self.status, TerminalStatus::Live) {
308                self.cursor.stop();
309            }
310        }
311        if reflowed {
312            self.hovered_link = None;
313        }
314        cx.notify();
315    }
316
317    /// The frame being drawn.
318    #[must_use]
319    pub fn frame(&self) -> &Arc<TerminalFrame> {
320        &self.frame
321    }
322
323    /// The program's status.
324    #[must_use]
325    pub fn status(&self) -> &TerminalStatus {
326        &self.status
327    }
328
329    /// The title the program set.
330    #[must_use]
331    pub fn title(&self) -> &SharedString {
332        &self.title
333    }
334
335    /// The working directory: the one the shell reported through its
336    /// integration (OSC 7), or else the foreground process's.
337    #[must_use]
338    pub fn cwd(&self) -> Option<&Path> {
339        self.cwd
340            .as_deref()
341            .or_else(|| self.foreground.as_ref()?.cwd())
342    }
343
344    /// The process in the foreground, such as the shell at its prompt or an
345    /// editor it started, when the source reports one. A local pty does.
346    /// [`TerminalEvent::ForegroundChanged`] reports a change.
347    #[must_use]
348    pub fn foreground(&self) -> Option<&ForegroundProcess> {
349        self.foreground.as_ref()
350    }
351
352    /// Whether closing the terminal would end a program other than the one
353    /// it started, such as an editor or a build in the foreground, so an
354    /// application should ask first, as Ghostty does. Without a foreground
355    /// process, as from a stream, it is whether the cursor is away from a
356    /// shell prompt.
357    #[must_use]
358    pub fn needs_confirm_close(&self) -> bool {
359        if !matches!(self.status, TerminalStatus::Live) {
360            return false;
361        }
362        match &self.foreground {
363            Some(process) => !process.is_shell(),
364            None => !self.at_prompt,
365        }
366    }
367
368    /// Queued input bytes not yet delivered; the busy signal.
369    #[must_use]
370    pub fn input_backlog(&self) -> usize {
371        self.handle.as_ref().map_or(0, |h| h.input_backlog())
372    }
373
374    /// The appearance.
375    #[must_use]
376    pub fn appearance(&self) -> &TerminalAppearance {
377        &self.appearance
378    }
379
380    /// The font size the grid draws with: the zoomed size, else the
381    /// appearance's, else the theme's code font size.
382    pub fn font_size(&self, cx: &App) -> Pixels {
383        self.font_size
384            .unwrap_or_else(|| self.appearance.resolved(cx.theme()).size())
385    }
386
387    /// Draw the grid at `size`, clamped to 1-255 points as Ghostty clamps
388    /// it. The grid is measured again at the next paint, which resizes
389    /// the program's grid to fit. Emits [`TerminalEvent::FontSizeChanged`].
390    pub fn set_font_size(&mut self, size: impl Into<Pixels>, cx: &mut Context<Self>) {
391        let size = size.into().clamp(gpui_kit::px(1.), gpui_kit::px(255.));
392        if self.font_size == Some(size) {
393            return;
394        }
395        self.font_size = Some(size);
396        cx.emit(TerminalEvent::FontSizeChanged);
397        cx.notify();
398    }
399
400    /// Zoom the font in by `points`, as Ghostty's `increase_font_size`.
401    pub fn increase_font_size(&mut self, points: impl Into<Pixels>, cx: &mut Context<Self>) {
402        let size = self.font_size(cx) + points.into();
403        self.set_font_size(size, cx);
404    }
405
406    /// Zoom the font out by `points`, as Ghostty's `decrease_font_size`.
407    pub fn decrease_font_size(&mut self, points: impl Into<Pixels>, cx: &mut Context<Self>) {
408        let size = self.font_size(cx) - points.into();
409        self.set_font_size(size, cx);
410    }
411
412    /// Go back to the appearance's font size, or the theme's.
413    pub fn reset_font_size(&mut self, cx: &mut Context<Self>) {
414        if self.font_size.take().is_some() {
415            cx.emit(TerminalEvent::FontSizeChanged);
416            cx.notify();
417        }
418    }
419
420    /// The appearance with the theme's values filled in and the zoom on top,
421    /// as the grid draws with it.
422    pub(crate) fn drawn_appearance(&self, theme: &crate::theme::ThemeTokens) -> TerminalAppearance {
423        let mut appearance = self.appearance.resolved(theme);
424        if let Some(size) = self.font_size {
425            appearance.font_size = Some(size);
426        }
427        appearance
428    }
429
430    /// Change the appearance. The grid is measured again at the next paint.
431    pub fn set_appearance(&mut self, appearance: TerminalAppearance, cx: &mut Context<Self>) {
432        if self.appearance == appearance {
433            return;
434        }
435        let cursor = appearance
436            .cursor_shape
437            .map(|shape| (shape, appearance.cursor_blink.unwrap_or(true)));
438        if cursor
439            != self
440                .appearance
441                .cursor_shape
442                .map(|shape| (shape, self.appearance.cursor_blink.unwrap_or(true)))
443            && let (Some(handle), Some((shape, blink))) = (&self.handle, cursor)
444        {
445            handle.set_cursor(shape, blink);
446        }
447        self.appearance = appearance;
448        cx.notify();
449    }
450
451    /// The colors.
452    #[must_use]
453    pub fn colors(&self) -> &TerminalColors {
454        &self.colors
455    }
456
457    /// Change the colors. The next frame carries them.
458    pub fn set_colors(&mut self, colors: TerminalColors, cx: &mut Context<Self>) {
459        if self.colors == colors {
460            return;
461        }
462        if let Some(handle) = &self.handle {
463            handle.set_colors(colors.clone());
464        }
465        self.colors = colors;
466        cx.notify();
467    }
468
469    /// Whether a program is running.
470    #[must_use]
471    pub fn is_live(&self) -> bool {
472        matches!(self.status, TerminalStatus::Live) && !self.closed
473    }
474
475    /// Whether the terminal was closed.
476    #[must_use]
477    pub fn is_closed(&self) -> bool {
478        self.closed
479    }
480
481    /// Whether the terminal is visible.
482    #[must_use]
483    pub fn is_visible(&self) -> bool {
484        self.visible
485    }
486
487    /// Hidden terminals keep parsing output but stop producing frames and
488    /// stop the cursor cadence. Revealing one asks for a current frame.
489    pub fn set_visible(&mut self, visible: bool, cx: &mut Context<Self>) {
490        if self.visible == visible {
491            return;
492        }
493        self.visible = visible;
494        if let Some(handle) = &self.handle {
495            handle.set_visible(visible);
496        }
497        if !visible {
498            self.cursor.stop();
499        }
500        cx.notify();
501    }
502
503    /// Stop the program and the source. Idempotent.
504    pub fn close(&mut self, cx: &mut Context<Self>) {
505        if self.closed {
506            return;
507        }
508        self.closed = true;
509        if let Some(handle) = &self.handle {
510            handle.close();
511        }
512        self.selection = None;
513        self.cursor.stop();
514        cx.notify();
515    }
516
517    /// Whether a right click goes to a program that reports the mouse. It
518    /// does by default. When it does not, or a modifier is held, or no
519    /// program reports the mouse, the click is left to the host, such as for
520    /// a context menu.
521    pub fn set_right_click_passthrough(&mut self, passthrough: bool) {
522        self.right_click_passthrough = passthrough;
523    }
524
525    fn leaves_right_click(&self, button: MouseButton, modifiers: Modifiers, _: &Window) -> bool {
526        button == MouseButton::Right
527            && (!self.right_click_passthrough
528                || modifiers != Modifiers::default()
529                || !self.reports_mouse(modifiers))
530    }
531
532    /// Whether a non-empty selection exists.
533    #[must_use]
534    pub fn has_selection(&self) -> bool {
535        self.selection.is_some_and(|s| !s.is_empty())
536    }
537
538    /// The selected text as shown, without scrollback.
539    #[must_use]
540    pub fn selected_text(&self) -> Option<String> {
541        let selection = self.selection.filter(|s| !s.is_empty())?;
542        let text = selection.text(&self.frame);
543        (!text.is_empty()).then_some(text)
544    }
545
546    /// The link under the pointer while the platform modifier is held.
547    #[must_use]
548    pub fn hovered_link(&self) -> Option<&str> {
549        self.hovered_link
550            .as_ref()
551            .map(|(_, link)| link.url.as_str())
552    }
553
554    pub(crate) fn hovered_link_span(&self) -> Option<(u16, std::ops::Range<u16>)> {
555        self.hovered_link
556            .as_ref()
557            .map(|(row, link)| (*row, link.columns.clone()))
558    }
559
560    /// Copy the selection to the clipboard. A live engine is asked for the
561    /// text because it can reach scrollback the frame does not carry.
562    pub fn copy(&mut self, cx: &mut Context<Self>) {
563        let Some(selection) = self.selection.filter(|s| !s.is_empty()) else {
564            return;
565        };
566        let trim = self.appearance.trim_trailing_spaces;
567        if let Some(reply) = self
568            .handle
569            .as_ref()
570            .and_then(|handle| handle.copy(selection.to_cells()))
571        {
572            cx.spawn(async move |state, cx| {
573                if let Ok(Ok(text)) = reply.recv().await {
574                    let _ = state.update(cx, |_, cx| {
575                        cx.write_to_clipboard(ClipboardItem::new_string(finish(&text, trim)));
576                    });
577                }
578            })
579            .detach();
580            return;
581        }
582        let text = selection.text(&self.frame);
583        if !text.is_empty() {
584            cx.write_to_clipboard(ClipboardItem::new_string(finish(&text, trim)));
585        }
586    }
587
588    /// Select the whole viewport.
589    pub fn select_all(&mut self, cx: &mut Context<Self>) {
590        let rows = self.frame.rows.len().min(usize::from(u16::MAX));
591        let columns = self.frame.viewport.columns();
592        if rows == 0 || columns == 0 {
593            return;
594        }
595        #[allow(clippy::cast_possible_truncation)]
596        let last_row = (rows - 1) as u16;
597        let mut selection = Selection::new(Cell::new(0, 0), self.frame.content_revision, false);
598        selection.extend(Cell::new(columns, last_row));
599        selection.finish();
600        self.selection = Some(selection);
601        cx.notify();
602    }
603
604    /// Drop the selection. Returns whether there was one.
605    pub fn clear_selection(&mut self, cx: &mut Context<Self>) -> bool {
606        if self.selection.take().is_some() {
607            cx.notify();
608            return true;
609        }
610        false
611    }
612
613    /// Send form feed, which clears the screen at most shells.
614    pub fn clear(&mut self, cx: &mut Context<Self>) {
615        self.selection = None;
616        self.send(TerminalInput::Text("\u{c}".to_owned()), cx);
617    }
618
619    /// Paste the clipboard text.
620    pub fn paste(&mut self, cx: &mut Context<Self>) {
621        if let Some(text) = cx.read_from_clipboard().and_then(|item| item.text()) {
622            self.paste_text(text, cx);
623        }
624    }
625
626    /// Paste text, bracketed when the program asked for it.
627    pub fn paste_text(&mut self, text: impl Into<String>, cx: &mut Context<Self>) {
628        self.selection = None;
629        self.send(TerminalInput::Paste(text.into()), cx);
630    }
631
632    /// Send raw text to the program.
633    pub fn send_text(&mut self, text: impl Into<String>, cx: &mut Context<Self>) {
634        self.send(TerminalInput::Text(text.into()), cx);
635    }
636
637    /// Scroll the viewport.
638    pub fn scroll(&mut self, request: ScrollRequest, cx: &mut Context<Self>) {
639        if let Some(handle) = &self.handle {
640            let _ = handle.input(TerminalInput::Scroll(request));
641        }
642        cx.notify();
643    }
644
645    /// Scroll by whole pages; negative is toward older output.
646    pub fn scroll_pages(&mut self, pages: isize, cx: &mut Context<Self>) {
647        let rows = isize::try_from(self.frame.viewport.rows()).unwrap_or(24);
648        self.scroll(ScrollRequest::Lines(pages.saturating_mul(rows)), cx);
649    }
650
651    /// Whether the viewport is scrolled into history.
652    #[must_use]
653    pub fn is_scrolled(&self) -> bool {
654        !self.frame.scroll.at_bottom()
655    }
656
657    /// The focus handle.
658    #[must_use]
659    pub fn focus_handle(&self) -> &FocusHandle {
660        &self.focus
661    }
662
663    pub(crate) fn send(&mut self, input: TerminalInput, cx: &mut Context<Self>) {
664        let Some(handle) = &self.handle else {
665            return;
666        };
667        let scroll = !matches!(input, TerminalInput::Scroll(_) | TerminalInput::Focus(_))
668            && self.is_scrolled();
669        let _ = handle.input(input);
670        if scroll {
671            let _ = handle.input(TerminalInput::Scroll(ScrollRequest::Bottom));
672        }
673        cx.notify();
674    }
675
676    /// Grant credit for the next frame after a real paint. Never notifies,
677    /// or it would drive a repaint loop.
678    pub(crate) fn frame_painted(&self, painted: bool) {
679        if painted
680            && self.visible
681            && !self.closed
682            && matches!(self.status, TerminalStatus::Live)
683            && let Some(handle) = &self.handle
684        {
685            handle.request_frame();
686        }
687    }
688
689    pub(crate) fn geometry_scale_value(&self) -> f32 {
690        self.geometry_scale.unwrap_or(1.0)
691    }
692
693    /// Record geometry measured during paint and resize the source only
694    /// when the grid or the physical cell size changed.
695    pub(crate) fn set_geometry(&mut self, geometry: Geometry, scale: f32, cx: &mut Context<Self>) {
696        if self.geometry == Some(geometry) && self.geometry_scale == Some(scale) {
697            return;
698        }
699        self.geometry = Some(geometry);
700        self.geometry_scale = Some(scale);
701        #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
702        let viewport = Viewport::new(
703            geometry.columns,
704            geometry.rows,
705            (f32::from(geometry.cell.width) * scale).round().max(1.0) as u32,
706            (f32::from(geometry.cell.height) * scale).round().max(1.0) as u32,
707        );
708        let Some(viewport) = viewport else {
709            return;
710        };
711        if self.requested == viewport {
712            return;
713        }
714        self.requested = viewport;
715        if let Some(handle) = &self.handle {
716            handle.resize(viewport);
717        }
718        cx.notify();
719    }
720
721    fn focus_changed(&mut self, focused: bool, cx: &mut Context<Self>) {
722        if self.focused == focused {
723            return;
724        }
725        self.focused = focused;
726        self.send(TerminalInput::Focus(focused), cx);
727        if focused {
728            cx.emit(TerminalEvent::Focused);
729        } else {
730            self.pressed_keys.clear();
731            self.cursor.stop();
732        }
733        cx.notify();
734    }
735
736    /// Keys the terminal consumes stop propagation, which is what prevents
737    /// printable text arriving twice: once as a key and again as an IME
738    /// commit.
739    pub(crate) fn on_key_down(
740        &mut self,
741        event: &KeyDownEvent,
742        _: &mut Window,
743        cx: &mut Context<Self>,
744    ) {
745        if self.marked_text.is_some() {
746            return;
747        }
748        let keystroke = &event.keystroke;
749        if keystroke.key == "escape"
750            && keystroke.modifiers == Default::default()
751            && self.clear_selection(cx)
752        {
753            cx.stop_propagation();
754            return;
755        }
756        if let Some(key) = keys::key(keystroke, keys::key_action(event.is_held)) {
757            self.selection = None;
758            self.pressed_keys.insert(keystroke.key.clone());
759            self.send(TerminalInput::Key(key), cx);
760            cx.stop_propagation();
761        }
762    }
763
764    pub(crate) fn send_keystroke(
765        &mut self,
766        keystroke: &str,
767        window: &mut Window,
768        cx: &mut Context<Self>,
769    ) {
770        let Ok(keystroke) = Keystroke::parse_with_os(keystroke, cx.platform_info().os) else {
771            return;
772        };
773        let event = KeyDownEvent {
774            keystroke,
775            is_held: false,
776            prefer_character_input: false,
777        };
778        self.on_key_down(&event, window, cx);
779    }
780
781    /// Takes a key from a binding outside the terminal that would keep it
782    /// from the program, before the binding runs. The terminal is focused.
783    fn claim_key(&mut self, event: &KeystrokeEvent, window: &mut Window, cx: &mut Context<Self>) {
784        let winner = cx
785            .key_bindings()
786            .borrow()
787            .bindings_for_input(std::slice::from_ref(&event.keystroke), &event.context_stack)
788            .0
789            .into_iter()
790            .next();
791        let Some(binding) = winner else {
792            return;
793        };
794        match keys::claim(binding.action().name(), &event.keystroke) {
795            keys::Claim::None => {}
796            keys::Claim::Send => {
797                let key = KeyDownEvent {
798                    keystroke: event.keystroke.clone(),
799                    is_held: false,
800                    prefer_character_input: false,
801                };
802                self.on_key_down(&key, window, cx);
803                cx.stop_propagation();
804            }
805            keys::Claim::Copy => {
806                if self.has_selection() {
807                    self.copy(cx);
808                    cx.stop_propagation();
809                }
810            }
811        }
812    }
813
814    pub(crate) fn on_key_up(&mut self, event: &KeyUpEvent, _: &mut Window, cx: &mut Context<Self>) {
815        if !self.pressed_keys.remove(&event.keystroke.key) || self.marked_text.is_some() {
816            return;
817        }
818        if let Some(key) = keys::key(&event.keystroke, KeyAction::Release) {
819            self.send(TerminalInput::Key(key), cx);
820        }
821    }
822
823    pub(crate) fn on_scroll(
824        &mut self,
825        event: &ScrollEvent,
826        window: &mut Window,
827        cx: &mut Context<Self>,
828    ) {
829        // The terminal scrolls its own content, so the wheel step is its
830        // own even at the end of the scrollback: the page around it must
831        // not scroll as well.
832        cx.stop_propagation();
833        let Some(geometry) = self.geometry else {
834            return;
835        };
836        let delta = event.delta.pixel_delta(geometry.cell.height).y / geometry.cell.height;
837        if self.reports_mouse(event.modifiers) {
838            self.scroll.reset();
839            let action = if delta > 0.0 {
840                MouseAction::ScrollUp
841            } else {
842                MouseAction::ScrollDown
843            };
844            if delta != 0.0 {
845                self.report_mouse(action, None, event.modifiers, event.position, window, cx);
846            }
847            return;
848        }
849        if self.frame.modes.alternate_screen && self.frame.modes.alternate_scroll {
850            // Mode 1007: the wheel becomes arrow keys on the alternate screen.
851            if let Some(ScrollRequest::Lines(lines)) = self.scroll.push(delta) {
852                let key = if lines < 0 { "up" } else { "down" };
853                for _ in 0..lines.unsigned_abs().min(64) {
854                    self.send_keystroke(key, window, cx);
855                }
856            }
857            return;
858        }
859        if let Some(request) = self.scroll.push(delta) {
860            self.scroll(request, cx);
861        }
862    }
863
864    /// Update the hovered link. A link is live only while the platform
865    /// modifier is held, so dragging a selection across a URL opens nothing.
866    pub(crate) fn hover_link(
867        &mut self,
868        position: Point<Pixels>,
869        active: bool,
870        cx: &mut Context<Self>,
871    ) {
872        let found = active
873            .then(|| {
874                let geometry = self.geometry?;
875                if !geometry.bounds.contains(&position) {
876                    return None;
877                }
878                let cell = geometry.cell_at(position);
879                let row = self.frame.rows.get(usize::from(cell.row))?;
880                links::at(row, cell.column).map(|link| (cell.row, link))
881            })
882            .flatten();
883        if found != self.hovered_link {
884            self.hovered_link = found;
885            cx.notify();
886        }
887    }
888
889    fn reports_mouse(&self, modifiers: Modifiers) -> bool {
890        keys::reports_mouse(self.frame.modes.mouse_reporting, modifiers)
891    }
892
893    fn report_mouse(
894        &mut self,
895        action: MouseAction,
896        button: Option<MouseButton>,
897        modifiers: Modifiers,
898        position: Point<Pixels>,
899        window: &mut Window,
900        cx: &mut Context<Self>,
901    ) {
902        let Some(geometry) = self.geometry else {
903            return;
904        };
905        let scale = window.scale_factor();
906        let local = position - geometry.origin;
907        let mouse = keys::mouse(
908            action,
909            button.and_then(keys::mouse_button),
910            modifiers,
911            f32::from(local.x) * scale,
912            f32::from(local.y) * scale,
913        );
914        self.send(TerminalInput::Mouse(mouse), cx);
915    }
916
917    pub(crate) fn on_mouse_down(
918        &mut self,
919        event: &MouseDownEvent,
920        window: &mut Window,
921        cx: &mut Context<Self>,
922    ) {
923        window.focus(&self.focus, cx);
924        if self.leaves_right_click(event.button, event.modifiers, window) {
925            return;
926        }
927        if event.button == MouseButton::Left && event.modifiers.platform {
928            self.hover_link(event.position, true, cx);
929            if let Some((_, link)) = self.hovered_link.clone() {
930                self.selection = None;
931                cx.open_url(&link.url);
932                cx.stop_propagation();
933                return;
934            }
935        }
936        if self.reports_mouse(event.modifiers) {
937            if self.selection.take().is_some() {
938                cx.notify();
939            }
940            self.report_mouse(
941                MouseAction::Press,
942                Some(event.button),
943                event.modifiers,
944                event.position,
945                window,
946                cx,
947            );
948            cx.stop_propagation();
949            return;
950        }
951        if event.button == MouseButton::Left
952            && let Some(geometry) = self.geometry
953        {
954            self.selection = Some(Selection::new(
955                geometry.selection_boundary_at(event.position),
956                self.frame.content_revision,
957                event.modifiers.alt,
958            ));
959            cx.notify();
960        }
961    }
962
963    pub(crate) fn on_mouse_move(
964        &mut self,
965        event: &MouseMoveEvent,
966        window: &mut Window,
967        cx: &mut Context<Self>,
968    ) {
969        if self.reports_mouse(event.modifiers) {
970            self.report_mouse(
971                MouseAction::Move,
972                event.pressed_button,
973                event.modifiers,
974                event.position,
975                window,
976                cx,
977            );
978        }
979    }
980
981    pub(crate) fn drag_selection(&mut self, position: Point<Pixels>, cx: &mut Context<Self>) {
982        let (Some(geometry), Some(selection)) = (self.geometry, self.selection.as_mut()) else {
983            return;
984        };
985        if !selection.is_dragging() {
986            return;
987        }
988        selection.extend(geometry.selection_boundary_at(position));
989        cx.notify();
990    }
991
992    pub(crate) fn finish_selection(&mut self, position: Point<Pixels>, cx: &mut Context<Self>) {
993        if !self.selection.is_some_and(Selection::is_dragging) {
994            return;
995        }
996        self.drag_selection(position, cx);
997        if let Some(selection) = self.selection.as_mut() {
998            selection.finish();
999        }
1000        if self.appearance.copy_on_select && self.has_selection() {
1001            self.copy(cx);
1002        }
1003    }
1004
1005    pub(crate) fn on_mouse_up(
1006        &mut self,
1007        event: &MouseUpEvent,
1008        window: &mut Window,
1009        cx: &mut Context<Self>,
1010    ) {
1011        if self.leaves_right_click(event.button, event.modifiers, window) {
1012            return;
1013        }
1014        if self.reports_mouse(event.modifiers) {
1015            self.report_mouse(
1016                MouseAction::Release,
1017                Some(event.button),
1018                event.modifiers,
1019                event.position,
1020                window,
1021                cx,
1022            );
1023        }
1024    }
1025
1026    /// The message shown over the grid when the program is not running.
1027    pub(crate) fn status_message(&self) -> Option<SharedString> {
1028        match &self.status {
1029            TerminalStatus::Failed(error) => Some(error.clone().into()),
1030            TerminalStatus::Exited(status) => Some(match (status.code, &status.signal) {
1031                (Some(0), None) => "Process exited".into(),
1032                (Some(code), None) => format!("Process exited with code {code}").into(),
1033                (_, Some(signal)) => format!("Process ended by {signal}").into(),
1034                (None, None) => "Process ended".into(),
1035            }),
1036            TerminalStatus::Starting | TerminalStatus::Live => None,
1037        }
1038    }
1039}
1040
1041fn finish(text: &str, trim: bool) -> String {
1042    if trim {
1043        selection::trim_lines(text)
1044    } else {
1045        text.to_owned()
1046    }
1047}
1048
1049impl EventEmitter<TerminalEvent> for TerminalState {}
1050
1051impl Focusable for TerminalState {
1052    fn focus_handle(&self, _: &App) -> FocusHandle {
1053        self.focus.clone()
1054    }
1055}
1056
1057impl Render for TerminalState {
1058    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1059        crate::terminal::element::Terminal::new(
1060            ElementId::NamedChild(ElementId::View(cx.entity_id()).into(), "terminal".into()),
1061            &cx.entity(),
1062        )
1063    }
1064}