aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/renderer/cursor.zig
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-04-04 19:22:04 +0300
committerCan Celik <ogulcancelik@gmail.com>2026-04-05 01:23:06 +0300
commit41dd733ca7123c9ac4bfc6c781697a660e95b052 (patch)
tree3fc91815ca139ef2e0b281bba0abda4ef15175f4 /vendor/libghostty-vt/src/renderer/cursor.zig
parent55c138263e190a1e79d4b997bfb9397306c0ec1f (diff)
feat: finish ghostty pane backend integration
Diffstat (limited to 'vendor/libghostty-vt/src/renderer/cursor.zig')
-rw-r--r--vendor/libghostty-vt/src/renderer/cursor.zig154
1 files changed, 154 insertions, 0 deletions
diff --git a/vendor/libghostty-vt/src/renderer/cursor.zig b/vendor/libghostty-vt/src/renderer/cursor.zig
new file mode 100644
index 00000000..33992bc5
--- /dev/null
+++ b/vendor/libghostty-vt/src/renderer/cursor.zig
@@ -0,0 +1,154 @@
+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);
+}