feat(terminal/osc12): define cursor color through style of cell that describes the cursor position
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 1m33s

The `.default` color will reset the cursor color to the terminal's default color
This commit is contained in:
2025-11-19 18:42:13 +01:00
parent 28c733352e
commit 424740d350
13 changed files with 55 additions and 30 deletions

View File

@@ -156,7 +156,7 @@ pub fn main() !void {
defer container.deinit(); defer container.deinit();
try container.append(try App.Container.init(allocator, .{ try container.append(try App.Container.init(allocator, .{
.rectangle = .{ .fill = .light_grey }, .rectangle = .{ .fill = .lightgrey },
.size = .{ .size = .{
.grow = .horizontal, .grow = .horizontal,
.dim = .{ .y = 10 }, .dim = .{ .y = 10 },
@@ -164,7 +164,7 @@ pub fn main() !void {
}, input_field.element())); }, input_field.element()));
const nested_container: App.Container = try .init(allocator, .{ const nested_container: App.Container = try .init(allocator, .{
.rectangle = .{ .fill = .light_grey }, .rectangle = .{ .fill = .lightgrey },
}, spinner.element()); }, spinner.element());
try container.append(nested_container); try container.append(nested_container);

View File

@@ -58,13 +58,13 @@ pub fn main() !void {
}, },
}, .{}); }, .{});
try box.append(try App.Container.init(allocator, .{ try box.append(try App.Container.init(allocator, .{
.rectangle = .{ .fill = .light_green }, .rectangle = .{ .fill = .lightgreen },
}, .{})); }, .{}));
try box.append(try App.Container.init(allocator, .{ try box.append(try App.Container.init(allocator, .{
.rectangle = .{ .fill = .light_green }, .rectangle = .{ .fill = .lightgreen },
}, .{})); }, .{}));
try box.append(try App.Container.init(allocator, .{ try box.append(try App.Container.init(allocator, .{
.rectangle = .{ .fill = .light_green }, .rectangle = .{ .fill = .lightgreen },
}, .{})); }, .{}));
defer box.deinit(); defer box.deinit();
@@ -93,7 +93,7 @@ pub fn main() !void {
.direction = .vertical, .direction = .vertical,
}, },
.border = .{ .border = .{
.color = .light_blue, .color = .lightblue,
.sides = .all, .sides = .all,
}, },
}, .{}); }, .{});

View File

@@ -102,7 +102,7 @@ pub fn main() !void {
}, quit_text.element()); }, quit_text.element());
defer container.deinit(); defer container.deinit();
try container.append(try App.Container.init(allocator, .{ .rectangle = .{ .fill = .light_grey } }, element)); try container.append(try App.Container.init(allocator, .{ .rectangle = .{ .fill = .lightgrey } }, element));
try container.append(try App.Container.init(allocator, .{ .rectangle = .{ .fill = .black } }, button.element())); try container.append(try App.Container.init(allocator, .{ .rectangle = .{ .fill = .black } }, button.element()));
try app.start(); try app.start();

View File

@@ -72,7 +72,7 @@ pub fn main() !void {
var renderer = zterm.Renderer.Buffered.init(allocator); var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit(); defer renderer.deinit();
var input_field: App.Input(.accept) = .init(allocator, &app.queue, .init(.black)); var input_field: App.Input(.accept) = .init(allocator, &app.queue, .init(.black, .blue));
defer input_field.deinit(); defer input_field.deinit();
var mouse_draw: MouseDraw = .{}; var mouse_draw: MouseDraw = .{};
@@ -89,7 +89,7 @@ pub fn main() !void {
defer container.deinit(); defer container.deinit();
try container.append(try App.Container.init(allocator, .{ try container.append(try App.Container.init(allocator, .{
.rectangle = .{ .fill = .light_grey }, .rectangle = .{ .fill = .lightgrey },
.size = .{ .size = .{
.grow = .horizontal, .grow = .horizontal,
.dim = .{ .y = 1 }, .dim = .{ .y = 1 },
@@ -101,7 +101,7 @@ pub fn main() !void {
.sides = .all, .sides = .all,
.color = .black, .color = .black,
}, },
.rectangle = .{ .fill = .light_grey }, .rectangle = .{ .fill = .lightgrey },
.layout = .{ .layout = .{
.separator = .{ .separator = .{
.enabled = true, .enabled = true,
@@ -110,10 +110,10 @@ pub fn main() !void {
}, },
}, .{}); }, .{});
try nested_container.append(try .init(allocator, .{ try nested_container.append(try .init(allocator, .{
.rectangle = .{ .fill = .light_grey }, .rectangle = .{ .fill = .lightgrey },
}, mouse_draw.element())); }, mouse_draw.element()));
try nested_container.append(try .init(allocator, .{ try nested_container.append(try .init(allocator, .{
.rectangle = .{ .fill = .light_grey }, .rectangle = .{ .fill = .lightgrey },
}, second_mouse_draw.element())); }, second_mouse_draw.element()));
try container.append(nested_container); try container.append(nested_container);

View File

@@ -89,19 +89,19 @@ pub fn main() !void {
}, },
}, .{}); }, .{});
try top_box.append(try App.Container.init(allocator, .{ try top_box.append(try App.Container.init(allocator, .{
.rectangle = .{ .fill = .light_green }, .rectangle = .{ .fill = .lightgreen },
.size = .{ .size = .{
.dim = .{ .y = 30 }, .dim = .{ .y = 30 },
}, },
}, .{})); }, .{}));
try top_box.append(try App.Container.init(allocator, .{ try top_box.append(try App.Container.init(allocator, .{
.rectangle = .{ .fill = .light_green }, .rectangle = .{ .fill = .lightgreen },
.size = .{ .size = .{
.dim = .{ .y = 5 }, .dim = .{ .y = 5 },
}, },
}, element)); }, element));
try top_box.append(try App.Container.init(allocator, .{ try top_box.append(try App.Container.init(allocator, .{
.rectangle = .{ .fill = .light_green }, .rectangle = .{ .fill = .lightgreen },
.size = .{ .size = .{
.dim = .{ .y = 2 }, .dim = .{ .y = 2 },
}, },

View File

@@ -93,6 +93,7 @@ pub fn App(comptime M: type, comptime E: type) type {
if (this.termios) |termios| { if (this.termios) |termios| {
try terminal.disableMouseSupport(); try terminal.disableMouseSupport();
try terminal.showCursor(); try terminal.showCursor();
try terminal.resetCursorColor();
try terminal.restoreScreen(); try terminal.restoreScreen();
try terminal.disableRawMode(&termios); try terminal.disableRawMode(&termios);
try terminal.exitAltScreen(); try terminal.exitAltScreen();
@@ -426,6 +427,7 @@ pub fn App(comptime M: type, comptime E: type) type {
pub fn panic_handler(msg: []const u8, _: ?*std.builtin.StackTrace, ret_addr: ?usize) noreturn { pub fn panic_handler(msg: []const u8, _: ?*std.builtin.StackTrace, ret_addr: ?usize) noreturn {
terminal.disableMouseSupport() catch {}; terminal.disableMouseSupport() catch {};
terminal.showCursor() catch {}; terminal.showCursor() catch {};
terminal.resetCursorColor() catch {};
terminal.restoreScreen() catch {}; terminal.restoreScreen() catch {};
terminal.disableRawMode(&.{ terminal.disableRawMode(&.{
.iflag = .{}, .iflag = .{},

View File

@@ -25,7 +25,7 @@ test "ascii styled text" {
.{ .cp = 'Y', .style = .{ .fg = .green, .bg = .grey, .emphasis = &.{} } }, .{ .cp = 'Y', .style = .{ .fg = .green, .bg = .grey, .emphasis = &.{} } },
.{ .cp = 'v', .style = .{ .emphasis = &.{ .bold, .underline } } }, .{ .cp = 'v', .style = .{ .emphasis = &.{ .bold, .underline } } },
.{ .cp = 'e', .style = .{ .emphasis = &.{.italic} } }, .{ .cp = 'e', .style = .{ .emphasis = &.{.italic} } },
.{ .cp = 's', .style = .{ .fg = .light_green, .bg = .black, .emphasis = &.{.underline} } }, .{ .cp = 's', .style = .{ .fg = .lightgreen, .bg = .black, .emphasis = &.{.underline} } },
}; };
var writer = std.Io.Writer.Allocating.init(std.testing.allocator); var writer = std.Io.Writer.Allocating.init(std.testing.allocator);
@@ -45,7 +45,7 @@ test "utf-8 styled text" {
.{ .cp = '╭', .style = .{ .fg = .green, .bg = .grey, .emphasis = &.{} } }, .{ .cp = '╭', .style = .{ .fg = .green, .bg = .grey, .emphasis = &.{} } },
.{ .cp = '─', .style = .{ .emphasis = &.{} } }, .{ .cp = '─', .style = .{ .emphasis = &.{} } },
.{ .cp = '┄', .style = .{ .emphasis = &.{} } }, .{ .cp = '┄', .style = .{ .emphasis = &.{} } },
.{ .cp = '┘', .style = .{ .fg = .light_green, .bg = .black, .emphasis = &.{.underline} } }, .{ .cp = '┘', .style = .{ .fg = .lightgreen, .bg = .black, .emphasis = &.{.underline} } },
}; };
var writer = std.Io.Writer.Allocating.init(std.testing.allocator); var writer = std.Io.Writer.Allocating.init(std.testing.allocator);

View File

@@ -1,13 +1,13 @@
pub const Color = enum(u8) { pub const Color = enum(u8) {
default = 0, default = 0,
black = 16, black = 16,
light_red = 1, lightred = 1,
light_green, lightgreen,
light_yellow, lightyellow,
light_blue, lightblue,
light_magenta, lightmagenta,
light_cyan, lightcyan,
light_grey, lightgrey,
grey, grey,
red, red,
green, green,

View File

@@ -139,5 +139,6 @@ pub const osc10_reset = "\x1b]110\x1b\\"; // reset fg to terminal default
pub const osc11_query = "\x1b]11;?\x1b\\"; // bg pub const osc11_query = "\x1b]11;?\x1b\\"; // bg
pub const osc11_set = "\x1b]11;rgb:{x:0>2}{x:0>2}/{x:0>2}{x:0>2}/{x:0>2}{x:0>2}\x1b\\"; // set default terminal bg pub const osc11_set = "\x1b]11;rgb:{x:0>2}{x:0>2}/{x:0>2}{x:0>2}/{x:0>2}{x:0>2}\x1b\\"; // set default terminal bg
pub const osc11_reset = "\x1b]111\x1b\\"; // reset bg to terminal default pub const osc11_reset = "\x1b]111\x1b\\"; // reset bg to terminal default
pub const osc12_set = "\x1b]12;{s}\x1b\\"; // set the cursor color through the name of the 8 base colors!
pub const osc12_query = "\x1b]12;?\x1b\\"; // cursor color pub const osc12_query = "\x1b]12;?\x1b\\"; // cursor color
pub const osc12_reset = "\x1b]112\x1b\\"; // reset cursor to terminal default pub const osc12_reset = "\x1b]112\x1b\\"; // reset cursor to terminal default

View File

@@ -519,9 +519,13 @@ pub fn Input(Model: type, Event: type, Queue: type) fn (meta.FieldEnum(Event)) t
/// Configuration for InputField's. /// Configuration for InputField's.
pub const Configuration = packed struct { pub const Configuration = packed struct {
color: Color, color: Color,
cursor: Color,
pub fn init(color: Color) @This() { pub fn init(color: Color, cursor: Color) @This() {
return .{ .color = color }; return .{
.color = color,
.cursor = cursor,
};
} }
}; };
@@ -680,10 +684,13 @@ pub fn Input(Model: type, Event: type, Queue: type) fn (meta.FieldEnum(Event)) t
break; break;
} }
} }
if (this.input.items.len < cells.len) if (this.input.items.len < cells.len) {
cells[this.input.items.len - this.cursor_offset].style.cursor = true cells[this.input.items.len - this.cursor_offset].style.cursor = true;
else cells[this.input.items.len - this.cursor_offset].style.cursor_color = this.configuration.cursor;
} else {
cells[this.input.items.len - offset - this.cursor_offset].style.cursor = true; cells[this.input.items.len - offset - this.cursor_offset].style.cursor = true;
cells[this.input.items.len - offset - this.cursor_offset].style.cursor_color = this.configuration.cursor;
}
} }
}; };
} }
@@ -1615,7 +1622,7 @@ test "input element" {
}; };
var queue: Queue = .{}; var queue: Queue = .{};
var input_element: Input(Model, Event, Queue)(.accept) = .init(allocator, &queue, .init(.black)); var input_element: Input(Model, Event, Queue)(.accept) = .init(allocator, &queue, .init(.black, .default));
defer input_element.deinit(); defer input_element.deinit();
const input_container: Container(Model, Event) = try .init(allocator, .{ const input_container: Container(Model, Event) = try .init(allocator, .{

View File

@@ -104,6 +104,7 @@ pub const Buffered = struct {
.x = @truncate(col), .x = @truncate(col),
.y = @truncate(row), .y = @truncate(row),
}; };
try cvs.style.set_cursor_color(&writer);
} }
if (cs.eql(cvs)) continue; if (cs.eql(cvs)) continue;

View File

@@ -12,6 +12,7 @@ fg: Color = .default,
bg: Color = .default, bg: Color = .default,
ul: Color = .default, ul: Color = .default,
cursor: bool = false, cursor: bool = false,
cursor_color: Color = .default,
ul_style: Underline = .off, ul_style: Underline = .off,
emphasis: []const Emphasis, emphasis: []const Emphasis,
@@ -40,6 +41,14 @@ pub fn eql(this: Style, other: Style) bool {
return meta.eql(this, other); return meta.eql(this, other);
} }
pub fn set_cursor_color(this: Style, writer: *std.Io.Writer) !void {
if (!this.cursor) return;
switch (this.cursor_color) {
.default => try writer.print(ctlseqs.osc12_reset, .{}),
else => try writer.print(ctlseqs.osc12_set, .{@tagName(this.cursor_color)}),
}
}
pub fn value(this: Style, writer: *std.Io.Writer, cp: u21) !void { pub fn value(this: Style, writer: *std.Io.Writer, cp: u21) !void {
var buffer: [4]u8 = undefined; var buffer: [4]u8 = undefined;
const bytes = try unicode.utf8Encode(cp, &buffer); const bytes = try unicode.utf8Encode(cp, &buffer);
@@ -72,4 +81,5 @@ const unicode = std.unicode;
const meta = std.meta; const meta = std.meta;
const assert = std.debug.assert; const assert = std.debug.assert;
const Color = @import("color.zig").Color; const Color = @import("color.zig").Color;
const ctlseqs = @import("ctlseqs.zig");
const Style = @This(); const Style = @This();

View File

@@ -42,6 +42,10 @@ pub fn showCursor() !void {
_ = try posix.write(posix.STDIN_FILENO, ctlseqs.show_cursor); _ = try posix.write(posix.STDIN_FILENO, ctlseqs.show_cursor);
} }
pub fn resetCursorColor() !void {
_ = try posix.write(posix.STDIN_FILENO, ctlseqs.osc12_reset);
}
pub fn setCursorPositionHome() !void { pub fn setCursorPositionHome() !void {
_ = try posix.write(posix.STDIN_FILENO, ctlseqs.home); _ = try posix.write(posix.STDIN_FILENO, ctlseqs.home);
} }