Skip to content

Commit 2fd2db4

Browse files
authored
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.
1 parent 07cfe8b commit 2fd2db4

File tree

13 files changed

+316
-71
lines changed

13 files changed

+316
-71
lines changed

Diff for: CHANGELOG.md

+6
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
2020
- Wide characters sometimes being cut off
2121
- Preserve vi mode across terminal `reset`
2222

23+
### Added
24+
25+
- New `cursor.style.blinking` option to set the default blinking state
26+
- New `cursor.blink_interval` option to configure the blinking frequency
27+
- Support for cursor blinking escapes (`CSI ? 12 h`, `CSI ? 12 l` and `CSI Ps SP q`)
28+
2329
## 0.6.0
2430

2531
### Packaging

Diff for: alacritty.yml

+20-6
Original file line numberDiff line numberDiff line change
@@ -341,12 +341,23 @@
341341

342342
#cursor:
343343
# Cursor style
344-
#
345-
# Values for `style`:
346-
# - ▇ Block
347-
# - _ Underline
348-
# - | Beam
349-
#style: Block
344+
#style:
345+
# Cursor shape
346+
#
347+
# Values for `shape`:
348+
# - ▇ Block
349+
# - _ Underline
350+
# - | Beam
351+
#shape: Block
352+
353+
# Cursor blinking state
354+
#
355+
# Values for `blinking`:
356+
# - Never: Prevent the cursor from ever blinking
357+
# - Off: Disable blinking by default
358+
# - On: Enable blinking by default
359+
# - Always: Force the cursor to always blink
360+
#blinking: Off
350361

351362
# Vi mode cursor style
352363
#
@@ -356,6 +367,9 @@
356367
# See `cursor.style` for available options.
357368
#vi_mode_style: None
358369

370+
# Cursor blinking interval in milliseconds.
371+
#blink_interval: 750
372+
359373
# If this is `true`, the cursor will be rendered as a hollow box when the
360374
# window is not focused.
361375
#unfocused_hollow: true

Diff for: alacritty/src/cursor.rs

+7-7
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22
33
use crossfont::{BitmapBuffer, Metrics, RasterizedGlyph};
44

5-
use alacritty_terminal::ansi::CursorStyle;
5+
use alacritty_terminal::ansi::CursorShape;
66

77
pub fn get_cursor_glyph(
8-
cursor: CursorStyle,
8+
cursor: CursorShape,
99
metrics: Metrics,
1010
offset_x: i8,
1111
offset_y: i8,
@@ -26,11 +26,11 @@ pub fn get_cursor_glyph(
2626
}
2727

2828
match cursor {
29-
CursorStyle::HollowBlock => get_box_cursor_glyph(height, width, line_width),
30-
CursorStyle::Underline => get_underline_cursor_glyph(width, line_width),
31-
CursorStyle::Beam => get_beam_cursor_glyph(height, line_width),
32-
CursorStyle::Block => get_block_cursor_glyph(height, width),
33-
CursorStyle::Hidden => RasterizedGlyph::default(),
29+
CursorShape::HollowBlock => get_box_cursor_glyph(height, width, line_width),
30+
CursorShape::Underline => get_underline_cursor_glyph(width, line_width),
31+
CursorShape::Beam => get_beam_cursor_glyph(height, line_width),
32+
CursorShape::Block => get_block_cursor_glyph(height, width),
33+
CursorShape::Hidden => RasterizedGlyph::default(),
3434
}
3535
}
3636

Diff for: alacritty/src/display.rs

+6-1
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,9 @@ pub struct Display {
160160
#[cfg(not(any(target_os = "macos", windows)))]
161161
pub is_x11: bool,
162162

163+
/// UI cursor visibility for blinking.
164+
pub cursor_hidden: bool,
165+
163166
renderer: QuadRenderer,
164167
glyph_cache: GlyphCache,
165168
meter: Meter,
@@ -300,6 +303,7 @@ impl Display {
300303
is_x11,
301304
#[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))]
302305
wayland_event_queue,
306+
cursor_hidden: false,
303307
})
304308
}
305309

@@ -442,8 +446,9 @@ impl Display {
442446
let viewport_match = search_state
443447
.focused_match()
444448
.and_then(|focused_match| terminal.grid().clamp_buffer_range_to_visible(focused_match));
449+
let cursor_hidden = self.cursor_hidden || search_state.regex().is_some();
445450

446-
let grid_cells = terminal.renderable_cells(config, !search_active).collect::<Vec<_>>();
451+
let grid_cells = terminal.renderable_cells(config, !cursor_hidden).collect::<Vec<_>>();
447452
let visual_bell_intensity = terminal.visual_bell.intensity();
448453
let background_color = terminal.background_color();
449454
let cursor_point = terminal.grid().cursor.point;

Diff for: alacritty/src/event.rs

+70-1
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ pub enum Event {
6767
Scroll(Scroll),
6868
ConfigReload(PathBuf),
6969
Message(Message),
70+
BlinkCursor,
7071
SearchNext,
7172
}
7273

@@ -150,6 +151,7 @@ pub struct ActionContext<'a, N, T> {
150151
pub urls: &'a Urls,
151152
pub scheduler: &'a mut Scheduler,
152153
pub search_state: &'a mut SearchState,
154+
cursor_hidden: &'a mut bool,
153155
cli_options: &'a CLIOptions,
154156
font_size: &'a mut Size,
155157
}
@@ -495,6 +497,28 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionCon
495497
}
496498
}
497499

500+
/// Handle keyboard typing start.
501+
///
502+
/// This will temporarily disable some features like terminal cursor blinking or the mouse
503+
/// cursor.
504+
///
505+
/// All features are re-enabled again automatically.
506+
#[inline]
507+
fn on_typing_start(&mut self) {
508+
// Disable cursor blinking.
509+
let blink_interval = self.config.cursor.blink_interval();
510+
if let Some(timer) = self.scheduler.get_mut(TimerId::BlinkCursor) {
511+
timer.deadline = Instant::now() + Duration::from_millis(blink_interval);
512+
*self.cursor_hidden = false;
513+
self.terminal.dirty = true;
514+
}
515+
516+
// Hide mouse cursor.
517+
if self.config.ui_config.mouse.hide_when_typing {
518+
self.window.set_mouse_visible(false);
519+
}
520+
}
521+
498522
#[inline]
499523
fn search_direction(&self) -> Direction {
500524
self.search_state.direction
@@ -667,6 +691,33 @@ impl<'a, N: Notify + 'a, T: EventListener> ActionContext<'a, N, T> {
667691
origin.line = (origin.line as isize + self.search_state.display_offset_delta) as usize;
668692
origin
669693
}
694+
695+
/// Update the cursor blinking state.
696+
fn update_cursor_blinking(&mut self) {
697+
// Get config cursor style.
698+
let mut cursor_style = self.config.cursor.style;
699+
if self.terminal.mode().contains(TermMode::VI) {
700+
cursor_style = self.config.cursor.vi_mode_style.unwrap_or(cursor_style);
701+
};
702+
703+
// Check terminal cursor style.
704+
let terminal_blinking = self.terminal.cursor_style().blinking;
705+
let blinking = cursor_style.blinking_override().unwrap_or(terminal_blinking);
706+
707+
// Update cursor blinking state.
708+
self.scheduler.unschedule(TimerId::BlinkCursor);
709+
if blinking && self.terminal.is_focused {
710+
self.scheduler.schedule(
711+
GlutinEvent::UserEvent(Event::BlinkCursor),
712+
Duration::from_millis(self.config.cursor.blink_interval()),
713+
true,
714+
TimerId::BlinkCursor,
715+
)
716+
} else {
717+
*self.cursor_hidden = false;
718+
self.terminal.dirty = true;
719+
}
720+
}
670721
}
671722

672723
#[derive(Debug, Eq, PartialEq)]
@@ -804,6 +855,12 @@ impl<N: Notify + OnResize> Processor<N> {
804855
{
805856
let mut scheduler = Scheduler::new();
806857

858+
// Start the initial cursor blinking timer.
859+
if self.config.cursor.style().blinking {
860+
let event: Event = TerminalEvent::CursorBlinkingChange(true).into();
861+
self.event_queue.push(event.into());
862+
}
863+
807864
event_loop.run_return(|event, event_loop, control_flow| {
808865
if self.config.ui_config.debug.print_events {
809866
info!("glutin event: {:?}", event);
@@ -873,6 +930,7 @@ impl<N: Notify + OnResize> Processor<N> {
873930
scheduler: &mut scheduler,
874931
search_state: &mut self.search_state,
875932
cli_options: &self.cli_options,
933+
cursor_hidden: &mut self.display.cursor_hidden,
876934
event_loop,
877935
};
878936
let mut processor = input::Processor::new(context, &self.display.highlighted_url);
@@ -953,6 +1011,10 @@ impl<N: Notify + OnResize> Processor<N> {
9531011
Event::SearchNext => processor.ctx.goto_match(None),
9541012
Event::ConfigReload(path) => Self::reload_config(&path, processor),
9551013
Event::Scroll(scroll) => processor.ctx.scroll(scroll),
1014+
Event::BlinkCursor => {
1015+
*processor.ctx.cursor_hidden ^= true;
1016+
processor.ctx.terminal.dirty = true;
1017+
},
9561018
Event::TerminalEvent(event) => match event {
9571019
TerminalEvent::Title(title) => {
9581020
let ui_config = &processor.ctx.config.ui_config;
@@ -983,6 +1045,9 @@ impl<N: Notify + OnResize> Processor<N> {
9831045
},
9841046
TerminalEvent::MouseCursorDirty => processor.reset_mouse_cursor(),
9851047
TerminalEvent::Exit => (),
1048+
TerminalEvent::CursorBlinkingChange(_) => {
1049+
processor.ctx.update_cursor_blinking();
1050+
},
9861051
},
9871052
},
9881053
GlutinEvent::RedrawRequested(_) => processor.ctx.terminal.dirty = true,
@@ -1033,6 +1098,7 @@ impl<N: Notify + OnResize> Processor<N> {
10331098
processor.ctx.window.set_mouse_visible(true);
10341099
}
10351100

1101+
processor.ctx.update_cursor_blinking();
10361102
processor.on_focus_change(is_focused);
10371103
}
10381104
},
@@ -1111,7 +1177,7 @@ impl<N: Notify + OnResize> Processor<N> {
11111177

11121178
processor.ctx.terminal.update_config(&config);
11131179

1114-
// Reload cursor if we've changed its thickness.
1180+
// Reload cursor if its thickness has changed.
11151181
if (processor.ctx.config.cursor.thickness() - config.cursor.thickness()).abs()
11161182
> std::f64::EPSILON
11171183
{
@@ -1154,6 +1220,9 @@ impl<N: Notify + OnResize> Processor<N> {
11541220

11551221
*processor.ctx.config = config;
11561222

1223+
// Update cursor blinking.
1224+
processor.ctx.update_cursor_blinking();
1225+
11571226
processor.ctx.terminal.dirty = true;
11581227
}
11591228

Diff for: alacritty/src/input.rs

+15-17
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ pub trait ActionContext<T: EventListener> {
103103
fn advance_search_origin(&mut self, direction: Direction);
104104
fn search_direction(&self) -> Direction;
105105
fn search_active(&self) -> bool;
106+
fn on_typing_start(&mut self);
106107
}
107108

108109
trait Execute<T: EventListener> {
@@ -138,9 +139,7 @@ impl<T: EventListener> Execute<T> for Action {
138139
fn execute<A: ActionContext<T>>(&self, ctx: &mut A) {
139140
match *self {
140141
Action::Esc(ref s) => {
141-
if ctx.config().ui_config.mouse.hide_when_typing {
142-
ctx.window_mut().set_mouse_visible(false);
143-
}
142+
ctx.on_typing_start();
144143

145144
ctx.clear_selection();
146145
ctx.scroll(Scroll::Bottom);
@@ -167,10 +166,7 @@ impl<T: EventListener> Execute<T> for Action {
167166
Action::ClearSelection => ctx.clear_selection(),
168167
Action::ToggleViMode => ctx.terminal_mut().toggle_vi_mode(),
169168
Action::ViMotion(motion) => {
170-
if ctx.config().ui_config.mouse.hide_when_typing {
171-
ctx.window_mut().set_mouse_visible(false);
172-
}
173-
169+
ctx.on_typing_start();
174170
ctx.terminal_mut().vi_motion(motion)
175171
},
176172
Action::ViAction(ViAction::ToggleNormalSelection) => {
@@ -870,6 +866,13 @@ impl<'a, T: EventListener, A: ActionContext<T>> Processor<'a, T, A> {
870866
self.ctx.window_mut().set_mouse_cursor(mouse_state.into());
871867
}
872868

869+
/// Reset mouse cursor based on modifier and terminal state.
870+
#[inline]
871+
pub fn reset_mouse_cursor(&mut self) {
872+
let mouse_state = self.mouse_state();
873+
self.ctx.window_mut().set_mouse_cursor(mouse_state.into());
874+
}
875+
873876
/// Process a received character.
874877
pub fn received_char(&mut self, c: char) {
875878
let suppress_chars = *self.ctx.suppress_chars();
@@ -890,9 +893,7 @@ impl<'a, T: EventListener, A: ActionContext<T>> Processor<'a, T, A> {
890893
return;
891894
}
892895

893-
if self.ctx.config().ui_config.mouse.hide_when_typing {
894-
self.ctx.window_mut().set_mouse_visible(false);
895-
}
896+
self.ctx.on_typing_start();
896897

897898
self.ctx.scroll(Scroll::Bottom);
898899
self.ctx.clear_selection();
@@ -917,13 +918,6 @@ impl<'a, T: EventListener, A: ActionContext<T>> Processor<'a, T, A> {
917918
*self.ctx.received_count() += 1;
918919
}
919920

920-
/// Reset mouse cursor based on modifier and terminal state.
921-
#[inline]
922-
pub fn reset_mouse_cursor(&mut self) {
923-
let mouse_state = self.mouse_state();
924-
self.ctx.window_mut().set_mouse_cursor(mouse_state.into());
925-
}
926-
927921
/// Attempt to find a binding and execute its action.
928922
///
929923
/// The provided mode, mods, and key must match what is allowed by a binding
@@ -1270,6 +1264,10 @@ mod tests {
12701264
fn scheduler_mut(&mut self) -> &mut Scheduler {
12711265
unimplemented!();
12721266
}
1267+
1268+
fn on_typing_start(&mut self) {
1269+
unimplemented!();
1270+
}
12731271
}
12741272

12751273
macro_rules! test_clickstate {

Diff for: alacritty/src/renderer/mod.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -1010,7 +1010,7 @@ impl<'a> RenderApi<'a> {
10101010
let metrics = glyph_cache.metrics;
10111011
let glyph = glyph_cache.cursor_cache.entry(cursor_key).or_insert_with(|| {
10121012
self.load_glyph(&cursor::get_cursor_glyph(
1013-
cursor_key.style,
1013+
cursor_key.shape,
10141014
metrics,
10151015
self.config.font.offset.x,
10161016
self.config.font.offset.y,

Diff for: alacritty/src/scheduler.rs

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ type Event = GlutinEvent<'static, AlacrittyEvent>;
1414
pub enum TimerId {
1515
SelectionScrolling,
1616
DelayedSearch,
17+
BlinkCursor,
1718
}
1819

1920
/// Event scheduled to be emitted at a specific time.

0 commit comments

Comments
 (0)