From 0d0aeccf0fdc01a97fba0832b9af6303995b90c1 Mon Sep 17 00:00:00 2001 From: Dmitry Zhlobo Date: Sat, 7 Dec 2024 19:46:39 +0100 Subject: [PATCH 01/11] fix unwanted resize of non-native fullscreen window Removing autoHideDock and autoHideMenuBar options cause window to resize. Fix #2516 --- macos/Sources/Helpers/Fullscreen.swift | 53 ++------------------------ 1 file changed, 3 insertions(+), 50 deletions(-) diff --git a/macos/Sources/Helpers/Fullscreen.swift b/macos/Sources/Helpers/Fullscreen.swift index bb3859e07..ca93ac533 100644 --- a/macos/Sources/Helpers/Fullscreen.swift +++ b/macos/Sources/Helpers/Fullscreen.swift @@ -167,6 +167,9 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle { self.savedState = savedState // We hide the dock if the window is on a screen with the dock. + // 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() } @@ -176,18 +179,6 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle { 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. NotificationCenter.default.addObserver( self, @@ -218,8 +209,6 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle { // Remove all our notifications. We remove them one by one because // we don't want to remove the observers that our superclass sets. 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) // Unhide our elements @@ -311,42 +300,6 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle { 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 private func hideDock() { From 080afce64947d7f37cd5732be82e739add544edf Mon Sep 17 00:00:00 2001 From: Dmitry Zhlobo Date: Sat, 7 Dec 2024 19:44:22 +0100 Subject: [PATCH 02/11] found a better explanation for the reason to hide dock before menu --- macos/Sources/Helpers/Fullscreen.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/macos/Sources/Helpers/Fullscreen.swift b/macos/Sources/Helpers/Fullscreen.swift index ca93ac533..0d7b8b962 100644 --- a/macos/Sources/Helpers/Fullscreen.swift +++ b/macos/Sources/Helpers/Fullscreen.swift @@ -167,9 +167,9 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle { self.savedState = savedState // We hide the dock if the window is on a screen with the dock. - // 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. + // 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) { hideDock() } From e2e12efbbf8331eee5aae1c676065269664f4789 Mon Sep 17 00:00:00 2001 From: Abhinav Gupta Date: Tue, 10 Dec 2024 21:05:33 -0800 Subject: [PATCH 03/11] keybind: format leader bindings into multiple entries **Context** Currently, if there are multiple keybindings with a shared prefix, they are grouped into a nested series of Binding.Sets. For example, as reported in #2734, the following bindings: keybind = ctrl+z>1=goto_tab:1 keybind = ctrl+z>2=goto_tab:2 keybind = ctrl+z>3=goto_tab:3 Result in roughly the following structure (in pseudo-code): Keybinds{ Trigger("ctrl+z"): Value.leader{ Trigger("1"): Value.leaf{action: "goto_tab:1"}), Trigger("2"): Value.leaf{action: "goto_tab:2"}), Trigger("3"): Value.leaf{action: "goto_tab:3"}), } } When this is formatted into a string (and therefore in +list-keybinds), it is turned into the following as Value.format just concatenates all the sibling bindings ('1', '2', '3') into consecutive bindings, and this is then fed into a single configuration entry: keybind = ctrl+z>1=goto_tab:1>3=goto_tab:3>2=goto_tab:2 **Fix** To fix this, Value needs to produce a separate configuration entry for each sibling binding in the Value.leader case. So we can't produce the entry (formatter.formatEntry) in Keybinds and need to pass information down the Value tree to the leaf nodes, each of which will produce a separate entry with that function. This is accomplished with the help of a new Value.formatEntries method that recursively builds up the prefix for the keybinding, finally flushing it to the formatter when it reaches a leaf node. This is done without extra allocations by using a FixedBufferStream with the same buffer as before, sharing it between calls to nested siblings of the same prefix. **Caveats** We do not track the order in which the bindings were added so the order is not retained in the formatConfig output. Resolves #2734 --- src/config/Config.zig | 61 +++++++++++++++++++++++++++++++++++++------ src/input/Binding.zig | 35 +++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 8 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index fa531dc7e..47174aa82 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -4213,14 +4213,9 @@ pub const Keybinds = struct { } } - try formatter.formatEntry( - []const u8, - std.fmt.bufPrint( - &buf, - "{}{}", - .{ k, v }, - ) catch return error.OutOfMemory, - ); + var buffer_stream = std.io.fixedBufferStream(&buf); + try std.fmt.format(buffer_stream.writer(), "{}", .{k}); + try v.formatEntries(&buffer_stream, formatter); } } @@ -4254,6 +4249,56 @@ pub const Keybinds = struct { try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); 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. diff --git a/src/input/Binding.zig b/src/input/Binding.zig index a467bfc2b..ebccac196 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -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 + try std.fmt.format(buffer_stream.writer(), ">{s}", .{binding.key_ptr.*}); + 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. + try std.fmt.format(buffer_stream.writer(), "={s}", .{leaf.action}); + try formatter.formatEntry([]const u8, buffer_stream.getWritten()); + }, + } + } }; /// Leaf node of a set is an action to trigger. This is a "leaf" compared From cb5848c7b7b03680df1943c7b612e15af23023bb Mon Sep 17 00:00:00 2001 From: Khang Nguyen Duy Date: Wed, 11 Dec 2024 16:47:20 +0700 Subject: [PATCH 04/11] command: fix hostname test compatibility hostname is not guaranteed on *nix as in the comment. For example, Arch does not have hostname by default. --- src/Command.zig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Command.zig b/src/Command.zig index daca54f94..2b600979f 100644 --- a/src/Command.zig +++ b/src/Command.zig @@ -440,9 +440,9 @@ fn isExecutable(mode: std.fs.File.Mode) bool { 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" { - 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)).?; defer testing.allocator.free(path); try testing.expect(path.len > executable.len); From c7deea6a7f9978869d711311ded88b3ec2f1333d Mon Sep 17 00:00:00 2001 From: Anund Date: Wed, 11 Dec 2024 19:55:39 +1100 Subject: [PATCH 05/11] zsh: add completions generation --- build.zig | 13 +++ src/build/fish_completions.zig | 4 +- src/build/zsh_completions.zig | 201 +++++++++++++++++++++++++++++++++ src/cli/action.zig | 20 ++++ src/cli/list_fonts.zig | 10 +- src/cli/version.zig | 2 + 6 files changed, 243 insertions(+), 7 deletions(-) create mode 100644 src/build/zsh_completions.zig diff --git a/build.zig b/build.zig index d233bff1f..0c5cfaf41 100644 --- a/build.zig +++ b/build.zig @@ -12,6 +12,7 @@ const terminfo = @import("src/terminfo/main.zig"); const config_vim = @import("src/config/vim.zig"); const config_sublime_syntax = @import("src/config/sublime_syntax.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 BuildConfig = build_config.BuildConfig; 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 { const wf = b.addWriteFiles(); diff --git a/src/build/fish_completions.zig b/src/build/fish_completions.zig index 2ac67bdad..87a82e7ee 100644 --- a/src/build/fish_completions.zig +++ b/src/build/fish_completions.zig @@ -2,7 +2,7 @@ const std = @import("std"); const Config = @import("../config/Config.zig"); const Action = @import("../cli/action.zig").Action; -const ListFontsConfig = @import("../cli/list_fonts.zig").Config; +const ListFontsOptions = @import("../cli/list_fonts.zig").Options; const ShowConfigOptions = @import("../cli/show_config.zig").Options; const ListKeybindsOptions = @import("../cli/list_keybinds.zig").Options; @@ -100,7 +100,7 @@ fn writeFishCompletions(writer: anytype) !void { try writer.writeAll("\"\n"); } - for (@typeInfo(ListFontsConfig).Struct.fields) |field| { + for (@typeInfo(ListFontsOptions).Struct.fields) |field| { if (field.name[0] == '_') continue; try writer.writeAll("complete -c ghostty -n \"__fish_seen_subcommand_from +list-fonts\" -l "); try writer.writeAll(field.name); diff --git a/src/build/zsh_completions.zig b/src/build/zsh_completions.zig new file mode 100644 index 000000000..78d256ee2 --- /dev/null +++ b/src/build/zsh_completions.zig @@ -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 "$@" + \\ + ); +} diff --git a/src/cli/action.zig b/src/cli/action.zig index 1da0c0609..2f4b63638 100644 --- a/src/cli/action.zig +++ b/src/cli/action.zig @@ -163,6 +163,26 @@ pub const Action = enum { 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" { diff --git a/src/cli/list_fonts.zig b/src/cli/list_fonts.zig index aba596b64..9d1f34cd1 100644 --- a/src/cli/list_fonts.zig +++ b/src/cli/list_fonts.zig @@ -7,7 +7,7 @@ const font = @import("../font/main.zig"); const log = std.log.scoped(.list_fonts); -pub const Config = struct { +pub const Options = struct { /// This is set by the CLI parser for deinit. _arena: ?ArenaAllocator = null, @@ -23,13 +23,13 @@ pub const Config = struct { bold: bool = false, italic: bool = false, - pub fn deinit(self: *Config) void { + pub fn deinit(self: *Options) void { if (self._arena) |arena| arena.deinit(); self.* = undefined; } /// Enables "-h" and "--help" to work. - pub fn help(self: Config) !void { + pub fn help(self: Options) !void { _ = self; return Action.help_error; } @@ -59,9 +59,9 @@ pub fn run(alloc: Allocator) !u8 { } fn runArgs(alloc_gpa: Allocator, argsIter: anytype) !u8 { - var config: Config = .{}; + var config: Options = .{}; 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 var arena = ArenaAllocator.init(alloc_gpa); diff --git a/src/cli/version.zig b/src/cli/version.zig index 26d5dcc74..259cb7453 100644 --- a/src/cli/version.zig +++ b/src/cli/version.zig @@ -7,6 +7,8 @@ const xev = @import("xev"); const renderer = @import("../renderer.zig"); 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. pub fn run(alloc: Allocator) !u8 { _ = alloc; From 54bd012443d45a89d49f7b18522b7cfe8aa5abb6 Mon Sep 17 00:00:00 2001 From: Anund Date: Wed, 11 Dec 2024 19:55:59 +1100 Subject: [PATCH 06/11] fish: reuse Action options iteration code --- src/build/fish_completions.zig | 88 +++++++++++----------------------- 1 file changed, 27 insertions(+), 61 deletions(-) diff --git a/src/build/fish_completions.zig b/src/build/fish_completions.zig index 87a82e7ee..5212dab61 100644 --- a/src/build/fish_completions.zig +++ b/src/build/fish_completions.zig @@ -2,9 +2,6 @@ const std = @import("std"); const Config = @import("../config/Config.zig"); const Action = @import("../cli/action.zig").Action; -const ListFontsOptions = @import("../cli/list_fonts.zig").Options; -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 /// and options. @@ -100,66 +97,35 @@ fn writeFishCompletions(writer: anytype) !void { try writer.writeAll("\"\n"); } - for (@typeInfo(ListFontsOptions).Struct.fields) |field| { - if (field.name[0] == '_') continue; - try writer.writeAll("complete -c ghostty -n \"__fish_seen_subcommand_from +list-fonts\" -l "); - 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(Action).Enum.fields) |field| { + if (std.mem.eql(u8, "help", field.name)) continue; + if (std.mem.eql(u8, "version", field.name)) continue; - for (@typeInfo(ShowConfigOptions).Struct.fields) |field| { - if (field.name[0] == '_') continue; - try writer.writeAll("complete -c ghostty -n \"__fish_seen_subcommand_from +show-config\" -l "); - 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"); - } + const options = @field(Action, field.name).options(); + for (@typeInfo(options).Struct.fields) |opt| { + if (opt.name[0] == '_') continue; + try writer.writeAll("complete -c ghostty -n \"__fish_seen_subcommand_from +" ++ field.name ++ "\" -l "); + try writer.writeAll(opt.name); + try writer.writeAll(if (opt.type != bool) " -r" else ""); - for (@typeInfo(ListKeybindsOptions).Struct.fields) |field| { - if (field.name[0] == '_') continue; - try writer.writeAll("complete -c ghostty -n \"__fish_seen_subcommand_from +list-keybinds\" -l "); - 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 => {}, + // special case +validate_config --config-file + if (std.mem.eql(u8, "config-file", opt.name)) { + try writer.writeAll(" -F"); + } else try writer.writeAll(" -f"); + + switch (@typeInfo(opt.type)) { + .Bool => try writer.writeAll(" -a \"true false\""), + .Enum => |info| { + try writer.writeAll(" -a \""); + for (info.opts, 0..) |f, i| { + if (i > 0) try writer.writeAll(" "); + try writer.writeAll(f.name); + } + try writer.writeAll("\""); + }, + else => {}, + } + try writer.writeAll("\n"); } - try writer.writeAll("\n"); } } From 495e4081e4945042eb918b66942b59a60d0d6f57 Mon Sep 17 00:00:00 2001 From: Abhinav Gupta Date: Wed, 11 Dec 2024 09:21:31 -0800 Subject: [PATCH 07/11] fix: NoSpaceLeft => OutOfMemory NoSpaceLeft is not permitted to be returned in this context, so turn it into OutOfMemory when we fail to write to the buffer. --- src/config/Config.zig | 2 +- src/input/Binding.zig | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index 47174aa82..0bcfc743e 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -4214,7 +4214,7 @@ pub const Keybinds = struct { } var buffer_stream = std.io.fixedBufferStream(&buf); - try std.fmt.format(buffer_stream.writer(), "{}", .{k}); + std.fmt.format(buffer_stream.writer(), "{}", .{k}) catch return error.OutOfMemory; try v.formatEntries(&buffer_stream, formatter); } } diff --git a/src/input/Binding.zig b/src/input/Binding.zig index ebccac196..b451b5ec9 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -1171,7 +1171,7 @@ pub const Set = struct { var iter = set.bindings.iterator(); while (iter.next()) |binding| { buffer_stream.seekTo(pos) catch unreachable; // can't fail - try std.fmt.format(buffer_stream.writer(), ">{s}", .{binding.key_ptr.*}); + std.fmt.format(buffer_stream.writer(), ">{s}", .{binding.key_ptr.*}) catch return error.OutOfMemory; try binding.value_ptr.*.formatEntries(buffer_stream, formatter); } }, @@ -1179,7 +1179,7 @@ pub const Set = struct { .leaf => |leaf| { // When we get to the leaf, the buffer_stream contains // the full sequence of keys needed to reach this action. - try std.fmt.format(buffer_stream.writer(), "={s}", .{leaf.action}); + std.fmt.format(buffer_stream.writer(), "={s}", .{leaf.action}) catch return error.OutOfMemory; try formatter.formatEntry([]const u8, buffer_stream.getWritten()); }, } From cb67fbd08db5472dddd12a2f966d1218c79cf49d Mon Sep 17 00:00:00 2001 From: Khang Nguyen Duy Date: Thu, 12 Dec 2024 00:13:25 +0700 Subject: [PATCH 08/11] gtk: pass surface to clipboard window by reference instead of by value The surface might be mutated during the clipboard confirmation (resized in my case), leading to the copied cursor page_pin being invalidated. --- src/apprt/gtk/ClipboardConfirmationWindow.zig | 6 +++--- src/apprt/gtk/Surface.zig | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/apprt/gtk/ClipboardConfirmationWindow.zig b/src/apprt/gtk/ClipboardConfirmationWindow.zig index bcefb9d8a..30b38f1d4 100644 --- a/src/apprt/gtk/ClipboardConfirmationWindow.zig +++ b/src/apprt/gtk/ClipboardConfirmationWindow.zig @@ -17,13 +17,13 @@ window: *c.GtkWindow, view: PrimaryView, data: [:0]u8, -core_surface: CoreSurface, +core_surface: *CoreSurface, pending_req: apprt.ClipboardRequest, pub fn create( app: *App, data: []const u8, - core_surface: CoreSurface, + core_surface: *CoreSurface, request: apprt.ClipboardRequest, ) !void { if (app.clipboard_confirmation_window != null) return error.WindowAlreadyExists; @@ -54,7 +54,7 @@ fn init( self: *ClipboardConfirmation, app: *App, data: []const u8, - core_surface: CoreSurface, + core_surface: *CoreSurface, request: apprt.ClipboardRequest, ) !void { // Create the window diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 9a361c228..3ad695909 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -1051,7 +1051,7 @@ pub fn clipboardRequest( } pub fn setClipboardString( - self: *const Surface, + self: *Surface, val: [:0]const u8, clipboard_type: apprt.Clipboard, confirm: bool, @@ -1065,7 +1065,7 @@ pub fn setClipboardString( ClipboardConfirmationWindow.create( self.app, val, - self.core_surface, + &self.core_surface, .{ .osc_52_write = clipboard_type }, ) catch |window_err| { log.err("failed to create clipboard confirmation window err={}", .{window_err}); @@ -1113,7 +1113,7 @@ fn gtkClipboardRead( ClipboardConfirmationWindow.create( self.app, str, - self.core_surface, + &self.core_surface, req.state, ) catch |window_err| { log.err("failed to create clipboard confirmation window err={}", .{window_err}); From df97c19a375ea23e3d0db9737357c525e6c86ea9 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 11 Dec 2024 09:34:54 -0800 Subject: [PATCH 09/11] macOS: "option-as-alt" defaults to "true" for US keyboard layouts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A common issue for US-centric users of a terminal is that the "option" key on macOS is not treated as the "alt" key in the terminal. ## Background macOS does not have an "alt" key, but instead has an "option" key. The "option" key is used for a variety of purposes, but the troublesome behavior for some (and expected/desired behavior for others) is that it is used to input special characters. For example, on a US standard layout, `option-b` inputs `∫`. This is not a typically desired character when using a terminal and most users will instead expect that `option-b` maps to `alt-b` for keybinding purposes with whatever shell, TUI, editor, etc. they're using. On non-US layouts, the "option" key is a critical modifier key for inputting certain characters in the same way "shift" is a critical modifier key for inputting certain characters on US layouts. We previously tried to change the default for `macos-option-as-alt` to `left` (so that the left option key behaves as alt) because I had the wrong assumption that international users always used the right option key with terminals or were used to this. But very quickly beta users with different layouts (such as German, I believe) noted that this is not the case and broke their idiomatic input behavior. This behavior was therefore reverted. ## Solution This confusing behavior happened frequently enough that I decided to implement the more complex behavior in this commit. The new behavior is that when a US layout is active, `macos-option-as-alt` defaults to true if it is unset. When a non-US layout is active, `macos-option-as-alt` defaults to false if it is unset. This happens live as users change their keyboard layout. **An important goal of Ghostty is to have zero-config defaults** that satisfy the majority of users. Fiddling with configurations is -- for most -- an annoying task and software that works well enough out of the box is delightful. Based on surveying beta users, I believe this commit will result in less configuration for the majority of users. ## Other Terminals This behavior is unique amongst terminals as far as I know. Terminal.app, Kitty, iTerm2, Alacritty (I stopped checking there) all default to the default macOS behavior (option is option and special characters are inputted). All of the aforementioned terminals have a setting to change this behavior, identical to Ghostty (or, Ghostty identical to them perhaps since they all predate Ghostty). I couldn't find any history where users requested the behavior of defaulting this to something else for US based keyboards. That's interesting since this has come up so frequently during the Ghostty beta! --- src/Surface.zig | 18 ++++++++++-- src/apprt/embedded.zig | 38 ++++++++++++++++++++++--- src/config/Config.zig | 37 ++++++++++++++++++------ src/input.zig | 2 ++ src/input/KeyEncoder.zig | 4 +-- src/input/KeymapDarwin.zig | 26 +++++++++++++++++ src/input/keyboard.zig | 58 ++++++++++++++++++++++++++++++++++++++ 7 files changed, 167 insertions(+), 16 deletions(-) create mode 100644 src/input/keyboard.zig diff --git a/src/Surface.zig b/src/Surface.zig index 3e7300d08..9fc5b1d90 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -245,7 +245,7 @@ const DerivedConfig = struct { mouse_scroll_multiplier: f64, mouse_shift_capture: configpkg.MouseShiftCapture, macos_non_native_fullscreen: configpkg.NonNativeFullscreen, - macos_option_as_alt: configpkg.OptionAsAlt, + macos_option_as_alt: ?configpkg.OptionAsAlt, vt_kam_allowed: bool, window_padding_top: u32, window_padding_bottom: u32, @@ -1990,12 +1990,26 @@ fn encodeKey( // inputs there are many keybindings that result in no encoding // whatsoever. 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(); defer self.renderer_state.mutex.unlock(); const t = &self.io.terminal; break :enc .{ .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), .cursor_key_application = t.modes.get(.cursor_keys), .keypad_key_application = t.modes.get(.keypad_keys), diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index 6a4411a85..451605af7 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -105,11 +105,14 @@ pub const App = struct { var config_clone = try config.clone(alloc); errdefer config_clone.deinit(); + var keymap = try input.Keymap.init(); + errdefer keymap.deinit(); + return .{ .core_app = core_app, .config = config_clone, .opts = opts, - .keymap = try input.Keymap.init(), + .keymap = keymap, .keymap_state = .{}, }; } @@ -161,8 +164,15 @@ pub const App = struct { // then we strip the alt modifier from the mods for translation. const translate_mods = translate_mods: { var translate_mods = mods; - if (comptime builtin.target.isDarwin()) { - const strip = switch (self.config.@"macos-option-as-alt") { + if ((comptime builtin.target.isDarwin()) and translate_mods.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, .true => mods.alt, .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 { self.opts.wakeup(self.opts.userdata); } @@ -1551,7 +1580,8 @@ pub const CAPI = struct { @truncate(@as(c_uint, @bitCast(mods_raw))), )); 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))); } diff --git a/src/config/Config.zig b/src/config/Config.zig index fa531dc7e..7771a60ec 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -1574,20 +1574,41 @@ keybind: Keybinds = .{}, /// editor, etc. @"macos-titlebar-proxy-icon": MacTitlebarProxyIcon = .visible, -/// 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. +/// macOS doesn't have a distinct "alt" key and instead has the "option" +/// key which behaves slightly differently. On macOS by default, the +/// option key plus a character will sometimes produces a Unicode character. +/// For example, on US standard layouts option-b produces "∫". This may be +/// 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* -/// key, respectively. +/// This configuration lets you change the behavior so that option is treated +/// 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 /// 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. -@"macos-option-as-alt": OptionAsAlt = .false, +@"macos-option-as-alt": ?OptionAsAlt = null, /// Whether to enable the macOS window shadow. The default value is true. /// With some window managers and window transparency settings, you may diff --git a/src/input.zig b/src/input.zig index 9e3997d97..83be38d3d 100644 --- a/src/input.zig +++ b/src/input.zig @@ -3,6 +3,7 @@ const builtin = @import("builtin"); const mouse = @import("input/mouse.zig"); const key = @import("input/key.zig"); +const keyboard = @import("input/keyboard.zig"); pub const function_keys = @import("input/function_keys.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 Link = @import("input/Link.zig"); pub const Key = key.Key; +pub const KeyboardLayout = keyboard.Layout; pub const KeyEncoder = @import("input/KeyEncoder.zig"); pub const KeyEvent = key.KeyEvent; pub const InspectorMode = Binding.Action.InspectorMode; diff --git a/src/input/KeyEncoder.zig b/src/input/KeyEncoder.zig index 25d85e78d..4bac7ee6b 100644 --- a/src/input/KeyEncoder.zig +++ b/src/input/KeyEncoder.zig @@ -208,7 +208,7 @@ fn kitty( // Determine if the Alt modifier should be treated as an actual // modifier (in which case it prevents associated text) or as // 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) { .left => all_mods.sides.alt == .left, .right => all_mods.sides.alt == .right, @@ -422,7 +422,7 @@ fn legacyAltPrefix( // On macOS, we only handle option like alt in certain // circumstances. Otherwise, macOS does a unicode translation // and we allow that to happen. - if (comptime builtin.target.isDarwin()) { + if (comptime builtin.os.tag == .macos) { switch (self.macos_option_as_alt) { .false => return null, .left => if (mods.sides.alt == .right) return null, diff --git a/src/input/KeymapDarwin.zig b/src/input/KeymapDarwin.zig index 5ba7c6440..3d81b0f4b 100644 --- a/src/input/KeymapDarwin.zig +++ b/src/input/KeymapDarwin.zig @@ -14,6 +14,7 @@ const Keymap = @This(); const std = @import("std"); const builtin = @import("builtin"); +const Allocator = std.mem.Allocator; const macos = @import("macos"); const codes = @import("keycodes.zig").entries; const Key = @import("key.zig").Key; @@ -72,6 +73,24 @@ pub fn reload(self: *Keymap) !void { 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 /// with the keymap is already freed. fn reinit(self: *Keymap) !void { @@ -89,6 +108,12 @@ fn reinit(self: *Keymap) !void { // The CFDataRef contains a UCKeyboardLayout pointer 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. @@ -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 const kTISPropertyLocalizedName: *CFString; extern const kTISPropertyUnicodeKeyLayoutData: *CFString; +extern const kTISPropertyInputSourceID: *CFString; const TISInputSource = opaque {}; const UCKeyboardLayout = opaque {}; const kUCKeyActionDown: u16 = 0; diff --git a/src/input/keyboard.zig b/src/input/keyboard.zig new file mode 100644 index 000000000..73674df2c --- /dev/null +++ b/src/input/keyboard.zig @@ -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, + }; + } +}; From ab60fbc096674da013e87c769f2d79a4f30944f6 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 11 Dec 2024 11:14:36 -0800 Subject: [PATCH 10/11] apprt/glfw: add noop keyboardLayout func to satisfy tests and builds --- src/apprt/glfw.zig | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/apprt/glfw.zig b/src/apprt/glfw.zig index bf4c44ad0..64b0cbe81 100644 --- a/src/apprt/glfw.zig +++ b/src/apprt/glfw.zig @@ -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 and the artifact is a standalone exe. We don't target libs because /// the embedded API doesn't do windowing. From d016bf8392c9c6a82d2d8e77a1dea4912744621d Mon Sep 17 00:00:00 2001 From: Gregory Anders Date: Wed, 11 Dec 2024 13:15:24 -0600 Subject: [PATCH 11/11] mdgen: use bold face for option and action names --- src/build/mdgen/mdgen.zig | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/build/mdgen/mdgen.zig b/src/build/mdgen/mdgen.zig index 7e05596d7..c7e8c8638 100644 --- a/src/build/mdgen/mdgen.zig +++ b/src/build/mdgen/mdgen.zig @@ -30,10 +30,10 @@ pub fn genConfig(writer: anytype, cli: bool) !void { inline for (@typeInfo(Config).Struct.fields) |field| { if (field.name[0] == '_') continue; - try writer.writeAll("`"); + try writer.writeAll("**`"); if (cli) try writer.writeAll("--"); try writer.writeAll(field.name); - try writer.writeAll("`\n\n"); + try writer.writeAll("`**\n\n"); if (@hasDecl(help_strings.Config, field.name)) { var iter = std.mem.splitScalar(u8, @field(help_strings.Config, field.name), '\n'); var first = true; @@ -60,12 +60,12 @@ pub fn genActions(writer: anytype) !void { const action = std.meta.stringToEnum(Action, field.name).?; switch (action) { - .help => try writer.writeAll("`--help`\n\n"), - .version => try writer.writeAll("`--version`\n\n"), + .help => try writer.writeAll("**`--help`**\n\n"), + .version => try writer.writeAll("**`--version`**\n\n"), else => { - try writer.writeAll("`+"); + try writer.writeAll("**`+"); 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| { if (field.name[0] == '_') continue; - try writer.writeAll("`"); + try writer.writeAll("**`"); try writer.writeAll(field.name); - try writer.writeAll("`\n\n"); + try writer.writeAll("`**\n\n"); if (@hasDecl(help_strings.KeybindAction, field.name)) { var iter = std.mem.splitScalar(u8, @field(help_strings.KeybindAction, field.name), '\n');