Files
ghostty/src/renderer/cursor.zig
ClearAspect 7a4bddd37b renderer: added cursor style and visibility uniforms
Specifically:
iCurrentCursorStyle
iPreviousCursorStyle
iCurrentCursorVisible
iPreviousCursorVisible

Visibility calculated and updated independently from the typical cursor
unifrom updates to preserve cursor style even when not in the viewport
or set to be hidden
2026-02-23 14:11:36 -08:00

155 lines
6.2 KiB
Zig

const std = @import("std");
const terminal = @import("../terminal/main.zig");
/// Available cursor styles for drawing that renderers must support.
/// This is a superset of terminal cursor styles since the renderer supports
/// some additional cursor states such as the hollow block.
pub const Style = enum {
// Typical cursor input styles
block,
block_hollow,
bar,
underline,
// Special cursor styles
lock,
/// Create a cursor style from the terminal style request.
pub fn fromTerminal(term: terminal.CursorStyle) Style {
return switch (term) {
.bar => .bar,
.block => .block,
.block_hollow => .block_hollow,
.underline => .underline,
};
}
};
pub const StyleOptions = struct {
preedit: bool = false,
focused: bool = false,
blink_visible: bool = false,
};
/// Returns the cursor style to use for the current render state or null
/// if a cursor should not be rendered at all.
pub fn style(
state: *const terminal.RenderState,
opts: StyleOptions,
) ?Style {
// Note the order of conditionals below is important. It represents
// a priority system of how we determine what state overrides cursor
// visibility and style.
// The cursor must be visible in the viewport to be rendered.
if (state.cursor.viewport == null) return null;
// If we are in preedit, then we always show the block cursor. We do
// this even if the cursor is explicitly not visible because it shows
// an important editing state to the user.
if (opts.preedit) return .block;
// If we're at a password input its always a lock.
if (state.cursor.password_input) return .lock;
// If the cursor is explicitly not visible by terminal mode, we don't render.
if (!state.cursor.visible) return null;
// If we're not focused, our cursor is always visible so that
// we can show the hollow box.
if (!opts.focused) return .block_hollow;
// If the cursor is blinking and our blink state is not visible,
// then we don't show the cursor.
if (state.cursor.blinking and !opts.blink_visible) return null;
// Otherwise, we use whatever style the terminal wants.
return .fromTerminal(state.cursor.visual_style);
}
test "cursor: default uses configured style" {
const testing = std.testing;
const alloc = testing.allocator;
var term: terminal.Terminal = try .init(alloc, .{ .cols = 10, .rows = 10 });
defer term.deinit(alloc);
term.screens.active.cursor.cursor_style = .bar;
term.modes.set(.cursor_blinking, true);
var state: terminal.RenderState = .empty;
defer state.deinit(alloc);
try state.update(alloc, &term);
try testing.expect(style(&state, .{ .preedit = false, .focused = true, .blink_visible = true }) == .bar);
try testing.expect(style(&state, .{ .preedit = false, .focused = false, .blink_visible = true }) == .block_hollow);
try testing.expect(style(&state, .{ .preedit = false, .focused = false, .blink_visible = false }) == .block_hollow);
try testing.expect(style(&state, .{ .preedit = false, .focused = true, .blink_visible = false }) == null);
}
test "cursor: blinking disabled" {
const testing = std.testing;
const alloc = testing.allocator;
var term = try terminal.Terminal.init(alloc, .{ .cols = 10, .rows = 10 });
defer term.deinit(alloc);
term.screens.active.cursor.cursor_style = .bar;
term.modes.set(.cursor_blinking, false);
var state: terminal.RenderState = .empty;
defer state.deinit(alloc);
try state.update(alloc, &term);
try testing.expect(style(&state, .{ .focused = true, .blink_visible = true }) == .bar);
try testing.expect(style(&state, .{ .focused = true, .blink_visible = false }) == .bar);
try testing.expect(style(&state, .{ .focused = false, .blink_visible = true }) == .block_hollow);
try testing.expect(style(&state, .{ .focused = false, .blink_visible = false }) == .block_hollow);
}
test "cursor: explicitly not visible" {
const testing = std.testing;
const alloc = testing.allocator;
var term = try terminal.Terminal.init(alloc, .{ .cols = 10, .rows = 10 });
defer term.deinit(alloc);
term.screens.active.cursor.cursor_style = .bar;
term.modes.set(.cursor_visible, false);
term.modes.set(.cursor_blinking, false);
var state: terminal.RenderState = .empty;
defer state.deinit(alloc);
try state.update(alloc, &term);
try testing.expect(style(&state, .{ .focused = true, .blink_visible = true }) == null);
try testing.expect(style(&state, .{ .focused = true, .blink_visible = false }) == null);
try testing.expect(style(&state, .{ .focused = false, .blink_visible = true }) == null);
try testing.expect(style(&state, .{ .focused = false, .blink_visible = false }) == null);
}
test "cursor: always block with preedit" {
const testing = std.testing;
const alloc = testing.allocator;
var term = try terminal.Terminal.init(alloc, .{ .cols = 10, .rows = 10 });
defer term.deinit(alloc);
var state: terminal.RenderState = .empty;
defer state.deinit(alloc);
try state.update(alloc, &term);
// In any bool state
try testing.expect(style(&state, .{ .preedit = true, .focused = false, .blink_visible = false }) == .block);
try testing.expect(style(&state, .{ .preedit = true, .focused = true, .blink_visible = false }) == .block);
try testing.expect(style(&state, .{ .preedit = true, .focused = true, .blink_visible = true }) == .block);
try testing.expect(style(&state, .{ .preedit = true, .focused = false, .blink_visible = true }) == .block);
// If we're scrolled though, then we don't show the cursor.
for (0..100) |_| try term.index();
term.scrollViewport(.{ .top = {} });
try state.update(alloc, &term);
// In any bool state
try testing.expect(style(&state, .{ .preedit = true, .focused = false, .blink_visible = false }) == null);
try testing.expect(style(&state, .{ .preedit = true, .focused = true, .blink_visible = false }) == null);
try testing.expect(style(&state, .{ .preedit = true, .focused = true, .blink_visible = true }) == null);
try testing.expect(style(&state, .{ .preedit = true, .focused = false, .blink_visible = true }) == null);
}