mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-20 10:46:07 +03:00
Merge branch 'main' of github.com:AnthonyZhOon/ghostty
This commit is contained in:
13
build.zig
13
build.zig
@ -12,6 +12,7 @@ const terminfo = @import("src/terminfo/main.zig");
|
|||||||
const config_vim = @import("src/config/vim.zig");
|
const config_vim = @import("src/config/vim.zig");
|
||||||
const config_sublime_syntax = @import("src/config/sublime_syntax.zig");
|
const config_sublime_syntax = @import("src/config/sublime_syntax.zig");
|
||||||
const fish_completions = @import("src/build/fish_completions.zig");
|
const fish_completions = @import("src/build/fish_completions.zig");
|
||||||
|
const zsh_completions = @import("src/build/zsh_completions.zig");
|
||||||
const build_config = @import("src/build_config.zig");
|
const build_config = @import("src/build_config.zig");
|
||||||
const BuildConfig = build_config.BuildConfig;
|
const BuildConfig = build_config.BuildConfig;
|
||||||
const WasmTarget = @import("src/os/wasm/target.zig").Target;
|
const WasmTarget = @import("src/os/wasm/target.zig").Target;
|
||||||
@ -504,6 +505,18 @@ pub fn build(b: *std.Build) !void {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// zsh shell completions
|
||||||
|
{
|
||||||
|
const wf = b.addWriteFiles();
|
||||||
|
_ = wf.add("_ghostty", zsh_completions.zsh_completions);
|
||||||
|
|
||||||
|
b.installDirectory(.{
|
||||||
|
.source_dir = wf.getDirectory(),
|
||||||
|
.install_dir = .prefix,
|
||||||
|
.install_subdir = "share/zsh/site-functions",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Vim plugin
|
// Vim plugin
|
||||||
{
|
{
|
||||||
const wf = b.addWriteFiles();
|
const wf = b.addWriteFiles();
|
||||||
|
@ -167,6 +167,9 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle {
|
|||||||
self.savedState = savedState
|
self.savedState = savedState
|
||||||
|
|
||||||
// We hide the dock if the window is on a screen with the dock.
|
// We hide the dock if the window is on a screen with the dock.
|
||||||
|
// We must hide the dock FIRST then hide the menu:
|
||||||
|
// If you specify autoHideMenuBar, it must be accompanied by either hideDock or autoHideDock.
|
||||||
|
// https://developer.apple.com/documentation/appkit/nsapplication/presentationoptions-swift.struct
|
||||||
if (savedState.dock) {
|
if (savedState.dock) {
|
||||||
hideDock()
|
hideDock()
|
||||||
}
|
}
|
||||||
@ -176,18 +179,6 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle {
|
|||||||
hideMenu()
|
hideMenu()
|
||||||
}
|
}
|
||||||
|
|
||||||
// When this window becomes or resigns main we need to run some logic.
|
|
||||||
NotificationCenter.default.addObserver(
|
|
||||||
self,
|
|
||||||
selector: #selector(windowDidBecomeMain),
|
|
||||||
name: NSWindow.didBecomeMainNotification,
|
|
||||||
object: window)
|
|
||||||
NotificationCenter.default.addObserver(
|
|
||||||
self,
|
|
||||||
selector: #selector(windowDidResignMain),
|
|
||||||
name: NSWindow.didResignMainNotification,
|
|
||||||
object: window)
|
|
||||||
|
|
||||||
// When we change screens we need to redo everything.
|
// When we change screens we need to redo everything.
|
||||||
NotificationCenter.default.addObserver(
|
NotificationCenter.default.addObserver(
|
||||||
self,
|
self,
|
||||||
@ -222,8 +213,6 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle {
|
|||||||
// Remove all our notifications. We remove them one by one because
|
// Remove all our notifications. We remove them one by one because
|
||||||
// we don't want to remove the observers that our superclass sets.
|
// we don't want to remove the observers that our superclass sets.
|
||||||
let center = NotificationCenter.default
|
let center = NotificationCenter.default
|
||||||
center.removeObserver(self, name: NSWindow.didBecomeMainNotification, object: window)
|
|
||||||
center.removeObserver(self, name: NSWindow.didResignMainNotification, object: window)
|
|
||||||
center.removeObserver(self, name: NSWindow.didChangeScreenNotification, object: window)
|
center.removeObserver(self, name: NSWindow.didChangeScreenNotification, object: window)
|
||||||
|
|
||||||
// Unhide our elements
|
// Unhide our elements
|
||||||
@ -315,42 +304,6 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle {
|
|||||||
exit()
|
exit()
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func windowDidBecomeMain(_ notification: Notification) {
|
|
||||||
guard let savedState else { return }
|
|
||||||
|
|
||||||
// This should always be true due to how we register but just be sure
|
|
||||||
guard let object = notification.object as? NSWindow,
|
|
||||||
object == window else { return }
|
|
||||||
|
|
||||||
// This is crazy but at least on macOS 15.0, you must hide the dock
|
|
||||||
// FIRST then hide the menu. If you do the opposite, it does not
|
|
||||||
// work.
|
|
||||||
|
|
||||||
if savedState.dock {
|
|
||||||
hideDock()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (properties.hideMenu) {
|
|
||||||
hideMenu()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc func windowDidResignMain(_ notification: Notification) {
|
|
||||||
guard let savedState else { return }
|
|
||||||
|
|
||||||
// This should always be true due to how we register but just be sure
|
|
||||||
guard let object = notification.object as? NSWindow,
|
|
||||||
object == window else { return }
|
|
||||||
|
|
||||||
if (properties.hideMenu) {
|
|
||||||
unhideMenu()
|
|
||||||
}
|
|
||||||
|
|
||||||
if savedState.dock {
|
|
||||||
unhideDock()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: Dock
|
// MARK: Dock
|
||||||
|
|
||||||
private func hideDock() {
|
private func hideDock() {
|
||||||
|
@ -440,9 +440,9 @@ fn isExecutable(mode: std.fs.File.Mode) bool {
|
|||||||
return mode & 0o0111 != 0;
|
return mode & 0o0111 != 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// `hostname` is present on both *nix and windows
|
// `uname -n` is the *nix equivalent of `hostname.exe` on Windows
|
||||||
test "expandPath: hostname" {
|
test "expandPath: hostname" {
|
||||||
const executable = if (builtin.os.tag == .windows) "hostname.exe" else "hostname";
|
const executable = if (builtin.os.tag == .windows) "hostname.exe" else "uname";
|
||||||
const path = (try expandPath(testing.allocator, executable)).?;
|
const path = (try expandPath(testing.allocator, executable)).?;
|
||||||
defer testing.allocator.free(path);
|
defer testing.allocator.free(path);
|
||||||
try testing.expect(path.len > executable.len);
|
try testing.expect(path.len > executable.len);
|
||||||
|
@ -245,7 +245,7 @@ const DerivedConfig = struct {
|
|||||||
mouse_scroll_multiplier: f64,
|
mouse_scroll_multiplier: f64,
|
||||||
mouse_shift_capture: configpkg.MouseShiftCapture,
|
mouse_shift_capture: configpkg.MouseShiftCapture,
|
||||||
macos_non_native_fullscreen: configpkg.NonNativeFullscreen,
|
macos_non_native_fullscreen: configpkg.NonNativeFullscreen,
|
||||||
macos_option_as_alt: configpkg.OptionAsAlt,
|
macos_option_as_alt: ?configpkg.OptionAsAlt,
|
||||||
vt_kam_allowed: bool,
|
vt_kam_allowed: bool,
|
||||||
window_padding_top: u32,
|
window_padding_top: u32,
|
||||||
window_padding_bottom: u32,
|
window_padding_bottom: u32,
|
||||||
@ -1990,12 +1990,26 @@ fn encodeKey(
|
|||||||
// inputs there are many keybindings that result in no encoding
|
// inputs there are many keybindings that result in no encoding
|
||||||
// whatsoever.
|
// whatsoever.
|
||||||
const enc: input.KeyEncoder = enc: {
|
const enc: input.KeyEncoder = enc: {
|
||||||
|
const option_as_alt: configpkg.OptionAsAlt = self.config.macos_option_as_alt orelse detect: {
|
||||||
|
// Non-macOS doesn't use this value so ignore.
|
||||||
|
if (comptime builtin.os.tag != .macos) break :detect .false;
|
||||||
|
|
||||||
|
// If we don't have alt pressed, it doesn't matter what this
|
||||||
|
// config is so we can just say "false" and break out and avoid
|
||||||
|
// more expensive checks below.
|
||||||
|
if (!event.mods.alt) break :detect .false;
|
||||||
|
|
||||||
|
// Alt is pressed, we're on macOS. We break some encapsulation
|
||||||
|
// here and assume libghostty for ease...
|
||||||
|
break :detect self.rt_app.keyboardLayout().detectOptionAsAlt();
|
||||||
|
};
|
||||||
|
|
||||||
self.renderer_state.mutex.lock();
|
self.renderer_state.mutex.lock();
|
||||||
defer self.renderer_state.mutex.unlock();
|
defer self.renderer_state.mutex.unlock();
|
||||||
const t = &self.io.terminal;
|
const t = &self.io.terminal;
|
||||||
break :enc .{
|
break :enc .{
|
||||||
.event = event,
|
.event = event,
|
||||||
.macos_option_as_alt = self.config.macos_option_as_alt,
|
.macos_option_as_alt = option_as_alt,
|
||||||
.alt_esc_prefix = t.modes.get(.alt_esc_prefix),
|
.alt_esc_prefix = t.modes.get(.alt_esc_prefix),
|
||||||
.cursor_key_application = t.modes.get(.cursor_keys),
|
.cursor_key_application = t.modes.get(.cursor_keys),
|
||||||
.keypad_key_application = t.modes.get(.keypad_keys),
|
.keypad_key_application = t.modes.get(.keypad_keys),
|
||||||
|
@ -105,11 +105,14 @@ pub const App = struct {
|
|||||||
var config_clone = try config.clone(alloc);
|
var config_clone = try config.clone(alloc);
|
||||||
errdefer config_clone.deinit();
|
errdefer config_clone.deinit();
|
||||||
|
|
||||||
|
var keymap = try input.Keymap.init();
|
||||||
|
errdefer keymap.deinit();
|
||||||
|
|
||||||
return .{
|
return .{
|
||||||
.core_app = core_app,
|
.core_app = core_app,
|
||||||
.config = config_clone,
|
.config = config_clone,
|
||||||
.opts = opts,
|
.opts = opts,
|
||||||
.keymap = try input.Keymap.init(),
|
.keymap = keymap,
|
||||||
.keymap_state = .{},
|
.keymap_state = .{},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -161,8 +164,15 @@ pub const App = struct {
|
|||||||
// then we strip the alt modifier from the mods for translation.
|
// then we strip the alt modifier from the mods for translation.
|
||||||
const translate_mods = translate_mods: {
|
const translate_mods = translate_mods: {
|
||||||
var translate_mods = mods;
|
var translate_mods = mods;
|
||||||
if (comptime builtin.target.isDarwin()) {
|
if ((comptime builtin.target.isDarwin()) and translate_mods.alt) {
|
||||||
const strip = switch (self.config.@"macos-option-as-alt") {
|
// Note: the keyboardLayout() function is not super cheap
|
||||||
|
// so we only want to run it if alt is already pressed hence
|
||||||
|
// the above condition.
|
||||||
|
const option_as_alt: configpkg.OptionAsAlt =
|
||||||
|
self.config.@"macos-option-as-alt" orelse
|
||||||
|
self.keyboardLayout().detectOptionAsAlt();
|
||||||
|
|
||||||
|
const strip = switch (option_as_alt) {
|
||||||
.false => false,
|
.false => false,
|
||||||
.true => mods.alt,
|
.true => mods.alt,
|
||||||
.left => mods.sides.alt == .left,
|
.left => mods.sides.alt == .left,
|
||||||
@ -382,6 +392,25 @@ pub const App = struct {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Loads the keyboard layout.
|
||||||
|
///
|
||||||
|
/// Kind of expensive so this should be avoided if possible. When I say
|
||||||
|
/// "kind of expensive" I mean that its not something you probably want
|
||||||
|
/// to run on every keypress.
|
||||||
|
pub fn keyboardLayout(self: *const App) input.KeyboardLayout {
|
||||||
|
// We only support keyboard layout detection on macOS.
|
||||||
|
if (comptime builtin.os.tag != .macos) return .unknown;
|
||||||
|
|
||||||
|
// Any layout larger than this is not something we can handle.
|
||||||
|
var buf: [256]u8 = undefined;
|
||||||
|
const id = self.keymap.sourceId(&buf) catch |err| {
|
||||||
|
comptime assert(@TypeOf(err) == error{OutOfMemory});
|
||||||
|
return .unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
return input.KeyboardLayout.mapAppleId(id) orelse .unknown;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn wakeup(self: *const App) void {
|
pub fn wakeup(self: *const App) void {
|
||||||
self.opts.wakeup(self.opts.userdata);
|
self.opts.wakeup(self.opts.userdata);
|
||||||
}
|
}
|
||||||
@ -1551,7 +1580,8 @@ pub const CAPI = struct {
|
|||||||
@truncate(@as(c_uint, @bitCast(mods_raw))),
|
@truncate(@as(c_uint, @bitCast(mods_raw))),
|
||||||
));
|
));
|
||||||
const result = mods.translation(
|
const result = mods.translation(
|
||||||
surface.core_surface.config.macos_option_as_alt,
|
surface.core_surface.config.macos_option_as_alt orelse
|
||||||
|
surface.app.keyboardLayout().detectOptionAsAlt(),
|
||||||
);
|
);
|
||||||
return @intCast(@as(input.Mods.Backing, @bitCast(result)));
|
return @intCast(@as(input.Mods.Backing, @bitCast(result)));
|
||||||
}
|
}
|
||||||
|
@ -409,6 +409,13 @@ pub const App = struct {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn keyboardLayout(self: *const App) input.KeyboardLayout {
|
||||||
|
_ = self;
|
||||||
|
|
||||||
|
// Not supported by glfw
|
||||||
|
return .unknown;
|
||||||
|
}
|
||||||
|
|
||||||
/// Mac-specific settings. This is only enabled when the target is
|
/// Mac-specific settings. This is only enabled when the target is
|
||||||
/// Mac and the artifact is a standalone exe. We don't target libs because
|
/// Mac and the artifact is a standalone exe. We don't target libs because
|
||||||
/// the embedded API doesn't do windowing.
|
/// the embedded API doesn't do windowing.
|
||||||
|
@ -17,13 +17,13 @@ window: *c.GtkWindow,
|
|||||||
view: PrimaryView,
|
view: PrimaryView,
|
||||||
|
|
||||||
data: [:0]u8,
|
data: [:0]u8,
|
||||||
core_surface: CoreSurface,
|
core_surface: *CoreSurface,
|
||||||
pending_req: apprt.ClipboardRequest,
|
pending_req: apprt.ClipboardRequest,
|
||||||
|
|
||||||
pub fn create(
|
pub fn create(
|
||||||
app: *App,
|
app: *App,
|
||||||
data: []const u8,
|
data: []const u8,
|
||||||
core_surface: CoreSurface,
|
core_surface: *CoreSurface,
|
||||||
request: apprt.ClipboardRequest,
|
request: apprt.ClipboardRequest,
|
||||||
) !void {
|
) !void {
|
||||||
if (app.clipboard_confirmation_window != null) return error.WindowAlreadyExists;
|
if (app.clipboard_confirmation_window != null) return error.WindowAlreadyExists;
|
||||||
@ -54,7 +54,7 @@ fn init(
|
|||||||
self: *ClipboardConfirmation,
|
self: *ClipboardConfirmation,
|
||||||
app: *App,
|
app: *App,
|
||||||
data: []const u8,
|
data: []const u8,
|
||||||
core_surface: CoreSurface,
|
core_surface: *CoreSurface,
|
||||||
request: apprt.ClipboardRequest,
|
request: apprt.ClipboardRequest,
|
||||||
) !void {
|
) !void {
|
||||||
// Create the window
|
// Create the window
|
||||||
|
@ -1051,7 +1051,7 @@ pub fn clipboardRequest(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn setClipboardString(
|
pub fn setClipboardString(
|
||||||
self: *const Surface,
|
self: *Surface,
|
||||||
val: [:0]const u8,
|
val: [:0]const u8,
|
||||||
clipboard_type: apprt.Clipboard,
|
clipboard_type: apprt.Clipboard,
|
||||||
confirm: bool,
|
confirm: bool,
|
||||||
@ -1065,7 +1065,7 @@ pub fn setClipboardString(
|
|||||||
ClipboardConfirmationWindow.create(
|
ClipboardConfirmationWindow.create(
|
||||||
self.app,
|
self.app,
|
||||||
val,
|
val,
|
||||||
self.core_surface,
|
&self.core_surface,
|
||||||
.{ .osc_52_write = clipboard_type },
|
.{ .osc_52_write = clipboard_type },
|
||||||
) catch |window_err| {
|
) catch |window_err| {
|
||||||
log.err("failed to create clipboard confirmation window err={}", .{window_err});
|
log.err("failed to create clipboard confirmation window err={}", .{window_err});
|
||||||
@ -1113,7 +1113,7 @@ fn gtkClipboardRead(
|
|||||||
ClipboardConfirmationWindow.create(
|
ClipboardConfirmationWindow.create(
|
||||||
self.app,
|
self.app,
|
||||||
str,
|
str,
|
||||||
self.core_surface,
|
&self.core_surface,
|
||||||
req.state,
|
req.state,
|
||||||
) catch |window_err| {
|
) catch |window_err| {
|
||||||
log.err("failed to create clipboard confirmation window err={}", .{window_err});
|
log.err("failed to create clipboard confirmation window err={}", .{window_err});
|
||||||
|
@ -2,9 +2,6 @@ const std = @import("std");
|
|||||||
|
|
||||||
const Config = @import("../config/Config.zig");
|
const Config = @import("../config/Config.zig");
|
||||||
const Action = @import("../cli/action.zig").Action;
|
const Action = @import("../cli/action.zig").Action;
|
||||||
const ListFontsConfig = @import("../cli/list_fonts.zig").Config;
|
|
||||||
const ShowConfigOptions = @import("../cli/show_config.zig").Options;
|
|
||||||
const ListKeybindsOptions = @import("../cli/list_keybinds.zig").Options;
|
|
||||||
|
|
||||||
/// A fish completions configuration that contains all the available commands
|
/// A fish completions configuration that contains all the available commands
|
||||||
/// and options.
|
/// and options.
|
||||||
@ -100,66 +97,35 @@ fn writeFishCompletions(writer: anytype) !void {
|
|||||||
try writer.writeAll("\"\n");
|
try writer.writeAll("\"\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
for (@typeInfo(ListFontsConfig).Struct.fields) |field| {
|
for (@typeInfo(Action).Enum.fields) |field| {
|
||||||
if (field.name[0] == '_') continue;
|
if (std.mem.eql(u8, "help", field.name)) continue;
|
||||||
try writer.writeAll("complete -c ghostty -n \"__fish_seen_subcommand_from +list-fonts\" -l ");
|
if (std.mem.eql(u8, "version", field.name)) continue;
|
||||||
try writer.writeAll(field.name);
|
|
||||||
try writer.writeAll(if (field.type != bool) " -r" else " ");
|
|
||||||
try writer.writeAll(" -f");
|
|
||||||
switch (@typeInfo(field.type)) {
|
|
||||||
.Bool => try writer.writeAll(" -a \"true false\""),
|
|
||||||
.Enum => |info| {
|
|
||||||
try writer.writeAll(" -a \"");
|
|
||||||
for (info.fields, 0..) |f, i| {
|
|
||||||
if (i > 0) try writer.writeAll(" ");
|
|
||||||
try writer.writeAll(f.name);
|
|
||||||
}
|
|
||||||
try writer.writeAll("\"");
|
|
||||||
},
|
|
||||||
else => {},
|
|
||||||
}
|
|
||||||
try writer.writeAll("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
for (@typeInfo(ShowConfigOptions).Struct.fields) |field| {
|
const options = @field(Action, field.name).options();
|
||||||
if (field.name[0] == '_') continue;
|
for (@typeInfo(options).Struct.fields) |opt| {
|
||||||
try writer.writeAll("complete -c ghostty -n \"__fish_seen_subcommand_from +show-config\" -l ");
|
if (opt.name[0] == '_') continue;
|
||||||
try writer.writeAll(field.name);
|
try writer.writeAll("complete -c ghostty -n \"__fish_seen_subcommand_from +" ++ field.name ++ "\" -l ");
|
||||||
try writer.writeAll(if (field.type != bool) " -r" else " ");
|
try writer.writeAll(opt.name);
|
||||||
try writer.writeAll(" -f");
|
try writer.writeAll(if (opt.type != bool) " -r" else "");
|
||||||
switch (@typeInfo(field.type)) {
|
|
||||||
.Bool => try writer.writeAll(" -a \"true false\""),
|
|
||||||
.Enum => |info| {
|
|
||||||
try writer.writeAll(" -a \"");
|
|
||||||
for (info.fields, 0..) |f, i| {
|
|
||||||
if (i > 0) try writer.writeAll(" ");
|
|
||||||
try writer.writeAll(f.name);
|
|
||||||
}
|
|
||||||
try writer.writeAll("\"");
|
|
||||||
},
|
|
||||||
else => {},
|
|
||||||
}
|
|
||||||
try writer.writeAll("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
for (@typeInfo(ListKeybindsOptions).Struct.fields) |field| {
|
// special case +validate_config --config-file
|
||||||
if (field.name[0] == '_') continue;
|
if (std.mem.eql(u8, "config-file", opt.name)) {
|
||||||
try writer.writeAll("complete -c ghostty -n \"__fish_seen_subcommand_from +list-keybinds\" -l ");
|
try writer.writeAll(" -F");
|
||||||
try writer.writeAll(field.name);
|
} else try writer.writeAll(" -f");
|
||||||
try writer.writeAll(if (field.type != bool) " -r" else " ");
|
|
||||||
try writer.writeAll(" -f");
|
switch (@typeInfo(opt.type)) {
|
||||||
switch (@typeInfo(field.type)) {
|
.Bool => try writer.writeAll(" -a \"true false\""),
|
||||||
.Bool => try writer.writeAll(" -a \"true false\""),
|
.Enum => |info| {
|
||||||
.Enum => |info| {
|
try writer.writeAll(" -a \"");
|
||||||
try writer.writeAll(" -a \"");
|
for (info.opts, 0..) |f, i| {
|
||||||
for (info.fields, 0..) |f, i| {
|
if (i > 0) try writer.writeAll(" ");
|
||||||
if (i > 0) try writer.writeAll(" ");
|
try writer.writeAll(f.name);
|
||||||
try writer.writeAll(f.name);
|
}
|
||||||
}
|
try writer.writeAll("\"");
|
||||||
try writer.writeAll("\"");
|
},
|
||||||
},
|
else => {},
|
||||||
else => {},
|
}
|
||||||
|
try writer.writeAll("\n");
|
||||||
}
|
}
|
||||||
try writer.writeAll("\n");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -30,10 +30,10 @@ pub fn genConfig(writer: anytype, cli: bool) !void {
|
|||||||
inline for (@typeInfo(Config).Struct.fields) |field| {
|
inline for (@typeInfo(Config).Struct.fields) |field| {
|
||||||
if (field.name[0] == '_') continue;
|
if (field.name[0] == '_') continue;
|
||||||
|
|
||||||
try writer.writeAll("`");
|
try writer.writeAll("**`");
|
||||||
if (cli) try writer.writeAll("--");
|
if (cli) try writer.writeAll("--");
|
||||||
try writer.writeAll(field.name);
|
try writer.writeAll(field.name);
|
||||||
try writer.writeAll("`\n\n");
|
try writer.writeAll("`**\n\n");
|
||||||
if (@hasDecl(help_strings.Config, field.name)) {
|
if (@hasDecl(help_strings.Config, field.name)) {
|
||||||
var iter = std.mem.splitScalar(u8, @field(help_strings.Config, field.name), '\n');
|
var iter = std.mem.splitScalar(u8, @field(help_strings.Config, field.name), '\n');
|
||||||
var first = true;
|
var first = true;
|
||||||
@ -60,12 +60,12 @@ pub fn genActions(writer: anytype) !void {
|
|||||||
const action = std.meta.stringToEnum(Action, field.name).?;
|
const action = std.meta.stringToEnum(Action, field.name).?;
|
||||||
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
.help => try writer.writeAll("`--help`\n\n"),
|
.help => try writer.writeAll("**`--help`**\n\n"),
|
||||||
.version => try writer.writeAll("`--version`\n\n"),
|
.version => try writer.writeAll("**`--version`**\n\n"),
|
||||||
else => {
|
else => {
|
||||||
try writer.writeAll("`+");
|
try writer.writeAll("**`+");
|
||||||
try writer.writeAll(field.name);
|
try writer.writeAll(field.name);
|
||||||
try writer.writeAll("`\n\n");
|
try writer.writeAll("`**\n\n");
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -97,9 +97,9 @@ pub fn genKeybindActions(writer: anytype) !void {
|
|||||||
inline for (info.Union.fields) |field| {
|
inline for (info.Union.fields) |field| {
|
||||||
if (field.name[0] == '_') continue;
|
if (field.name[0] == '_') continue;
|
||||||
|
|
||||||
try writer.writeAll("`");
|
try writer.writeAll("**`");
|
||||||
try writer.writeAll(field.name);
|
try writer.writeAll(field.name);
|
||||||
try writer.writeAll("`\n\n");
|
try writer.writeAll("`**\n\n");
|
||||||
|
|
||||||
if (@hasDecl(help_strings.KeybindAction, field.name)) {
|
if (@hasDecl(help_strings.KeybindAction, field.name)) {
|
||||||
var iter = std.mem.splitScalar(u8, @field(help_strings.KeybindAction, field.name), '\n');
|
var iter = std.mem.splitScalar(u8, @field(help_strings.KeybindAction, field.name), '\n');
|
||||||
|
201
src/build/zsh_completions.zig
Normal file
201
src/build/zsh_completions.zig
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const Config = @import("../config/Config.zig");
|
||||||
|
const Action = @import("../cli/action.zig").Action;
|
||||||
|
|
||||||
|
/// A zsh completions configuration that contains all the available commands
|
||||||
|
/// and options.
|
||||||
|
pub const zsh_completions = comptimeGenerateZshCompletions();
|
||||||
|
|
||||||
|
fn comptimeGenerateZshCompletions() []const u8 {
|
||||||
|
comptime {
|
||||||
|
@setEvalBranchQuota(19000);
|
||||||
|
var counter = std.io.countingWriter(std.io.null_writer);
|
||||||
|
try writeZshCompletions(&counter.writer());
|
||||||
|
|
||||||
|
var buf: [counter.bytes_written]u8 = undefined;
|
||||||
|
var stream = std.io.fixedBufferStream(&buf);
|
||||||
|
try writeZshCompletions(stream.writer());
|
||||||
|
const final = buf;
|
||||||
|
return final[0..stream.getWritten().len];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn writeZshCompletions(writer: anytype) !void {
|
||||||
|
try writer.writeAll(
|
||||||
|
\\#compdef ghostty
|
||||||
|
\\
|
||||||
|
\\_fonts () {
|
||||||
|
\\ local font_list=$(ghostty +list-fonts | grep -Z '^[A-Z]')
|
||||||
|
\\ local fonts=(${(f)font_list})
|
||||||
|
\\ _describe -t fonts 'fonts' fonts
|
||||||
|
\\}
|
||||||
|
\\
|
||||||
|
\\_themes() {
|
||||||
|
\\ local theme_list=$(ghostty +list-themes | sed -E 's/^(.*) \(.*\$/\0/')
|
||||||
|
\\ local themes=(${(f)theme_list})
|
||||||
|
\\ _describe -t themes 'themes' themes
|
||||||
|
\\}
|
||||||
|
\\
|
||||||
|
);
|
||||||
|
|
||||||
|
try writer.writeAll("_config() {\n");
|
||||||
|
try writer.writeAll(" _arguments \\\n");
|
||||||
|
try writer.writeAll(" \"--help\" \\\n");
|
||||||
|
try writer.writeAll(" \"--version\" \\\n");
|
||||||
|
for (@typeInfo(Config).Struct.fields) |field| {
|
||||||
|
if (field.name[0] == '_') continue;
|
||||||
|
try writer.writeAll(" \"--");
|
||||||
|
try writer.writeAll(field.name);
|
||||||
|
try writer.writeAll("=-:::");
|
||||||
|
|
||||||
|
if (std.mem.startsWith(u8, field.name, "font-family"))
|
||||||
|
try writer.writeAll("_fonts")
|
||||||
|
else if (std.mem.eql(u8, "theme", field.name))
|
||||||
|
try writer.writeAll("_themes")
|
||||||
|
else if (std.mem.eql(u8, "working-directory", field.name))
|
||||||
|
try writer.writeAll("{_files -/}")
|
||||||
|
else if (field.type == Config.RepeatablePath)
|
||||||
|
try writer.writeAll("_files") // todo check if this is needed
|
||||||
|
else {
|
||||||
|
try writer.writeAll("(");
|
||||||
|
switch (@typeInfo(field.type)) {
|
||||||
|
.Bool => try writer.writeAll("true false"),
|
||||||
|
.Enum => |info| {
|
||||||
|
for (info.fields, 0..) |f, i| {
|
||||||
|
if (i > 0) try writer.writeAll(" ");
|
||||||
|
try writer.writeAll(f.name);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.Struct => |info| {
|
||||||
|
if (!@hasDecl(field.type, "parseCLI") and info.layout == .@"packed") {
|
||||||
|
for (info.fields, 0..) |f, i| {
|
||||||
|
if (i > 0) try writer.writeAll(" ");
|
||||||
|
try writer.writeAll(f.name);
|
||||||
|
try writer.writeAll(" no-");
|
||||||
|
try writer.writeAll(f.name);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
//resize-overlay-duration
|
||||||
|
//keybind
|
||||||
|
//window-padding-x ...-y
|
||||||
|
//link
|
||||||
|
//palette
|
||||||
|
//background
|
||||||
|
//foreground
|
||||||
|
//font-variation*
|
||||||
|
//font-feature
|
||||||
|
try writer.writeAll(" ");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
else => try writer.writeAll(" "),
|
||||||
|
}
|
||||||
|
try writer.writeAll(")");
|
||||||
|
}
|
||||||
|
|
||||||
|
try writer.writeAll("\" \\\n");
|
||||||
|
}
|
||||||
|
try writer.writeAll("\n}\n\n");
|
||||||
|
|
||||||
|
try writer.writeAll(
|
||||||
|
\\_ghostty() {
|
||||||
|
\\ typeset -A opt_args
|
||||||
|
\\ local context state line
|
||||||
|
\\ local opt=('--help' '--version')
|
||||||
|
\\
|
||||||
|
\\ _arguments -C \
|
||||||
|
\\ '1:actions:->actions' \
|
||||||
|
\\ '*:: :->rest' \
|
||||||
|
\\
|
||||||
|
\\ if [[ "$line[1]" == "--help" || "$line[1]" == "--version" ]]; then
|
||||||
|
\\ return
|
||||||
|
\\ fi
|
||||||
|
\\
|
||||||
|
\\ if [[ "$line[1]" == -* ]]; then
|
||||||
|
\\ _config
|
||||||
|
\\ return
|
||||||
|
\\ fi
|
||||||
|
\\
|
||||||
|
\\ case "$state" in
|
||||||
|
\\ (actions)
|
||||||
|
\\ local actions; actions=(
|
||||||
|
\\
|
||||||
|
);
|
||||||
|
|
||||||
|
{
|
||||||
|
// how to get 'commands'
|
||||||
|
var count: usize = 0;
|
||||||
|
const padding = " ";
|
||||||
|
for (@typeInfo(Action).Enum.fields) |field| {
|
||||||
|
try writer.writeAll(padding ++ "'+");
|
||||||
|
try writer.writeAll(field.name);
|
||||||
|
try writer.writeAll("'\n");
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try writer.writeAll(
|
||||||
|
\\ )
|
||||||
|
\\ _describe '' opt
|
||||||
|
\\ _describe -t action 'action' actions
|
||||||
|
\\ ;;
|
||||||
|
\\ (rest)
|
||||||
|
\\ if [[ "$line[2]" == "--help" ]]; then
|
||||||
|
\\ return
|
||||||
|
\\ fi
|
||||||
|
\\
|
||||||
|
\\ local help=('--help')
|
||||||
|
\\ _describe '' help
|
||||||
|
\\
|
||||||
|
\\ case $line[1] in
|
||||||
|
\\
|
||||||
|
);
|
||||||
|
{
|
||||||
|
const padding = " ";
|
||||||
|
for (@typeInfo(Action).Enum.fields) |field| {
|
||||||
|
if (std.mem.eql(u8, "help", field.name)) continue;
|
||||||
|
if (std.mem.eql(u8, "version", field.name)) continue;
|
||||||
|
|
||||||
|
const options = @field(Action, field.name).options();
|
||||||
|
// assumes options will never be created with only <_name> members
|
||||||
|
if (@typeInfo(options).Struct.fields.len == 0) continue;
|
||||||
|
|
||||||
|
try writer.writeAll(padding ++ "(+" ++ field.name ++ ")\n");
|
||||||
|
try writer.writeAll(padding ++ " _arguments \\\n");
|
||||||
|
for (@typeInfo(options).Struct.fields) |opt| {
|
||||||
|
if (opt.name[0] == '_') continue;
|
||||||
|
|
||||||
|
try writer.writeAll(padding ++ " '--");
|
||||||
|
try writer.writeAll(opt.name);
|
||||||
|
try writer.writeAll("=-:::");
|
||||||
|
switch (@typeInfo(opt.type)) {
|
||||||
|
.Bool => try writer.writeAll("(true false)"),
|
||||||
|
.Enum => |info| {
|
||||||
|
try writer.writeAll("(");
|
||||||
|
for (info.opts, 0..) |f, i| {
|
||||||
|
if (i > 0) try writer.writeAll(" ");
|
||||||
|
try writer.writeAll(f.name);
|
||||||
|
}
|
||||||
|
try writer.writeAll(")");
|
||||||
|
},
|
||||||
|
else => {
|
||||||
|
if (std.mem.eql(u8, "config-file", opt.name)) {
|
||||||
|
try writer.writeAll("_files");
|
||||||
|
} else try writer.writeAll("( )");
|
||||||
|
},
|
||||||
|
}
|
||||||
|
try writer.writeAll("' \\\n");
|
||||||
|
}
|
||||||
|
try writer.writeAll(padding ++ ";;\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try writer.writeAll(
|
||||||
|
\\ esac
|
||||||
|
\\ ;;
|
||||||
|
\\ esac
|
||||||
|
\\}
|
||||||
|
\\
|
||||||
|
\\_ghostty "$@"
|
||||||
|
\\
|
||||||
|
);
|
||||||
|
}
|
@ -163,6 +163,26 @@ pub const Action = enum {
|
|||||||
return "cli/" ++ filename ++ ".zig";
|
return "cli/" ++ filename ++ ".zig";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the options of action. Supports generating shell completions
|
||||||
|
/// without duplicating the mapping from Action to relevant Option
|
||||||
|
/// @import(..) declaration.
|
||||||
|
pub fn options(comptime self: Action) type {
|
||||||
|
comptime {
|
||||||
|
return switch (self) {
|
||||||
|
.version => version.Options,
|
||||||
|
.help => help.Options,
|
||||||
|
.@"list-fonts" => list_fonts.Options,
|
||||||
|
.@"list-keybinds" => list_keybinds.Options,
|
||||||
|
.@"list-themes" => list_themes.Options,
|
||||||
|
.@"list-colors" => list_colors.Options,
|
||||||
|
.@"list-actions" => list_actions.Options,
|
||||||
|
.@"show-config" => show_config.Options,
|
||||||
|
.@"validate-config" => validate_config.Options,
|
||||||
|
.@"crash-report" => crash_report.Options,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
test "parse action none" {
|
test "parse action none" {
|
||||||
|
@ -7,7 +7,7 @@ const font = @import("../font/main.zig");
|
|||||||
|
|
||||||
const log = std.log.scoped(.list_fonts);
|
const log = std.log.scoped(.list_fonts);
|
||||||
|
|
||||||
pub const Config = struct {
|
pub const Options = struct {
|
||||||
/// This is set by the CLI parser for deinit.
|
/// This is set by the CLI parser for deinit.
|
||||||
_arena: ?ArenaAllocator = null,
|
_arena: ?ArenaAllocator = null,
|
||||||
|
|
||||||
@ -23,13 +23,13 @@ pub const Config = struct {
|
|||||||
bold: bool = false,
|
bold: bool = false,
|
||||||
italic: bool = false,
|
italic: bool = false,
|
||||||
|
|
||||||
pub fn deinit(self: *Config) void {
|
pub fn deinit(self: *Options) void {
|
||||||
if (self._arena) |arena| arena.deinit();
|
if (self._arena) |arena| arena.deinit();
|
||||||
self.* = undefined;
|
self.* = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Enables "-h" and "--help" to work.
|
/// Enables "-h" and "--help" to work.
|
||||||
pub fn help(self: Config) !void {
|
pub fn help(self: Options) !void {
|
||||||
_ = self;
|
_ = self;
|
||||||
return Action.help_error;
|
return Action.help_error;
|
||||||
}
|
}
|
||||||
@ -59,9 +59,9 @@ pub fn run(alloc: Allocator) !u8 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn runArgs(alloc_gpa: Allocator, argsIter: anytype) !u8 {
|
fn runArgs(alloc_gpa: Allocator, argsIter: anytype) !u8 {
|
||||||
var config: Config = .{};
|
var config: Options = .{};
|
||||||
defer config.deinit();
|
defer config.deinit();
|
||||||
try args.parse(Config, alloc_gpa, &config, argsIter);
|
try args.parse(Options, alloc_gpa, &config, argsIter);
|
||||||
|
|
||||||
// Use an arena for all our memory allocs
|
// Use an arena for all our memory allocs
|
||||||
var arena = ArenaAllocator.init(alloc_gpa);
|
var arena = ArenaAllocator.init(alloc_gpa);
|
||||||
|
@ -7,6 +7,8 @@ const xev = @import("xev");
|
|||||||
const renderer = @import("../renderer.zig");
|
const renderer = @import("../renderer.zig");
|
||||||
const gtk = if (build_config.app_runtime == .gtk) @import("../apprt/gtk/c.zig").c else void;
|
const gtk = if (build_config.app_runtime == .gtk) @import("../apprt/gtk/c.zig").c else void;
|
||||||
|
|
||||||
|
pub const Options = struct {};
|
||||||
|
|
||||||
/// The `version` command is used to display information about Ghostty.
|
/// The `version` command is used to display information about Ghostty.
|
||||||
pub fn run(alloc: Allocator) !u8 {
|
pub fn run(alloc: Allocator) !u8 {
|
||||||
_ = alloc;
|
_ = alloc;
|
||||||
|
@ -1574,20 +1574,41 @@ keybind: Keybinds = .{},
|
|||||||
/// editor, etc.
|
/// editor, etc.
|
||||||
@"macos-titlebar-proxy-icon": MacTitlebarProxyIcon = .visible,
|
@"macos-titlebar-proxy-icon": MacTitlebarProxyIcon = .visible,
|
||||||
|
|
||||||
/// If `true`, the *Option* key will be treated as *Alt*. This makes terminal
|
/// macOS doesn't have a distinct "alt" key and instead has the "option"
|
||||||
/// sequences expecting *Alt* to work properly, but will break Unicode input
|
/// key which behaves slightly differently. On macOS by default, the
|
||||||
/// sequences on macOS if you use them via the *Alt* key. You may set this to
|
/// option key plus a character will sometimes produces a Unicode character.
|
||||||
/// `false` to restore the macOS *Alt* key unicode sequences but this will break
|
/// For example, on US standard layouts option-b produces "∫". This may be
|
||||||
/// terminal sequences expecting *Alt* to work.
|
/// undesirable if you want to use "option" as an "alt" key for keybindings
|
||||||
|
/// in terminal programs or shells.
|
||||||
///
|
///
|
||||||
/// The values `left` or `right` enable this for the left or right *Option*
|
/// This configuration lets you change the behavior so that option is treated
|
||||||
/// key, respectively.
|
/// as alt.
|
||||||
|
///
|
||||||
|
/// The default behavior (unset) will depend on your active keyboard
|
||||||
|
/// layout. If your keyboard layout is one of the keyboard layouts listed
|
||||||
|
/// below, then the default value is "true". Otherwise, the default
|
||||||
|
/// value is "false". Keyboard layouts with a default value of "true" are:
|
||||||
|
///
|
||||||
|
/// - U.S. Standard
|
||||||
|
/// - U.S. International
|
||||||
///
|
///
|
||||||
/// Note that if an *Option*-sequence doesn't produce a printable character, it
|
/// Note that if an *Option*-sequence doesn't produce a printable character, it
|
||||||
/// will be treated as *Alt* regardless of this setting. (i.e. `alt+ctrl+a`).
|
/// will be treated as *Alt* regardless of this setting. (i.e. `alt+ctrl+a`).
|
||||||
///
|
///
|
||||||
|
/// Explicit values that can be set:
|
||||||
|
///
|
||||||
|
/// If `true`, the *Option* key will be treated as *Alt*. This makes terminal
|
||||||
|
/// sequences expecting *Alt* to work properly, but will break Unicode input
|
||||||
|
/// sequences on macOS if you use them via the *Alt* key.
|
||||||
|
///
|
||||||
|
/// You may set this to `false` to restore the macOS *Alt* key unicode
|
||||||
|
/// sequences but this will break terminal sequences expecting *Alt* to work.
|
||||||
|
///
|
||||||
|
/// The values `left` or `right` enable this for the left or right *Option*
|
||||||
|
/// key, respectively.
|
||||||
|
///
|
||||||
/// This does not work with GLFW builds.
|
/// This does not work with GLFW builds.
|
||||||
@"macos-option-as-alt": OptionAsAlt = .false,
|
@"macos-option-as-alt": ?OptionAsAlt = null,
|
||||||
|
|
||||||
/// Whether to enable the macOS window shadow. The default value is true.
|
/// Whether to enable the macOS window shadow. The default value is true.
|
||||||
/// With some window managers and window transparency settings, you may
|
/// With some window managers and window transparency settings, you may
|
||||||
@ -4213,14 +4234,9 @@ pub const Keybinds = struct {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try formatter.formatEntry(
|
var buffer_stream = std.io.fixedBufferStream(&buf);
|
||||||
[]const u8,
|
std.fmt.format(buffer_stream.writer(), "{}", .{k}) catch return error.OutOfMemory;
|
||||||
std.fmt.bufPrint(
|
try v.formatEntries(&buffer_stream, formatter);
|
||||||
&buf,
|
|
||||||
"{}{}",
|
|
||||||
.{ k, v },
|
|
||||||
) catch return error.OutOfMemory,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -4254,6 +4270,56 @@ pub const Keybinds = struct {
|
|||||||
try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer()));
|
try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer()));
|
||||||
try std.testing.expectEqualSlices(u8, "a = shift+a=csi:hello\n", buf.items);
|
try std.testing.expectEqualSlices(u8, "a = shift+a=csi:hello\n", buf.items);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Regression test for https://github.com/ghostty-org/ghostty/issues/2734
|
||||||
|
test "formatConfig multiple items" {
|
||||||
|
const testing = std.testing;
|
||||||
|
var buf = std.ArrayList(u8).init(testing.allocator);
|
||||||
|
defer buf.deinit();
|
||||||
|
|
||||||
|
var arena = ArenaAllocator.init(testing.allocator);
|
||||||
|
defer arena.deinit();
|
||||||
|
const alloc = arena.allocator();
|
||||||
|
|
||||||
|
var list: Keybinds = .{};
|
||||||
|
try list.parseCLI(alloc, "ctrl+z>1=goto_tab:1");
|
||||||
|
try list.parseCLI(alloc, "ctrl+z>2=goto_tab:2");
|
||||||
|
try list.formatEntry(formatterpkg.entryFormatter("keybind", buf.writer()));
|
||||||
|
|
||||||
|
const want =
|
||||||
|
\\keybind = ctrl+z>1=goto_tab:1
|
||||||
|
\\keybind = ctrl+z>2=goto_tab:2
|
||||||
|
\\
|
||||||
|
;
|
||||||
|
try std.testing.expectEqualStrings(want, buf.items);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "formatConfig multiple items nested" {
|
||||||
|
const testing = std.testing;
|
||||||
|
var buf = std.ArrayList(u8).init(testing.allocator);
|
||||||
|
defer buf.deinit();
|
||||||
|
|
||||||
|
var arena = ArenaAllocator.init(testing.allocator);
|
||||||
|
defer arena.deinit();
|
||||||
|
const alloc = arena.allocator();
|
||||||
|
|
||||||
|
var list: Keybinds = .{};
|
||||||
|
try list.parseCLI(alloc, "ctrl+a>ctrl+b>n=new_window");
|
||||||
|
try list.parseCLI(alloc, "ctrl+a>ctrl+b>w=close_window");
|
||||||
|
try list.parseCLI(alloc, "ctrl+a>ctrl+c>t=new_tab");
|
||||||
|
try list.parseCLI(alloc, "ctrl+b>ctrl+d>a=previous_tab");
|
||||||
|
try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer()));
|
||||||
|
|
||||||
|
// NB: This does not currently retain the order of the keybinds.
|
||||||
|
const want =
|
||||||
|
\\a = ctrl+a>ctrl+b>w=close_window
|
||||||
|
\\a = ctrl+a>ctrl+b>n=new_window
|
||||||
|
\\a = ctrl+a>ctrl+c>t=new_tab
|
||||||
|
\\a = ctrl+b>ctrl+d>a=previous_tab
|
||||||
|
\\
|
||||||
|
;
|
||||||
|
try std.testing.expectEqualStrings(want, buf.items);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/// See "font-codepoint-map" for documentation.
|
/// See "font-codepoint-map" for documentation.
|
||||||
|
@ -3,6 +3,7 @@ const builtin = @import("builtin");
|
|||||||
|
|
||||||
const mouse = @import("input/mouse.zig");
|
const mouse = @import("input/mouse.zig");
|
||||||
const key = @import("input/key.zig");
|
const key = @import("input/key.zig");
|
||||||
|
const keyboard = @import("input/keyboard.zig");
|
||||||
|
|
||||||
pub const function_keys = @import("input/function_keys.zig");
|
pub const function_keys = @import("input/function_keys.zig");
|
||||||
pub const keycodes = @import("input/keycodes.zig");
|
pub const keycodes = @import("input/keycodes.zig");
|
||||||
@ -13,6 +14,7 @@ pub const Action = key.Action;
|
|||||||
pub const Binding = @import("input/Binding.zig");
|
pub const Binding = @import("input/Binding.zig");
|
||||||
pub const Link = @import("input/Link.zig");
|
pub const Link = @import("input/Link.zig");
|
||||||
pub const Key = key.Key;
|
pub const Key = key.Key;
|
||||||
|
pub const KeyboardLayout = keyboard.Layout;
|
||||||
pub const KeyEncoder = @import("input/KeyEncoder.zig");
|
pub const KeyEncoder = @import("input/KeyEncoder.zig");
|
||||||
pub const KeyEvent = key.KeyEvent;
|
pub const KeyEvent = key.KeyEvent;
|
||||||
pub const InspectorMode = Binding.Action.InspectorMode;
|
pub const InspectorMode = Binding.Action.InspectorMode;
|
||||||
|
@ -1149,6 +1149,41 @@ pub const Set = struct {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Writes the configuration entries for the binding
|
||||||
|
/// that this value is part of.
|
||||||
|
///
|
||||||
|
/// The value may be part of multiple configuration entries
|
||||||
|
/// if they're all part of the same prefix sequence (e.g. 'a>b', 'a>c').
|
||||||
|
/// These will result in multiple separate entries in the configuration.
|
||||||
|
///
|
||||||
|
/// `buffer_stream` is a FixedBufferStream used for temporary storage
|
||||||
|
/// that is shared between calls to nested levels of the set.
|
||||||
|
/// For example, 'a>b>c=x' and 'a>b>d=y' will re-use the 'a>b' written
|
||||||
|
/// to the buffer before flushing it to the formatter with 'c=x' and 'd=y'.
|
||||||
|
pub fn formatEntries(self: Value, buffer_stream: anytype, formatter: anytype) !void {
|
||||||
|
switch (self) {
|
||||||
|
.leader => |set| {
|
||||||
|
// We'll rewind to this position after each sub-entry,
|
||||||
|
// sharing the prefix between siblings.
|
||||||
|
const pos = try buffer_stream.getPos();
|
||||||
|
|
||||||
|
var iter = set.bindings.iterator();
|
||||||
|
while (iter.next()) |binding| {
|
||||||
|
buffer_stream.seekTo(pos) catch unreachable; // can't fail
|
||||||
|
std.fmt.format(buffer_stream.writer(), ">{s}", .{binding.key_ptr.*}) catch return error.OutOfMemory;
|
||||||
|
try binding.value_ptr.*.formatEntries(buffer_stream, formatter);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
.leaf => |leaf| {
|
||||||
|
// When we get to the leaf, the buffer_stream contains
|
||||||
|
// the full sequence of keys needed to reach this action.
|
||||||
|
std.fmt.format(buffer_stream.writer(), "={s}", .{leaf.action}) catch return error.OutOfMemory;
|
||||||
|
try formatter.formatEntry([]const u8, buffer_stream.getWritten());
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Leaf node of a set is an action to trigger. This is a "leaf" compared
|
/// Leaf node of a set is an action to trigger. This is a "leaf" compared
|
||||||
|
@ -208,7 +208,7 @@ fn kitty(
|
|||||||
// Determine if the Alt modifier should be treated as an actual
|
// Determine if the Alt modifier should be treated as an actual
|
||||||
// modifier (in which case it prevents associated text) or as
|
// modifier (in which case it prevents associated text) or as
|
||||||
// the macOS Option key, which does not prevent associated text.
|
// the macOS Option key, which does not prevent associated text.
|
||||||
const alt_prevents_text = if (comptime builtin.target.isDarwin())
|
const alt_prevents_text = if (comptime builtin.os.tag == .macos)
|
||||||
switch (self.macos_option_as_alt) {
|
switch (self.macos_option_as_alt) {
|
||||||
.left => all_mods.sides.alt == .left,
|
.left => all_mods.sides.alt == .left,
|
||||||
.right => all_mods.sides.alt == .right,
|
.right => all_mods.sides.alt == .right,
|
||||||
@ -422,7 +422,7 @@ fn legacyAltPrefix(
|
|||||||
// On macOS, we only handle option like alt in certain
|
// On macOS, we only handle option like alt in certain
|
||||||
// circumstances. Otherwise, macOS does a unicode translation
|
// circumstances. Otherwise, macOS does a unicode translation
|
||||||
// and we allow that to happen.
|
// and we allow that to happen.
|
||||||
if (comptime builtin.target.isDarwin()) {
|
if (comptime builtin.os.tag == .macos) {
|
||||||
switch (self.macos_option_as_alt) {
|
switch (self.macos_option_as_alt) {
|
||||||
.false => return null,
|
.false => return null,
|
||||||
.left => if (mods.sides.alt == .right) return null,
|
.left => if (mods.sides.alt == .right) return null,
|
||||||
|
@ -14,6 +14,7 @@ const Keymap = @This();
|
|||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const builtin = @import("builtin");
|
const builtin = @import("builtin");
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
const macos = @import("macos");
|
const macos = @import("macos");
|
||||||
const codes = @import("keycodes.zig").entries;
|
const codes = @import("keycodes.zig").entries;
|
||||||
const Key = @import("key.zig").Key;
|
const Key = @import("key.zig").Key;
|
||||||
@ -72,6 +73,24 @@ pub fn reload(self: *Keymap) !void {
|
|||||||
try self.reinit();
|
try self.reinit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the input source ID for the current keyboard layout. The input
|
||||||
|
/// source ID is a unique identifier for the keyboard layout which is uniquely
|
||||||
|
/// defined by Apple.
|
||||||
|
///
|
||||||
|
/// This is macOS-only. Other platforms don't have an equivalent of this
|
||||||
|
/// so this isn't expected to be generally implemented.
|
||||||
|
pub fn sourceId(self: *const Keymap, buf: []u8) Allocator.Error![]const u8 {
|
||||||
|
// Get the raw CFStringRef
|
||||||
|
const id_raw = TISGetInputSourceProperty(
|
||||||
|
self.source,
|
||||||
|
kTISPropertyInputSourceID,
|
||||||
|
) orelse return error.OutOfMemory;
|
||||||
|
|
||||||
|
// Convert the CFStringRef to a C string into our buffer.
|
||||||
|
const id: *CFString = @ptrCast(id_raw);
|
||||||
|
return id.cstring(buf, .utf8) orelse error.OutOfMemory;
|
||||||
|
}
|
||||||
|
|
||||||
/// Reinit reinitializes the keymap. It assumes that all the memory associated
|
/// Reinit reinitializes the keymap. It assumes that all the memory associated
|
||||||
/// with the keymap is already freed.
|
/// with the keymap is already freed.
|
||||||
fn reinit(self: *Keymap) !void {
|
fn reinit(self: *Keymap) !void {
|
||||||
@ -89,6 +108,12 @@ fn reinit(self: *Keymap) !void {
|
|||||||
// The CFDataRef contains a UCKeyboardLayout pointer
|
// The CFDataRef contains a UCKeyboardLayout pointer
|
||||||
break :layout @ptrCast(data.getPointer());
|
break :layout @ptrCast(data.getPointer());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (comptime builtin.mode == .Debug) id: {
|
||||||
|
var buf: [256]u8 = undefined;
|
||||||
|
const id = self.sourceId(&buf) catch break :id;
|
||||||
|
std.log.debug("keyboard layout={s}", .{id});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Translate a single key input into a utf8 sequence.
|
/// Translate a single key input into a utf8 sequence.
|
||||||
@ -200,6 +225,7 @@ extern "c" fn LMGetKbdType() u8;
|
|||||||
extern "c" fn UCKeyTranslate(*const UCKeyboardLayout, u16, u16, u32, u32, u32, *u32, c_ulong, *c_ulong, [*]u16) i32;
|
extern "c" fn UCKeyTranslate(*const UCKeyboardLayout, u16, u16, u32, u32, u32, *u32, c_ulong, *c_ulong, [*]u16) i32;
|
||||||
extern const kTISPropertyLocalizedName: *CFString;
|
extern const kTISPropertyLocalizedName: *CFString;
|
||||||
extern const kTISPropertyUnicodeKeyLayoutData: *CFString;
|
extern const kTISPropertyUnicodeKeyLayoutData: *CFString;
|
||||||
|
extern const kTISPropertyInputSourceID: *CFString;
|
||||||
const TISInputSource = opaque {};
|
const TISInputSource = opaque {};
|
||||||
const UCKeyboardLayout = opaque {};
|
const UCKeyboardLayout = opaque {};
|
||||||
const kUCKeyActionDown: u16 = 0;
|
const kUCKeyActionDown: u16 = 0;
|
||||||
|
58
src/input/keyboard.zig
Normal file
58
src/input/keyboard.zig
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const OptionAsAlt = @import("../config.zig").OptionAsAlt;
|
||||||
|
|
||||||
|
/// Keyboard layouts.
|
||||||
|
///
|
||||||
|
/// These aren't heavily used in Ghostty and having a fully comprehensive
|
||||||
|
/// list is not important. We only need to distinguish between a few
|
||||||
|
/// different layouts for some nice-to-have features, such as setting a default
|
||||||
|
/// value for "macos-option-as-alt".
|
||||||
|
pub const Layout = enum {
|
||||||
|
// Unknown, unmapped layout. Ghostty should not make any assumptions
|
||||||
|
// about the layout of the keyboard.
|
||||||
|
unknown,
|
||||||
|
|
||||||
|
// The remaining should be fairly self-explanatory:
|
||||||
|
us_standard,
|
||||||
|
us_international,
|
||||||
|
|
||||||
|
/// Map an Apple keyboard layout ID to a value in this enum. The layout
|
||||||
|
/// ID can be retrieved using Carbon's TIKeyboardLayoutGetInputSourceProperty
|
||||||
|
/// function.
|
||||||
|
///
|
||||||
|
/// Even though our layout supports "unknown", we return null if we don't
|
||||||
|
/// recognize the layout ID so callers can detect this scenario.
|
||||||
|
pub fn mapAppleId(id: []const u8) ?Layout {
|
||||||
|
if (std.mem.eql(u8, id, "com.apple.keylayout.US")) {
|
||||||
|
return .us_standard;
|
||||||
|
} else if (std.mem.eql(u8, id, "com.apple.keylayout.USInternational")) {
|
||||||
|
return .us_international;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the default macos-option-as-alt value for this layout.
|
||||||
|
///
|
||||||
|
/// We apply some heuristics to change the default based on the keyboard
|
||||||
|
/// layout if "macos-option-as-alt" is unset. We do this because on some
|
||||||
|
/// keyboard layouts such as US standard layouts, users generally expect
|
||||||
|
/// an input such as option-b to map to alt-b but macOS by default will
|
||||||
|
/// convert it to the codepoint "∫".
|
||||||
|
///
|
||||||
|
/// This behavior however is desired on international layout where the
|
||||||
|
/// option key is used for important, regularly used inputs.
|
||||||
|
pub fn detectOptionAsAlt(self: Layout) OptionAsAlt {
|
||||||
|
return switch (self) {
|
||||||
|
// On US standard, the option key is typically used as alt
|
||||||
|
// and not as a modifier for other codepoints. For example,
|
||||||
|
// option-B = ∫ but usually the user wants alt-B.
|
||||||
|
.us_standard,
|
||||||
|
.us_international,
|
||||||
|
=> .true,
|
||||||
|
|
||||||
|
.unknown,
|
||||||
|
=> .false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
Reference in New Issue
Block a user