diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index e24996899c..19029decdf 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -226,8 +226,7 @@ "cmd-p": "file_finder::Toggle", "cmd-shift-P": "command_palette::Toggle", "cmd-shift-M": "diagnostics::Deploy", - "cmd-alt-s": "workspace::SaveAll", - "shift-cmd-T": "terminal::Deploy" + "cmd-alt-s": "workspace::SaveAll" } }, // Bindings from Sublime Text @@ -353,5 +352,20 @@ "f2": "project_panel::Rename", "backspace": "project_panel::Delete" } + }, + { + "context": "Terminal", + "bindings": { + "ctrl-c": "terminal::SIGINT", + "escape": "terminal::ESCAPE", + "ctrl-d": "terminal::Quit", + "backspace": "terminal::DEL", + "enter": "terminal::RETURN", + "left": "terminal::LEFT", + "right": "terminal::RIGHT", + "up": "terminal::HistoryBack", + "down": "terminal::HistoryForward", + "tab": "terminal::AutoComplete" + } } ] \ No newline at end of file diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 8c4e264cc8..f6237eed2b 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -1,16 +1,14 @@ use std::sync::Arc; use alacritty_terminal::{ - // ansi::Handler, config::{Config, Program, PtyConfig}, event::{Event, EventListener, Notify}, - event_loop::{EventLoop, Notifier}, - grid::{Indexed, Scroll}, + event_loop::{EventLoop, Msg, Notifier}, + grid::Indexed, index::Point, sync::FairMutex, term::{cell::Cell, SizeInfo}, - tty, - Term, + tty, Term, }; use futures::{ channel::mpsc::{unbounded, UnboundedSender}, @@ -23,7 +21,7 @@ use gpui::{ fonts::{with_font_cache, TextStyle}, geometry::{rect::RectF, vector::vec2f}, impl_internal_actions, - keymap::Keystroke, + json::json, text_layout::Line, Entity, Event::KeyDown, @@ -35,36 +33,52 @@ use smallvec::SmallVec; use workspace::{Item, Workspace}; //ASCII Control characters on a keyboard -const BACKSPACE: char = 8_u8 as char; -const TAB: char = 9_u8 as char; -const CARRIAGE_RETURN: char = 13_u8 as char; -const ESC: char = 27_u8 as char; -const DEL: char = 127_u8 as char; - -#[derive(Clone, Debug, PartialEq, Eq)] -enum Direction { - LEFT, - RIGHT, -} - -impl Default for Direction { - fn default() -> Self { - Direction::LEFT - } -} +//Consts -> Structs -> Impls -> Functions, Vaguely in order of importance +const ETX_CHAR: char = 3_u8 as char; //'End of text', the control code for 'ctrl-c' +const TAB_CHAR: char = 9_u8 as char; +const CARRIAGE_RETURN_CHAR: char = 13_u8 as char; +const ESC_CHAR: char = 27_u8 as char; +const DEL_CHAR: char = 127_u8 as char; +const LEFT_SEQ: &str = "\x1b[D"; +const RIGHT_SEQ: &str = "\x1b[C"; +const UP_SEQ: &str = "\x1b[A"; +const DOWN_SEQ: &str = "\x1b[B"; +const DEFAULT_TITLE: &str = "Terminal"; #[derive(Clone, Default, Debug, PartialEq, Eq)] -struct KeyInput(char); -#[derive(Clone, Default, Debug, PartialEq, Eq)] -struct DirectionInput(Direction); +struct Input(String); -actions!(terminal, [Deploy]); -impl_internal_actions!(terminal, [KeyInput, DirectionInput]); +actions!( + terminal, + [ + Deploy, + SIGINT, + ESCAPE, + Quit, + DEL, + RETURN, + LEFT, + RIGHT, + HistoryBack, + HistoryForward, + AutoComplete + ] +); +impl_internal_actions!(terminal, [Input]); pub fn init(cx: &mut MutableAppContext) { - cx.add_action(TerminalView::deploy); - cx.add_action(TerminalView::write_key_to_pty); - cx.add_action(TerminalView::move_cursor); + cx.add_action(Terminal::deploy); + cx.add_action(Terminal::write_to_pty); + cx.add_action(Terminal::send_sigint); //TODO figure out how to do this properly + cx.add_action(Terminal::escape); + cx.add_action(Terminal::quit); + cx.add_action(Terminal::del); + cx.add_action(Terminal::carriage_return); + cx.add_action(Terminal::left); + cx.add_action(Terminal::right); + cx.add_action(Terminal::history_back); + cx.add_action(Terminal::history_forward); + cx.add_action(Terminal::autocomplete); } #[derive(Clone)] @@ -76,46 +90,59 @@ impl EventListener for ZedListener { } } -struct TerminalView { +struct Terminal { pty_tx: Notifier, term: Arc>>, title: String, } -impl Entity for TerminalView { +impl Entity for Terminal { type Event = (); } -impl TerminalView { +impl Terminal { fn new(cx: &mut ViewContext) -> Self { + //Spawn a task so the Alacritty EventLoop to communicate with us let (events_tx, mut events_rx) = unbounded(); - cx.spawn(|this, mut cx| async move { + cx.spawn_weak(|this, mut cx| async move { while let Some(event) = events_rx.next().await { - this.update(&mut cx, |this, cx| { - this.process_terminal_event(event, cx); - cx.notify(); - }); + match this.upgrade(&cx) { + Some(handle) => { + handle.update(&mut cx, |this, cx| { + this.process_terminal_event(event, cx); + cx.notify(); + }); + } + None => break, + } } }) .detach(); + //TODO: Load from settings let pty_config = PtyConfig { shell: Some(Program::Just("zsh".to_string())), working_directory: None, hold: false, }; + //TODO: Properly configure this let config = Config { pty_config: pty_config.clone(), ..Default::default() }; + + //TODO: derive this let size_info = SizeInfo::new(400., 100.0, 5., 5., 0., 0., false); + //Set up the terminal... let term = Term::new(&config, size_info, ZedListener(events_tx.clone())); let term = Arc::new(FairMutex::new(term)); + //Setup the pty... let pty = tty::new(&pty_config, &size_info, None).expect("Could not create tty"); + //And connect them together let event_loop = EventLoop::new( term.clone(), ZedListener(events_tx.clone()), @@ -124,18 +151,18 @@ impl TerminalView { false, ); + //Kick things off let pty_tx = Notifier(event_loop.channel()); - let _io_thread = event_loop.spawn(); //todo cleanup - - TerminalView { - title: "Terminal".to_string(), + let _io_thread = event_loop.spawn(); + Terminal { + title: DEFAULT_TITLE.to_string(), term, pty_tx, } } fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext) { - workspace.add_item(Box::new(cx.add_view(|cx| TerminalView::new(cx))), cx); + workspace.add_item(Box::new(cx.add_view(|cx| Terminal::new(cx))), cx); } fn process_terminal_event( @@ -144,35 +171,94 @@ impl TerminalView { cx: &mut ViewContext, ) { match event { - alacritty_terminal::event::Event::Wakeup => cx.notify(), - alacritty_terminal::event::Event::PtyWrite(out) => self.pty_tx.notify(out.into_bytes()), - _ => {} + Event::Wakeup => cx.notify(), + Event::PtyWrite(out) => self.write_to_pty(&Input(out), cx), + Event::MouseCursorDirty => todo!(), //I think this is outside of Zed's loop + Event::Title(title) => self.title = title, + Event::ResetTitle => self.title = DEFAULT_TITLE.to_string(), + Event::ClipboardStore(_, _) => todo!(), + Event::ClipboardLoad(_, _) => todo!(), + Event::ColorRequest(_, _) => todo!(), + Event::CursorBlinkingChange => todo!(), + Event::Bell => todo!(), + Event::Exit => todo!(), + Event::MouseCursorDirty => todo!(), } // } - fn write_key_to_pty(&mut self, action: &KeyInput, cx: &mut ViewContext) { - let mut bytes = vec![0; action.0.len_utf8()]; - action.0.encode_utf8(&mut bytes[..]); - self.pty_tx.notify(bytes); + fn shutdown_pty(&mut self) { + self.pty_tx.0.send(Msg::Shutdown).ok(); } - fn move_cursor(&mut self, action: &DirectionInput, cx: &mut ViewContext) { - let term = self.term.lock(); - match action.0 { - Direction::LEFT => { - self.pty_tx.notify("\x1b[C".to_string().into_bytes()); - } - Direction::RIGHT => { - self.pty_tx.notify("\x1b[D".to_string().into_bytes()); - } - } + fn history_back(&mut self, _: &HistoryBack, cx: &mut ViewContext) { + self.write_to_pty(&Input(UP_SEQ.to_string()), cx); + + //Noop.. for now... + //This might just need to be forwarded to the terminal? + //Behavior changes based on mode... + } + + fn history_forward(&mut self, _: &HistoryForward, cx: &mut ViewContext) { + self.write_to_pty(&Input(DOWN_SEQ.to_string()), cx); + //Noop.. for now... + //This might just need to be forwarded to the terminal by the pty? + //Behvaior changes based on mode + } + + fn autocomplete(&mut self, _: &AutoComplete, cx: &mut ViewContext) { + self.write_to_pty(&Input(TAB_CHAR.to_string()), cx); + //Noop.. for now... + //This might just need to be forwarded to the terminal by the pty? + //Behvaior changes based on mode + } + + fn write_to_pty(&mut self, input: &Input, _cx: &mut ViewContext) { + self.pty_tx.notify(input.0.clone().into_bytes()); + } + + fn send_sigint(&mut self, _: &SIGINT, cx: &mut ViewContext) { + self.write_to_pty(&Input(ETX_CHAR.to_string()), cx); + } + + fn escape(&mut self, _: &ESCAPE, cx: &mut ViewContext) { + self.write_to_pty(&Input(ESC_CHAR.to_string()), cx); + } + + fn del(&mut self, _: &DEL, cx: &mut ViewContext) { + self.write_to_pty(&Input(DEL_CHAR.to_string()), cx); + } + + fn carriage_return(&mut self, _: &RETURN, cx: &mut ViewContext) { + self.write_to_pty(&Input(CARRIAGE_RETURN_CHAR.to_string()), cx); + } + + fn left(&mut self, _: &LEFT, cx: &mut ViewContext) { + self.write_to_pty(&Input(LEFT_SEQ.to_string()), cx); + } + + fn right(&mut self, _: &RIGHT, cx: &mut ViewContext) { + self.write_to_pty(&Input(RIGHT_SEQ.to_string()), cx); + } + + fn quit(&mut self, _: &Quit, _cx: &mut ViewContext) { + //TODO + // cx.dispatch_action(cx.window_id(), workspace::CloseItem()); + } + + // ShowHistory, + // AutoComplete +} + +impl Drop for Terminal { + fn drop(&mut self) { + self.shutdown_pty(); } } -impl View for TerminalView { +impl View for Terminal { fn ui_name() -> &'static str { - "TerminalView" + "Terminal" } fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox { @@ -198,6 +284,7 @@ impl TerminalEl { struct LayoutState { lines: Vec, line_height: f32, + cursor: RectF, } impl Element for TerminalEl { @@ -209,9 +296,55 @@ impl Element for TerminalEl { constraint: gpui::SizeConstraint, cx: &mut gpui::LayoutContext, ) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) { - let term = self.term.lock(); + let size = constraint.max; + //Get terminal content + let mut term = self.term.lock(); + + //Set up text rendering + + let text_style = with_font_cache(cx.font_cache.clone(), || TextStyle { + color: Color::white(), + ..Default::default() + }); + let line_height = cx.font_cache.line_height(text_style.font_size); + let em_width = cx + .font_cache() + .em_width(text_style.font_id, text_style.font_size); + + term.resize(SizeInfo::new( + size.x(), + size.y(), + em_width, + line_height, + 0., + 0., + false, + )); + let content = term.renderable_content(); + // //Dual owned system from Neovide + // let mut block_width = cursor_row_layout.x_for_index(cursor_column + 1) - cursor_character_x; + // if block_width == 0.0 { + // block_width = layout.em_width; + // } + let cursor = RectF::new( + vec2f( + content.cursor.point.column.0 as f32 * em_width, + content.cursor.point.line.0 as f32 * line_height, + ), + vec2f(em_width, line_height), + ); + + // let cursor = Cursor { + // color: selection_style.cursor, + // block_width, + // origin: content_origin + vec2f(x, y), + // line_height: layout.line_height, + // shape: self.cursor_shape, + // block_text, + // } + let mut lines = vec![]; let mut cur_line = vec![]; let mut last_line = 0; @@ -235,11 +368,6 @@ impl Element for TerminalEl { let chunks = vec![(&line[..], None)].into_iter(); - let text_style = with_font_cache(cx.font_cache.clone(), || TextStyle { - color: Color::white(), - ..Default::default() - }); - let shaped_lines = layout_highlighted_chunks( chunks, &text_style, @@ -248,13 +376,13 @@ impl Element for TerminalEl { usize::MAX, line.matches('\n').count() + 1, ); - let line_height = cx.font_cache.line_height(text_style.font_size); ( constraint.max, LayoutState { lines: shaped_lines, line_height, + cursor, }, ) } @@ -278,19 +406,11 @@ impl Element for TerminalEl { origin.set_y(boundaries.max_y()); } - let term = self.term.lock(); - let cursor = term.renderable_content().cursor; - - let bounds = RectF::new( - vec2f( - cursor.point.column.0 as f32 * 10.0 + 150.0, - cursor.point.line.0 as f32 * 10.0 + 150.0, - ), - vec2f(10.0, 10.0), - ); + let new_origin = bounds.origin() + layout.cursor.origin(); + let new_cursor = RectF::new(new_origin, layout.cursor.size()); cx.scene.push_quad(Quad { - bounds, + bounds: new_cursor, background: Some(Color::red()), border: Default::default(), corner_radius: 0., @@ -310,38 +430,8 @@ impl Element for TerminalEl { KeyDown { input: Some(input), .. } => { - dbg!(event); - cx.dispatch_action(KeyInput(input.chars().next().unwrap())); + cx.dispatch_action(Input(input.to_string())); true - } //TODO: Write control characters (ctrl-c) to pty - KeyDown { - keystroke: Keystroke { key, .. }, - input: None, - .. - } => { - dbg!(event); - if key == "backspace" { - cx.dispatch_action(KeyInput(DEL)); - true - } else if key == "enter" { - //There may be some subtlety here in how our terminal works - cx.dispatch_action(KeyInput(CARRIAGE_RETURN)); - true - } else if key == "tab" { - cx.dispatch_action(KeyInput(TAB)); - true - } else if key == "left" { - cx.dispatch_action(DirectionInput(Direction::LEFT)); - true - } else if key == "right" { - cx.dispatch_action(DirectionInput(Direction::RIGHT)); - true - // } else if key == "escape" { //TODO - // cx.dispatch_action(KeyInput(ESC)); - // true - } else { - false - } } _ => false, } @@ -354,11 +444,13 @@ impl Element for TerminalEl { _paint: &Self::PaintState, _cx: &gpui::DebugContext, ) -> gpui::serde_json::Value { - unreachable!("Should never be called hopefully") + json!({ + "type": "TerminalElement", + }) } } -impl Item for TerminalView { +impl Item for Terminal { fn tab_content(&self, style: &theme::Tab, cx: &gpui::AppContext) -> ElementBox { let settings = cx.global::(); let search_theme = &settings.theme.search; @@ -378,7 +470,7 @@ impl Item for TerminalView { } fn project_entry_ids(&self, _cx: &gpui::AppContext) -> SmallVec<[project::ProjectEntryId; 3]> { - todo!() + SmallVec::new() } fn is_singleton(&self, _cx: &gpui::AppContext) -> bool { diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index ae269c00cb..37a408d85f 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -33,6 +33,7 @@ pub struct Theme { pub contact_notification: ContactNotification, pub update_notification: UpdateNotification, pub tooltip: TooltipStyle, + // pub terminal: Terminal, } #[derive(Deserialize, Default)]