feat(cursor): add vintage cursor style with configurable height
Add new `vintage` cursor style that renders a partial-height block from the bottom of the cell, similar to classic terminal cursors. New config options: - cursor-style = vintage - cursor-style-vintage-height = 1..100 (percent of cell height, default 25) Height is encoded in the sprite codepoint offset so each height value gets its own cache entry. The renderer injects cursor_vintage_height into grid_metrics before sprite rendering so Face.zig passes the correct value to the draw function. Sprite cache is invalidated on config reload so height changes apply immediately. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>pull/12860/head
parent
90175950d5
commit
938f20b78d
|
|
@ -872,8 +872,15 @@ palette: Palette = .{},
|
|||
/// * `bar`
|
||||
/// * `underline`
|
||||
/// * `block_hollow`
|
||||
/// * `vintage`
|
||||
@"cursor-style": terminal.CursorStyle = .block,
|
||||
|
||||
/// The height of the vintage cursor as a percentage of the cell height.
|
||||
/// This only takes effect when `cursor-style` is set to `vintage`.
|
||||
///
|
||||
/// Valid values are integers from 1 to 100.
|
||||
@"cursor-style-vintage-height": u8 = 25,
|
||||
|
||||
/// Sets the default blinking state of the cursor. This is just the default
|
||||
/// state; running programs may override the cursor style using `DECSCUSR` (`CSI
|
||||
/// q`).
|
||||
|
|
|
|||
|
|
@ -36,6 +36,9 @@ cursor_thickness: u32 = 1,
|
|||
/// The height in pixels of the cursor sprite.
|
||||
cursor_height: u32,
|
||||
|
||||
/// The height of the vintage cursor as a percentage (1-100).
|
||||
cursor_vintage_height: u32 = 25,
|
||||
|
||||
/// The constraint height for nerd fonts icons.
|
||||
icon_height: f64,
|
||||
|
||||
|
|
@ -619,6 +622,7 @@ fn init() Metrics {
|
|||
.overline_thickness = 0,
|
||||
.box_thickness = 0,
|
||||
.cursor_height = 0,
|
||||
.cursor_vintage_height = 25,
|
||||
.icon_height = 0.0,
|
||||
.icon_height_single = 0.0,
|
||||
.face_width = 0.0,
|
||||
|
|
|
|||
|
|
@ -116,6 +116,21 @@ pub fn init(
|
|||
}
|
||||
|
||||
/// Deinit. Assumes no concurrent access so no lock is taken.
|
||||
/// Remove all cached sprite glyphs so they are redrawn on next use.
|
||||
/// Call this when sprite drawing parameters change (e.g. cursor height).
|
||||
pub fn invalidateSpriteGlyphs(self: *SharedGrid, alloc: Allocator) void {
|
||||
self.lock.lock();
|
||||
defer self.lock.unlock();
|
||||
var it = self.glyphs.iterator();
|
||||
while (it.next()) |entry| {
|
||||
if (entry.key_ptr.index.special() == .sprite) {
|
||||
self.glyphs.removeByPtr(entry.key_ptr);
|
||||
it = self.glyphs.iterator();
|
||||
}
|
||||
}
|
||||
_ = alloc;
|
||||
}
|
||||
|
||||
pub fn deinit(self: *SharedGrid, alloc: Allocator) void {
|
||||
self.codepoints.deinit(alloc);
|
||||
self.glyphs.deinit(alloc);
|
||||
|
|
|
|||
|
|
@ -33,6 +33,10 @@ pub const Sprite = enum(u32) {
|
|||
cursor_hollow_rect,
|
||||
cursor_bar,
|
||||
cursor_underline,
|
||||
// cursor_vintage is the base codepoint for vintage cursor sprites.
|
||||
// Heights 1..100 are encoded as cursor_vintage+0 .. cursor_vintage+99.
|
||||
// Use sprite.cursorVintageCp() to get the codepoint for a given height.
|
||||
cursor_vintage,
|
||||
|
||||
test {
|
||||
const testing = std.testing;
|
||||
|
|
@ -40,6 +44,13 @@ pub const Sprite = enum(u32) {
|
|||
}
|
||||
};
|
||||
|
||||
/// Returns the codepoint for a vintage cursor of the given height percent.
|
||||
/// Height 1..100 maps to cursor_vintage+0 .. cursor_vintage+99.
|
||||
pub fn cursorVintageCp(height_pct: u32) u32 {
|
||||
const h = @max(1, @min(100, height_pct));
|
||||
return @intFromEnum(Sprite.cursor_vintage) + h - 1;
|
||||
}
|
||||
|
||||
test {
|
||||
@import("std").testing.refAllDecls(@This());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -145,6 +145,13 @@ const ranges: []const Range = ranges: {
|
|||
};
|
||||
|
||||
fn getDrawFn(cp: u32) ?*const DrawFn {
|
||||
// cursor_vintage occupies a range of 100 codepoints (heights 1..100).
|
||||
if (cp >= @intFromEnum(Sprite.cursor_vintage) and
|
||||
cp <= font.sprite.cursorVintageCp(100))
|
||||
{
|
||||
return special.cursor_vintage;
|
||||
}
|
||||
|
||||
// For special sprites (cursors, underlines, etc.) all sprites are drawn
|
||||
// by functions from `Special` that share the name of the enum field.
|
||||
if (cp >= Sprite.start) switch (@as(Sprite, @enumFromInt(cp))) {
|
||||
|
|
@ -214,7 +221,15 @@ pub fn renderGlyph(
|
|||
var canvas = try font.sprite.Canvas.init(alloc, width, height, padding_x, padding_y);
|
||||
defer canvas.deinit();
|
||||
|
||||
try draw(cp, &canvas, width, height, metrics);
|
||||
// For cursor_vintage we pass opts.grid_metrics so the caller can
|
||||
// inject cursor_vintage_height without affecting the cache key.
|
||||
const draw_metrics = if (cp >= @intFromEnum(Sprite.cursor_vintage) and
|
||||
cp <= font.sprite.cursorVintageCp(100))
|
||||
opts.grid_metrics
|
||||
else
|
||||
metrics;
|
||||
|
||||
try draw(cp, &canvas, width, height, draw_metrics);
|
||||
|
||||
// Write the drawing to the atlas
|
||||
const region = try canvas.writeAtlas(alloc, atlas);
|
||||
|
|
|
|||
|
|
@ -370,3 +370,23 @@ pub fn cursor_underline(
|
|||
.height = @intCast(metrics.cursor_thickness),
|
||||
}, .on);
|
||||
}
|
||||
|
||||
pub fn cursor_vintage(
|
||||
cp: u32,
|
||||
canvas: *font.sprite.Canvas,
|
||||
width: u32,
|
||||
height: u32,
|
||||
metrics: font.Metrics,
|
||||
) !void {
|
||||
_ = cp;
|
||||
|
||||
const pct: u32 = @max(1, @min(100, metrics.cursor_vintage_height));
|
||||
const cursor_h: u32 = @max(1, height * pct / 100);
|
||||
const y: u32 = height -| cursor_h;
|
||||
canvas.rect(.{
|
||||
.x = 0,
|
||||
.y = @intCast(y),
|
||||
.width = @intCast(width),
|
||||
.height = @intCast(cursor_h),
|
||||
}, .on);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -152,7 +152,12 @@ pub const Contents = struct {
|
|||
// Block cursors should be drawn first
|
||||
.block => self.fg_rows.lists[0].appendAssumeCapacity(cell),
|
||||
// Other cursor styles should be drawn last
|
||||
.block_hollow, .bar, .underline, .lock => self.fg_rows.lists[self.size.rows + 1].appendAssumeCapacity(cell),
|
||||
.block_hollow,
|
||||
.bar,
|
||||
.underline,
|
||||
.vintage,
|
||||
.lock,
|
||||
=> self.fg_rows.lists[self.size.rows + 1].appendAssumeCapacity(cell),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ pub const Style = enum {
|
|||
block_hollow,
|
||||
bar,
|
||||
underline,
|
||||
vintage,
|
||||
|
||||
// Special cursor styles
|
||||
lock,
|
||||
|
|
@ -21,6 +22,7 @@ pub const Style = enum {
|
|||
.block => .block,
|
||||
.block_hollow => .block_hollow,
|
||||
.underline => .underline,
|
||||
.vintage => .vintage,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -544,6 +544,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
|||
cursor_color: ?configpkg.Config.TerminalColor,
|
||||
cursor_opacity: f64,
|
||||
cursor_text: ?configpkg.Config.TerminalColor,
|
||||
cursor_vintage_height: u32,
|
||||
background: terminal.color.RGB,
|
||||
background_opacity: f64,
|
||||
background_opacity_cells: bool,
|
||||
|
|
@ -616,6 +617,9 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
|||
.cursor_color = config.@"cursor-color",
|
||||
.cursor_text = config.@"cursor-text",
|
||||
.cursor_opacity = @max(0, @min(1, config.@"cursor-opacity")),
|
||||
.cursor_vintage_height = @max(
|
||||
1, @min(100, config.@"cursor-style-vintage-height"),
|
||||
),
|
||||
|
||||
.background = config.background.toTerminalRGB(),
|
||||
.foreground = config.foreground.toTerminalRGB(),
|
||||
|
|
@ -1888,6 +1892,9 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
|||
|
||||
self.config.deinit();
|
||||
self.config = config.*;
|
||||
// Always invalidate sprite cache on config change so cursor
|
||||
// vintage height is always re-rendered with current settings.
|
||||
self.font_grid.invalidateSpriteGlyphs(self.alloc);
|
||||
|
||||
// If our background image path changed, prepare the new bg image.
|
||||
if (bg_image_changed) try self.prepBackgroundImage();
|
||||
|
|
@ -3259,7 +3266,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
|||
.block_hollow => .cursor_hollow_rect,
|
||||
.bar => .cursor_bar,
|
||||
.underline => .cursor_underline,
|
||||
.lock => unreachable,
|
||||
.lock, .vintage => unreachable,
|
||||
};
|
||||
|
||||
break :render self.font_grid.renderGlyph(
|
||||
|
|
@ -3276,6 +3283,29 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
|||
};
|
||||
},
|
||||
|
||||
.vintage => render: {
|
||||
var vintage_metrics = self.grid_metrics;
|
||||
vintage_metrics.cursor_vintage_height =
|
||||
self.config.cursor_vintage_height;
|
||||
break :render self.font_grid.renderGlyph(
|
||||
self.alloc,
|
||||
font.sprite_index,
|
||||
font.sprite.cursorVintageCp(
|
||||
self.config.cursor_vintage_height,
|
||||
),
|
||||
.{
|
||||
.cell_width = if (wide) 2 else 1,
|
||||
.grid_metrics = vintage_metrics,
|
||||
},
|
||||
) catch |err| {
|
||||
log.warn(
|
||||
"error rendering vintage cursor err={}",
|
||||
.{err},
|
||||
);
|
||||
return;
|
||||
};
|
||||
},
|
||||
|
||||
.lock => self.font_grid.renderCodepoint(
|
||||
self.alloc,
|
||||
0xF023, // lock symbol
|
||||
|
|
|
|||
|
|
@ -83,6 +83,8 @@ pub const CursorVisualStyle = enum(c_int) {
|
|||
.block => .block,
|
||||
.underline => .underline,
|
||||
.block_hollow => .block_hollow,
|
||||
// vintage is a Ghostty-only style; report as underline
|
||||
.vintage => .underline,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -12,4 +12,9 @@ pub const Style = enum {
|
|||
/// Hollow block cursor. This is a block cursor with the center empty.
|
||||
/// Reported as DECSCUSR 1 or 2 (block).
|
||||
block_hollow,
|
||||
|
||||
/// Vintage cursor. A partial-height block that fills the cell from
|
||||
/// the bottom up. Height is controlled by cursor-style-vintage-height.
|
||||
/// Reported as DECSCUSR 3 or 4 (underline).
|
||||
vintage,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -504,6 +504,7 @@ pub const StreamHandler = struct {
|
|||
// Below here, the cursor styles aren't represented by
|
||||
// DECSCUSR so we map it to some other style.
|
||||
.block_hollow => if (blink) 1 else 2,
|
||||
.vintage => if (blink) 3 else 4,
|
||||
};
|
||||
try writer.print("{d} q", .{style});
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in New Issue