diff --git a/core/src/command.rs b/core/src/command.rs index 5d6a829e..be43265b 100644 --- a/core/src/command.rs +++ b/core/src/command.rs @@ -21,7 +21,7 @@ data::EditorKind, editor::{EditorLocation, EditorLocationNew, HighlightTextLayout}, palette::{NewPaletteItem, PaletteType}, - proxy::TerminalContent, + proxy::{CursorShape, TerminalContent}, split::SplitMoveDirection, state::LapceWorkspace, }; @@ -376,7 +376,12 @@ pub enum LapceUICommand { UpdateLineChanges(BufferId), PublishDiagnostics(PublishDiagnosticsParams), UpdateDiffFiles(Vec), - TerminalUpdateContent(TermId, TerminalContent), + TerminalUpdateContent( + TermId, + TerminalContent, + alacritty_terminal::index::Point, + CursorShape, + ), ReloadBuffer(BufferId, u64, String), EnsureVisible((Rect, (f64, f64), Option)), EnsureRectVisible(Rect), diff --git a/core/src/config.rs b/core/src/config.rs index 481dfb31..9c1000a6 100644 --- a/core/src/config.rs +++ b/core/src/config.rs @@ -4,7 +4,7 @@ use directories::ProjectDirs; use druid::{ piet::{PietText, Text, TextLayout, TextLayoutBuilder}, - theme, Color, Env, FontDescriptor, FontFamily, Key, + theme, Color, Env, FontDescriptor, FontFamily, Key, Size, }; use serde::{Deserialize, Deserializer, Serialize}; @@ -37,6 +37,10 @@ impl LapceTheme { pub const EDITOR_SELECTION: &'static str = "editor.selection"; pub const EDITOR_CURRENT_LINE: &'static str = "editor.current_line"; + pub const TERMINAL_CURSOR: &'static str = "terminal.cursor"; + pub const TERMINAL_BACKGROUND: &'static str = "terminal.background"; + pub const TERMINAL_FOREGROUND: &'static str = "terminal.foreground"; + pub const PALETTE_BACKGROUND: &'static str = "palette.background"; pub const PALETTE_CURRENT: &'static str = "palette.current"; @@ -183,6 +187,15 @@ pub fn editor_text_width(&self, text: &mut PietText, c: &str) -> f64 { text_layout.size().width } + pub fn editor_text_size(&self, text: &mut PietText, c: &str) -> Size { + let text_layout = text + .new_text_layout(c.to_string()) + .font(self.editor.font_family(), self.editor.font_size as f64) + .build() + .unwrap(); + text_layout.size() + } + pub fn reload_env(&self, env: &mut Env) { env.set(theme::SCROLLBAR_RADIUS, 0.0); env.set(theme::SCROLLBAR_EDGE_WIDTH, 0.0); diff --git a/core/src/keypress.rs b/core/src/keypress.rs index 9dc14a7a..1f4609ad 100644 --- a/core/src/keypress.rs +++ b/core/src/keypress.rs @@ -53,6 +53,9 @@ pub struct KeyMap { pub trait KeyPressFocus { fn get_mode(&self) -> Mode; fn check_condition(&self, condition: &str) -> bool; + fn is_terminal(&self) -> bool { + false + } fn run_command( &mut self, ctx: &mut EventCtx, @@ -236,7 +239,8 @@ fn match_keymap( .iter() .filter(|keymap| { if keymap.modes.len() > 0 - && !keymap.modes.contains(&check.get_mode()) + && (!keymap.modes.contains(&check.get_mode()) + || check.is_terminal()) { return false; } diff --git a/core/src/proxy.rs b/core/src/proxy.rs index 942f65ee..d4fc2bfd 100644 --- a/core/src/proxy.rs +++ b/core/src/proxy.rs @@ -354,6 +354,24 @@ pub fn stop(&self) { } } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum CursorShape { + /// Cursor is a block like `▒`. + Block, + + /// Cursor is an underscore like `_`. + Underline, + + /// Cursor is a vertical bar `⎸`. + Beam, + + /// Cursor is a box like `☐`. + HollowBlock, + + /// Invisible cursor. + Hidden, +} + #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] #[serde(tag = "method", content = "params")] @@ -383,6 +401,8 @@ pub enum Notification { TerminalUpdateContent { id: TermId, content: TerminalContent, + cursor_shape: CursorShape, + cursor_point: alacritty_terminal::index::Point, }, DiffFiles { files: Vec, @@ -426,7 +446,7 @@ fn handle_notification( Notification::ListDir { mut items } => {} Notification::DiffFiles { files } => {} Notification::PublishDiagnostics { diagnostics } => {} - Notification::TerminalUpdateContent { id, content } => {} + Notification::TerminalUpdateContent { id, content, .. } => {} } } @@ -561,10 +581,20 @@ fn handle_notification( Target::Widget(self.tab_id), ); } - Notification::TerminalUpdateContent { id, content } => { + Notification::TerminalUpdateContent { + id, + content, + cursor_shape, + cursor_point, + } => { self.event_sink.submit_command( LAPCE_UI_COMMAND, - LapceUICommand::TerminalUpdateContent(id, content), + LapceUICommand::TerminalUpdateContent( + id, + content, + cursor_point, + cursor_shape, + ), Target::Widget(self.tab_id), ); } diff --git a/core/src/tab.rs b/core/src/tab.rs index db64e550..353f507a 100644 --- a/core/src/tab.rs +++ b/core/src/tab.rs @@ -226,12 +226,20 @@ fn event( Arc::make_mut(buffer).load_content(content); ctx.set_handled(); } - LapceUICommand::TerminalUpdateContent(id, content) => { + LapceUICommand::TerminalUpdateContent( + id, + content, + cursor_point, + cursor_shape, + ) => { let terminal = Arc::make_mut(&mut data.terminal) .terminals .get_mut(id) .unwrap(); - Arc::make_mut(terminal).content = content.to_owned(); + let terminal = Arc::make_mut(terminal); + terminal.content = content.to_owned(); + terminal.cursor_point = cursor_point.to_owned(); + terminal.cursor_shape = cursor_shape.to_owned(); ctx.set_handled(); } LapceUICommand::UpdateDiffFiles(files) => { diff --git a/core/src/terminal.rs b/core/src/terminal.rs index 7eb1fcd5..40cd5611 100644 --- a/core/src/terminal.rs +++ b/core/src/terminal.rs @@ -1,22 +1,30 @@ use std::sync::Arc; +use alacritty_terminal::ansi; use druid::{ piet::{Text, TextLayoutBuilder}, - BoxConstraints, Data, Env, Event, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, - PaintCtx, Point, RenderContext, Size, UpdateCtx, Widget, WidgetExt, WidgetId, - WidgetPod, + BoxConstraints, Color, Data, Env, Event, EventCtx, KbKey, LayoutCtx, LifeCycle, + LifeCycleCtx, Modifiers, PaintCtx, Point, RenderContext, Size, UpdateCtx, + Widget, WidgetExt, WidgetId, WidgetPod, }; use lapce_proxy::terminal::TermId; use crate::{ + command::LapceCommand, config::LapceTheme, data::LapceTabData, keypress::KeyPressFocus, - proxy::{LapceProxy, TerminalContent}, + proxy::{CursorShape, LapceProxy, TerminalContent}, + scroll::LapcePadding, split::LapceSplitNew, state::Mode, }; +const CTRL_CHARS: &'static [char] = &[ + '@', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', + 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '[', '\\', ']', '^', '_', +]; + #[derive(Clone)] pub struct TerminalSplitData { pub widget_id: WidgetId, @@ -46,18 +54,25 @@ pub struct LapceTerminalViewData { } impl KeyPressFocus for LapceTerminalViewData { + fn is_terminal(&self) -> bool { + true + } + fn get_mode(&self) -> Mode { Mode::Insert } fn check_condition(&self, condition: &str) -> bool { - false + match condition { + "terminal_focus" => true, + _ => false, + } } fn run_command( &mut self, ctx: &mut EventCtx, - command: &crate::command::LapceCommand, + command: &LapceCommand, count: Option, env: &Env, ) { @@ -72,6 +87,8 @@ fn insert(&mut self, ctx: &mut EventCtx, c: &str) { pub struct LapceTerminalData { id: TermId, pub content: TerminalContent, + pub cursor_point: alacritty_terminal::index::Point, + pub cursor_shape: CursorShape, } impl LapceTerminalData { @@ -83,6 +100,8 @@ pub fn new(proxy: Arc) -> Self { Self { id, content: TerminalContent::new(), + cursor_point: alacritty_terminal::index::Point::default(), + cursor_shape: CursorShape::Block, } } } @@ -95,7 +114,7 @@ pub struct TerminalPanel { impl TerminalPanel { pub fn new(data: &LapceTabData) -> Self { let (term_id, _) = data.terminal.terminals.iter().next().unwrap(); - let terminal = LapceTerminal::new(*term_id); + let terminal = LapcePadding::new(10.0, LapceTerminal::new(*term_id)); let split = LapceSplitNew::new(data.terminal.split_id).with_flex_child( terminal.boxed(), None, @@ -211,12 +230,42 @@ fn event( } Event::KeyDown(key_event) => { let mut keypress = data.keypress.clone(); - Arc::make_mut(&mut keypress).key_down( + if !Arc::make_mut(&mut keypress).key_down( ctx, key_event, &mut term_data, env, - ); + ) { + let s = match &key_event.key { + KbKey::Character(c) => { + let mut s = "".to_string(); + let mut mods = key_event.mods.clone(); + if mods.ctrl() { + mods.set(Modifiers::CONTROL, false); + if mods.is_empty() && c.chars().count() == 1 { + let c = c.chars().next().unwrap(); + if let Some(i) = + CTRL_CHARS.iter().position(|e| &c == e) + { + s = char::from_u32(i as u32) + .unwrap() + .to_string() + } + } + } + + s + } + KbKey::Backspace => "\x08".to_string(), + KbKey::Tab => "\x09".to_string(), + KbKey::Enter => "\x0a".to_string(), + KbKey::Escape => "\x1b".to_string(), + _ => "".to_string(), + }; + if s != "" { + data.proxy.terminal_insert(self.term_id, &s); + } + } data.keypress = keypress.clone(); } _ => (), @@ -235,6 +284,12 @@ fn lifecycle( data: &LapceTabData, env: &Env, ) { + match event { + LifeCycle::FocusChanged(_) => { + ctx.request_paint(); + } + _ => (), + } } fn update( @@ -259,21 +314,64 @@ fn layout( self.height = size.height; let width = data.config.editor_text_width(ctx.text(), "W"); let line_height = data.config.editor.line_height as f64; - let width = (self.width / width).ceil() as usize; - let height = (self.height / line_height).ceil() as usize; + let width = (self.width / width).floor() as usize; + let height = (self.height / line_height).floor() as usize; data.proxy.terminal_resize(self.term_id, width, height); } size } fn paint(&mut self, ctx: &mut PaintCtx, data: &LapceTabData, env: &Env) { - let width = data.config.editor_text_width(ctx.text(), "W"); + let char_size = data.config.editor_text_size(ctx.text(), "W"); + let char_width = char_size.width; let line_height = data.config.editor.line_height as f64; + let y_shift = (line_height - char_size.height) / 2.0; let terminal = data.terminal.terminals.get(&self.term_id).unwrap(); + + let rect = + Size::new(char_width, line_height) + .to_rect() + .with_origin(Point::new( + terminal.cursor_point.column.0 as f64 * char_width, + terminal.cursor_point.line.0 as f64 * line_height, + )); + if ctx.is_focused() { + ctx.fill( + rect, + data.config.get_color_unchecked(LapceTheme::TERMINAL_CURSOR), + ); + } else { + ctx.stroke( + rect, + data.config.get_color_unchecked(LapceTheme::TERMINAL_CURSOR), + 1.0, + ); + } + for (p, cell) in terminal.content.iter() { - let x = p.column.0 as f64 * width; - let y = p.line.0 as f64 * line_height; + let x = p.column.0 as f64 * char_width; + let y = p.line.0 as f64 * line_height + y_shift; + let fg = match cell.fg { + ansi::Color::Named(color) => { + let color = match color { + ansi::NamedColor::Cursor => LapceTheme::TERMINAL_CURSOR, + ansi::NamedColor::Foreground => { + LapceTheme::TERMINAL_FOREGROUND + } + ansi::NamedColor::Background => { + LapceTheme::TERMINAL_BACKGROUND + } + _ => LapceTheme::TERMINAL_FOREGROUND, + }; + data.config.get_color_unchecked(color).clone() + } + ansi::Color::Spec(rgb) => Color::rgb8(rgb.r, rgb.g, rgb.b), + ansi::Color::Indexed(index) => data + .config + .get_color_unchecked(LapceTheme::TERMINAL_FOREGROUND) + .clone(), + }; let text_layout = ctx .text() .new_text_layout(cell.c.to_string()) @@ -281,6 +379,7 @@ fn paint(&mut self, ctx: &mut PaintCtx, data: &LapceTabData, env: &Env) { data.config.editor.font_family(), data.config.editor.font_size as f64, ) + .text_color(fg) .build() .unwrap(); ctx.draw_text(&text_layout, Point::new(x, y)); diff --git a/defaults/dark-theme.toml b/defaults/dark-theme.toml index 1bca7b8f..fccc6ba9 100644 --- a/defaults/dark-theme.toml +++ b/defaults/dark-theme.toml @@ -17,6 +17,10 @@ cyan = "#56b6c2" "lapce.dropdown_shadow" = "#000000" "lapce.border" = "#000000" +"terminal.cursor" = "$white" +"terminal.foreground" = "$white" +"terminal.background" = "$black" + "editor.background" = "$black" "editor.foreground" = "$white" "editor.dim" = "#A0A1A7" diff --git a/defaults/keymaps-macos.toml b/defaults/keymaps-macos.toml index 95c1755c..e8628919 100644 --- a/defaults/keymaps-macos.toml +++ b/defaults/keymaps-macos.toml @@ -487,6 +487,21 @@ key = "meta+;" command = "split_vertical" mode = "n" +[[keymaps]] +key = "meta+;" +command = "split_vertical" +when = "terminal_focus" + +[[keymaps]] +key = "meta+l" +command = "split_right" +when = "terminal_focus" + +[[keymaps]] +key = "meta+h" +command = "split_left" +when = "terminal_focus" + [[keymaps]] key = "meta+g" command = "split_horizontal" diff --git a/defaults/light-theme.toml b/defaults/light-theme.toml index 49894491..1fa9578c 100644 --- a/defaults/light-theme.toml +++ b/defaults/light-theme.toml @@ -16,6 +16,10 @@ cyan = "#0184bc" "lapce.dropdown_shadow" = "#b4b4b4" "lapce.border" = "#b4b4b4" +"terminal.cursor" = "$black" +"terminal.foreground" = "$black" +"terminal.background" = "$white" + "editor.background" = "$white" "editor.foreground" = "$black" "editor.dim" = "#A0A1A7" diff --git a/proxy/src/dispatch.rs b/proxy/src/dispatch.rs index 36fe2d45..46c96104 100644 --- a/proxy/src/dispatch.rs +++ b/proxy/src/dispatch.rs @@ -2,7 +2,8 @@ use crate::core_proxy::CoreProxy; use crate::lsp::LspCatalog; use crate::plugin::PluginCatalog; -use crate::terminal::{TermId, Terminal, TerminalEvent}; +use crate::terminal::{TermId, Terminal, TerminalHostEvent}; +use alacritty_terminal::ansi::CursorShape; use anyhow::{anyhow, Result}; use crossbeam_channel::{unbounded, Receiver, Sender}; use git2::{DiffOptions, Oid, Repository}; @@ -444,11 +445,20 @@ fn handle_notification(&self, rpc: Notification) { loop { let event = receiver.recv()?; match event { - TerminalEvent::UpdateContent(content) => { + TerminalHostEvent::UpdateContent { cursor, content } => { + let shape = match cursor.shape { + CursorShape::Block => "Block", + CursorShape::Underline => "Underline", + CursorShape::Beam => "Beam", + CursorShape::HollowBlock => "HollowBlock", + CursorShape::Hidden => "Hidden", + }; local_proxy.send_notification( "terminal_update_content", json!({ "id": term_id, + "cursor_shape": shape, + "cursor_point": cursor.point, "content": content, }), ); diff --git a/proxy/src/terminal.rs b/proxy/src/terminal.rs index 2b2e542e..842ac04b 100644 --- a/proxy/src/terminal.rs +++ b/proxy/src/terminal.rs @@ -8,13 +8,14 @@ }; use alacritty_terminal::{ + ansi, config::Config, - event::{Event, EventListener, Notify}, - event_loop::{EventLoop, Notifier}, + event::{Event, EventListener, Notify, OnResize}, + event_loop::{EventLoop, Msg, Notifier}, grid::GridCell, index::Point, sync::FairMutex, - term::{cell::Cell, SizeInfo}, + term::{cell::Cell, RenderableCursor, SizeInfo}, tty, Term, }; use anyhow::Result; @@ -44,34 +45,42 @@ pub fn next() -> Self { } } +pub enum TerminalHostEvent { + UpdateContent { + cursor: RenderableCursor, + content: Vec<(Point, Cell)>, + }, +} + pub enum TerminalEvent { - UpdateContent(Vec<(Point, Cell)>), + resize(SizeInfo), + event(Event), } #[derive(Clone)] pub struct Terminal { pub term: Arc>>, - sender: Sender, - host_sender: Sender, + sender: Sender, + host_sender: Sender, } pub type TermConfig = Config>; #[derive(Clone)] pub struct EventProxy { - sender: Sender, + sender: Sender, } impl EventProxy {} impl EventListener for EventProxy { fn send_event(&self, event: alacritty_terminal::event::Event) { - self.sender.send(event); + self.sender.send(TerminalEvent::event(event)); } } impl Terminal { - pub fn new(width: usize, height: usize) -> (Self, Receiver) { + pub fn new(width: usize, height: usize) -> (Self, Receiver) { let config = TermConfig::default(); let (sender, receiver) = crossbeam_channel::unbounded(); let event_proxy = EventProxy { @@ -99,59 +108,69 @@ pub fn new(width: usize, height: usize) -> (Self, Receiver) { } pub fn resize(&self, width: usize, height: usize) { - self.term.lock().resize(SizeInfo::new( - width as f32, - height as f32, - 1.0, - 1.0, - 0.0, - 0.0, - true, - )); + let size = + SizeInfo::new(width as f32, height as f32, 1.0, 1.0, 0.0, 0.0, true); + self.sender.send(TerminalEvent::resize(size)); + self.term.lock().resize(size.clone()); } - fn run(&self, receiver: Receiver, notifier: Notifier) { + fn run(&self, receiver: Receiver, mut notifier: Notifier) { let term = self.term.clone(); let host_sender = self.host_sender.clone(); std::thread::spawn(move || -> Result<()> { loop { let event = receiver.recv()?; match event { - Event::MouseCursorDirty => {} - Event::Title(_) => {} - Event::ResetTitle => {} - Event::ClipboardStore(_, _) => {} - Event::ClipboardLoad(_, _) => {} - Event::ColorRequest(_, _) => {} - Event::PtyWrite(s) => { - eprintln!("pty write {}", s); - notifier.notify(s.into_bytes()); + TerminalEvent::resize(size) => { + notifier.on_resize(&size); } - Event::CursorBlinkingChange(_) => {} - Event::Wakeup => { - let content = term - .lock() - .renderable_content() - .display_iter - .filter_map(|c| { - if c.is_empty() { - None - } else { - Some((c.point, c.cell.clone())) - } - }) - .collect::>(); - let event = TerminalEvent::UpdateContent(content); - host_sender.send(event); - } - Event::Bell => {} - Event::Exit => {} + TerminalEvent::event(event) => match event { + Event::MouseCursorDirty => {} + Event::Title(_) => {} + Event::ResetTitle => {} + Event::ClipboardStore(_, _) => {} + Event::ClipboardLoad(_, _) => {} + Event::ColorRequest(_, _) => {} + Event::PtyWrite(s) => { + notifier.notify(s.into_bytes()); + } + Event::CursorBlinkingChange(_) => {} + Event::Wakeup => { + let cursor = + term.lock().renderable_content().cursor.clone(); + let content = term + .lock() + .renderable_content() + .display_iter + .filter_map(|c| { + if (c.c == ' ' || c.c == '\t') + && c.bg + == ansi::Color::Named( + ansi::NamedColor::Background, + ) + { + None + } else { + Some((c.point, c.cell.clone())) + } + }) + .collect::>(); + let event = TerminalHostEvent::UpdateContent { + content: content, + cursor: cursor, + }; + host_sender.send(event); + } + Event::Bell => {} + Event::Exit => {} + }, } } }); } pub fn insert>(&self, data: B) { - self.sender.send(Event::PtyWrite(data.into())); + self.sender + .send(TerminalEvent::event(Event::PtyWrite(data.into()))); } }