refactor cursor implementation, implement cursor visible (mode 25)

This cleans up a ton of state management around cursor styles,
visibility, blinking, etc. This was long in the tooth and when I was
trying to implement mode 25 I realized it was impossible with the
spaghetti mess I had. This made it really clean.

With this refactor, the Window keeps the "terminal_cursor" field the
proper state, and the render callback properly updates the gpu cells for
the cursor settings.

This also implements mode 25 (cursor visible) which makes neovim not
"flash" when vertically scrolling a vertical split. Neovim does some
cursor stuff but while doing so hides the cursor. This now respects
that.
This commit is contained in:
Mitchell Hashimoto
2022-08-03 21:39:42 -07:00
parent 1680aee880
commit e163e4962b
2 changed files with 73 additions and 58 deletions

View File

@ -59,9 +59,8 @@ terminal: terminal.Terminal,
/// The stream parser.
terminal_stream: terminal.Stream(*Window),
/// Timer that blinks the cursor.
cursor_timer: libuv.Timer,
cursor_style: terminal.CursorStyle,
/// Cursor state.
terminal_cursor: Cursor,
/// Render at least 60fps.
render_timer: RenderTimer,
@ -76,10 +75,6 @@ write_req_pool: SegmentedPool(libuv.WriteReq.T, WRITE_REQ_PREALLOC) = .{},
/// The pool of available buffers for writing to the pty.
write_buf_pool: SegmentedPool([64]u8, WRITE_REQ_PREALLOC) = .{},
/// Set this to true whenver an event occurs that we may want to wake up
/// the event loop. Only set this from the main thread.
wakeup: bool = false,
/// The app configuration
config: *const Config,
@ -98,6 +93,42 @@ bracketed_paste: bool = false,
/// like such as "control-v" will write a "v" even if they're intercepted.
ignore_char: bool = false,
/// Information related to the current cursor for the window.
//
// QUESTION(mitchellh): should this be attached to the Screen instead?
// I'm not sure if the cursor settings stick to the screen, i.e. if you
// change to an alternate screen if those are preserved. Need to check this.
const Cursor = struct {
/// Timer for cursor blinking.
timer: libuv.Timer,
/// Current cursor style. This can be set by escape sequences. To get
/// the default style, the config has to be referenced.
style: terminal.CursorStyle = .default,
/// Whether the cursor is visible at all. This should not be used for
/// "blink" settings, see "blink" for that. This is used to turn the
/// cursor ON or OFF.
visible: bool = true,
/// Whether the cursor is currently blinking. If it is blinking, then
/// the cursor will not be rendered.
blink: bool = false,
/// Start (or restart) the timer. This is idempotent.
pub fn startTimer(self: Cursor) !void {
try self.timer.start(
cursorTimerCallback,
0,
self.timer.getRepeat(),
);
}
pub fn stopTimer(self: Cursor) !void {
try self.timer.stop();
}
};
/// Create a new window. This allocates and returns a pointer because we
/// need a stable pointer for user data callbacks. Therefore, a stack-only
/// initialization is not currently possible.
@ -231,8 +262,10 @@ pub fn create(alloc: Allocator, loop: libuv.Loop, config: *const Config) !*Windo
.command = cmd,
.terminal = term,
.terminal_stream = .{ .handler = self },
.cursor_timer = timer,
.cursor_style = .blinking_block,
.terminal_cursor = .{
.timer = timer,
.style = .blinking_block,
},
.render_timer = try RenderTimer.init(loop, self, 16, 64),
.pty_stream = stream,
.config = config,
@ -265,7 +298,7 @@ pub fn destroy(self: *Window) void {
self.grid.deinit();
self.window.destroy();
self.cursor_timer.close((struct {
self.terminal_cursor.timer.close((struct {
fn callback(t: *libuv.Timer) void {
const alloc = t.loop().getData(Allocator).?.*;
t.deinit(alloc);
@ -318,25 +351,6 @@ fn queueWrite(self: *Window, data: []const u8) !void {
}
}
/// Updates te style of the cursor.
fn updateCursorStyle(self: *Window, style: Grid.CursorStyle, blink: bool) !void {
self.grid.cursor_style = style;
self.grid.cursor_visible = !blink;
if (blink) {
try self.cursor_timer.start(
cursorTimerCallback,
0,
self.cursor_timer.getRepeat(),
);
} else {
try self.cursor_timer.stop();
}
// Always schedule a render when we change cursors
try self.render_timer.schedule();
}
fn sizeCallback(window: glfw.Window, width: i32, height: i32) void {
const tracy = trace(@src());
defer tracy.end();
@ -483,21 +497,17 @@ fn focusCallback(window: glfw.Window, focused: bool) void {
if (win.focused == focused) return;
// We have to schedule a render because no matter what we're changing
// the cursor.
// the cursor. If we're focused its reappearing, if we're not then
// its changing to hollow and not blinking.
win.render_timer.schedule() catch unreachable;
// Set our focused state on the window.
win.focused = focused;
if (focused) {
win.wakeup = true;
win.updateCursorStyle(
Grid.CursorStyle.fromTerminal(win.cursor_style) orelse .box,
win.cursor_style.blinking(),
) catch unreachable;
} else {
win.updateCursorStyle(.box_hollow, false) catch unreachable;
}
if (focused)
win.terminal_cursor.startTimer() catch unreachable
else
win.terminal_cursor.stopTimer() catch unreachable;
}
fn refreshCallback(window: glfw.Window) void {
@ -536,7 +546,13 @@ fn cursorTimerCallback(t: *libuv.Timer) void {
defer tracy.end();
const win = t.getData(Window) orelse return;
win.grid.cursor_visible = !win.grid.cursor_visible;
// If the cursor is currently invisible, then we do nothing. Ideally
// in this state the timer would be cancelled but no big deal.
if (!win.terminal_cursor.visible) return;
// Swap blink state and schedule a render
win.terminal_cursor.blink = !win.terminal_cursor.blink;
win.render_timer.schedule() catch unreachable;
}
@ -570,11 +586,11 @@ fn ttyRead(t: *libuv.Tty, n: isize, buf: []const u8) void {
return;
};
// Whenever a character is typed, we ensure the cursor is visible
// and we restart the cursor timer.
win.grid.cursor_visible = true;
if (win.cursor_timer.isActive() catch false) {
_ = win.cursor_timer.again() catch null;
// Whenever a character is typed, we ensure the cursor is in the
// non-blink state so it is rendered if visible.
win.terminal_cursor.blink = false;
if (win.terminal_cursor.timer.isActive() catch false) {
_ = win.terminal_cursor.timer.again() catch null;
}
// Schedule a render
@ -607,6 +623,10 @@ fn renderTimerCallback(t: *libuv.Timer) void {
const win = t.getData(Window).?;
// Setup our cursor settings
win.grid.cursor_visible = win.terminal_cursor.visible and !win.terminal_cursor.blink;
win.grid.cursor_style = Grid.CursorStyle.fromTerminal(win.terminal_cursor.style) orelse .box;
// Calculate foreground and background colors
const bg = win.grid.background;
const fg = win.grid.foreground;
@ -788,6 +808,10 @@ pub fn setMode(self: *Window, mode: terminal.Mode, enabled: bool) !void {
self.terminal.modes.autowrap = @boolToInt(enabled);
},
.cursor_visible => {
self.terminal_cursor.visible = enabled;
},
.alt_screen_save_cursor_clear_enter => {
const opts: terminal.Terminal.AlternateScreenOptions = .{
.cursor_save = true,
@ -887,19 +911,7 @@ pub fn setCursorStyle(
self: *Window,
style: terminal.CursorStyle,
) !void {
// Get the style that we use in the renderer
const grid_style = Grid.CursorStyle.fromTerminal(style) orelse {
log.warn("unimplemented cursor style: {}", .{style});
return;
};
// Set our style
self.cursor_style = style;
// If we're currently focused, we update our style, since our unfocused
// cursor is manually managed. If we're not focused, we ignore it because
// it'll be updated the next time the window comes into focus.
if (self.focused) try self.updateCursorStyle(grid_style, style.blinking());
self.terminal_cursor.style = style;
}
pub fn decaln(self: *Window) !void {

View File

@ -58,6 +58,9 @@ pub const Mode = enum(u16) {
/// Enable or disable automatic line wrapping.
autowrap = 7,
/// Set whether the cursor is visible or not.
cursor_visible = 25,
/// Enables or disables mode ?3. If disabled, the terminal will resize
/// to the size of the window. If enabled, this will take effect when
/// mode ?3 is set or unset.