1use 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#[derive(Clone, Debug, PartialEq, Eq)]
30#[non_exhaustive]
31pub enum TerminalEvent {
32 Focused,
34 TitleChanged,
36 CwdChanged,
38 Bell,
40 ClipboardWritten,
42 Notification {
44 title: SharedString,
46 body: SharedString,
48 },
49 Exited(ExitStatus),
51 Failed(SharedString),
53 FontSizeChanged,
56 ForegroundChanged,
60}
61
62#[derive(Clone, Debug, Default, PartialEq)]
64#[non_exhaustive]
65pub struct TerminalConfig {
66 colors: TerminalColors,
67 appearance: TerminalAppearance,
68}
69
70impl TerminalConfig {
71 #[must_use]
73 pub fn with_colors(mut self, colors: TerminalColors) -> Self {
74 self.colors = colors;
75 self
76 }
77
78 #[must_use]
80 pub fn with_appearance(mut self, appearance: TerminalAppearance) -> Self {
81 self.appearance = appearance;
82 self
83 }
84
85 pub fn colors(&self) -> &TerminalColors {
87 &self.colors
88 }
89
90 pub fn appearance(&self) -> &TerminalAppearance {
92 &self.appearance
93 }
94}
95
96#[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 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 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 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 #[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 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 #[must_use]
319 pub fn frame(&self) -> &Arc<TerminalFrame> {
320 &self.frame
321 }
322
323 #[must_use]
325 pub fn status(&self) -> &TerminalStatus {
326 &self.status
327 }
328
329 #[must_use]
331 pub fn title(&self) -> &SharedString {
332 &self.title
333 }
334
335 #[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 #[must_use]
348 pub fn foreground(&self) -> Option<&ForegroundProcess> {
349 self.foreground.as_ref()
350 }
351
352 #[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 #[must_use]
370 pub fn input_backlog(&self) -> usize {
371 self.handle.as_ref().map_or(0, |h| h.input_backlog())
372 }
373
374 #[must_use]
376 pub fn appearance(&self) -> &TerminalAppearance {
377 &self.appearance
378 }
379
380 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 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 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 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 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 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 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 #[must_use]
453 pub fn colors(&self) -> &TerminalColors {
454 &self.colors
455 }
456
457 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 #[must_use]
471 pub fn is_live(&self) -> bool {
472 matches!(self.status, TerminalStatus::Live) && !self.closed
473 }
474
475 #[must_use]
477 pub fn is_closed(&self) -> bool {
478 self.closed
479 }
480
481 #[must_use]
483 pub fn is_visible(&self) -> bool {
484 self.visible
485 }
486
487 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 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 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 #[must_use]
534 pub fn has_selection(&self) -> bool {
535 self.selection.is_some_and(|s| !s.is_empty())
536 }
537
538 #[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 #[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 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 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 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 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 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 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 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 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 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 #[must_use]
653 pub fn is_scrolled(&self) -> bool {
654 !self.frame.scroll.at_bottom()
655 }
656
657 #[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 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 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 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 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 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 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 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 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}