From 3c26828a3fae00e9fd7f915180d490a5a41c69f2 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 2 Feb 2024 12:42:22 -0800 Subject: [PATCH 1/7] terminal: distinguish between DSRs with "?" and not --- src/terminal/ansi.zig | 9 ----- src/terminal/main.zig | 1 + src/terminal/stream.zig | 80 +++++++++++++++++++++++++++-------------- src/termio/Exec.zig | 4 +-- 4 files changed, 57 insertions(+), 37 deletions(-) diff --git a/src/terminal/ansi.zig b/src/terminal/ansi.zig index 27f9971aa..43c2a9a1c 100644 --- a/src/terminal/ansi.zig +++ b/src/terminal/ansi.zig @@ -55,15 +55,6 @@ pub const DeviceAttributeReq = enum { tertiary, // = }; -/// The device status request type (ESC [ n). -pub const DeviceStatusReq = enum(u16) { - operating_status = 5, - cursor_position = 6, - - // Non-exhaustive so that @intToEnum never fails for unsupported modes. - _, -}; - /// Possible cursor styles (ESC [ q) pub const CursorStyle = enum(u16) { default = 0, diff --git a/src/terminal/main.zig b/src/terminal/main.zig index 1ca385c27..0ce4610b5 100644 --- a/src/terminal/main.zig +++ b/src/terminal/main.zig @@ -12,6 +12,7 @@ pub const dcs = @import("dcs.zig"); pub const osc = @import("osc.zig"); pub const point = @import("point.zig"); pub const color = @import("color.zig"); +pub const device_status = @import("device_status.zig"); pub const kitty = @import("kitty.zig"); pub const modes = @import("modes.zig"); pub const parse_table = @import("parse_table.zig"); diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index da8dfa606..f33f52942 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -3,6 +3,7 @@ const testing = std.testing; const Parser = @import("Parser.zig"); const ansi = @import("ansi.zig"); const charsets = @import("charsets.zig"); +const device_status = @import("device_status.zig"); const csi = @import("csi.zig"); const kitty = @import("kitty.zig"); const modes = @import("modes.zig"); @@ -637,36 +638,63 @@ pub fn Stream(comptime Handler: type) type { }, // TODO: test - 'n' => switch (action.intermediates.len) { - 0 => if (@hasDecl(T, "deviceStatusReport")) try self.handler.deviceStatusReport( - switch (action.params.len) { - 1 => @enumFromInt(action.params[0]), - else => { - log.warn("invalid device status report command: {}", .{action}); - return; - }, - }, - ) else log.warn("unimplemented CSI callback: {}", .{action}), + 'n' => { + // Handle deviceStatusReport first + if (action.intermediates.len == 0 or + action.intermediates[0] == '?') + { + if (!@hasDecl(T, "deviceStatusReport")) { + log.warn("unimplemented CSI callback: {}", .{action}); + return; + } - 1 => switch (action.intermediates[0]) { - '>' => if (@hasDecl(T, "setModifyKeyFormat")) { - // This isn't strictly correct. CSI > n has parameters that - // control what exactly is being disabled. However, we - // only support reverting back to modify other keys in - // numeric except format. - try self.handler.setModifyKeyFormat(.{ .other_keys = .numeric_except }); - } else log.warn("unimplemented setModifyKeyFormat: {}", .{action}), + if (action.params.len != 1) { + log.warn("invalid device status report command: {}", .{action}); + return; + } + + const question = question: { + if (action.intermediates.len == 0) break :question false; + if (action.intermediates.len == 1 and + action.intermediates[0] == '?') break :question true; + + log.warn("invalid set mode command: {}", .{action}); + return; + }; + + const req = device_status.reqFromInt(action.params[0], question) orelse { + log.warn("invalid device status report command: {}", .{action}); + return; + }; + + try self.handler.deviceStatusReport(req); + return; + } + + // Handle other forms of CSI n + switch (action.intermediates.len) { + 0 => unreachable, // handled above + + 1 => switch (action.intermediates[0]) { + '>' => if (@hasDecl(T, "setModifyKeyFormat")) { + // This isn't strictly correct. CSI > n has parameters that + // control what exactly is being disabled. However, we + // only support reverting back to modify other keys in + // numeric except format. + try self.handler.setModifyKeyFormat(.{ .other_keys = .numeric_except }); + } else log.warn("unimplemented setModifyKeyFormat: {}", .{action}), + + else => log.warn( + "unknown CSI n with intermediate: {}", + .{action.intermediates[0]}, + ), + }, else => log.warn( - "unknown CSI n with intermediate: {}", - .{action.intermediates[0]}, + "ignoring unimplemented CSI n with intermediates: {s}", + .{action.intermediates}, ), - }, - - else => log.warn( - "ignoring unimplemented CSI n with intermediates: {s}", - .{action.intermediates}, - ), + } }, // DECRQM - Request Mode diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index e98365fd8..37c61a8aa 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -2345,7 +2345,7 @@ const StreamHandler = struct { pub fn deviceStatusReport( self: *StreamHandler, - req: terminal.DeviceStatusReq, + req: terminal.device_status.Request, ) !void { switch (req) { .operating_status => self.messageWriter(.{ .write_stable = "\x1B[0n" }), @@ -2375,7 +2375,7 @@ const StreamHandler = struct { self.messageWriter(msg); }, - else => log.warn("unimplemented device status req: {}", .{req}), + .theme => {}, } } From ae8f5f3ceb3c9faa4eb04e902a9b4930979dc753 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 2 Feb 2024 12:51:20 -0800 Subject: [PATCH 2/7] core: colorSchemeCallback on surface, can report --- src/Surface.zig | 21 +++++++++++ src/apprt/structs.zig | 6 +++ src/apprt/surface.zig | 3 ++ src/terminal/device_status.zig | 67 ++++++++++++++++++++++++++++++++++ src/termio/Exec.zig | 2 +- 5 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 src/terminal/device_status.zig 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 = {} }), } } From 258d51395c331d3999c7e4aac7e2ad00eeb47e92 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 2 Feb 2024 13:02:16 -0800 Subject: [PATCH 3/7] apprt/embedded: add API for reporting color scheme --- include/ghostty.h | 6 ++++++ src/Surface.zig | 2 +- src/apprt/embedded.zig | 20 ++++++++++++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/include/ghostty.h b/include/ghostty.h index 2e5a01eef..db62fbc31 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -144,6 +144,11 @@ typedef enum { GHOSTTY_TAB_NEXT = -2, } ghostty_tab_e; +typedef enum { + GHOSTTY_COLOR_SCHEME_LIGHT = 0, + GHOSTTY_COLOR_SCHEME_DARK = 1, +} ghostty_color_scheme_e; + // This is a packed struct (see src/input/mouse.zig) but the C standard // afaik doesn't let us reliably define packed structs so we build it up // from scratch. @@ -475,6 +480,7 @@ void ghostty_surface_refresh(ghostty_surface_t); void ghostty_surface_set_content_scale(ghostty_surface_t, double, double); void ghostty_surface_set_focus(ghostty_surface_t, bool); void ghostty_surface_set_size(ghostty_surface_t, uint32_t, uint32_t); +void ghostty_surface_set_color_scheme(ghostty_surface_t, ghostty_color_scheme_e); ghostty_input_mods_e ghostty_surface_key_translation_mods(ghostty_surface_t, ghostty_input_mods_e); void ghostty_surface_key(ghostty_surface_t, ghostty_input_key_s); void ghostty_surface_text(ghostty_surface_t, const char *, uintptr_t); diff --git a/src/Surface.zig b/src/Surface.zig index 1707289d0..23dd0064a 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -2803,7 +2803,7 @@ fn dragLeftClickBefore( /// Call to notify Ghostty that the color scheme for the terminal has /// changed. -pub fn colorSchemeCallback(self: *Surface, scheme: apprt.ColorScheme) void { +pub fn colorSchemeCallback(self: *Surface, scheme: apprt.ColorScheme) !void { self.color_scheme = scheme; } diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index 66681df58..26473646f 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -689,6 +689,13 @@ pub const Surface = struct { }; } + pub fn colorSchemeCallback(self: *Surface, scheme: apprt.ColorScheme) void { + self.core_surface.colorSchemeCallback(scheme) catch |err| { + log.err("error setting color scheme err={}", .{err}); + return; + }; + } + pub fn mouseButtonCallback( self: *Surface, action: input.MouseButtonState, @@ -1516,6 +1523,19 @@ pub const CAPI = struct { surface.updateSize(w, h); } + /// Update the color scheme of the surface. + export fn ghostty_surface_set_color_scheme(surface: *Surface, scheme_raw: c_int) void { + const scheme = std.meta.intToEnum(apprt.ColorScheme, scheme_raw) catch { + log.warn( + "invalid color scheme to ghostty_surface_set_color_scheme value={}", + .{scheme_raw}, + ); + return; + }; + + surface.colorSchemeCallback(scheme); + } + /// Update the content scale of the surface. export fn ghostty_surface_set_content_scale(surface: *Surface, x: f64, y: f64) void { surface.updateContentScale(x, y); From 6fe83760734bf4b18fd2f27d72c8e40f993c3b3d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 2 Feb 2024 13:08:10 -0800 Subject: [PATCH 4/7] terminal: support mode 2031 --- src/Surface.zig | 33 ++++++++++++++++++++++++--------- src/terminal/modes.zig | 1 + 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 23dd0064a..59b8b803d 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -838,18 +838,21 @@ 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(); - }, + .report_color_scheme => try self.reportColorScheme(), } } +/// Sends a DSR response for the current color scheme to the pty. +fn reportColorScheme(self: *const Surface) !void { + 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(); +} + /// Call this when modifiers change. This is safe to call even if modifiers /// match the previous state. /// @@ -2804,7 +2807,19 @@ fn dragLeftClickBefore( /// Call to notify Ghostty that the color scheme for the terminal has /// changed. pub fn colorSchemeCallback(self: *Surface, scheme: apprt.ColorScheme) !void { + // If our scheme didn't change, then we don't do anything. + if (self.color_scheme == scheme) return; + + // Set our new scheme self.color_scheme = scheme; + + // If mode 2031 is on, then we report the change live. + const report = report: { + self.renderer_state.mutex.lock(); + defer self.renderer_state.mutex.unlock(); + break :report self.renderer_state.terminal.modes.get(.report_color_scheme); + }; + if (report) try self.reportColorScheme(); } fn posToViewport(self: Surface, xpos: f64, ypos: f64) terminal.point.Viewport { diff --git a/src/terminal/modes.zig b/src/terminal/modes.zig index dab87c718..e42efa16e 100644 --- a/src/terminal/modes.zig +++ b/src/terminal/modes.zig @@ -215,6 +215,7 @@ const entries: []const ModeEntry = &.{ .{ .name = "bracketed_paste", .value = 2004 }, .{ .name = "synchronized_output", .value = 2026 }, .{ .name = "grapheme_cluster", .value = 2027 }, + .{ .name = "report_color_scheme", .value = 2031 }, }; test { From 62785d9e541691c62acdb05fa616dd9783afe35b Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 2 Feb 2024 13:19:46 -0800 Subject: [PATCH 5/7] macos: report color scheme for surface to libghostty --- .../Sources/Ghostty/SurfaceView_AppKit.swift | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift index 251ddeaa8..40d4795cc 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -74,6 +74,7 @@ extension Ghostty { private(set) var focused: Bool = true private var cursor: NSCursor = .iBeam private var cursorVisible: CursorVisibility = .visible + private var appearanceObserver: NSKeyValueObservation? = nil // This is set to non-null during keyDown to accumulate insertText contents private var keyTextAccumulator: [String]? = nil @@ -123,6 +124,26 @@ extension Ghostty { // Setup our tracking area so we get mouse moved events updateTrackingAreas() + + // Observe our appearance so we can report the correct value to libghostty. + // This is the best way I know of to get appearance change notifications. + self.appearanceObserver = observe(\.effectiveAppearance, options: [.new, .initial]) { view, change in + guard let appearance = change.newValue else { return } + guard let surface = view.surface else { return } + let scheme: ghostty_color_scheme_e + switch (appearance.name) { + case .aqua, .vibrantLight: + scheme = GHOSTTY_COLOR_SCHEME_LIGHT + + case .darkAqua, .vibrantDark: + scheme = GHOSTTY_COLOR_SCHEME_DARK + + default: + return + } + + ghostty_surface_set_color_scheme(surface, scheme) + } } required init?(coder: NSCoder) { From 7aeb7c1a58dd0d2db584a660f05519ec8a88e31a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 2 Feb 2024 13:20:59 -0800 Subject: [PATCH 6/7] terminal: rename theme to color_scheme for dsr --- src/terminal/device_status.zig | 2 +- src/termio/Exec.zig | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/terminal/device_status.zig b/src/terminal/device_status.zig index b732f944d..78147ddd4 100644 --- a/src/terminal/device_status.zig +++ b/src/terminal/device_status.zig @@ -63,5 +63,5 @@ const Entry = struct { const entries: []const Entry = &.{ .{ .name = "operating_status", .value = 5 }, .{ .name = "cursor_position", .value = 6 }, - .{ .name = "theme", .value = 996, .question = true }, + .{ .name = "color_scheme", .value = 996, .question = true }, }; diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index 319cb1b52..2ab631fe8 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -2375,7 +2375,7 @@ const StreamHandler = struct { self.messageWriter(msg); }, - .theme => self.surfaceMessageWriter(.{ .report_color_scheme = {} }), + .color_scheme => self.surfaceMessageWriter(.{ .report_color_scheme = {} }), } } From 986fa34d3e432ea6772b3f230b787874d6504bc8 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 2 Feb 2024 13:28:19 -0800 Subject: [PATCH 7/7] terminal: remove unused const --- src/terminal/main.zig | 1 - 1 file changed, 1 deletion(-) diff --git a/src/terminal/main.zig b/src/terminal/main.zig index 0ce4610b5..a4224e63a 100644 --- a/src/terminal/main.zig +++ b/src/terminal/main.zig @@ -33,7 +33,6 @@ pub const Stream = stream.Stream; pub const Cursor = Screen.Cursor; pub const CursorStyleReq = ansi.CursorStyle; pub const DeviceAttributeReq = ansi.DeviceAttributeReq; -pub const DeviceStatusReq = ansi.DeviceStatusReq; pub const Mode = modes.Mode; pub const ModifyKeyFormat = ansi.ModifyKeyFormat; pub const ProtectedMode = ansi.ProtectedMode;