1
0
Fork 0
mirror of https://github.com/alacritty/alacritty.git synced 2024-11-25 14:05:41 -05:00

Add blinking cursor support

This adds support for blinking the terminal cursor. This can be
controlled either using the configuration file, or using escape
sequences.

The supported control sequences for changing the blinking state are
`CSI Ps SP q` and private mode 12.
This commit is contained in:
Dettorer 2020-11-24 00:11:03 +01:00 committed by GitHub
parent 07cfe8bbba
commit 2fd2db4afa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 316 additions and 71 deletions

View file

@ -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 - Wide characters sometimes being cut off
- Preserve vi mode across terminal `reset` - 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 ## 0.6.0
### Packaging ### Packaging

View file

@ -341,12 +341,23 @@
#cursor: #cursor:
# Cursor style # Cursor style
#style:
# Cursor shape
# #
# Values for `style`: # Values for `shape`:
# - ▇ Block # - ▇ Block
# - _ Underline # - _ Underline
# - | Beam # - | Beam
#style: Block #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 # Vi mode cursor style
# #
@ -356,6 +367,9 @@
# See `cursor.style` for available options. # See `cursor.style` for available options.
#vi_mode_style: None #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 # If this is `true`, the cursor will be rendered as a hollow box when the
# window is not focused. # window is not focused.
#unfocused_hollow: true #unfocused_hollow: true

View file

@ -2,10 +2,10 @@
use crossfont::{BitmapBuffer, Metrics, RasterizedGlyph}; use crossfont::{BitmapBuffer, Metrics, RasterizedGlyph};
use alacritty_terminal::ansi::CursorStyle; use alacritty_terminal::ansi::CursorShape;
pub fn get_cursor_glyph( pub fn get_cursor_glyph(
cursor: CursorStyle, cursor: CursorShape,
metrics: Metrics, metrics: Metrics,
offset_x: i8, offset_x: i8,
offset_y: i8, offset_y: i8,
@ -26,11 +26,11 @@ pub fn get_cursor_glyph(
} }
match cursor { match cursor {
CursorStyle::HollowBlock => get_box_cursor_glyph(height, width, line_width), CursorShape::HollowBlock => get_box_cursor_glyph(height, width, line_width),
CursorStyle::Underline => get_underline_cursor_glyph(width, line_width), CursorShape::Underline => get_underline_cursor_glyph(width, line_width),
CursorStyle::Beam => get_beam_cursor_glyph(height, line_width), CursorShape::Beam => get_beam_cursor_glyph(height, line_width),
CursorStyle::Block => get_block_cursor_glyph(height, width), CursorShape::Block => get_block_cursor_glyph(height, width),
CursorStyle::Hidden => RasterizedGlyph::default(), CursorShape::Hidden => RasterizedGlyph::default(),
} }
} }

View file

@ -160,6 +160,9 @@ pub struct Display {
#[cfg(not(any(target_os = "macos", windows)))] #[cfg(not(any(target_os = "macos", windows)))]
pub is_x11: bool, pub is_x11: bool,
/// UI cursor visibility for blinking.
pub cursor_hidden: bool,
renderer: QuadRenderer, renderer: QuadRenderer,
glyph_cache: GlyphCache, glyph_cache: GlyphCache,
meter: Meter, meter: Meter,
@ -300,6 +303,7 @@ impl Display {
is_x11, is_x11,
#[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))]
wayland_event_queue, wayland_event_queue,
cursor_hidden: false,
}) })
} }
@ -442,8 +446,9 @@ impl Display {
let viewport_match = search_state let viewport_match = search_state
.focused_match() .focused_match()
.and_then(|focused_match| terminal.grid().clamp_buffer_range_to_visible(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::<Vec<_>>(); let grid_cells = terminal.renderable_cells(config, !cursor_hidden).collect::<Vec<_>>();
let visual_bell_intensity = terminal.visual_bell.intensity(); let visual_bell_intensity = terminal.visual_bell.intensity();
let background_color = terminal.background_color(); let background_color = terminal.background_color();
let cursor_point = terminal.grid().cursor.point; let cursor_point = terminal.grid().cursor.point;

View file

@ -67,6 +67,7 @@ pub enum Event {
Scroll(Scroll), Scroll(Scroll),
ConfigReload(PathBuf), ConfigReload(PathBuf),
Message(Message), Message(Message),
BlinkCursor,
SearchNext, SearchNext,
} }
@ -150,6 +151,7 @@ pub struct ActionContext<'a, N, T> {
pub urls: &'a Urls, pub urls: &'a Urls,
pub scheduler: &'a mut Scheduler, pub scheduler: &'a mut Scheduler,
pub search_state: &'a mut SearchState, pub search_state: &'a mut SearchState,
cursor_hidden: &'a mut bool,
cli_options: &'a CLIOptions, cli_options: &'a CLIOptions,
font_size: &'a mut Size, font_size: &'a mut Size,
} }
@ -495,6 +497,28 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> 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] #[inline]
fn search_direction(&self) -> Direction { fn search_direction(&self) -> Direction {
self.search_state.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.line = (origin.line as isize + self.search_state.display_offset_delta) as usize;
origin 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)] #[derive(Debug, Eq, PartialEq)]
@ -804,6 +855,12 @@ impl<N: Notify + OnResize> Processor<N> {
{ {
let mut scheduler = Scheduler::new(); 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| { event_loop.run_return(|event, event_loop, control_flow| {
if self.config.ui_config.debug.print_events { if self.config.ui_config.debug.print_events {
info!("glutin event: {:?}", event); info!("glutin event: {:?}", event);
@ -873,6 +930,7 @@ impl<N: Notify + OnResize> Processor<N> {
scheduler: &mut scheduler, scheduler: &mut scheduler,
search_state: &mut self.search_state, search_state: &mut self.search_state,
cli_options: &self.cli_options, cli_options: &self.cli_options,
cursor_hidden: &mut self.display.cursor_hidden,
event_loop, event_loop,
}; };
let mut processor = input::Processor::new(context, &self.display.highlighted_url); let mut processor = input::Processor::new(context, &self.display.highlighted_url);
@ -953,6 +1011,10 @@ impl<N: Notify + OnResize> Processor<N> {
Event::SearchNext => processor.ctx.goto_match(None), Event::SearchNext => processor.ctx.goto_match(None),
Event::ConfigReload(path) => Self::reload_config(&path, processor), Event::ConfigReload(path) => Self::reload_config(&path, processor),
Event::Scroll(scroll) => processor.ctx.scroll(scroll), Event::Scroll(scroll) => processor.ctx.scroll(scroll),
Event::BlinkCursor => {
*processor.ctx.cursor_hidden ^= true;
processor.ctx.terminal.dirty = true;
},
Event::TerminalEvent(event) => match event { Event::TerminalEvent(event) => match event {
TerminalEvent::Title(title) => { TerminalEvent::Title(title) => {
let ui_config = &processor.ctx.config.ui_config; let ui_config = &processor.ctx.config.ui_config;
@ -983,6 +1045,9 @@ impl<N: Notify + OnResize> Processor<N> {
}, },
TerminalEvent::MouseCursorDirty => processor.reset_mouse_cursor(), TerminalEvent::MouseCursorDirty => processor.reset_mouse_cursor(),
TerminalEvent::Exit => (), TerminalEvent::Exit => (),
TerminalEvent::CursorBlinkingChange(_) => {
processor.ctx.update_cursor_blinking();
},
}, },
}, },
GlutinEvent::RedrawRequested(_) => processor.ctx.terminal.dirty = true, GlutinEvent::RedrawRequested(_) => processor.ctx.terminal.dirty = true,
@ -1033,6 +1098,7 @@ impl<N: Notify + OnResize> Processor<N> {
processor.ctx.window.set_mouse_visible(true); processor.ctx.window.set_mouse_visible(true);
} }
processor.ctx.update_cursor_blinking();
processor.on_focus_change(is_focused); processor.on_focus_change(is_focused);
} }
}, },
@ -1111,7 +1177,7 @@ impl<N: Notify + OnResize> Processor<N> {
processor.ctx.terminal.update_config(&config); 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() if (processor.ctx.config.cursor.thickness() - config.cursor.thickness()).abs()
> std::f64::EPSILON > std::f64::EPSILON
{ {
@ -1154,6 +1220,9 @@ impl<N: Notify + OnResize> Processor<N> {
*processor.ctx.config = config; *processor.ctx.config = config;
// Update cursor blinking.
processor.ctx.update_cursor_blinking();
processor.ctx.terminal.dirty = true; processor.ctx.terminal.dirty = true;
} }

View file

@ -103,6 +103,7 @@ pub trait ActionContext<T: EventListener> {
fn advance_search_origin(&mut self, direction: Direction); fn advance_search_origin(&mut self, direction: Direction);
fn search_direction(&self) -> Direction; fn search_direction(&self) -> Direction;
fn search_active(&self) -> bool; fn search_active(&self) -> bool;
fn on_typing_start(&mut self);
} }
trait Execute<T: EventListener> { trait Execute<T: EventListener> {
@ -138,9 +139,7 @@ impl<T: EventListener> Execute<T> for Action {
fn execute<A: ActionContext<T>>(&self, ctx: &mut A) { fn execute<A: ActionContext<T>>(&self, ctx: &mut A) {
match *self { match *self {
Action::Esc(ref s) => { Action::Esc(ref s) => {
if ctx.config().ui_config.mouse.hide_when_typing { ctx.on_typing_start();
ctx.window_mut().set_mouse_visible(false);
}
ctx.clear_selection(); ctx.clear_selection();
ctx.scroll(Scroll::Bottom); ctx.scroll(Scroll::Bottom);
@ -167,10 +166,7 @@ impl<T: EventListener> Execute<T> for Action {
Action::ClearSelection => ctx.clear_selection(), Action::ClearSelection => ctx.clear_selection(),
Action::ToggleViMode => ctx.terminal_mut().toggle_vi_mode(), Action::ToggleViMode => ctx.terminal_mut().toggle_vi_mode(),
Action::ViMotion(motion) => { Action::ViMotion(motion) => {
if ctx.config().ui_config.mouse.hide_when_typing { ctx.on_typing_start();
ctx.window_mut().set_mouse_visible(false);
}
ctx.terminal_mut().vi_motion(motion) ctx.terminal_mut().vi_motion(motion)
}, },
Action::ViAction(ViAction::ToggleNormalSelection) => { Action::ViAction(ViAction::ToggleNormalSelection) => {
@ -870,6 +866,13 @@ impl<'a, T: EventListener, A: ActionContext<T>> Processor<'a, T, A> {
self.ctx.window_mut().set_mouse_cursor(mouse_state.into()); 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. /// Process a received character.
pub fn received_char(&mut self, c: char) { pub fn received_char(&mut self, c: char) {
let suppress_chars = *self.ctx.suppress_chars(); let suppress_chars = *self.ctx.suppress_chars();
@ -890,9 +893,7 @@ impl<'a, T: EventListener, A: ActionContext<T>> Processor<'a, T, A> {
return; return;
} }
if self.ctx.config().ui_config.mouse.hide_when_typing { self.ctx.on_typing_start();
self.ctx.window_mut().set_mouse_visible(false);
}
self.ctx.scroll(Scroll::Bottom); self.ctx.scroll(Scroll::Bottom);
self.ctx.clear_selection(); self.ctx.clear_selection();
@ -917,13 +918,6 @@ impl<'a, T: EventListener, A: ActionContext<T>> Processor<'a, T, A> {
*self.ctx.received_count() += 1; *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. /// Attempt to find a binding and execute its action.
/// ///
/// The provided mode, mods, and key must match what is allowed by a binding /// 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 { fn scheduler_mut(&mut self) -> &mut Scheduler {
unimplemented!(); unimplemented!();
} }
fn on_typing_start(&mut self) {
unimplemented!();
}
} }
macro_rules! test_clickstate { macro_rules! test_clickstate {

View file

@ -1010,7 +1010,7 @@ impl<'a> RenderApi<'a> {
let metrics = glyph_cache.metrics; let metrics = glyph_cache.metrics;
let glyph = glyph_cache.cursor_cache.entry(cursor_key).or_insert_with(|| { let glyph = glyph_cache.cursor_cache.entry(cursor_key).or_insert_with(|| {
self.load_glyph(&cursor::get_cursor_glyph( self.load_glyph(&cursor::get_cursor_glyph(
cursor_key.style, cursor_key.shape,
metrics, metrics,
self.config.font.offset.x, self.config.font.offset.x,
self.config.font.offset.y, self.config.font.offset.y,

View file

@ -14,6 +14,7 @@ type Event = GlutinEvent<'static, AlacrittyEvent>;
pub enum TimerId { pub enum TimerId {
SelectionScrolling, SelectionScrolling,
DelayedSearch, DelayedSearch,
BlinkCursor,
} }
/// Event scheduled to be emitted at a specific time. /// Event scheduled to be emitted at a specific time.

View file

@ -141,6 +141,9 @@ pub trait Handler {
/// Set the cursor style. /// Set the cursor style.
fn set_cursor_style(&mut self, _: Option<CursorStyle>) {} fn set_cursor_style(&mut self, _: Option<CursorStyle>) {}
/// Set the cursor shape.
fn set_cursor_shape(&mut self, _shape: CursorShape) {}
/// A character to be displayed. /// A character to be displayed.
fn input(&mut self, _c: char) {} fn input(&mut self, _c: char) {}
@ -324,9 +327,16 @@ pub trait Handler {
fn text_area_size_chars<W: io::Write>(&mut self, _: &mut W) {} fn text_area_size_chars<W: io::Write>(&mut self, _: &mut W) {}
} }
/// Describes shape of cursor. /// Terminal cursor configuration.
#[derive(Debug, Eq, PartialEq, Copy, Clone, Hash, Deserialize)] #[derive(Deserialize, Default, Debug, Eq, PartialEq, Copy, Clone, Hash)]
pub enum CursorStyle { 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 `▒`. /// Cursor is a block like `▒`.
Block, Block,
@ -345,9 +355,9 @@ pub enum CursorStyle {
Hidden, Hidden,
} }
impl Default for CursorStyle { impl Default for CursorShape {
fn default() -> CursorStyle { fn default() -> CursorShape {
CursorStyle::Block CursorShape::Block
} }
} }
@ -874,13 +884,13 @@ where
&& params[1].len() >= 13 && params[1].len() >= 13
&& params[1][0..12] == *b"CursorShape=" && params[1][0..12] == *b"CursorShape="
{ {
let style = match params[1][12] as char { let shape = match params[1][12] as char {
'0' => CursorStyle::Block, '0' => CursorShape::Block,
'1' => CursorStyle::Beam, '1' => CursorShape::Beam,
'2' => CursorStyle::Underline, '2' => CursorShape::Underline,
_ => return unhandled(params), _ => return unhandled(params),
}; };
self.handler.set_cursor_style(Some(style)); self.handler.set_cursor_shape(shape);
return; return;
} }
unhandled(params); unhandled(params);
@ -1065,18 +1075,21 @@ where
('P', None) => handler.delete_chars(Column(next_param_or(1) as usize)), ('P', None) => handler.delete_chars(Column(next_param_or(1) as usize)),
('q', Some(b' ')) => { ('q', Some(b' ')) => {
// DECSCUSR (CSI Ps SP q) -- Set Cursor Style. // 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, 0 => None,
1 | 2 => Some(CursorStyle::Block), 1 | 2 => Some(CursorShape::Block),
3 | 4 => Some(CursorStyle::Underline), 3 | 4 => Some(CursorShape::Underline),
5 | 6 => Some(CursorStyle::Beam), 5 | 6 => Some(CursorShape::Beam),
_ => { _ => {
unhandled!(); unhandled!();
return; 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) => { ('r', None) => {
let top = next_param_or(1) as usize; let top = next_param_or(1) as usize;

View file

@ -1,3 +1,4 @@
use std::cmp::max;
use std::collections::HashMap; use std::collections::HashMap;
use std::fmt::Display; use std::fmt::Display;
use std::path::PathBuf; use std::path::PathBuf;
@ -10,15 +11,16 @@ mod bell;
mod colors; mod colors;
mod scrolling; mod scrolling;
use crate::ansi::CursorStyle; use crate::ansi::{CursorShape, CursorStyle};
pub use crate::config::bell::{BellAnimation, BellConfig}; pub use crate::config::bell::{BellAnimation, BellConfig};
pub use crate::config::colors::Colors; pub use crate::config::colors::Colors;
pub use crate::config::scrolling::Scrolling; pub use crate::config::scrolling::Scrolling;
pub const LOG_TARGET_CONFIG: &str = "alacritty_config"; pub const LOG_TARGET_CONFIG: &str = "alacritty_config";
const MAX_SCROLLBACK_LINES: u32 = 100_000;
const DEFAULT_CURSOR_THICKNESS: f32 = 0.15; const DEFAULT_CURSOR_THICKNESS: f32 = 0.15;
const MAX_SCROLLBACK_LINES: u32 = 100_000;
const MIN_BLINK_INTERVAL: u64 = 10;
pub type MockConfig = Config<HashMap<String, serde_yaml::Value>>; pub type MockConfig = Config<HashMap<String, serde_yaml::Value>>;
@ -121,9 +123,11 @@ impl Default for EscapeChars {
#[derive(Deserialize, Copy, Clone, Debug, PartialEq)] #[derive(Deserialize, Copy, Clone, Debug, PartialEq)]
pub struct Cursor { pub struct Cursor {
#[serde(deserialize_with = "failure_default")] #[serde(deserialize_with = "failure_default")]
pub style: CursorStyle, pub style: ConfigCursorStyle,
#[serde(deserialize_with = "option_explicit_none")] #[serde(deserialize_with = "option_explicit_none")]
pub vi_mode_style: Option<CursorStyle>, pub vi_mode_style: Option<ConfigCursorStyle>,
#[serde(deserialize_with = "failure_default")]
blink_interval: BlinkInterval,
#[serde(deserialize_with = "deserialize_cursor_thickness")] #[serde(deserialize_with = "deserialize_cursor_thickness")]
thickness: Percentage, thickness: Percentage,
#[serde(deserialize_with = "failure_default")] #[serde(deserialize_with = "failure_default")]
@ -140,6 +144,21 @@ impl Cursor {
pub fn thickness(self) -> f64 { pub fn thickness(self) -> f64 {
self.thickness.0 as f64 self.thickness.0 as f64
} }
#[inline]
pub fn style(self) -> CursorStyle {
self.style.into()
}
#[inline]
pub fn vi_mode_style(self) -> Option<CursorStyle> {
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 { impl Default for Cursor {
@ -149,10 +168,20 @@ impl Default for Cursor {
vi_mode_style: Default::default(), vi_mode_style: Default::default(),
thickness: Percentage::new(DEFAULT_CURSOR_THICKNESS), thickness: Percentage::new(DEFAULT_CURSOR_THICKNESS),
unfocused_hollow: Default::default(), 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<Percentage, D::Error> fn deserialize_cursor_thickness<'a, D>(deserializer: D) -> Result<Percentage, D::Error>
where where
D: Deserializer<'a>, 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<bool> {
match self {
Self::Shape(_) => None,
Self::WithBlinking { blinking, .. } => blinking.blinking_override(),
}
}
}
impl From<ConfigCursorStyle> 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<bool> {
match self {
Self::Never => Some(false),
Self::Off | Self::On => None,
Self::Always => Some(true),
}
}
}
impl Into<bool> for CursorBlinking {
fn into(self) -> bool {
self == Self::On || self == Self::Always
}
}
#[serde(untagged)] #[serde(untagged)]
#[derive(Deserialize, Debug, Clone, PartialEq, Eq)] #[derive(Deserialize, Debug, Clone, PartialEq, Eq)]
pub enum Program { pub enum Program {

View file

@ -11,6 +11,7 @@ pub enum Event {
ResetTitle, ResetTitle,
ClipboardStore(ClipboardType, String), ClipboardStore(ClipboardType, String),
ClipboardLoad(ClipboardType, Arc<dyn Fn(&str) -> String + Sync + Send + 'static>), ClipboardLoad(ClipboardType, Arc<dyn Fn(&str) -> String + Sync + Send + 'static>),
CursorBlinkingChange(bool),
Wakeup, Wakeup,
Bell, Bell,
Exit, Exit,
@ -27,6 +28,7 @@ impl Debug for Event {
Event::Wakeup => write!(f, "Wakeup"), Event::Wakeup => write!(f, "Wakeup"),
Event::Bell => write!(f, "Bell"), Event::Bell => write!(f, "Bell"),
Event::Exit => write!(f, "Exit"), Event::Exit => write!(f, "Exit"),
Event::CursorBlinkingChange(blinking) => write!(f, "CursorBlinking({})", blinking),
} }
} }
} }

View file

@ -12,7 +12,7 @@ use serde::{Deserialize, Serialize};
use unicode_width::UnicodeWidthChar; use unicode_width::UnicodeWidthChar;
use crate::ansi::{ 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::config::{BellAnimation, BellConfig, Config};
use crate::event::{Event, EventListener}; use crate::event::{Event, EventListener};
@ -61,7 +61,7 @@ struct RenderableCursor {
/// A key for caching cursor glyphs. /// A key for caching cursor glyphs.
#[derive(Debug, Eq, PartialEq, Copy, Clone, Hash, Deserialize)] #[derive(Debug, Eq, PartialEq, Copy, Clone, Hash, Deserialize)]
pub struct CursorKey { pub struct CursorKey {
pub style: CursorStyle, pub shape: CursorShape,
pub is_wide: bool, pub is_wide: bool,
} }
@ -202,7 +202,7 @@ impl<'a, C> RenderableCellsIter<'a, C> {
let cell = self.inner.next()?; let cell = self.inner.next()?;
let mut cell = RenderableCell::new(self, cell); 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 { cell.fg = match self.cursor.cursor_color {
// Apply cursor color, or invert the cursor if it has a fixed background // Apply cursor color, or invert the cursor if it has a fixed background
// close to the cell's 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. // 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 && self.cursor.point == point
&& (selection.start == point && (selection.start == point
|| selection.end == point || selection.end == point
@ -855,8 +855,8 @@ impl<T> Term<T> {
original_colors: colors, original_colors: colors,
semantic_escape_chars: config.selection.semantic_escape_chars().to_owned(), semantic_escape_chars: config.selection.semantic_escape_chars().to_owned(),
cursor_style: None, cursor_style: None,
default_cursor_style: config.cursor.style, default_cursor_style: config.cursor.style(),
vi_mode_cursor_style: config.cursor.vi_mode_style, vi_mode_cursor_style: config.cursor.vi_mode_style(),
event_proxy, event_proxy,
is_focused: true, is_focused: true,
title: None, title: None,
@ -885,8 +885,8 @@ impl<T> Term<T> {
if let Some(0) = config.scrolling.faux_multiplier() { if let Some(0) = config.scrolling.faux_multiplier() {
self.mode.remove(TermMode::ALTERNATE_SCROLL); self.mode.remove(TermMode::ALTERNATE_SCROLL);
} }
self.default_cursor_style = config.cursor.style; self.default_cursor_style = config.cursor.style();
self.vi_mode_cursor_style = config.cursor.vi_mode_style; self.vi_mode_cursor_style = config.cursor.vi_mode_style();
let title_event = match &self.title { let title_event = match &self.title {
Some(title) => Event::Title(title.clone()), Some(title) => Event::Title(title.clone()),
@ -1207,7 +1207,10 @@ impl<T> Term<T> {
/// Toggle the vi mode. /// Toggle the vi mode.
#[inline] #[inline]
pub fn toggle_vi_mode(&mut self) { pub fn toggle_vi_mode(&mut self)
where
T: EventListener,
{
self.mode ^= TermMode::VI; self.mode ^= TermMode::VI;
let vi_mode = self.mode.contains(TermMode::VI); let vi_mode = self.mode.contains(TermMode::VI);
@ -1226,6 +1229,9 @@ impl<T> Term<T> {
self.cancel_search(); self.cancel_search();
} }
// Update UI about cursor blinking state changes.
self.event_proxy.send_event(Event::CursorBlinkingChange(self.cursor_style().blinking));
self.dirty = true; self.dirty = true;
} }
@ -1332,6 +1338,20 @@ impl<T> Term<T> {
&self.semantic_escape_chars &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. /// Insert a linebreak at the current cursor position.
#[inline] #[inline]
fn wrapline(&mut self) fn wrapline(&mut self)
@ -1395,18 +1415,18 @@ impl<T> Term<T> {
// Cursor shape. // Cursor shape.
let hidden = let hidden =
!self.mode.contains(TermMode::SHOW_CURSOR) || point.line >= self.screen_lines(); !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); point.line = Line(0);
CursorStyle::Hidden CursorShape::Hidden
} else if !self.is_focused && config.cursor.unfocused_hollow() { } else if !self.is_focused && config.cursor.unfocused_hollow() {
CursorStyle::HollowBlock CursorShape::HollowBlock
} else { } else {
let cursor_style = self.cursor_style.unwrap_or(self.default_cursor_style); let cursor_style = self.cursor_style.unwrap_or(self.default_cursor_style);
if vi_mode { if vi_mode {
self.vi_mode_cursor_style.unwrap_or(cursor_style) self.vi_mode_cursor_style.unwrap_or(cursor_style).shape
} else { } else {
cursor_style cursor_style.shape
} }
}; };
@ -1432,7 +1452,7 @@ impl<T> Term<T> {
RenderableCursor { RenderableCursor {
text_color, text_color,
cursor_color, cursor_color,
key: CursorKey { style: cursor_style, is_wide }, key: CursorKey { shape: cursor_shape, is_wide },
point, point,
rendered: false, rendered: false,
} }
@ -2098,6 +2118,9 @@ impl<T: EventListener> Handler for Term<T> {
// Preserve vi mode across resets. // Preserve vi mode across resets.
self.mode &= TermMode::VI; self.mode &= TermMode::VI;
self.mode.insert(TermMode::default()); self.mode.insert(TermMode::default());
let blinking = self.cursor_style().blinking;
self.event_proxy.send_event(Event::CursorBlinkingChange(blinking));
} }
#[inline] #[inline]
@ -2199,7 +2222,9 @@ impl<T: EventListener> Handler for Term<T> {
ansi::Mode::DECCOLM => self.deccolm(), ansi::Mode::DECCOLM => self.deccolm(),
ansi::Mode::Insert => self.mode.insert(TermMode::INSERT), ansi::Mode::Insert => self.mode.insert(TermMode::INSERT),
ansi::Mode::BlinkingCursor => { 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<T: EventListener> Handler for Term<T> {
ansi::Mode::DECCOLM => self.deccolm(), ansi::Mode::DECCOLM => self.deccolm(),
ansi::Mode::Insert => self.mode.remove(TermMode::INSERT), ansi::Mode::Insert => self.mode.remove(TermMode::INSERT),
ansi::Mode::BlinkingCursor => { 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<T: EventListener> Handler for Term<T> {
fn set_cursor_style(&mut self, style: Option<CursorStyle>) { fn set_cursor_style(&mut self, style: Option<CursorStyle>) {
trace!("Setting cursor style {:?}", style); trace!("Setting cursor style {:?}", style);
self.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] #[inline]

View file

@ -68,7 +68,7 @@ brevity.
| `CSI m` | PARTIAL | Only singular straight underlines are supported | | `CSI m` | PARTIAL | Only singular straight underlines are supported |
| `CSI n` | IMPLEMENTED | | | `CSI n` | IMPLEMENTED | |
| `CSI P` | IMPLEMENTED | | | `CSI P` | IMPLEMENTED | |
| `CSI SP q` | PARTIAL | No blinking support | | `CSI SP q` | IMPLEMENTED | |
| `CSI r` | IMPLEMENTED | | | `CSI r` | IMPLEMENTED | |
| `CSI S` | IMPLEMENTED | | | `CSI S` | IMPLEMENTED | |
| `CSI s` | IMPLEMENTED | | | `CSI s` | IMPLEMENTED | |