From 424740d350ee65cfd7c547024f1d16cfe09f9e88 Mon Sep 17 00:00:00 2001 From: Yves Biener Date: Wed, 19 Nov 2025 18:42:13 +0100 Subject: [PATCH] feat(terminal/osc12): define cursor color through style of cell that describes the cursor position The `.default` color will reset the cursor color to the terminal's default color --- examples/continuous.zig | 4 ++-- examples/demo.zig | 8 ++++---- examples/elements/button.zig | 2 +- examples/elements/input.zig | 10 +++++----- examples/elements/scrollable.zig | 6 +++--- src/app.zig | 2 ++ src/cell.zig | 4 ++-- src/color.zig | 14 +++++++------- src/ctlseqs.zig | 1 + src/element.zig | 19 +++++++++++++------ src/render.zig | 1 + src/style.zig | 10 ++++++++++ src/terminal.zig | 4 ++++ 13 files changed, 55 insertions(+), 30 deletions(-) diff --git a/examples/continuous.zig b/examples/continuous.zig index 49f1f55..b08cc7d 100644 --- a/examples/continuous.zig +++ b/examples/continuous.zig @@ -156,7 +156,7 @@ pub fn main() !void { defer container.deinit(); try container.append(try App.Container.init(allocator, .{ - .rectangle = .{ .fill = .light_grey }, + .rectangle = .{ .fill = .lightgrey }, .size = .{ .grow = .horizontal, .dim = .{ .y = 10 }, @@ -164,7 +164,7 @@ pub fn main() !void { }, input_field.element())); const nested_container: App.Container = try .init(allocator, .{ - .rectangle = .{ .fill = .light_grey }, + .rectangle = .{ .fill = .lightgrey }, }, spinner.element()); try container.append(nested_container); diff --git a/examples/demo.zig b/examples/demo.zig index b9d1ae3..194b65a 100644 --- a/examples/demo.zig +++ b/examples/demo.zig @@ -58,13 +58,13 @@ pub fn main() !void { }, }, .{}); try box.append(try App.Container.init(allocator, .{ - .rectangle = .{ .fill = .light_green }, + .rectangle = .{ .fill = .lightgreen }, }, .{})); try box.append(try App.Container.init(allocator, .{ - .rectangle = .{ .fill = .light_green }, + .rectangle = .{ .fill = .lightgreen }, }, .{})); try box.append(try App.Container.init(allocator, .{ - .rectangle = .{ .fill = .light_green }, + .rectangle = .{ .fill = .lightgreen }, }, .{})); defer box.deinit(); @@ -93,7 +93,7 @@ pub fn main() !void { .direction = .vertical, }, .border = .{ - .color = .light_blue, + .color = .lightblue, .sides = .all, }, }, .{}); diff --git a/examples/elements/button.zig b/examples/elements/button.zig index 57f250b..31ad4b4 100644 --- a/examples/elements/button.zig +++ b/examples/elements/button.zig @@ -102,7 +102,7 @@ pub fn main() !void { }, quit_text.element()); 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 app.start(); diff --git a/examples/elements/input.zig b/examples/elements/input.zig index bf76603..a0263a0 100644 --- a/examples/elements/input.zig +++ b/examples/elements/input.zig @@ -72,7 +72,7 @@ pub fn main() !void { var renderer = zterm.Renderer.Buffered.init(allocator); 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(); var mouse_draw: MouseDraw = .{}; @@ -89,7 +89,7 @@ pub fn main() !void { defer container.deinit(); try container.append(try App.Container.init(allocator, .{ - .rectangle = .{ .fill = .light_grey }, + .rectangle = .{ .fill = .lightgrey }, .size = .{ .grow = .horizontal, .dim = .{ .y = 1 }, @@ -101,7 +101,7 @@ pub fn main() !void { .sides = .all, .color = .black, }, - .rectangle = .{ .fill = .light_grey }, + .rectangle = .{ .fill = .lightgrey }, .layout = .{ .separator = .{ .enabled = true, @@ -110,10 +110,10 @@ pub fn main() !void { }, }, .{}); try nested_container.append(try .init(allocator, .{ - .rectangle = .{ .fill = .light_grey }, + .rectangle = .{ .fill = .lightgrey }, }, mouse_draw.element())); try nested_container.append(try .init(allocator, .{ - .rectangle = .{ .fill = .light_grey }, + .rectangle = .{ .fill = .lightgrey }, }, second_mouse_draw.element())); try container.append(nested_container); diff --git a/examples/elements/scrollable.zig b/examples/elements/scrollable.zig index 821b693..b48b4d7 100644 --- a/examples/elements/scrollable.zig +++ b/examples/elements/scrollable.zig @@ -89,19 +89,19 @@ pub fn main() !void { }, }, .{}); try top_box.append(try App.Container.init(allocator, .{ - .rectangle = .{ .fill = .light_green }, + .rectangle = .{ .fill = .lightgreen }, .size = .{ .dim = .{ .y = 30 }, }, }, .{})); try top_box.append(try App.Container.init(allocator, .{ - .rectangle = .{ .fill = .light_green }, + .rectangle = .{ .fill = .lightgreen }, .size = .{ .dim = .{ .y = 5 }, }, }, element)); try top_box.append(try App.Container.init(allocator, .{ - .rectangle = .{ .fill = .light_green }, + .rectangle = .{ .fill = .lightgreen }, .size = .{ .dim = .{ .y = 2 }, }, diff --git a/src/app.zig b/src/app.zig index 76bae82..b38779e 100644 --- a/src/app.zig +++ b/src/app.zig @@ -93,6 +93,7 @@ pub fn App(comptime M: type, comptime E: type) type { if (this.termios) |termios| { try terminal.disableMouseSupport(); try terminal.showCursor(); + try terminal.resetCursorColor(); try terminal.restoreScreen(); try terminal.disableRawMode(&termios); 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 { terminal.disableMouseSupport() catch {}; terminal.showCursor() catch {}; + terminal.resetCursorColor() catch {}; terminal.restoreScreen() catch {}; terminal.disableRawMode(&.{ .iflag = .{}, diff --git a/src/cell.zig b/src/cell.zig index 271f805..8d623cb 100644 --- a/src/cell.zig +++ b/src/cell.zig @@ -25,7 +25,7 @@ test "ascii styled text" { .{ .cp = 'Y', .style = .{ .fg = .green, .bg = .grey, .emphasis = &.{} } }, .{ .cp = 'v', .style = .{ .emphasis = &.{ .bold, .underline } } }, .{ .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); @@ -45,7 +45,7 @@ test "utf-8 styled text" { .{ .cp = '╭', .style = .{ .fg = .green, .bg = .grey, .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); diff --git a/src/color.zig b/src/color.zig index 4ee7c88..f79dff3 100644 --- a/src/color.zig +++ b/src/color.zig @@ -1,13 +1,13 @@ pub const Color = enum(u8) { default = 0, black = 16, - light_red = 1, - light_green, - light_yellow, - light_blue, - light_magenta, - light_cyan, - light_grey, + lightred = 1, + lightgreen, + lightyellow, + lightblue, + lightmagenta, + lightcyan, + lightgrey, grey, red, green, diff --git a/src/ctlseqs.zig b/src/ctlseqs.zig index a8c6e05..2a246b2 100644 --- a/src/ctlseqs.zig +++ b/src/ctlseqs.zig @@ -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_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 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_reset = "\x1b]112\x1b\\"; // reset cursor to terminal default diff --git a/src/element.zig b/src/element.zig index c564acf..3fa45d9 100644 --- a/src/element.zig +++ b/src/element.zig @@ -519,9 +519,13 @@ pub fn Input(Model: type, Event: type, Queue: type) fn (meta.FieldEnum(Event)) t /// Configuration for InputField's. pub const Configuration = packed struct { color: Color, + cursor: Color, - pub fn init(color: Color) @This() { - return .{ .color = color }; + pub fn init(color: Color, cursor: Color) @This() { + return .{ + .color = color, + .cursor = cursor, + }; } }; @@ -680,10 +684,13 @@ pub fn Input(Model: type, Event: type, Queue: type) fn (meta.FieldEnum(Event)) t break; } } - if (this.input.items.len < cells.len) - cells[this.input.items.len - this.cursor_offset].style.cursor = true - else + 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_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_color = this.configuration.cursor; + } } }; } @@ -1615,7 +1622,7 @@ test "input element" { }; 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(); const input_container: Container(Model, Event) = try .init(allocator, .{ diff --git a/src/render.zig b/src/render.zig index 20cebb8..9527607 100644 --- a/src/render.zig +++ b/src/render.zig @@ -104,6 +104,7 @@ pub const Buffered = struct { .x = @truncate(col), .y = @truncate(row), }; + try cvs.style.set_cursor_color(&writer); } if (cs.eql(cvs)) continue; diff --git a/src/style.zig b/src/style.zig index 5707d49..ec23997 100644 --- a/src/style.zig +++ b/src/style.zig @@ -12,6 +12,7 @@ fg: Color = .default, bg: Color = .default, ul: Color = .default, cursor: bool = false, +cursor_color: Color = .default, ul_style: Underline = .off, emphasis: []const Emphasis, @@ -40,6 +41,14 @@ pub fn eql(this: Style, other: Style) bool { 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 { var buffer: [4]u8 = undefined; const bytes = try unicode.utf8Encode(cp, &buffer); @@ -72,4 +81,5 @@ const unicode = std.unicode; const meta = std.meta; const assert = std.debug.assert; const Color = @import("color.zig").Color; +const ctlseqs = @import("ctlseqs.zig"); const Style = @This(); diff --git a/src/terminal.zig b/src/terminal.zig index 4a70f95..6170bb8 100644 --- a/src/terminal.zig +++ b/src/terminal.zig @@ -42,6 +42,10 @@ pub fn showCursor() !void { _ = 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 { _ = try posix.write(posix.STDIN_FILENO, ctlseqs.home); }