diff --git a/src/Surface.zig b/src/Surface.zig index 1f5b41e35..1707289d0 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -90,6 +90,11 @@ mouse: Mouse, /// less important. pressed_key: ?input.KeyEvent = null, +/// The current color scheme of the GUI element containing this surface. +/// This will default to light until the apprt sends us the actual color +/// scheme. This is used by mode 3031 and CSI 996 n. +color_scheme: apprt.ColorScheme = .light, + /// The hash value of the last keybinding trigger that we performed. This /// is only set if the last key input matched a keybinding, consumed it, /// and performed it. This is used to prevent sending release/repeat events @@ -832,6 +837,16 @@ pub fn handleMessage(self: *Surface, msg: Message) !void { }, .renderer_health => |health| self.updateRendererHealth(health), + + .report_color_scheme => { + const output = switch (self.color_scheme) { + .light => "\x1B[?997;2n", + .dark => "\x1B[?997;1n", + }; + + _ = self.io_thread.mailbox.push(.{ .write_stable = output }, .{ .forever = {} }); + try self.io_thread.wakeup.notify(); + }, } } @@ -2786,6 +2801,12 @@ fn dragLeftClickBefore( return screen_point.before(click_point); } +/// Call to notify Ghostty that the color scheme for the terminal has +/// changed. +pub fn colorSchemeCallback(self: *Surface, scheme: apprt.ColorScheme) void { + self.color_scheme = scheme; +} + fn posToViewport(self: Surface, xpos: f64, ypos: f64) terminal.point.Viewport { // xpos/ypos need to be adjusted for window padding // (i.e. "window-padding-*" settings. diff --git a/src/apprt/structs.zig b/src/apprt/structs.zig index aaf4738fe..68cd31702 100644 --- a/src/apprt/structs.zig +++ b/src/apprt/structs.zig @@ -61,3 +61,9 @@ pub const DesktopNotification = struct { /// The body of a notification. This will always be shown. body: []const u8, }; + +/// The color scheme in use (light vs dark). +pub const ColorScheme = enum(u2) { + light = 0, + dark = 1, +}; diff --git a/src/apprt/surface.zig b/src/apprt/surface.zig index 463d2f906..3060b7a5c 100644 --- a/src/apprt/surface.zig +++ b/src/apprt/surface.zig @@ -57,6 +57,9 @@ pub const Message = union(enum) { /// Health status change for the renderer. renderer_health: renderer.Health, + + /// Report the color scheme + report_color_scheme: void, }; /// A surface mailbox. diff --git a/src/terminal/device_status.zig b/src/terminal/device_status.zig new file mode 100644 index 000000000..b732f944d --- /dev/null +++ b/src/terminal/device_status.zig @@ -0,0 +1,67 @@ +const std = @import("std"); + +/// An enum(u16) of the available device status requests. +pub const Request = dsr_enum: { + const EnumField = std.builtin.Type.EnumField; + var fields: [entries.len]EnumField = undefined; + for (entries, 0..) |entry, i| { + fields[i] = .{ + .name = entry.name, + .value = @as(Tag.Backing, @bitCast(Tag{ + .value = entry.value, + .question = entry.question, + })), + }; + } + + break :dsr_enum @Type(.{ .Enum = .{ + .tag_type = Tag.Backing, + .fields = &fields, + .decls = &.{}, + .is_exhaustive = true, + } }); +}; + +/// The tag type for our enum is a u16 but we use a packed struct +/// in order to pack the question bit into the tag. The "u16" size is +/// chosen somewhat arbitrarily to match the largest expected size +/// we see as a multiple of 8 bits. +pub const Tag = packed struct(u16) { + pub const Backing = @typeInfo(@This()).Struct.backing_integer.?; + value: u15, + question: bool = false, + + test "order" { + const t: Tag = .{ .value = 1 }; + const int: Backing = @bitCast(t); + try std.testing.expectEqual(@as(Backing, 1), int); + } +}; + +pub fn reqFromInt(v: u16, question: bool) ?Request { + inline for (entries) |entry| { + if (entry.value == v and entry.question == question) { + const tag: Tag = .{ .question = question, .value = entry.value }; + const int: Tag.Backing = @bitCast(tag); + return @enumFromInt(int); + } + } + + return null; +} + +/// A single entry of a possible device status request we support. The +/// "question" field determines if it is valid with or without the "?" +/// prefix. +const Entry = struct { + name: [:0]const u8, + value: comptime_int, + question: bool = false, // "?" request +}; + +/// The full list of device status request entries. +const entries: []const Entry = &.{ + .{ .name = "operating_status", .value = 5 }, + .{ .name = "cursor_position", .value = 6 }, + .{ .name = "theme", .value = 996, .question = true }, +}; diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index 37c61a8aa..319cb1b52 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -2375,7 +2375,7 @@ const StreamHandler = struct { self.messageWriter(msg); }, - .theme => {}, + .theme => self.surfaceMessageWriter(.{ .report_color_scheme = {} }), } }