diff --git a/CHANGELOG.md b/CHANGELOG.md index a870d9c3..7e9e48ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Wide characters sometimes being cut off - Preserve vi mode across terminal `reset` +### Added + +- New `cursor.style.blinking` option to set the default blinking state +- New `cursor.blink_interval` option to configure the blinking frequency +- Support for cursor blinking escapes (`CSI ? 12 h`, `CSI ? 12 l` and `CSI Ps SP q`) + ## 0.6.0 ### Packaging diff --git a/alacritty.yml b/alacritty.yml index 3c6d18ec..2f2e5dc9 100644 --- a/alacritty.yml +++ b/alacritty.yml @@ -341,12 +341,23 @@ #cursor: # Cursor style - # - # Values for `style`: - # - ▇ Block - # - _ Underline - # - | Beam - #style: Block + #style: + # Cursor shape + # + # Values for `shape`: + # - ▇ Block + # - _ Underline + # - | Beam + #shape: Block + + # Cursor blinking state + # + # Values for `blinking`: + # - Never: Prevent the cursor from ever blinking + # - Off: Disable blinking by default + # - On: Enable blinking by default + # - Always: Force the cursor to always blink + #blinking: Off # Vi mode cursor style # @@ -356,6 +367,9 @@ # See `cursor.style` for available options. #vi_mode_style: None + # Cursor blinking interval in milliseconds. + #blink_interval: 750 + # If this is `true`, the cursor will be rendered as a hollow box when the # window is not focused. #unfocused_hollow: true diff --git a/alacritty/src/cursor.rs b/alacritty/src/cursor.rs index 8c782185..edf76bf3 100644 --- a/alacritty/src/cursor.rs +++ b/alacritty/src/cursor.rs @@ -2,10 +2,10 @@ use crossfont::{BitmapBuffer, Metrics, RasterizedGlyph}; -use alacritty_terminal::ansi::CursorStyle; +use alacritty_terminal::ansi::CursorShape; pub fn get_cursor_glyph( - cursor: CursorStyle, + cursor: CursorShape, metrics: Metrics, offset_x: i8, offset_y: i8, @@ -26,11 +26,11 @@ pub fn get_cursor_glyph( } match cursor { - CursorStyle::HollowBlock => get_box_cursor_glyph(height, width, line_width), - CursorStyle::Underline => get_underline_cursor_glyph(width, line_width), - CursorStyle::Beam => get_beam_cursor_glyph(height, line_width), - CursorStyle::Block => get_block_cursor_glyph(height, width), - CursorStyle::Hidden => RasterizedGlyph::default(), + CursorShape::HollowBlock => get_box_cursor_glyph(height, width, line_width), + CursorShape::Underline => get_underline_cursor_glyph(width, line_width), + CursorShape::Beam => get_beam_cursor_glyph(height, line_width), + CursorShape::Block => get_block_cursor_glyph(height, width), + CursorShape::Hidden => RasterizedGlyph::default(), } } diff --git a/alacritty/src/display.rs b/alacritty/src/display.rs index af21001e..451874c8 100644 --- a/alacritty/src/display.rs +++ b/alacritty/src/display.rs @@ -160,6 +160,9 @@ pub struct Display { #[cfg(not(any(target_os = "macos", windows)))] pub is_x11: bool, + /// UI cursor visibility for blinking. + pub cursor_hidden: bool, + renderer: QuadRenderer, glyph_cache: GlyphCache, meter: Meter, @@ -300,6 +303,7 @@ impl Display { is_x11, #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] wayland_event_queue, + cursor_hidden: false, }) } @@ -442,8 +446,9 @@ impl Display { let viewport_match = search_state .focused_match() .and_then(|focused_match| terminal.grid().clamp_buffer_range_to_visible(focused_match)); + let cursor_hidden = self.cursor_hidden || search_state.regex().is_some(); - let grid_cells = terminal.renderable_cells(config, !search_active).collect::>(); + let grid_cells = terminal.renderable_cells(config, !cursor_hidden).collect::>(); let visual_bell_intensity = terminal.visual_bell.intensity(); let background_color = terminal.background_color(); let cursor_point = terminal.grid().cursor.point; diff --git a/alacritty/src/event.rs b/alacritty/src/event.rs index c1f81300..8ecf6f00 100644 --- a/alacritty/src/event.rs +++ b/alacritty/src/event.rs @@ -67,6 +67,7 @@ pub enum Event { Scroll(Scroll), ConfigReload(PathBuf), Message(Message), + BlinkCursor, SearchNext, } @@ -150,6 +151,7 @@ pub struct ActionContext<'a, N, T> { pub urls: &'a Urls, pub scheduler: &'a mut Scheduler, pub search_state: &'a mut SearchState, + cursor_hidden: &'a mut bool, cli_options: &'a CLIOptions, font_size: &'a mut Size, } @@ -495,6 +497,28 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext for ActionCon } } + /// Handle keyboard typing start. + /// + /// This will temporarily disable some features like terminal cursor blinking or the mouse + /// cursor. + /// + /// All features are re-enabled again automatically. + #[inline] + fn on_typing_start(&mut self) { + // Disable cursor blinking. + let blink_interval = self.config.cursor.blink_interval(); + if let Some(timer) = self.scheduler.get_mut(TimerId::BlinkCursor) { + timer.deadline = Instant::now() + Duration::from_millis(blink_interval); + *self.cursor_hidden = false; + self.terminal.dirty = true; + } + + // Hide mouse cursor. + if self.config.ui_config.mouse.hide_when_typing { + self.window.set_mouse_visible(false); + } + } + #[inline] fn search_direction(&self) -> Direction { self.search_state.direction @@ -667,6 +691,33 @@ impl<'a, N: Notify + 'a, T: EventListener> ActionContext<'a, N, T> { origin.line = (origin.line as isize + self.search_state.display_offset_delta) as usize; origin } + + /// Update the cursor blinking state. + fn update_cursor_blinking(&mut self) { + // Get config cursor style. + let mut cursor_style = self.config.cursor.style; + if self.terminal.mode().contains(TermMode::VI) { + cursor_style = self.config.cursor.vi_mode_style.unwrap_or(cursor_style); + }; + + // Check terminal cursor style. + let terminal_blinking = self.terminal.cursor_style().blinking; + let blinking = cursor_style.blinking_override().unwrap_or(terminal_blinking); + + // Update cursor blinking state. + self.scheduler.unschedule(TimerId::BlinkCursor); + if blinking && self.terminal.is_focused { + self.scheduler.schedule( + GlutinEvent::UserEvent(Event::BlinkCursor), + Duration::from_millis(self.config.cursor.blink_interval()), + true, + TimerId::BlinkCursor, + ) + } else { + *self.cursor_hidden = false; + self.terminal.dirty = true; + } + } } #[derive(Debug, Eq, PartialEq)] @@ -804,6 +855,12 @@ impl Processor { { let mut scheduler = Scheduler::new(); + // Start the initial cursor blinking timer. + if self.config.cursor.style().blinking { + let event: Event = TerminalEvent::CursorBlinkingChange(true).into(); + self.event_queue.push(event.into()); + } + event_loop.run_return(|event, event_loop, control_flow| { if self.config.ui_config.debug.print_events { info!("glutin event: {:?}", event); @@ -873,6 +930,7 @@ impl Processor { scheduler: &mut scheduler, search_state: &mut self.search_state, cli_options: &self.cli_options, + cursor_hidden: &mut self.display.cursor_hidden, event_loop, }; let mut processor = input::Processor::new(context, &self.display.highlighted_url); @@ -953,6 +1011,10 @@ impl Processor { Event::SearchNext => processor.ctx.goto_match(None), Event::ConfigReload(path) => Self::reload_config(&path, processor), Event::Scroll(scroll) => processor.ctx.scroll(scroll), + Event::BlinkCursor => { + *processor.ctx.cursor_hidden ^= true; + processor.ctx.terminal.dirty = true; + }, Event::TerminalEvent(event) => match event { TerminalEvent::Title(title) => { let ui_config = &processor.ctx.config.ui_config; @@ -983,6 +1045,9 @@ impl Processor { }, TerminalEvent::MouseCursorDirty => processor.reset_mouse_cursor(), TerminalEvent::Exit => (), + TerminalEvent::CursorBlinkingChange(_) => { + processor.ctx.update_cursor_blinking(); + }, }, }, GlutinEvent::RedrawRequested(_) => processor.ctx.terminal.dirty = true, @@ -1033,6 +1098,7 @@ impl Processor { processor.ctx.window.set_mouse_visible(true); } + processor.ctx.update_cursor_blinking(); processor.on_focus_change(is_focused); } }, @@ -1111,7 +1177,7 @@ impl Processor { processor.ctx.terminal.update_config(&config); - // Reload cursor if we've changed its thickness. + // Reload cursor if its thickness has changed. if (processor.ctx.config.cursor.thickness() - config.cursor.thickness()).abs() > std::f64::EPSILON { @@ -1154,6 +1220,9 @@ impl Processor { *processor.ctx.config = config; + // Update cursor blinking. + processor.ctx.update_cursor_blinking(); + processor.ctx.terminal.dirty = true; } diff --git a/alacritty/src/input.rs b/alacritty/src/input.rs index 348db610..ce89625b 100644 --- a/alacritty/src/input.rs +++ b/alacritty/src/input.rs @@ -103,6 +103,7 @@ pub trait ActionContext { fn advance_search_origin(&mut self, direction: Direction); fn search_direction(&self) -> Direction; fn search_active(&self) -> bool; + fn on_typing_start(&mut self); } trait Execute { @@ -138,9 +139,7 @@ impl Execute for Action { fn execute>(&self, ctx: &mut A) { match *self { Action::Esc(ref s) => { - if ctx.config().ui_config.mouse.hide_when_typing { - ctx.window_mut().set_mouse_visible(false); - } + ctx.on_typing_start(); ctx.clear_selection(); ctx.scroll(Scroll::Bottom); @@ -167,10 +166,7 @@ impl Execute for Action { Action::ClearSelection => ctx.clear_selection(), Action::ToggleViMode => ctx.terminal_mut().toggle_vi_mode(), Action::ViMotion(motion) => { - if ctx.config().ui_config.mouse.hide_when_typing { - ctx.window_mut().set_mouse_visible(false); - } - + ctx.on_typing_start(); ctx.terminal_mut().vi_motion(motion) }, Action::ViAction(ViAction::ToggleNormalSelection) => { @@ -870,6 +866,13 @@ impl<'a, T: EventListener, A: ActionContext> Processor<'a, T, A> { self.ctx.window_mut().set_mouse_cursor(mouse_state.into()); } + /// Reset mouse cursor based on modifier and terminal state. + #[inline] + pub fn reset_mouse_cursor(&mut self) { + let mouse_state = self.mouse_state(); + self.ctx.window_mut().set_mouse_cursor(mouse_state.into()); + } + /// Process a received character. pub fn received_char(&mut self, c: char) { let suppress_chars = *self.ctx.suppress_chars(); @@ -890,9 +893,7 @@ impl<'a, T: EventListener, A: ActionContext> Processor<'a, T, A> { return; } - if self.ctx.config().ui_config.mouse.hide_when_typing { - self.ctx.window_mut().set_mouse_visible(false); - } + self.ctx.on_typing_start(); self.ctx.scroll(Scroll::Bottom); self.ctx.clear_selection(); @@ -917,13 +918,6 @@ impl<'a, T: EventListener, A: ActionContext> Processor<'a, T, A> { *self.ctx.received_count() += 1; } - /// Reset mouse cursor based on modifier and terminal state. - #[inline] - pub fn reset_mouse_cursor(&mut self) { - let mouse_state = self.mouse_state(); - self.ctx.window_mut().set_mouse_cursor(mouse_state.into()); - } - /// Attempt to find a binding and execute its action. /// /// The provided mode, mods, and key must match what is allowed by a binding @@ -1270,6 +1264,10 @@ mod tests { fn scheduler_mut(&mut self) -> &mut Scheduler { unimplemented!(); } + + fn on_typing_start(&mut self) { + unimplemented!(); + } } macro_rules! test_clickstate { diff --git a/alacritty/src/renderer/mod.rs b/alacritty/src/renderer/mod.rs index f63f92fd..af1f3c3e 100644 --- a/alacritty/src/renderer/mod.rs +++ b/alacritty/src/renderer/mod.rs @@ -1010,7 +1010,7 @@ impl<'a> RenderApi<'a> { let metrics = glyph_cache.metrics; let glyph = glyph_cache.cursor_cache.entry(cursor_key).or_insert_with(|| { self.load_glyph(&cursor::get_cursor_glyph( - cursor_key.style, + cursor_key.shape, metrics, self.config.font.offset.x, self.config.font.offset.y, diff --git a/alacritty/src/scheduler.rs b/alacritty/src/scheduler.rs index db6180ca..5e454141 100644 --- a/alacritty/src/scheduler.rs +++ b/alacritty/src/scheduler.rs @@ -14,6 +14,7 @@ type Event = GlutinEvent<'static, AlacrittyEvent>; pub enum TimerId { SelectionScrolling, DelayedSearch, + BlinkCursor, } /// Event scheduled to be emitted at a specific time. diff --git a/alacritty_terminal/src/ansi.rs b/alacritty_terminal/src/ansi.rs index 877fd65f..7567eba2 100644 --- a/alacritty_terminal/src/ansi.rs +++ b/alacritty_terminal/src/ansi.rs @@ -141,6 +141,9 @@ pub trait Handler { /// Set the cursor style. fn set_cursor_style(&mut self, _: Option) {} + /// Set the cursor shape. + fn set_cursor_shape(&mut self, _shape: CursorShape) {} + /// A character to be displayed. fn input(&mut self, _c: char) {} @@ -324,9 +327,16 @@ pub trait Handler { fn text_area_size_chars(&mut self, _: &mut W) {} } -/// Describes shape of cursor. -#[derive(Debug, Eq, PartialEq, Copy, Clone, Hash, Deserialize)] -pub enum CursorStyle { +/// Terminal cursor configuration. +#[derive(Deserialize, Default, Debug, Eq, PartialEq, Copy, Clone, Hash)] +pub struct CursorStyle { + pub shape: CursorShape, + pub blinking: bool, +} + +/// Terminal cursor shape. +#[derive(Deserialize, Debug, Eq, PartialEq, Copy, Clone, Hash)] +pub enum CursorShape { /// Cursor is a block like `▒`. Block, @@ -345,9 +355,9 @@ pub enum CursorStyle { Hidden, } -impl Default for CursorStyle { - fn default() -> CursorStyle { - CursorStyle::Block +impl Default for CursorShape { + fn default() -> CursorShape { + CursorShape::Block } } @@ -874,13 +884,13 @@ where && params[1].len() >= 13 && params[1][0..12] == *b"CursorShape=" { - let style = match params[1][12] as char { - '0' => CursorStyle::Block, - '1' => CursorStyle::Beam, - '2' => CursorStyle::Underline, + let shape = match params[1][12] as char { + '0' => CursorShape::Block, + '1' => CursorShape::Beam, + '2' => CursorShape::Underline, _ => return unhandled(params), }; - self.handler.set_cursor_style(Some(style)); + self.handler.set_cursor_shape(shape); return; } unhandled(params); @@ -1065,18 +1075,21 @@ where ('P', None) => handler.delete_chars(Column(next_param_or(1) as usize)), ('q', Some(b' ')) => { // DECSCUSR (CSI Ps SP q) -- Set Cursor Style. - let style = match next_param_or(0) { + let cursor_style_id = next_param_or(0); + let shape = match cursor_style_id { 0 => None, - 1 | 2 => Some(CursorStyle::Block), - 3 | 4 => Some(CursorStyle::Underline), - 5 | 6 => Some(CursorStyle::Beam), + 1 | 2 => Some(CursorShape::Block), + 3 | 4 => Some(CursorShape::Underline), + 5 | 6 => Some(CursorShape::Beam), _ => { unhandled!(); return; }, }; + let cursor_style = + shape.map(|shape| CursorStyle { shape, blinking: cursor_style_id % 2 == 1 }); - handler.set_cursor_style(style); + handler.set_cursor_style(cursor_style); }, ('r', None) => { let top = next_param_or(1) as usize; diff --git a/alacritty_terminal/src/config/mod.rs b/alacritty_terminal/src/config/mod.rs index 98849d90..f3221920 100644 --- a/alacritty_terminal/src/config/mod.rs +++ b/alacritty_terminal/src/config/mod.rs @@ -1,3 +1,4 @@ +use std::cmp::max; use std::collections::HashMap; use std::fmt::Display; use std::path::PathBuf; @@ -10,15 +11,16 @@ mod bell; mod colors; mod scrolling; -use crate::ansi::CursorStyle; +use crate::ansi::{CursorShape, CursorStyle}; pub use crate::config::bell::{BellAnimation, BellConfig}; pub use crate::config::colors::Colors; pub use crate::config::scrolling::Scrolling; pub const LOG_TARGET_CONFIG: &str = "alacritty_config"; -const MAX_SCROLLBACK_LINES: u32 = 100_000; const DEFAULT_CURSOR_THICKNESS: f32 = 0.15; +const MAX_SCROLLBACK_LINES: u32 = 100_000; +const MIN_BLINK_INTERVAL: u64 = 10; pub type MockConfig = Config>; @@ -121,9 +123,11 @@ impl Default for EscapeChars { #[derive(Deserialize, Copy, Clone, Debug, PartialEq)] pub struct Cursor { #[serde(deserialize_with = "failure_default")] - pub style: CursorStyle, + pub style: ConfigCursorStyle, #[serde(deserialize_with = "option_explicit_none")] - pub vi_mode_style: Option, + pub vi_mode_style: Option, + #[serde(deserialize_with = "failure_default")] + blink_interval: BlinkInterval, #[serde(deserialize_with = "deserialize_cursor_thickness")] thickness: Percentage, #[serde(deserialize_with = "failure_default")] @@ -140,6 +144,21 @@ impl Cursor { pub fn thickness(self) -> f64 { self.thickness.0 as f64 } + + #[inline] + pub fn style(self) -> CursorStyle { + self.style.into() + } + + #[inline] + pub fn vi_mode_style(self) -> Option { + self.vi_mode_style.map(From::from) + } + + #[inline] + pub fn blink_interval(self) -> u64 { + max(self.blink_interval.0, MIN_BLINK_INTERVAL) + } } impl Default for Cursor { @@ -149,10 +168,20 @@ impl Default for Cursor { vi_mode_style: Default::default(), thickness: Percentage::new(DEFAULT_CURSOR_THICKNESS), unfocused_hollow: Default::default(), + blink_interval: Default::default(), } } } +#[derive(Deserialize, Copy, Clone, Debug, PartialEq)] +struct BlinkInterval(u64); + +impl Default for BlinkInterval { + fn default() -> Self { + BlinkInterval(750) + } +} + fn deserialize_cursor_thickness<'a, D>(deserializer: D) -> Result where D: Deserializer<'a>, @@ -173,6 +202,75 @@ where } } +#[serde(untagged)] +#[derive(Deserialize, Debug, Copy, Clone, PartialEq, Eq)] +pub enum ConfigCursorStyle { + Shape(CursorShape), + WithBlinking { + #[serde(default, deserialize_with = "failure_default")] + shape: CursorShape, + #[serde(default, deserialize_with = "failure_default")] + blinking: CursorBlinking, + }, +} + +impl Default for ConfigCursorStyle { + fn default() -> Self { + Self::WithBlinking { shape: CursorShape::default(), blinking: CursorBlinking::default() } + } +} + +impl ConfigCursorStyle { + /// Check if blinking is force enabled/disabled. + pub fn blinking_override(&self) -> Option { + match self { + Self::Shape(_) => None, + Self::WithBlinking { blinking, .. } => blinking.blinking_override(), + } + } +} + +impl From for CursorStyle { + fn from(config_style: ConfigCursorStyle) -> Self { + match config_style { + ConfigCursorStyle::Shape(shape) => Self { shape, blinking: false }, + ConfigCursorStyle::WithBlinking { shape, blinking } => { + Self { shape, blinking: blinking.into() } + }, + } + } +} + +#[derive(Deserialize, Debug, Copy, Clone, PartialEq, Eq)] +pub enum CursorBlinking { + Never, + Off, + On, + Always, +} + +impl Default for CursorBlinking { + fn default() -> Self { + CursorBlinking::Off + } +} + +impl CursorBlinking { + fn blinking_override(&self) -> Option { + match self { + Self::Never => Some(false), + Self::Off | Self::On => None, + Self::Always => Some(true), + } + } +} + +impl Into for CursorBlinking { + fn into(self) -> bool { + self == Self::On || self == Self::Always + } +} + #[serde(untagged)] #[derive(Deserialize, Debug, Clone, PartialEq, Eq)] pub enum Program { diff --git a/alacritty_terminal/src/event.rs b/alacritty_terminal/src/event.rs index c7129b24..351b7bc2 100644 --- a/alacritty_terminal/src/event.rs +++ b/alacritty_terminal/src/event.rs @@ -11,6 +11,7 @@ pub enum Event { ResetTitle, ClipboardStore(ClipboardType, String), ClipboardLoad(ClipboardType, Arc String + Sync + Send + 'static>), + CursorBlinkingChange(bool), Wakeup, Bell, Exit, @@ -27,6 +28,7 @@ impl Debug for Event { Event::Wakeup => write!(f, "Wakeup"), Event::Bell => write!(f, "Bell"), Event::Exit => write!(f, "Exit"), + Event::CursorBlinkingChange(blinking) => write!(f, "CursorBlinking({})", blinking), } } } diff --git a/alacritty_terminal/src/term/mod.rs b/alacritty_terminal/src/term/mod.rs index 926b89d7..accb4dc1 100644 --- a/alacritty_terminal/src/term/mod.rs +++ b/alacritty_terminal/src/term/mod.rs @@ -12,7 +12,7 @@ use serde::{Deserialize, Serialize}; use unicode_width::UnicodeWidthChar; use crate::ansi::{ - self, Attr, CharsetIndex, Color, CursorStyle, Handler, NamedColor, StandardCharset, + self, Attr, CharsetIndex, Color, CursorShape, CursorStyle, Handler, NamedColor, StandardCharset, }; use crate::config::{BellAnimation, BellConfig, Config}; use crate::event::{Event, EventListener}; @@ -61,7 +61,7 @@ struct RenderableCursor { /// A key for caching cursor glyphs. #[derive(Debug, Eq, PartialEq, Copy, Clone, Hash, Deserialize)] pub struct CursorKey { - pub style: CursorStyle, + pub shape: CursorShape, pub is_wide: bool, } @@ -202,7 +202,7 @@ impl<'a, C> RenderableCellsIter<'a, C> { let cell = self.inner.next()?; let mut cell = RenderableCell::new(self, cell); - if self.cursor.key.style == CursorStyle::Block { + if self.cursor.key.shape == CursorShape::Block { cell.fg = match self.cursor.cursor_color { // Apply cursor color, or invert the cursor if it has a fixed background // close to the cell's background. @@ -249,7 +249,7 @@ impl<'a, C> RenderableCellsIter<'a, C> { }; // Do not invert block cursor at selection boundaries. - if self.cursor.key.style == CursorStyle::Block + if self.cursor.key.shape == CursorShape::Block && self.cursor.point == point && (selection.start == point || selection.end == point @@ -855,8 +855,8 @@ impl Term { original_colors: colors, semantic_escape_chars: config.selection.semantic_escape_chars().to_owned(), cursor_style: None, - default_cursor_style: config.cursor.style, - vi_mode_cursor_style: config.cursor.vi_mode_style, + default_cursor_style: config.cursor.style(), + vi_mode_cursor_style: config.cursor.vi_mode_style(), event_proxy, is_focused: true, title: None, @@ -885,8 +885,8 @@ impl Term { if let Some(0) = config.scrolling.faux_multiplier() { self.mode.remove(TermMode::ALTERNATE_SCROLL); } - self.default_cursor_style = config.cursor.style; - self.vi_mode_cursor_style = config.cursor.vi_mode_style; + self.default_cursor_style = config.cursor.style(); + self.vi_mode_cursor_style = config.cursor.vi_mode_style(); let title_event = match &self.title { Some(title) => Event::Title(title.clone()), @@ -1207,7 +1207,10 @@ impl Term { /// Toggle the vi mode. #[inline] - pub fn toggle_vi_mode(&mut self) { + pub fn toggle_vi_mode(&mut self) + where + T: EventListener, + { self.mode ^= TermMode::VI; let vi_mode = self.mode.contains(TermMode::VI); @@ -1226,6 +1229,9 @@ impl Term { self.cancel_search(); } + // Update UI about cursor blinking state changes. + self.event_proxy.send_event(Event::CursorBlinkingChange(self.cursor_style().blinking)); + self.dirty = true; } @@ -1332,6 +1338,20 @@ impl Term { &self.semantic_escape_chars } + /// Active terminal cursor style. + /// + /// While vi mode is active, this will automatically return the vi mode cursor style. + #[inline] + pub fn cursor_style(&self) -> CursorStyle { + let cursor_style = self.cursor_style.unwrap_or(self.default_cursor_style); + + if self.mode.contains(TermMode::VI) { + self.vi_mode_cursor_style.unwrap_or(cursor_style) + } else { + cursor_style + } + } + /// Insert a linebreak at the current cursor position. #[inline] fn wrapline(&mut self) @@ -1395,18 +1415,18 @@ impl Term { // Cursor shape. let hidden = !self.mode.contains(TermMode::SHOW_CURSOR) || point.line >= self.screen_lines(); - let cursor_style = if hidden && !vi_mode { + let cursor_shape = if hidden && !vi_mode { point.line = Line(0); - CursorStyle::Hidden + CursorShape::Hidden } else if !self.is_focused && config.cursor.unfocused_hollow() { - CursorStyle::HollowBlock + CursorShape::HollowBlock } else { let cursor_style = self.cursor_style.unwrap_or(self.default_cursor_style); if vi_mode { - self.vi_mode_cursor_style.unwrap_or(cursor_style) + self.vi_mode_cursor_style.unwrap_or(cursor_style).shape } else { - cursor_style + cursor_style.shape } }; @@ -1432,7 +1452,7 @@ impl Term { RenderableCursor { text_color, cursor_color, - key: CursorKey { style: cursor_style, is_wide }, + key: CursorKey { shape: cursor_shape, is_wide }, point, rendered: false, } @@ -2098,6 +2118,9 @@ impl Handler for Term { // Preserve vi mode across resets. self.mode &= TermMode::VI; self.mode.insert(TermMode::default()); + + let blinking = self.cursor_style().blinking; + self.event_proxy.send_event(Event::CursorBlinkingChange(blinking)); } #[inline] @@ -2199,7 +2222,9 @@ impl Handler for Term { ansi::Mode::DECCOLM => self.deccolm(), ansi::Mode::Insert => self.mode.insert(TermMode::INSERT), ansi::Mode::BlinkingCursor => { - trace!("... unimplemented mode"); + let style = self.cursor_style.get_or_insert(self.default_cursor_style); + style.blinking = true; + self.event_proxy.send_event(Event::CursorBlinkingChange(true)); }, } } @@ -2239,7 +2264,9 @@ impl Handler for Term { ansi::Mode::DECCOLM => self.deccolm(), ansi::Mode::Insert => self.mode.remove(TermMode::INSERT), ansi::Mode::BlinkingCursor => { - trace!("... unimplemented mode"); + let style = self.cursor_style.get_or_insert(self.default_cursor_style); + style.blinking = false; + self.event_proxy.send_event(Event::CursorBlinkingChange(false)); }, } } @@ -2296,6 +2323,18 @@ impl Handler for Term { fn set_cursor_style(&mut self, style: Option) { trace!("Setting cursor style {:?}", style); self.cursor_style = style; + + // Notify UI about blinking changes. + let blinking = style.unwrap_or(self.default_cursor_style).blinking; + self.event_proxy.send_event(Event::CursorBlinkingChange(blinking)); + } + + #[inline] + fn set_cursor_shape(&mut self, shape: CursorShape) { + trace!("Setting cursor shape {:?}", shape); + + let style = self.cursor_style.get_or_insert(self.default_cursor_style); + style.shape = shape; } #[inline] diff --git a/docs/escape_support.md b/docs/escape_support.md index f1a0337c..d7c36d33 100644 --- a/docs/escape_support.md +++ b/docs/escape_support.md @@ -68,7 +68,7 @@ brevity. | `CSI m` | PARTIAL | Only singular straight underlines are supported | | `CSI n` | IMPLEMENTED | | | `CSI P` | IMPLEMENTED | | -| `CSI SP q` | PARTIAL | No blinking support | +| `CSI SP q` | IMPLEMENTED | | | `CSI r` | IMPLEMENTED | | | `CSI S` | IMPLEMENTED | | | `CSI s` | IMPLEMENTED | |