zed/crates/terminal_view/src/terminal_view.rs

526 lines
16 KiB
Rust
Raw Normal View History

2022-12-09 00:10:22 +00:00
mod persistence;
pub mod terminal_container_view;
pub mod terminal_element;
use std::{ops::RangeInclusive, time::Duration};
2022-08-04 18:22:09 +00:00
2022-08-22 23:49:01 +00:00
use context_menu::{ContextMenu, ContextMenuItem};
2022-07-26 00:24:21 +00:00
use gpui::{
2022-08-22 23:49:01 +00:00
actions,
elements::{AnchorCorner, ChildView, ParentElement, Stack},
2022-08-22 23:49:01 +00:00
geometry::vector::Vector2F,
impl_actions, impl_internal_actions,
2022-08-22 23:49:01 +00:00
keymap::Keystroke,
AnyViewHandle, AppContext, Element, ElementBox, Entity, ModelHandle, MutableAppContext, Task,
View, ViewContext, ViewHandle,
2022-07-26 00:24:21 +00:00
};
use serde::Deserialize;
2022-08-22 23:49:01 +00:00
use settings::{Settings, TerminalBlink};
use smol::Timer;
use terminal::{
alacritty_terminal::{
index::Point,
term::{search::RegexSearch, TermMode},
},
2022-12-09 03:05:26 +00:00
Event, Terminal,
};
use util::ResultExt;
use workspace::{pane, ItemId, WorkspaceId};
2022-07-26 00:24:21 +00:00
2022-12-09 03:05:26 +00:00
use crate::{persistence::TERMINAL_DB, terminal_element::TerminalElement};
2022-07-26 00:24:21 +00:00
2022-08-22 23:49:01 +00:00
const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
2022-07-26 00:24:21 +00:00
2022-08-22 23:49:01 +00:00
///Event to transmit the scroll from the element to the view
#[derive(Clone, Debug, PartialEq)]
pub struct ScrollTerminal(pub i32);
2022-07-26 00:24:21 +00:00
2022-08-22 23:49:01 +00:00
#[derive(Clone, PartialEq)]
pub struct DeployContextMenu {
pub position: Vector2F,
2022-08-04 18:22:09 +00:00
}
#[derive(Clone, Default, Deserialize, PartialEq)]
pub struct SendText(String);
#[derive(Clone, Default, Deserialize, PartialEq)]
pub struct SendKeystroke(String);
2022-08-22 23:49:01 +00:00
actions!(
terminal,
2022-10-17 21:53:52 +00:00
[Clear, Copy, Paste, ShowCharacterPalette, SearchTest]
2022-08-22 23:49:01 +00:00
);
impl_actions!(terminal, [SendText, SendKeystroke]);
2022-08-22 23:49:01 +00:00
impl_internal_actions!(project_panel, [DeployContextMenu]);
2022-07-26 00:24:21 +00:00
2022-08-22 23:49:01 +00:00
pub fn init(cx: &mut MutableAppContext) {
2022-12-09 00:10:22 +00:00
terminal_container_view::init(cx);
2022-08-22 23:49:01 +00:00
//Useful terminal views
cx.add_action(TerminalView::send_text);
cx.add_action(TerminalView::send_keystroke);
2022-08-22 23:49:01 +00:00
cx.add_action(TerminalView::deploy_context_menu);
cx.add_action(TerminalView::copy);
cx.add_action(TerminalView::paste);
cx.add_action(TerminalView::clear);
cx.add_action(TerminalView::show_character_palette);
2022-07-26 00:24:21 +00:00
}
2022-08-22 23:49:01 +00:00
///A terminal view, maintains the PTY's file handles and communicates with the terminal
2022-07-26 00:24:21 +00:00
pub struct TerminalView {
2022-08-22 23:49:01 +00:00
terminal: ModelHandle<Terminal>,
has_new_content: bool,
//Currently using iTerm bell, show bell emoji in tab until input is received
has_bell: bool,
// Only for styling purposes. Doesn't effect behavior
2022-07-26 00:24:21 +00:00
modal: bool,
2022-08-22 23:49:01 +00:00
context_menu: ViewHandle<ContextMenu>,
blink_state: bool,
blinking_on: bool,
blinking_paused: bool,
blink_epoch: usize,
workspace_id: WorkspaceId,
item_id: ItemId,
2022-07-26 00:24:21 +00:00
}
impl Entity for TerminalView {
type Event = Event;
}
impl TerminalView {
pub fn from_terminal(
terminal: ModelHandle<Terminal>,
modal: bool,
workspace_id: WorkspaceId,
item_id: ItemId,
2022-07-26 00:24:21 +00:00
cx: &mut ViewContext<Self>,
) -> Self {
2022-08-22 23:49:01 +00:00
cx.observe(&terminal, |_, _, cx| cx.notify()).detach();
cx.subscribe(&terminal, |this, _, event, cx| match event {
Event::Wakeup => {
if !cx.is_self_focused() {
this.has_new_content = true;
cx.notify();
}
2022-09-03 20:46:36 +00:00
cx.emit(Event::Wakeup);
2022-08-22 23:49:01 +00:00
}
Event::Bell => {
this.has_bell = true;
cx.emit(Event::Wakeup);
}
Event::BlinkChanged => this.blinking_on = !this.blinking_on,
Event::TitleChanged => {
if let Some(foreground_info) = &this.terminal().read(cx).foreground_process_info {
let cwd = foreground_info.cwd.clone();
let item_id = this.item_id;
let workspace_id = this.workspace_id;
cx.background()
.spawn(async move {
TERMINAL_DB
.save_working_directory(item_id, workspace_id, cwd)
.await
.log_err();
})
.detach();
}
}
2022-08-22 23:49:01 +00:00
_ => cx.emit(*event),
})
.detach();
Self {
terminal,
has_new_content: true,
has_bell: false,
2022-07-26 00:24:21 +00:00
modal,
2022-08-22 23:49:01 +00:00
context_menu: cx.add_view(ContextMenu::new),
blink_state: true,
blinking_on: false,
blinking_paused: false,
blink_epoch: 0,
workspace_id,
item_id,
2022-07-26 00:24:21 +00:00
}
}
2022-08-22 23:49:01 +00:00
pub fn handle(&self) -> ModelHandle<Terminal> {
self.terminal.clone()
2022-07-26 00:24:21 +00:00
}
2022-08-22 23:49:01 +00:00
pub fn has_new_content(&self) -> bool {
self.has_new_content
2022-07-26 00:24:21 +00:00
}
2022-08-22 23:49:01 +00:00
pub fn has_bell(&self) -> bool {
self.has_bell
2022-07-26 00:24:21 +00:00
}
2022-08-22 23:49:01 +00:00
pub fn clear_bel(&mut self, cx: &mut ViewContext<TerminalView>) {
self.has_bell = false;
cx.emit(Event::Wakeup);
2022-07-26 00:24:21 +00:00
}
2022-08-22 23:49:01 +00:00
pub fn deploy_context_menu(&mut self, action: &DeployContextMenu, cx: &mut ViewContext<Self>) {
let menu_entries = vec![
ContextMenuItem::item("Clear", Clear),
ContextMenuItem::item("Close", pane::CloseActiveItem),
2022-08-22 23:49:01 +00:00
];
2022-07-26 00:24:21 +00:00
self.context_menu.update(cx, |menu, cx| {
menu.show(action.position, AnchorCorner::TopLeft, menu_entries, cx)
});
2022-07-26 00:24:21 +00:00
2022-08-22 23:49:01 +00:00
cx.notify();
}
2022-07-26 00:24:21 +00:00
2022-08-22 23:49:01 +00:00
fn show_character_palette(&mut self, _: &ShowCharacterPalette, cx: &mut ViewContext<Self>) {
if !self
.terminal
.read(cx)
.last_content
.mode
2022-08-22 23:49:01 +00:00
.contains(TermMode::ALT_SCREEN)
{
cx.show_character_palette();
} else {
2022-09-13 06:22:55 +00:00
self.terminal.update(cx, |term, cx| {
term.try_keystroke(
&Keystroke::parse("ctrl-cmd-space").unwrap(),
cx.global::<Settings>()
.terminal_overrides
.option_as_meta
.unwrap_or(false),
)
2022-08-22 23:49:01 +00:00
});
}
}
2022-07-26 00:24:21 +00:00
2022-08-22 23:49:01 +00:00
fn clear(&mut self, _: &Clear, cx: &mut ViewContext<Self>) {
self.terminal.update(cx, |term, _| term.clear());
cx.notify();
2022-07-26 00:24:21 +00:00
}
2022-08-22 23:49:01 +00:00
pub fn should_show_cursor(
2022-07-26 00:24:21 +00:00
&self,
2022-08-22 23:49:01 +00:00
focused: bool,
cx: &mut gpui::RenderContext<'_, Self>,
) -> bool {
//Don't blink the cursor when not focused, blinking is disabled, or paused
if !focused
|| !self.blinking_on
|| self.blinking_paused
|| self
.terminal
.read(cx)
.last_content
.mode
2022-08-22 23:49:01 +00:00
.contains(TermMode::ALT_SCREEN)
{
return true;
}
2022-07-26 00:24:21 +00:00
2022-08-22 23:49:01 +00:00
let setting = {
let settings = cx.global::<Settings>();
settings
.terminal_overrides
.blinking
.clone()
.unwrap_or(TerminalBlink::TerminalControlled)
};
2022-07-26 00:24:21 +00:00
2022-08-22 23:49:01 +00:00
match setting {
//If the user requested to never blink, don't blink it.
TerminalBlink::Off => true,
//If the terminal is controlling it, check terminal mode
TerminalBlink::TerminalControlled | TerminalBlink::On => self.blink_state,
}
2022-07-26 00:24:21 +00:00
}
2022-08-22 23:49:01 +00:00
fn blink_cursors(&mut self, epoch: usize, cx: &mut ViewContext<Self>) {
if epoch == self.blink_epoch && !self.blinking_paused {
self.blink_state = !self.blink_state;
cx.notify();
let epoch = self.next_blink_epoch();
cx.spawn(|this, mut cx| {
let this = this.downgrade();
async move {
Timer::after(CURSOR_BLINK_INTERVAL).await;
if let Some(this) = this.upgrade(&cx) {
this.update(&mut cx, |this, cx| this.blink_cursors(epoch, cx));
}
}
})
.detach();
}
2022-07-26 00:24:21 +00:00
}
2022-08-22 23:49:01 +00:00
pub fn pause_cursor_blinking(&mut self, cx: &mut ViewContext<Self>) {
self.blink_state = true;
cx.notify();
let epoch = self.next_blink_epoch();
cx.spawn(|this, mut cx| {
let this = this.downgrade();
async move {
Timer::after(CURSOR_BLINK_INTERVAL).await;
if let Some(this) = this.upgrade(&cx) {
this.update(&mut cx, |this, cx| this.resume_cursor_blinking(epoch, cx))
}
}
})
.detach();
2022-07-26 00:24:21 +00:00
}
pub fn find_matches(
&mut self,
query: project::search::SearchQuery,
cx: &mut ViewContext<Self>,
) -> Task<Vec<RangeInclusive<Point>>> {
let searcher = regex_search_for_query(query);
if let Some(searcher) = searcher {
self.terminal
.update(cx, |term, cx| term.find_matches(searcher, cx))
} else {
cx.background().spawn(async { Vec::new() })
}
}
pub fn terminal(&self) -> &ModelHandle<Terminal> {
&self.terminal
}
pub fn added_to_workspace(&mut self, new_id: WorkspaceId, cx: &mut ViewContext<Self>) {
cx.background()
.spawn(TERMINAL_DB.update_workspace_id(new_id, self.workspace_id, self.item_id))
.detach();
self.workspace_id = new_id;
}
2022-08-22 23:49:01 +00:00
fn next_blink_epoch(&mut self) -> usize {
self.blink_epoch += 1;
self.blink_epoch
2022-07-26 00:24:21 +00:00
}
2022-08-22 23:49:01 +00:00
fn resume_cursor_blinking(&mut self, epoch: usize, cx: &mut ViewContext<Self>) {
if epoch == self.blink_epoch {
self.blinking_paused = false;
self.blink_cursors(epoch, cx);
}
2022-07-26 00:24:21 +00:00
}
2022-08-22 23:49:01 +00:00
///Attempt to paste the clipboard into the terminal
fn copy(&mut self, _: &Copy, cx: &mut ViewContext<Self>) {
self.terminal.update(cx, |term, _| term.copy())
2022-07-26 00:24:21 +00:00
}
2022-08-22 23:49:01 +00:00
///Attempt to paste the clipboard into the terminal
fn paste(&mut self, _: &Paste, cx: &mut ViewContext<Self>) {
if let Some(item) = cx.read_from_clipboard() {
self.terminal
.update(cx, |terminal, _cx| terminal.paste(item.text()));
}
2022-07-26 00:24:21 +00:00
}
fn send_text(&mut self, text: &SendText, cx: &mut ViewContext<Self>) {
2022-08-22 23:49:01 +00:00
self.clear_bel(cx);
self.terminal.update(cx, |term, _| {
term.input(text.0.to_string());
2022-08-22 23:49:01 +00:00
});
2022-07-26 00:24:21 +00:00
}
fn send_keystroke(&mut self, text: &SendKeystroke, cx: &mut ViewContext<Self>) {
if let Some(keystroke) = Keystroke::parse(&text.0).log_err() {
self.clear_bel(cx);
self.terminal.update(cx, |term, cx| {
term.try_keystroke(
&keystroke,
cx.global::<Settings>()
.terminal_overrides
.option_as_meta
.unwrap_or(false),
);
});
}
2022-07-26 00:24:21 +00:00
}
}
pub fn regex_search_for_query(query: project::search::SearchQuery) -> Option<RegexSearch> {
let searcher = match query {
project::search::SearchQuery::Text { query, .. } => RegexSearch::new(&query),
project::search::SearchQuery::Regex { query, .. } => RegexSearch::new(&query),
};
searcher.ok()
}
2022-08-22 23:49:01 +00:00
impl View for TerminalView {
fn ui_name() -> &'static str {
"Terminal"
2022-07-26 00:24:21 +00:00
}
2022-08-22 23:49:01 +00:00
fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox {
let terminal_handle = self.terminal.clone().downgrade();
2022-07-26 00:24:21 +00:00
2022-08-22 23:49:01 +00:00
let self_id = cx.view_id();
let focused = cx
.focused_view_id(cx.window_id())
.filter(|view_id| *view_id == self_id)
.is_some();
2022-07-26 00:24:21 +00:00
2022-08-22 23:49:01 +00:00
Stack::new()
.with_child(
TerminalElement::new(
cx.handle(),
terminal_handle,
focused,
self.should_show_cursor(focused, cx),
)
.contained()
.boxed(),
)
.with_child(ChildView::new(&self.context_menu, cx).boxed())
2022-08-22 23:49:01 +00:00
.boxed()
}
2022-07-26 00:24:21 +00:00
fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
2022-08-22 23:49:01 +00:00
self.has_new_content = false;
self.terminal.read(cx).focus_in();
self.blink_cursors(self.blink_epoch, cx);
cx.notify();
}
2022-07-26 00:24:21 +00:00
fn focus_out(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
2022-09-24 15:32:06 +00:00
self.terminal.update(cx, |terminal, _| {
terminal.focus_out();
});
2022-08-22 23:49:01 +00:00
cx.notify();
2022-07-26 00:24:21 +00:00
}
fn key_down(&mut self, event: &gpui::KeyDownEvent, cx: &mut ViewContext<Self>) -> bool {
self.clear_bel(cx);
self.pause_cursor_blinking(cx);
self.terminal.update(cx, |term, cx| {
term.try_keystroke(
&event.keystroke,
cx.global::<Settings>()
.terminal_overrides
.option_as_meta
.unwrap_or(false),
)
})
}
2022-08-22 23:49:01 +00:00
//IME stuff
fn selected_text_range(&self, cx: &AppContext) -> Option<std::ops::Range<usize>> {
if self
.terminal
.read(cx)
.last_content
.mode
2022-08-22 23:49:01 +00:00
.contains(TermMode::ALT_SCREEN)
{
None
} else {
Some(0..0)
}
2022-07-26 00:24:21 +00:00
}
2022-08-22 23:49:01 +00:00
fn replace_text_in_range(
&mut self,
_: Option<std::ops::Range<usize>>,
text: &str,
cx: &mut ViewContext<Self>,
) {
self.terminal.update(cx, |terminal, _| {
terminal.input(text.into());
2022-07-26 00:24:21 +00:00
});
}
2022-08-22 23:49:01 +00:00
fn keymap_context(&self, cx: &gpui::AppContext) -> gpui::keymap::Context {
let mut context = Self::default_keymap_context();
if self.modal {
context.set.insert("ModalTerminal".into());
}
let mode = self.terminal.read(cx).last_content.mode;
2022-08-22 23:49:01 +00:00
context.map.insert(
"screen".to_string(),
(if mode.contains(TermMode::ALT_SCREEN) {
"alt"
} else {
"normal"
})
.to_string(),
);
if mode.contains(TermMode::APP_CURSOR) {
context.set.insert("DECCKM".to_string());
}
if mode.contains(TermMode::APP_KEYPAD) {
context.set.insert("DECPAM".to_string());
}
//Note the ! here
if !mode.contains(TermMode::APP_KEYPAD) {
context.set.insert("DECPNM".to_string());
}
if mode.contains(TermMode::SHOW_CURSOR) {
context.set.insert("DECTCEM".to_string());
}
if mode.contains(TermMode::LINE_WRAP) {
context.set.insert("DECAWM".to_string());
}
if mode.contains(TermMode::ORIGIN) {
context.set.insert("DECOM".to_string());
}
if mode.contains(TermMode::INSERT) {
context.set.insert("IRM".to_string());
}
//LNM is apparently the name for this. https://vt100.net/docs/vt510-rm/LNM.html
if mode.contains(TermMode::LINE_FEED_NEW_LINE) {
context.set.insert("LNM".to_string());
}
if mode.contains(TermMode::FOCUS_IN_OUT) {
context.set.insert("report_focus".to_string());
}
if mode.contains(TermMode::ALTERNATE_SCROLL) {
context.set.insert("alternate_scroll".to_string());
}
if mode.contains(TermMode::BRACKETED_PASTE) {
context.set.insert("bracketed_paste".to_string());
}
if mode.intersects(TermMode::MOUSE_MODE) {
context.set.insert("any_mouse_reporting".to_string());
}
{
let mouse_reporting = if mode.contains(TermMode::MOUSE_REPORT_CLICK) {
"click"
} else if mode.contains(TermMode::MOUSE_DRAG) {
"drag"
} else if mode.contains(TermMode::MOUSE_MOTION) {
"motion"
} else {
"off"
};
context
.map
.insert("mouse_reporting".to_string(), mouse_reporting.to_string());
}
{
let format = if mode.contains(TermMode::SGR_MOUSE) {
"sgr"
} else if mode.contains(TermMode::UTF8_MOUSE) {
"utf8"
} else {
"normal"
};
context
.map
.insert("mouse_format".to_string(), format.to_string());
}
context
2022-07-26 00:24:21 +00:00
}
}