mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-16 16:56:09 +03:00
Merge pull request #1444 from mitchellh/theme
Mode 2031, DSR 996: Color Scheme (Dark/Light) Query + Notification
This commit is contained in:
@ -144,6 +144,11 @@ typedef enum {
|
|||||||
GHOSTTY_TAB_NEXT = -2,
|
GHOSTTY_TAB_NEXT = -2,
|
||||||
} ghostty_tab_e;
|
} 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
|
// 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
|
// afaik doesn't let us reliably define packed structs so we build it up
|
||||||
// from scratch.
|
// 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_content_scale(ghostty_surface_t, double, double);
|
||||||
void ghostty_surface_set_focus(ghostty_surface_t, bool);
|
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_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);
|
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_key(ghostty_surface_t, ghostty_input_key_s);
|
||||||
void ghostty_surface_text(ghostty_surface_t, const char *, uintptr_t);
|
void ghostty_surface_text(ghostty_surface_t, const char *, uintptr_t);
|
||||||
|
@ -74,6 +74,7 @@ extension Ghostty {
|
|||||||
private(set) var focused: Bool = true
|
private(set) var focused: Bool = true
|
||||||
private var cursor: NSCursor = .iBeam
|
private var cursor: NSCursor = .iBeam
|
||||||
private var cursorVisible: CursorVisibility = .visible
|
private var cursorVisible: CursorVisibility = .visible
|
||||||
|
private var appearanceObserver: NSKeyValueObservation? = nil
|
||||||
|
|
||||||
// This is set to non-null during keyDown to accumulate insertText contents
|
// This is set to non-null during keyDown to accumulate insertText contents
|
||||||
private var keyTextAccumulator: [String]? = nil
|
private var keyTextAccumulator: [String]? = nil
|
||||||
@ -123,6 +124,26 @@ extension Ghostty {
|
|||||||
|
|
||||||
// Setup our tracking area so we get mouse moved events
|
// Setup our tracking area so we get mouse moved events
|
||||||
updateTrackingAreas()
|
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) {
|
required init?(coder: NSCoder) {
|
||||||
|
@ -90,6 +90,11 @@ mouse: Mouse,
|
|||||||
/// less important.
|
/// less important.
|
||||||
pressed_key: ?input.KeyEvent = null,
|
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
|
/// 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,
|
/// 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
|
/// and performed it. This is used to prevent sending release/repeat events
|
||||||
@ -832,9 +837,22 @@ pub fn handleMessage(self: *Surface, msg: Message) !void {
|
|||||||
},
|
},
|
||||||
|
|
||||||
.renderer_health => |health| self.updateRendererHealth(health),
|
.renderer_health => |health| self.updateRendererHealth(health),
|
||||||
|
|
||||||
|
.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
|
/// Call this when modifiers change. This is safe to call even if modifiers
|
||||||
/// match the previous state.
|
/// match the previous state.
|
||||||
///
|
///
|
||||||
@ -2786,6 +2804,24 @@ fn dragLeftClickBefore(
|
|||||||
return screen_point.before(click_point);
|
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 {
|
||||||
|
// 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 {
|
fn posToViewport(self: Surface, xpos: f64, ypos: f64) terminal.point.Viewport {
|
||||||
// xpos/ypos need to be adjusted for window padding
|
// xpos/ypos need to be adjusted for window padding
|
||||||
// (i.e. "window-padding-*" settings.
|
// (i.e. "window-padding-*" settings.
|
||||||
|
@ -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(
|
pub fn mouseButtonCallback(
|
||||||
self: *Surface,
|
self: *Surface,
|
||||||
action: input.MouseButtonState,
|
action: input.MouseButtonState,
|
||||||
@ -1516,6 +1523,19 @@ pub const CAPI = struct {
|
|||||||
surface.updateSize(w, h);
|
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.
|
/// Update the content scale of the surface.
|
||||||
export fn ghostty_surface_set_content_scale(surface: *Surface, x: f64, y: f64) void {
|
export fn ghostty_surface_set_content_scale(surface: *Surface, x: f64, y: f64) void {
|
||||||
surface.updateContentScale(x, y);
|
surface.updateContentScale(x, y);
|
||||||
|
@ -61,3 +61,9 @@ pub const DesktopNotification = struct {
|
|||||||
/// The body of a notification. This will always be shown.
|
/// The body of a notification. This will always be shown.
|
||||||
body: []const u8,
|
body: []const u8,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// The color scheme in use (light vs dark).
|
||||||
|
pub const ColorScheme = enum(u2) {
|
||||||
|
light = 0,
|
||||||
|
dark = 1,
|
||||||
|
};
|
||||||
|
@ -57,6 +57,9 @@ pub const Message = union(enum) {
|
|||||||
|
|
||||||
/// Health status change for the renderer.
|
/// Health status change for the renderer.
|
||||||
renderer_health: renderer.Health,
|
renderer_health: renderer.Health,
|
||||||
|
|
||||||
|
/// Report the color scheme
|
||||||
|
report_color_scheme: void,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// A surface mailbox.
|
/// A surface mailbox.
|
||||||
|
@ -55,15 +55,6 @@ pub const DeviceAttributeReq = enum {
|
|||||||
tertiary, // =
|
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)
|
/// Possible cursor styles (ESC [ q)
|
||||||
pub const CursorStyle = enum(u16) {
|
pub const CursorStyle = enum(u16) {
|
||||||
default = 0,
|
default = 0,
|
||||||
|
67
src/terminal/device_status.zig
Normal file
67
src/terminal/device_status.zig
Normal file
@ -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 = "color_scheme", .value = 996, .question = true },
|
||||||
|
};
|
@ -12,6 +12,7 @@ pub const dcs = @import("dcs.zig");
|
|||||||
pub const osc = @import("osc.zig");
|
pub const osc = @import("osc.zig");
|
||||||
pub const point = @import("point.zig");
|
pub const point = @import("point.zig");
|
||||||
pub const color = @import("color.zig");
|
pub const color = @import("color.zig");
|
||||||
|
pub const device_status = @import("device_status.zig");
|
||||||
pub const kitty = @import("kitty.zig");
|
pub const kitty = @import("kitty.zig");
|
||||||
pub const modes = @import("modes.zig");
|
pub const modes = @import("modes.zig");
|
||||||
pub const parse_table = @import("parse_table.zig");
|
pub const parse_table = @import("parse_table.zig");
|
||||||
@ -32,7 +33,6 @@ pub const Stream = stream.Stream;
|
|||||||
pub const Cursor = Screen.Cursor;
|
pub const Cursor = Screen.Cursor;
|
||||||
pub const CursorStyleReq = ansi.CursorStyle;
|
pub const CursorStyleReq = ansi.CursorStyle;
|
||||||
pub const DeviceAttributeReq = ansi.DeviceAttributeReq;
|
pub const DeviceAttributeReq = ansi.DeviceAttributeReq;
|
||||||
pub const DeviceStatusReq = ansi.DeviceStatusReq;
|
|
||||||
pub const Mode = modes.Mode;
|
pub const Mode = modes.Mode;
|
||||||
pub const ModifyKeyFormat = ansi.ModifyKeyFormat;
|
pub const ModifyKeyFormat = ansi.ModifyKeyFormat;
|
||||||
pub const ProtectedMode = ansi.ProtectedMode;
|
pub const ProtectedMode = ansi.ProtectedMode;
|
||||||
|
@ -215,6 +215,7 @@ const entries: []const ModeEntry = &.{
|
|||||||
.{ .name = "bracketed_paste", .value = 2004 },
|
.{ .name = "bracketed_paste", .value = 2004 },
|
||||||
.{ .name = "synchronized_output", .value = 2026 },
|
.{ .name = "synchronized_output", .value = 2026 },
|
||||||
.{ .name = "grapheme_cluster", .value = 2027 },
|
.{ .name = "grapheme_cluster", .value = 2027 },
|
||||||
|
.{ .name = "report_color_scheme", .value = 2031 },
|
||||||
};
|
};
|
||||||
|
|
||||||
test {
|
test {
|
||||||
|
@ -3,6 +3,7 @@ const testing = std.testing;
|
|||||||
const Parser = @import("Parser.zig");
|
const Parser = @import("Parser.zig");
|
||||||
const ansi = @import("ansi.zig");
|
const ansi = @import("ansi.zig");
|
||||||
const charsets = @import("charsets.zig");
|
const charsets = @import("charsets.zig");
|
||||||
|
const device_status = @import("device_status.zig");
|
||||||
const csi = @import("csi.zig");
|
const csi = @import("csi.zig");
|
||||||
const kitty = @import("kitty.zig");
|
const kitty = @import("kitty.zig");
|
||||||
const modes = @import("modes.zig");
|
const modes = @import("modes.zig");
|
||||||
@ -637,36 +638,63 @@ pub fn Stream(comptime Handler: type) type {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// TODO: test
|
// TODO: test
|
||||||
'n' => switch (action.intermediates.len) {
|
'n' => {
|
||||||
0 => if (@hasDecl(T, "deviceStatusReport")) try self.handler.deviceStatusReport(
|
// Handle deviceStatusReport first
|
||||||
switch (action.params.len) {
|
if (action.intermediates.len == 0 or
|
||||||
1 => @enumFromInt(action.params[0]),
|
action.intermediates[0] == '?')
|
||||||
else => {
|
{
|
||||||
log.warn("invalid device status report command: {}", .{action});
|
if (!@hasDecl(T, "deviceStatusReport")) {
|
||||||
return;
|
log.warn("unimplemented CSI callback: {}", .{action});
|
||||||
},
|
return;
|
||||||
},
|
}
|
||||||
) else log.warn("unimplemented CSI callback: {}", .{action}),
|
|
||||||
|
|
||||||
1 => switch (action.intermediates[0]) {
|
if (action.params.len != 1) {
|
||||||
'>' => if (@hasDecl(T, "setModifyKeyFormat")) {
|
log.warn("invalid device status report command: {}", .{action});
|
||||||
// This isn't strictly correct. CSI > n has parameters that
|
return;
|
||||||
// control what exactly is being disabled. However, we
|
}
|
||||||
// only support reverting back to modify other keys in
|
|
||||||
// numeric except format.
|
const question = question: {
|
||||||
try self.handler.setModifyKeyFormat(.{ .other_keys = .numeric_except });
|
if (action.intermediates.len == 0) break :question false;
|
||||||
} else log.warn("unimplemented setModifyKeyFormat: {}", .{action}),
|
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(
|
else => log.warn(
|
||||||
"unknown CSI n with intermediate: {}",
|
"ignoring unimplemented CSI n with intermediates: {s}",
|
||||||
.{action.intermediates[0]},
|
.{action.intermediates},
|
||||||
),
|
),
|
||||||
},
|
}
|
||||||
|
|
||||||
else => log.warn(
|
|
||||||
"ignoring unimplemented CSI n with intermediates: {s}",
|
|
||||||
.{action.intermediates},
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// DECRQM - Request Mode
|
// DECRQM - Request Mode
|
||||||
|
@ -2345,7 +2345,7 @@ const StreamHandler = struct {
|
|||||||
|
|
||||||
pub fn deviceStatusReport(
|
pub fn deviceStatusReport(
|
||||||
self: *StreamHandler,
|
self: *StreamHandler,
|
||||||
req: terminal.DeviceStatusReq,
|
req: terminal.device_status.Request,
|
||||||
) !void {
|
) !void {
|
||||||
switch (req) {
|
switch (req) {
|
||||||
.operating_status => self.messageWriter(.{ .write_stable = "\x1B[0n" }),
|
.operating_status => self.messageWriter(.{ .write_stable = "\x1B[0n" }),
|
||||||
@ -2375,7 +2375,7 @@ const StreamHandler = struct {
|
|||||||
self.messageWriter(msg);
|
self.messageWriter(msg);
|
||||||
},
|
},
|
||||||
|
|
||||||
else => log.warn("unimplemented device status req: {}", .{req}),
|
.color_scheme => self.surfaceMessageWriter(.{ .report_color_scheme = {} }),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user