diff --git a/Cargo.lock b/Cargo.lock index ccf5b2428d..1a59f23918 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4881,6 +4881,7 @@ dependencies = [ "futures", "gpui", "mio-extras", + "ordered-float", "project", "settings", "smallvec", diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 19029decdf..f9254176a9 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -363,9 +363,10 @@ "enter": "terminal::RETURN", "left": "terminal::LEFT", "right": "terminal::RIGHT", - "up": "terminal::HistoryBack", - "down": "terminal::HistoryForward", - "tab": "terminal::AutoComplete" + "up": "terminal::UP", + "down": "terminal::DOWN", + "tab": "terminal::TAB", + "cmd-k": "terminal::Clear" } } ] \ No newline at end of file diff --git a/crates/terminal/Cargo.toml b/crates/terminal/Cargo.toml index 4d0cbb3cfc..3cb4d631f3 100644 --- a/crates/terminal/Cargo.toml +++ b/crates/terminal/Cargo.toml @@ -19,3 +19,4 @@ project = { path = "../project" } smallvec = { version = "1.6", features = ["union"] } mio-extras = "2.0.6" futures = "0.3" +ordered-float = "2.1.1" diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index f6237eed2b..c8f936bda9 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -1,13 +1,18 @@ use std::sync::Arc; use alacritty_terminal::{ + ansi::Color as AnsiColor, config::{Config, Program, PtyConfig}, - event::{Event, EventListener, Notify}, + event::{Event as AlacTermEvent, EventListener, Notify}, event_loop::{EventLoop, Msg, Notifier}, grid::Indexed, index::Point, sync::FairMutex, - term::{cell::Cell, SizeInfo}, + term::{ + cell::{Cell, Flags}, + color::Rgb, + SizeInfo, + }, tty, Term, }; use futures::{ @@ -18,15 +23,17 @@ use gpui::{ actions, color::Color, elements::*, - fonts::{with_font_cache, TextStyle}, + fonts::{with_font_cache, HighlightStyle, TextStyle, Underline}, geometry::{rect::RectF, vector::vec2f}, impl_internal_actions, json::json, + platform::CursorStyle, text_layout::Line, - Entity, + ClipboardItem, Entity, Event::KeyDown, MutableAppContext, Quad, View, ViewContext, }; +use ordered_float::OrderedFloat; use project::{Project, ProjectPath}; use settings::Settings; use smallvec::SmallVec; @@ -43,6 +50,7 @@ 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 CLEAR_SEQ: &str = "\x1b[2J"; const DEFAULT_TITLE: &str = "Terminal"; #[derive(Clone, Default, Debug, PartialEq, Eq)] @@ -50,59 +58,57 @@ struct Input(String); actions!( terminal, - [ - Deploy, - SIGINT, - ESCAPE, - Quit, - DEL, - RETURN, - LEFT, - RIGHT, - HistoryBack, - HistoryForward, - AutoComplete - ] + [Deploy, SIGINT, ESCAPE, Quit, DEL, RETURN, LEFT, RIGHT, UP, DOWN, TAB, Clear] ); impl_internal_actions!(terminal, [Input]); pub fn init(cx: &mut MutableAppContext) { 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::send_sigint); 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::carriage_return); //TODO figure out how to do this properly. Should we be checking the terminal mode? 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); + cx.add_action(Terminal::up); + cx.add_action(Terminal::down); + cx.add_action(Terminal::tab); + cx.add_action(Terminal::clear); } #[derive(Clone)] -pub struct ZedListener(UnboundedSender); +pub struct ZedListener(UnboundedSender); impl EventListener for ZedListener { - fn send_event(&self, event: Event) { + fn send_event(&self, event: AlacTermEvent) { self.0.unbounded_send(event).ok(); } } +///A terminal renderer. struct Terminal { pty_tx: Notifier, term: Arc>>, title: String, + has_new_content: bool, + has_bell: bool, //Currently using iTerm bell, show bell emoji in tab until input is received +} + +enum ZedTermEvent { + TitleChanged, + CloseTerminal, } impl Entity for Terminal { - type Event = (); + type Event = ZedTermEvent; } impl Terminal { + ///Create a new Terminal view. This spawns a task, a thread, and opens the TTY devices fn new(cx: &mut ViewContext) -> Self { - //Spawn a task so the Alacritty EventLoop to communicate with us + //Spawn a task so the Alacritty EventLoop can communicate with us in a view context let (events_tx, mut events_rx) = unbounded(); cx.spawn_weak(|this, mut cx| async move { while let Some(event) = events_rx.next().await { @@ -158,65 +164,109 @@ impl Terminal { title: DEFAULT_TITLE.to_string(), term, pty_tx, + has_new_content: false, + has_bell: false, } } - fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext) { - workspace.add_item(Box::new(cx.add_view(|cx| Terminal::new(cx))), cx); - } - + ///Takes events from Alacritty and translates them to behavior on this view fn process_terminal_event( &mut self, event: alacritty_terminal::event::Event, cx: &mut ViewContext, ) { match event { - 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!(), + AlacTermEvent::Wakeup => { + if !cx.is_self_focused() { + //Need to figure out how to trigger a redraw when not in focus + self.has_new_content = true; //Change tab content + cx.emit(ZedTermEvent::TitleChanged); + } else { + cx.notify() + } + } + AlacTermEvent::PtyWrite(out) => self.write_to_pty(&Input(out), cx), + //TODO: + //What this is supposed to do is check the cursor state, then set it on the platform. + //See Processor::reset_mouse_cursor() and Processor::cursor_state() in alacritty/src/input.rs + //to see how this is Calculated. Question: Does this flow make sense with how GPUI hadles + //the mouse? + AlacTermEvent::MouseCursorDirty => { + //Calculate new cursor style. + //Check on correctly handling mouse events for terminals + cx.platform().set_cursor_style(CursorStyle::Arrow); //??? + println!("Mouse cursor dirty") + } + AlacTermEvent::Title(title) => { + self.title = title; + cx.emit(ZedTermEvent::TitleChanged); + } + AlacTermEvent::ResetTitle => { + self.title = DEFAULT_TITLE.to_string(); + cx.emit(ZedTermEvent::TitleChanged); + } + AlacTermEvent::ClipboardStore(_, data) => { + cx.write_to_clipboard(ClipboardItem::new(data)) + } + AlacTermEvent::ClipboardLoad(_, format) => self.write_to_pty( + &Input(format( + &cx.read_from_clipboard() + .map(|ci| ci.text().to_string()) + .unwrap_or("".to_string()), + )), + cx, + ), + AlacTermEvent::ColorRequest(index, format) => { + //TODO test this as well + //TODO: change to getting the display colors, like alacrityy, instead of a default + let color = self.term.lock().colors()[index].unwrap_or(Rgb::default()); + self.write_to_pty(&Input(format(color)), cx) + } + AlacTermEvent::CursorBlinkingChange => { + //So, it's our job to set a timer and cause the cursor to blink here + //Which means that I'm going to put this off until someone @ Zed looks at it + } + AlacTermEvent::Bell => { + self.has_bell = true; + cx.emit(ZedTermEvent::TitleChanged); + } + AlacTermEvent::Exit => self.quit(&Quit, cx), } - // } + ///Create a new Terminal + fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext) { + workspace.add_item(Box::new(cx.add_view(|cx| Terminal::new(cx))), cx); + } + + ///Send the shutdown message to Alacritty fn shutdown_pty(&mut self) { self.pty_tx.0.send(Msg::Shutdown).ok(); } - 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 quit(&mut self, _: &Quit, cx: &mut ViewContext) { + cx.emit(ZedTermEvent::CloseTerminal); } - 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) { + fn write_to_pty(&mut self, input: &Input, cx: &mut ViewContext) { + //iTerm bell behavior, bell stays until terminal is interacted with + self.has_bell = false; + cx.emit(ZedTermEvent::TitleChanged); self.pty_tx.notify(input.0.clone().into_bytes()); } + fn up(&mut self, _: &UP, cx: &mut ViewContext) { + self.write_to_pty(&Input(UP_SEQ.to_string()), cx); + } + + fn down(&mut self, _: &DOWN, cx: &mut ViewContext) { + self.write_to_pty(&Input(DOWN_SEQ.to_string()), cx); + } + + fn tab(&mut self, _: &TAB, cx: &mut ViewContext) { + self.write_to_pty(&Input(TAB_CHAR.to_string()), cx); + } + fn send_sigint(&mut self, _: &SIGINT, cx: &mut ViewContext) { self.write_to_pty(&Input(ETX_CHAR.to_string()), cx); } @@ -241,13 +291,9 @@ impl Terminal { 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()); + fn clear(&mut self, _: &Clear, cx: &mut ViewContext) { + self.write_to_pty(&Input(CLEAR_SEQ.to_string()), cx); } - - // ShowHistory, - // AutoComplete } impl Drop for Terminal { @@ -269,6 +315,98 @@ impl View for Terminal { // .with_style(theme.terminal.container) .boxed() } + + fn on_focus(&mut self, _: &mut ViewContext) { + self.has_new_content = false; + } +} + +impl Item for Terminal { + fn tab_content(&self, tab_theme: &theme::Tab, cx: &gpui::AppContext) -> ElementBox { + let settings = cx.global::(); + let search_theme = &settings.theme.search; //TODO properly integrate themes + + let mut flex = Flex::row(); + + if self.has_bell { + flex.add_child( + Svg::new("icons/zap.svg") + .with_color(tab_theme.label.text.color) + .constrained() + .with_width(search_theme.tab_icon_width) + .aligned() + .boxed(), + ); + }; + + flex.with_child( + Label::new(self.title.clone(), tab_theme.label.clone()) + .aligned() + .contained() + .with_margin_left(if self.has_bell { + search_theme.tab_icon_spacing + } else { + 0. + }) + .boxed(), + ) + .boxed() + } + + fn project_path(&self, _cx: &gpui::AppContext) -> Option { + None + } + + fn project_entry_ids(&self, _cx: &gpui::AppContext) -> SmallVec<[project::ProjectEntryId; 3]> { + SmallVec::new() + } + + fn is_singleton(&self, _cx: &gpui::AppContext) -> bool { + false + } + + fn set_nav_history(&mut self, _: workspace::ItemNavHistory, _: &mut ViewContext) {} + + fn can_save(&self, _cx: &gpui::AppContext) -> bool { + false + } + + fn save( + &mut self, + _project: gpui::ModelHandle, + _cx: &mut ViewContext, + ) -> gpui::Task> { + unreachable!("save should not have been called"); + } + + fn save_as( + &mut self, + _project: gpui::ModelHandle, + _abs_path: std::path::PathBuf, + _cx: &mut ViewContext, + ) -> gpui::Task> { + unreachable!("save_as should not have been called"); + } + + fn reload( + &mut self, + _project: gpui::ModelHandle, + _cx: &mut ViewContext, + ) -> gpui::Task> { + gpui::Task::ready(Ok(())) + } + + fn is_dirty(&self, _: &gpui::AppContext) -> bool { + self.has_new_content + } + + fn should_update_tab_on_event(event: &Self::Event) -> bool { + matches!(event, &ZedTermEvent::TitleChanged) + } + + fn should_close_item_on_event(event: &Self::Event) -> bool { + matches!(event, &ZedTermEvent::CloseTerminal) + } } struct TerminalEl { @@ -286,7 +424,39 @@ struct LayoutState { line_height: f32, cursor: RectF, } +/* TODO point calculation for selection + * take the current point's x: + * - subtract padding + * - divide by cell width + * - take the minimum of the x coord and the last colum of the size info + * Take the current point's y: + * - Subtract padding + * - Divide by cell height + * - Take the minimum of the y coord and the last line + * + * With this x and y, pass to term::viewport_to_point (module function) + * Also pass in the display offset from the term.grid().display_offset() + * (Display offset is for scrolling) + */ +/* TODO Selection + * 1. On click, calculate the single, double, and triple click based on timings + * 2. Convert mouse location to a terminal point + * 3. Generate each of the three kinds of selection needed + * 4. Assign a selection to the terminal's selection variable + * How to render? + * 1. On mouse moved, calculate a terminal point + * 2. if (lmb_pressed || rmb_pressed) && (self.ctx.modifiers().shift() || !self.ctx.mouse_mode() + * 3. Take the selection from the terminal, call selection.update(), and put it back + */ + +/* TODO Scroll + * 1. Convert scroll to a pixel delta (alacritty/src/input > Processor::mouse_wheel_input) + * 2. Divide by cell height + * 3. Create an alacritty_terminal::Scroll::Delta() object and call `self.terminal.scroll_display(scroll);` + * 4. Maybe do a cx.notify, just in case. + * 5. Also update the selected area, just check out for the logic alacritty/src/event.rs > ActionContext::scroll + */ impl Element for TerminalEl { type LayoutState = LayoutState; type PaintState = (); @@ -323,11 +493,6 @@ impl Element for TerminalEl { 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, @@ -336,45 +501,50 @@ impl Element for TerminalEl { 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 lines: Vec<(String, Option)> = vec![]; let mut last_line = 0; + + let mut cur_chunk = String::new(); + + let mut cur_highlight = HighlightStyle { + color: Some(Color::white()), + ..Default::default() + }; for cell in content.display_iter { let Indexed { point: Point { line, .. }, - cell: Cell { c, .. }, + cell: Cell { + c, fg, flags, .. // TODO: Add bg and flags + }, //TODO: Learn what 'CellExtra does' } = cell; + let new_highlight = make_style_from_cell(fg, flags); + HighlightStyle { + color: Some(alac_color_to_gpui_color(fg)), + ..Default::default() + }; + if line != last_line { - lines.push(cur_line); - cur_line = vec![]; + cur_chunk.push('\n'); last_line = line.0; } - cur_line.push(c); - } - let line = lines - .into_iter() - .map(|char_vec| char_vec.into_iter().collect::()) - .fold("".to_string(), |grid, line| grid + &line + "\n"); - let chunks = vec![(&line[..], None)].into_iter(); + if new_highlight != cur_highlight { + lines.push((cur_chunk.clone(), Some(cur_highlight.clone()))); + cur_chunk.clear(); + cur_highlight = new_highlight; + } + cur_chunk.push(*c) + } + lines.push((cur_chunk, Some(cur_highlight))); let shaped_lines = layout_highlighted_chunks( - chunks, + lines.iter().map(|(text, style)| (text.as_str(), *style)), &text_style, cx.text_layout_cache, &cx.font_cache, usize::MAX, - line.matches('\n').count() + 1, + last_line as usize, ); ( @@ -450,61 +620,58 @@ impl Element for TerminalEl { } } -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; - Flex::row() - .with_child( - Label::new(self.title.clone(), style.label.clone()) - .aligned() - .contained() - .with_margin_left(search_theme.tab_icon_spacing) - .boxed(), - ) - .boxed() - } - - fn project_path(&self, _cx: &gpui::AppContext) -> Option { +fn make_style_from_cell(fg: &AnsiColor, flags: &Flags) -> HighlightStyle { + let fg = Some(alac_color_to_gpui_color(fg)); + let underline = if flags.contains(Flags::UNDERLINE) { + Some(Underline { + color: fg, + squiggly: false, + thickness: OrderedFloat(1.), + }) + } else { None - } - - fn project_entry_ids(&self, _cx: &gpui::AppContext) -> SmallVec<[project::ProjectEntryId; 3]> { - SmallVec::new() - } - - fn is_singleton(&self, _cx: &gpui::AppContext) -> bool { - false - } - - fn set_nav_history(&mut self, _: workspace::ItemNavHistory, _: &mut ViewContext) {} - - fn can_save(&self, _cx: &gpui::AppContext) -> bool { - false - } - - fn save( - &mut self, - _project: gpui::ModelHandle, - _cx: &mut ViewContext, - ) -> gpui::Task> { - unreachable!("save should not have been called"); - } - - fn save_as( - &mut self, - _project: gpui::ModelHandle, - _abs_path: std::path::PathBuf, - _cx: &mut ViewContext, - ) -> gpui::Task> { - unreachable!("save_as should not have been called"); - } - - fn reload( - &mut self, - _project: gpui::ModelHandle, - _cx: &mut ViewContext, - ) -> gpui::Task> { - gpui::Task::ready(Ok(())) + }; + HighlightStyle { + color: fg, + underline, + ..Default::default() + } +} + +fn alac_color_to_gpui_color(allac_color: &AnsiColor) -> Color { + match allac_color { + alacritty_terminal::ansi::Color::Named(n) => match n { + alacritty_terminal::ansi::NamedColor::Black => Color::black(), + alacritty_terminal::ansi::NamedColor::Red => Color::red(), + alacritty_terminal::ansi::NamedColor::Green => Color::green(), + alacritty_terminal::ansi::NamedColor::Yellow => Color::yellow(), + alacritty_terminal::ansi::NamedColor::Blue => Color::blue(), + alacritty_terminal::ansi::NamedColor::Magenta => Color::new(188, 63, 188, 1), + alacritty_terminal::ansi::NamedColor::Cyan => Color::new(17, 168, 205, 1), + alacritty_terminal::ansi::NamedColor::White => Color::white(), + alacritty_terminal::ansi::NamedColor::BrightBlack => Color::new(102, 102, 102, 1), + alacritty_terminal::ansi::NamedColor::BrightRed => Color::new(102, 102, 102, 1), + alacritty_terminal::ansi::NamedColor::BrightGreen => Color::new(35, 209, 139, 1), + alacritty_terminal::ansi::NamedColor::BrightYellow => Color::new(245, 245, 67, 1), + alacritty_terminal::ansi::NamedColor::BrightBlue => Color::new(59, 142, 234, 1), + alacritty_terminal::ansi::NamedColor::BrightMagenta => Color::new(214, 112, 214, 1), + alacritty_terminal::ansi::NamedColor::BrightCyan => Color::new(41, 184, 219, 1), + alacritty_terminal::ansi::NamedColor::BrightWhite => Color::new(229, 229, 229, 1), + alacritty_terminal::ansi::NamedColor::Foreground => Color::white(), + alacritty_terminal::ansi::NamedColor::Background => Color::black(), + alacritty_terminal::ansi::NamedColor::Cursor => Color::white(), + alacritty_terminal::ansi::NamedColor::DimBlack => Color::white(), + alacritty_terminal::ansi::NamedColor::DimRed => Color::white(), + alacritty_terminal::ansi::NamedColor::DimGreen => Color::white(), + alacritty_terminal::ansi::NamedColor::DimYellow => Color::white(), + alacritty_terminal::ansi::NamedColor::DimBlue => Color::white(), + alacritty_terminal::ansi::NamedColor::DimMagenta => Color::white(), + alacritty_terminal::ansi::NamedColor::DimCyan => Color::white(), + alacritty_terminal::ansi::NamedColor::DimWhite => Color::white(), + alacritty_terminal::ansi::NamedColor::BrightForeground => Color::white(), + alacritty_terminal::ansi::NamedColor::DimForeground => Color::white(), + }, //Theme defined + alacritty_terminal::ansi::Color::Spec(rgb) => Color::new(rgb.r, rgb.g, rgb.b, 1), + alacritty_terminal::ansi::Color::Indexed(_) => Color::white(), //Color cube weirdness } }