Merge branch 'ghostty-org:main' into main

This commit is contained in:
plyght
2024-11-24 18:39:49 -05:00
committed by GitHub
134 changed files with 6697 additions and 2756 deletions

View File

@ -158,6 +158,9 @@ jobs:
name: ghostty name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
- name: XCode Select
run: sudo xcode-select -s /Applications/Xcode_16.0.app
# Setup Sparkle # Setup Sparkle
- name: Setup Sparkle - name: Setup Sparkle
env: env:
@ -187,7 +190,6 @@ jobs:
- name: Build Ghostty.app - name: Build Ghostty.app
run: | run: |
cd macos cd macos
sudo xcode-select -s /Applications/Xcode_16.0.app
xcodebuild -target Ghostty -configuration Release xcodebuild -target Ghostty -configuration Release
# We inject the "build number" as simply the number of commits since HEAD. # We inject the "build number" as simply the number of commits since HEAD.
@ -342,6 +344,9 @@ jobs:
name: ghostty name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
- name: XCode Select
run: sudo xcode-select -s /Applications/Xcode_16.0.app
# Setup Sparkle # Setup Sparkle
- name: Setup Sparkle - name: Setup Sparkle
env: env:
@ -371,7 +376,6 @@ jobs:
- name: Build Ghostty.app - name: Build Ghostty.app
run: | run: |
cd macos cd macos
sudo xcode-select -s /Applications/Xcode_16.0.app
xcodebuild -target Ghostty -configuration Release xcodebuild -target Ghostty -configuration Release
# We inject the "build number" as simply the number of commits since HEAD. # We inject the "build number" as simply the number of commits since HEAD.
@ -514,6 +518,9 @@ jobs:
name: ghostty name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
- name: XCode Select
run: sudo xcode-select -s /Applications/Xcode_16.0.app
# Setup Sparkle # Setup Sparkle
- name: Setup Sparkle - name: Setup Sparkle
env: env:
@ -543,7 +550,6 @@ jobs:
- name: Build Ghostty.app - name: Build Ghostty.app
run: | run: |
cd macos cd macos
sudo xcode-select -s /Applications/Xcode_16.0.app
xcodebuild -target Ghostty -configuration Release xcodebuild -target Ghostty -configuration Release
# We inject the "build number" as simply the number of commits since HEAD. # We inject the "build number" as simply the number of commits since HEAD.

View File

@ -196,6 +196,9 @@ jobs:
name: ghostty name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
- name: XCode Select
run: sudo xcode-select -s /Applications/Xcode_16.0.app
- name: Test All - name: Test All
run: | run: |
# OpenGL # OpenGL
@ -324,10 +327,10 @@ jobs:
run: nix develop -c zig build -Dapp-runtime=none test run: nix develop -c zig build -Dapp-runtime=none test
- name: Test GTK Build - name: Test GTK Build
run: nix develop -c zig build -Dapp-runtime=gtk -Dgtk-libadwaita=true -Demit-docs run: nix develop -c zig build -Dapp-runtime=gtk -Dgtk-adwaita=true -Demit-docs
- name: Test GTK Build (No Libadwaita) - name: Test GTK Build (No Libadwaita)
run: nix develop -c zig build -Dapp-runtime=gtk -Dgtk-libadwaita=false -Demit-docs run: nix develop -c zig build -Dapp-runtime=gtk -Dgtk-adwaita=false -Demit-docs
- name: Test GLFW Build - name: Test GLFW Build
run: nix develop -c zig build -Dapp-runtime=glfw run: nix develop -c zig build -Dapp-runtime=glfw
@ -352,6 +355,9 @@ jobs:
name: ghostty name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
- name: XCode Select
run: sudo xcode-select -s /Applications/Xcode_16.0.app
- name: test - name: test
run: nix develop -c zig build test run: nix develop -c zig build test

79
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,79 @@
# Ghostty Development Process
This document describes the development process for Ghostty. It is intended for
anyone considering opening an **issue** or **pull request**. If in doubt,
please open a [discussion](https://github.com/ghostty-org/ghostty/discussions);
we can always convert that to an issue later.
> [!NOTE]
>
> I'm sorry for the wall of text. I'm not trying to be difficult and I do
> appreciate your contributions. Ghostty is a personal project for me that
> I maintain in my free time. If you're expecting me to dedicate my personal
> time to fixing bugs, maintaining features, and reviewing code, I do kindly
> ask you spend a few minutes reading this document. Thank you. ❤️
## Quick Guide
**I'd like to contribute!**
All issues are actionable. Pick one and start working on it. Thank you.
If you need help or guidance, comment on the issue. Issues that are extra
friendly to new contributors are tagged with "contributor friendly".
**I have a bug!**
1. Search the issue tracker and discussions for similar issues.
2. If you don't have steps to reproduce, open a discussion.
3. If you have steps to reproduce, open an issue.
**I have an idea for a feature!**
1. Open a discussion.
**I've implemented a feature!**
1. If there is an issue for the feature, open a pull request.
2. If there is no issue, open a discussion and link to your branch.
3. If you want to live dangerously, open a pull request and hope for the best.
**I have a question!**
1. Open a discussion or use Discord.
## General Patterns
### Issues are Actionable
The Ghostty [issue tracker](https://github.com/ghostty-org/ghostty/issues)
is for _actionable items_.
Unlike some other projects, Ghostty **does not use the issue tracker for
discussion or feature requests**. Instead, we use GitHub
[discussions](https://github.com/ghostty-org/ghostty/discussions) for that.
Once a discussion reaches a point where a well-understood, actionable
item is identified, it is moved to the issue tracker. **This pattern
makes it easier for maintainers or contributors to find issues to work on
since _every issue_ is ready to be worked on.**
If you are experiencing a bug and have clear steps to reproduce it, please
open an issue. If you are experiencing a bug but you are not sure how to
reproduce it or aren't sure if it's a bug, please open a discussion.
If you have an idea for a feature, please open a discussion.
### Pull Requests Implement an Issue
Pull requests should be associated with a previously accepted issue.
**If you open a pull request for something that wasn't previously discussed,**
it may be closed or remain stale for an indefinite period of time. I'm not
saying it will never be accepted, but the odds are stacked against you.
Issues tagged with "feature" represent accepted, well-scoped feature requests.
If you implement an issue tagged with feature as described in the issue, your
pull request will be accepted with a high degree of certainty.
> [!NOTE]
>
> **Pull requests are NOT a place to discuss feature design.** Please do
> not open a WIP pull request to discuss a feature. Instead, use a discussion
> and link to your branch.

View File

@ -80,7 +80,9 @@ relevant to package maintainers:
- `--system`: The path to the offline cache directory. This disables - `--system`: The path to the offline cache directory. This disables
any package fetching from the internet. This flag also triggers all any package fetching from the internet. This flag also triggers all
dependencies to be dynamically linked by default. dependencies to be dynamically linked by default. This flag also makes
the binary a PIE (Position Independent Executable) by default (override
with `-Dpie`).
- `-Doptimize=ReleaseFast`: Build with optimizations enabled and safety checks - `-Doptimize=ReleaseFast`: Build with optimizations enabled and safety checks
disabled. This is the recommended build mode for distribution. I'd prefer disabled. This is the recommended build mode for distribution. I'd prefer
@ -91,3 +93,8 @@ relevant to package maintainers:
- `-Dcpu=baseline`: Build for the "baseline" CPU of the target architecture. - `-Dcpu=baseline`: Build for the "baseline" CPU of the target architecture.
This avoids building for newer CPU features that may not be available on This avoids building for newer CPU features that may not be available on
all target machines. all target machines.
- `-Dtarget=$arch-$os-$abi`: Build for a specific target triple. This is
often necessary for system packages to specify a specific minimum Linux
version, glibc, etc. Run `zig targets` to a get a full list of available
targets.

View File

@ -63,7 +63,7 @@ placed at `$XDG_CONFIG_HOME/ghostty/config`, which defaults to
The file format is documented below as an example: The file format is documented below as an example:
``` ```ini
# The syntax is "key = value". The whitespace around the equals doesn't matter. # The syntax is "key = value". The whitespace around the equals doesn't matter.
background = 282c34 background = 282c34
foreground= ffffff foreground= ffffff
@ -375,9 +375,9 @@ test cases.
We believe Ghostty is one of the most compliant terminal emulators available. We believe Ghostty is one of the most compliant terminal emulators available.
Terminal behavior is partially a dejour standard Terminal behavior is partially a de jure standard
(i.e. [ECMA-48](https://ecma-international.org/publications-and-standards/standards/ecma-48/)) (i.e. [ECMA-48](https://ecma-international.org/publications-and-standards/standards/ecma-48/))
but mostly a defacto standard as defined by popular terminal emulators but mostly a de facto standard as defined by popular terminal emulators
worldwide. Ghostty takes the approach that our behavior is defined by worldwide. Ghostty takes the approach that our behavior is defined by
(1) standards, if available, (2) xterm, if the feature exists, (3) (1) standards, if available, (2) xterm, if the feature exists, (3)
other popular terminals, in that order. This defines what the Ghostty project other popular terminals, in that order. This defines what the Ghostty project
@ -789,7 +789,14 @@ Below is an example:
# #
# Instead, either run `nix flake update` or `nixos-rebuild build` # Instead, either run `nix flake update` or `nixos-rebuild build`
# as the current user, and then run `sudo nixos-rebuild switch`. # as the current user, and then run `sudo nixos-rebuild switch`.
ghostty.url = "git+ssh://git@github.com/ghostty-org/ghostty"; ghostty = {
url = "git+ssh://git@github.com/ghostty-org/ghostty";
# NOTE: The below 2 lines are only required on nixos-unstable,
# if you're on stable, they may break your build
inputs.nixpkgs-stable.follows = "nixpkgs";
inputs.nixpkgs-unstable.follows = "nixpkgs";
};
}; };
outputs = { nixpkgs, ghostty, ... }: { outputs = { nixpkgs, ghostty, ... }: {

View File

@ -19,4 +19,3 @@ Mac:
Major Features: Major Features:
- Bell - Bell
- Sixels: https://saitoha.github.io/libsixel/

View File

@ -10,6 +10,7 @@ const font = @import("src/font/main.zig");
const renderer = @import("src/renderer.zig"); const renderer = @import("src/renderer.zig");
const terminfo = @import("src/terminfo/main.zig"); 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 fish_completions = @import("src/build/fish_completions.zig"); const fish_completions = @import("src/build/fish_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;
@ -56,6 +57,11 @@ pub fn build(b: *std.Build) !void {
break :target result; break :target result;
}; };
// This is set to true when we're building a system package. For now
// this is trivially detected using the "system_package_mode" bool
// but we may want to make this more sophisticated in the future.
const system_package: bool = b.graph.system_package_mode;
const wasm_target: WasmTarget = .browser; const wasm_target: WasmTarget = .browser;
// We use env vars throughout the build so we grab them immediately here. // We use env vars throughout the build so we grab them immediately here.
@ -91,12 +97,18 @@ pub fn build(b: *std.Build) !void {
"The app runtime to use. Not all values supported on all platforms.", "The app runtime to use. Not all values supported on all platforms.",
) orelse renderer.Impl.default(target.result, wasm_target); ) orelse renderer.Impl.default(target.result, wasm_target);
config.libadwaita = b.option( config.adwaita = b.option(
bool, bool,
"gtk-libadwaita", "gtk-adwaita",
"Enables the use of libadwaita when using the gtk rendering backend.", "Enables the use of Adwaita when using the GTK rendering backend.",
) orelse true; ) orelse true;
const pie = b.option(
bool,
"pie",
"Build a Position Independent Executable. Default true for system packages.",
) orelse system_package;
const conformance = b.option( const conformance = b.option(
[]const u8, []const u8,
"conformance", "conformance",
@ -129,6 +141,9 @@ pub fn build(b: *std.Build) !void {
// If we are emitting any other artifacts then we default to false. // If we are emitting any other artifacts then we default to false.
if (emit_bench or emit_test_exe or emit_helpgen) break :emit_docs false; if (emit_bench or emit_test_exe or emit_helpgen) break :emit_docs false;
// We always emit docs in system package mode.
if (system_package) break :emit_docs true;
// We only default to true if we can find pandoc. // We only default to true if we can find pandoc.
const path = Command.expandPath(b.allocator, "pandoc") catch const path = Command.expandPath(b.allocator, "pandoc") catch
break :emit_docs false; break :emit_docs false;
@ -281,6 +296,9 @@ pub fn build(b: *std.Build) !void {
// Exe // Exe
if (exe_) |exe| { if (exe_) |exe| {
// Set PIE if requested
if (pie) exe.pie = true;
// Add the shared dependencies // Add the shared dependencies
_ = try addDeps(b, exe, config); _ = try addDeps(b, exe, config);
@ -499,6 +517,38 @@ pub fn build(b: *std.Build) !void {
}); });
} }
// Neovim plugin
// This is just a copy-paste of the Vim plugin, but using a Neovim subdir.
// By default, Neovim doesn't look inside share/vim/vimfiles. Some distros
// configure it to do that however. Fedora, does not as a counterexample.
{
const wf = b.addWriteFiles();
_ = wf.add("syntax/ghostty.vim", config_vim.syntax);
_ = wf.add("ftdetect/ghostty.vim", config_vim.ftdetect);
_ = wf.add("ftplugin/ghostty.vim", config_vim.ftplugin);
b.installDirectory(.{
.source_dir = wf.getDirectory(),
.install_dir = .prefix,
.install_subdir = "share/nvim/site",
});
}
// Sublime syntax highlighting for bat cli tool
// NOTE: The current implementation requires symlinking the generated
// 'ghostty.sublime-syntax' file from zig-out to the '~.config/bat/syntaxes'
// directory. The syntax then needs to be mapped to the correct language in
// the config file within the '~.config/bat' directory
// (ex: --map-syntax "/Users/user/.config/ghostty/config:Ghostty Config").
{
const wf = b.addWriteFiles();
_ = wf.add("ghostty.sublime-syntax", config_sublime_syntax.syntax);
b.installDirectory(.{
.source_dir = wf.getDirectory(),
.install_dir = .prefix,
.install_subdir = "share/bat/syntaxes",
});
}
// Documentation // Documentation
if (emit_docs) { if (emit_docs) {
try buildDocumentation(b, config); try buildDocumentation(b, config);
@ -975,7 +1025,7 @@ fn addDeps(
if (b.systemIntegrationOption("freetype", .{})) { if (b.systemIntegrationOption("freetype", .{})) {
step.linkSystemLibrary2("bzip2", dynamic_link_opts); step.linkSystemLibrary2("bzip2", dynamic_link_opts);
step.linkSystemLibrary2("freetype", dynamic_link_opts); step.linkSystemLibrary2("freetype2", dynamic_link_opts);
} else { } else {
step.linkLibrary(freetype_dep.artifact("freetype")); step.linkLibrary(freetype_dep.artifact("freetype"));
try static_libs.append(freetype_dep.artifact("freetype").getEmittedBin()); try static_libs.append(freetype_dep.artifact("freetype").getEmittedBin());
@ -1054,7 +1104,8 @@ fn addDeps(
}); });
step.root_module.addImport("oniguruma", oniguruma_dep.module("oniguruma")); step.root_module.addImport("oniguruma", oniguruma_dep.module("oniguruma"));
if (b.systemIntegrationOption("oniguruma", .{})) { if (b.systemIntegrationOption("oniguruma", .{})) {
step.linkSystemLibrary2("oniguruma", dynamic_link_opts); // Oniguruma is compiled and distributed as libonig.so
step.linkSystemLibrary2("onig", dynamic_link_opts);
} else { } else {
step.linkLibrary(oniguruma_dep.artifact("oniguruma")); step.linkLibrary(oniguruma_dep.artifact("oniguruma"));
try static_libs.append(oniguruma_dep.artifact("oniguruma").getEmittedBin()); try static_libs.append(oniguruma_dep.artifact("oniguruma").getEmittedBin());
@ -1068,6 +1119,7 @@ fn addDeps(
step.root_module.addImport("glslang", glslang_dep.module("glslang")); step.root_module.addImport("glslang", glslang_dep.module("glslang"));
if (b.systemIntegrationOption("glslang", .{})) { if (b.systemIntegrationOption("glslang", .{})) {
step.linkSystemLibrary2("glslang", dynamic_link_opts); step.linkSystemLibrary2("glslang", dynamic_link_opts);
step.linkSystemLibrary2("glslang-default-resource-limits", dynamic_link_opts);
} else { } else {
step.linkLibrary(glslang_dep.artifact("glslang")); step.linkLibrary(glslang_dep.artifact("glslang"));
try static_libs.append(glslang_dep.artifact("glslang").getEmittedBin()); try static_libs.append(glslang_dep.artifact("glslang").getEmittedBin());
@ -1190,8 +1242,6 @@ fn addDeps(
step.root_module.addImport("vaxis", b.dependency("vaxis", .{ step.root_module.addImport("vaxis", b.dependency("vaxis", .{
.target = target, .target = target,
.optimize = optimize, .optimize = optimize,
.libxev = false,
.images = false,
}).module("vaxis")); }).module("vaxis"));
step.root_module.addImport("wuffs", b.dependency("wuffs", .{ step.root_module.addImport("wuffs", b.dependency("wuffs", .{
.target = target, .target = target,
@ -1213,6 +1263,7 @@ fn addDeps(
step.root_module.addImport("zf", b.dependency("zf", .{ step.root_module.addImport("zf", b.dependency("zf", .{
.target = target, .target = target,
.optimize = optimize, .optimize = optimize,
.with_tui = false,
}).module("zf")); }).module("zf"));
// Mac Stuff // Mac Stuff
@ -1226,14 +1277,7 @@ fn addDeps(
.optimize = optimize, .optimize = optimize,
}); });
// This is a bit of a hack that should probably be fixed upstream step.root_module.addImport("objc", objc_dep.module("objc"));
// in zig-objc, but we need to add the apple SDK paths to the
// zig-objc module so that it can find the objc runtime headers.
const module = objc_dep.module("objc");
module.resolved_target = step.root_module.resolved_target;
try @import("apple_sdk").addPaths(b, module);
step.root_module.addImport("objc", module);
step.root_module.addImport("macos", macos_dep.module("macos")); step.root_module.addImport("macos", macos_dep.module("macos"));
step.linkLibrary(macos_dep.artifact("macos")); step.linkLibrary(macos_dep.artifact("macos"));
try static_libs.append(macos_dep.artifact("macos").getEmittedBin()); try static_libs.append(macos_dep.artifact("macos").getEmittedBin());
@ -1294,7 +1338,7 @@ fn addDeps(
.gtk => { .gtk => {
step.linkSystemLibrary2("gtk4", dynamic_link_opts); step.linkSystemLibrary2("gtk4", dynamic_link_opts);
if (config.libadwaita) step.linkSystemLibrary2("adwaita-1", dynamic_link_opts); if (config.adwaita) step.linkSystemLibrary2("adwaita-1", dynamic_link_opts);
{ {
const gresource = @import("src/apprt/gtk/gresource.zig"); const gresource = @import("src/apprt/gtk/gresource.zig");

View File

@ -14,8 +14,8 @@
.lazy = true, .lazy = true,
}, },
.zig_objc = .{ .zig_objc = .{
.url = "https://github.com/mitchellh/zig-objc/archive/fe5ac419530cf800294369d996133fe9cd067aec.tar.gz", .url = "https://github.com/mitchellh/zig-objc/archive/9b8ba849b0f58fe207ecd6ab7c147af55b17556e.tar.gz",
.hash = "122034b3e15d582d8d101a7713e5f13c872b8b8eb6d9cb47515b8e34ee75e122630d", .hash = "1220e17e64ef0ef561b3e4b9f3a96a2494285f2ec31c097721bf8c8677ec4415c634",
}, },
.zig_js = .{ .zig_js = .{
.url = "https://github.com/mitchellh/zig-js/archive/d0b8b0a57c52fbc89f9d9fecba75ca29da7dd7d1.tar.gz", .url = "https://github.com/mitchellh/zig-js/archive/d0b8b0a57c52fbc89f9d9fecba75ca29da7dd7d1.tar.gz",
@ -49,16 +49,16 @@
// Other // Other
.apple_sdk = .{ .path = "./pkg/apple-sdk" }, .apple_sdk = .{ .path = "./pkg/apple-sdk" },
.iterm2_themes = .{ .iterm2_themes = .{
.url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/b4a9c4d.tar.gz", .url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/80543b14552b7c9fef88fad826552e6ac5632abe.tar.gz",
.hash = "122056fbb29863ec1678b7954fb76b1533ad8c581a34577c1b2efe419e29e05596df", .hash = "1220217ae916146a4c598f8ba5bfff0ff940335d00572e337f20b4accf24fa2ca4fc",
}, },
.vaxis = .{ .vaxis = .{
.url = "git+https://github.com/rockorager/libvaxis?ref=main#a1b43d24653670d612b91f0855b165e6c987b809", .url = "git+https://github.com/rockorager/libvaxis/?ref=main#6d729a2dc3b934818dffe06d2ba3ce02841ed74b",
.hash = "1220e4d6fc82c487178339422887fdfd5094b15c242fe31ad596c4b2fcdc60ef667f", .hash = "12200df4ebeaed45de26cb2c9f3b6f3746d8013b604e035dae658f86f586c8c91d2f",
}, },
.zf = .{ .zf = .{
.url = "git+https://github.com/natecraddock/zf.git?ref=main#bb27a917c3513785c6a91f0b1c10002a5029cacc", .url = "git+https://github.com/natecraddock/zf/?ref=main#ed99ca18b02dda052e20ba467e90b623c04690dd",
.hash = "1220a74107c7f153a2f809e41c7fa7e8dbf75c91043e39fad998247804e5edac2cc8", .hash = "1220edc3b8d8bedbb50555947987e5e8e2f93871ca3c8e8d4cc8f1377c15b5dd35e8",
}, },
.z2d = .{ .z2d = .{
.url = "git+https://github.com/vancluever/z2d?ref=main#285a796eb9c25a2389f087d008f0e60faf0b8eda", .url = "git+https://github.com/vancluever/z2d?ref=main#285a796eb9c25a2389f087d008f0e60faf0b8eda",

View File

@ -430,6 +430,11 @@ typedef struct {
const char* title; const char* title;
} ghostty_action_set_title_s; } ghostty_action_set_title_s;
// apprt.action.Pwd.C
typedef struct {
const char* pwd;
} ghostty_action_pwd_s;
// terminal.MouseShape // terminal.MouseShape
typedef enum { typedef enum {
GHOSTTY_MOUSE_SHAPE_DEFAULT, GHOSTTY_MOUSE_SHAPE_DEFAULT,
@ -512,6 +517,31 @@ typedef struct {
ghostty_input_trigger_s trigger; ghostty_input_trigger_s trigger;
} ghostty_action_key_sequence_s; } ghostty_action_key_sequence_s;
// apprt.action.ColorKind
typedef enum {
GHOSTTY_ACTION_COLOR_KIND_FOREGROUND = -1,
GHOSTTY_ACTION_COLOR_KIND_BACKGROUND = -2,
GHOSTTY_ACTION_COLOR_KIND_CURSOR = -3,
} ghostty_action_color_kind_e;
// apprt.action.ColorChange
typedef struct {
ghostty_action_color_kind_e kind;
uint8_t r;
uint8_t g;
uint8_t b;
} ghostty_action_color_change_s;
// apprt.action.ConfigChange
typedef struct {
ghostty_config_t config;
} ghostty_action_config_change_s;
// apprt.action.ReloadConfig
typedef struct {
bool soft;
} ghostty_action_reload_config_s;
// apprt.Action.Key // apprt.Action.Key
typedef enum { typedef enum {
GHOSTTY_ACTION_NEW_WINDOW, GHOSTTY_ACTION_NEW_WINDOW,
@ -537,6 +567,7 @@ typedef enum {
GHOSTTY_ACTION_RENDER_INSPECTOR, GHOSTTY_ACTION_RENDER_INSPECTOR,
GHOSTTY_ACTION_DESKTOP_NOTIFICATION, GHOSTTY_ACTION_DESKTOP_NOTIFICATION,
GHOSTTY_ACTION_SET_TITLE, GHOSTTY_ACTION_SET_TITLE,
GHOSTTY_ACTION_PWD,
GHOSTTY_ACTION_MOUSE_SHAPE, GHOSTTY_ACTION_MOUSE_SHAPE,
GHOSTTY_ACTION_MOUSE_VISIBILITY, GHOSTTY_ACTION_MOUSE_VISIBILITY,
GHOSTTY_ACTION_MOUSE_OVER_LINK, GHOSTTY_ACTION_MOUSE_OVER_LINK,
@ -545,6 +576,9 @@ typedef enum {
GHOSTTY_ACTION_QUIT_TIMER, GHOSTTY_ACTION_QUIT_TIMER,
GHOSTTY_ACTION_SECURE_INPUT, GHOSTTY_ACTION_SECURE_INPUT,
GHOSTTY_ACTION_KEY_SEQUENCE, GHOSTTY_ACTION_KEY_SEQUENCE,
GHOSTTY_ACTION_COLOR_CHANGE,
GHOSTTY_ACTION_RELOAD_CONFIG,
GHOSTTY_ACTION_CONFIG_CHANGE,
} ghostty_action_tag_e; } ghostty_action_tag_e;
typedef union { typedef union {
@ -560,6 +594,7 @@ typedef union {
ghostty_action_inspector_e inspector; ghostty_action_inspector_e inspector;
ghostty_action_desktop_notification_s desktop_notification; ghostty_action_desktop_notification_s desktop_notification;
ghostty_action_set_title_s set_title; ghostty_action_set_title_s set_title;
ghostty_action_pwd_s pwd;
ghostty_action_mouse_shape_e mouse_shape; ghostty_action_mouse_shape_e mouse_shape;
ghostty_action_mouse_visibility_e mouse_visibility; ghostty_action_mouse_visibility_e mouse_visibility;
ghostty_action_mouse_over_link_s mouse_over_link; ghostty_action_mouse_over_link_s mouse_over_link;
@ -567,6 +602,9 @@ typedef union {
ghostty_action_quit_timer_e quit_timer; ghostty_action_quit_timer_e quit_timer;
ghostty_action_secure_input_e secure_input; ghostty_action_secure_input_e secure_input;
ghostty_action_key_sequence_s key_sequence; ghostty_action_key_sequence_s key_sequence;
ghostty_action_color_change_s color_change;
ghostty_action_reload_config_s reload_config;
ghostty_action_config_change_s config_change;
} ghostty_action_u; } ghostty_action_u;
typedef struct { typedef struct {
@ -575,7 +613,6 @@ typedef struct {
} ghostty_action_s; } ghostty_action_s;
typedef void (*ghostty_runtime_wakeup_cb)(void*); typedef void (*ghostty_runtime_wakeup_cb)(void*);
typedef const ghostty_config_t (*ghostty_runtime_reload_config_cb)(void*);
typedef void (*ghostty_runtime_read_clipboard_cb)(void*, typedef void (*ghostty_runtime_read_clipboard_cb)(void*,
ghostty_clipboard_e, ghostty_clipboard_e,
void*); void*);
@ -598,7 +635,6 @@ typedef struct {
bool supports_selection_clipboard; bool supports_selection_clipboard;
ghostty_runtime_wakeup_cb wakeup_cb; ghostty_runtime_wakeup_cb wakeup_cb;
ghostty_runtime_action_cb action_cb; ghostty_runtime_action_cb action_cb;
ghostty_runtime_reload_config_cb reload_config_cb;
ghostty_runtime_read_clipboard_cb read_clipboard_cb; ghostty_runtime_read_clipboard_cb read_clipboard_cb;
ghostty_runtime_confirm_read_clipboard_cb confirm_read_clipboard_cb; ghostty_runtime_confirm_read_clipboard_cb confirm_read_clipboard_cb;
ghostty_runtime_write_clipboard_cb write_clipboard_cb; ghostty_runtime_write_clipboard_cb write_clipboard_cb;
@ -614,6 +650,7 @@ ghostty_info_s ghostty_info(void);
ghostty_config_t ghostty_config_new(); ghostty_config_t ghostty_config_new();
void ghostty_config_free(ghostty_config_t); void ghostty_config_free(ghostty_config_t);
ghostty_config_t ghostty_config_clone(ghostty_config_t);
void ghostty_config_load_cli_args(ghostty_config_t); void ghostty_config_load_cli_args(ghostty_config_t);
void ghostty_config_load_default_files(ghostty_config_t); void ghostty_config_load_default_files(ghostty_config_t);
void ghostty_config_load_recursive_files(ghostty_config_t); void ghostty_config_load_recursive_files(ghostty_config_t);
@ -635,9 +672,10 @@ void ghostty_app_set_focus(ghostty_app_t, bool);
bool ghostty_app_key(ghostty_app_t, ghostty_input_key_s); bool ghostty_app_key(ghostty_app_t, ghostty_input_key_s);
void ghostty_app_keyboard_changed(ghostty_app_t); void ghostty_app_keyboard_changed(ghostty_app_t);
void ghostty_app_open_config(ghostty_app_t); void ghostty_app_open_config(ghostty_app_t);
void ghostty_app_reload_config(ghostty_app_t); void ghostty_app_update_config(ghostty_app_t, ghostty_config_t);
bool ghostty_app_needs_confirm_quit(ghostty_app_t); bool ghostty_app_needs_confirm_quit(ghostty_app_t);
bool ghostty_app_has_global_keybinds(ghostty_app_t); bool ghostty_app_has_global_keybinds(ghostty_app_t);
void ghostty_app_set_color_scheme(ghostty_app_t, ghostty_color_scheme_e);
ghostty_surface_config_s ghostty_surface_config_new(); ghostty_surface_config_s ghostty_surface_config_new();
@ -646,6 +684,7 @@ void ghostty_surface_free(ghostty_surface_t);
void* ghostty_surface_userdata(ghostty_surface_t); void* ghostty_surface_userdata(ghostty_surface_t);
ghostty_app_t ghostty_surface_app(ghostty_surface_t); ghostty_app_t ghostty_surface_app(ghostty_surface_t);
ghostty_surface_config_s ghostty_surface_inherited_config(ghostty_surface_t); ghostty_surface_config_s ghostty_surface_inherited_config(ghostty_surface_t);
void ghostty_surface_update_config(ghostty_surface_t, ghostty_config_t);
bool ghostty_surface_needs_confirm_quit(ghostty_surface_t); bool ghostty_surface_needs_confirm_quit(ghostty_surface_t);
void ghostty_surface_refresh(ghostty_surface_t); void ghostty_surface_refresh(ghostty_surface_t);
void ghostty_surface_draw(ghostty_surface_t); void ghostty_surface_draw(ghostty_surface_t);
@ -688,7 +727,6 @@ void ghostty_surface_complete_clipboard_request(ghostty_surface_t,
const char*, const char*,
void*, void*,
bool); bool);
uintptr_t ghostty_surface_pwd(ghostty_surface_t, char*, uintptr_t);
bool ghostty_surface_has_selection(ghostty_surface_t); bool ghostty_surface_has_selection(ghostty_surface_t);
uintptr_t ghostty_surface_selection(ghostty_surface_t, char*, uintptr_t); uintptr_t ghostty_surface_selection(ghostty_surface_t, char*, uintptr_t);

View File

@ -7,6 +7,7 @@
objects = { objects = {
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
29C15B1D2CDC3B2900520DD4 /* bat in Resources */ = {isa = PBXBuildFile; fileRef = 29C15B1C2CDC3B2000520DD4 /* bat */; };
55154BE02B33911F001622DC /* ghostty in Resources */ = {isa = PBXBuildFile; fileRef = 55154BDF2B33911F001622DC /* ghostty */; }; 55154BE02B33911F001622DC /* ghostty in Resources */ = {isa = PBXBuildFile; fileRef = 55154BDF2B33911F001622DC /* ghostty */; };
552964E62B34A9B400030505 /* vim in Resources */ = {isa = PBXBuildFile; fileRef = 552964E52B34A9B400030505 /* vim */; }; 552964E62B34A9B400030505 /* vim in Resources */ = {isa = PBXBuildFile; fileRef = 552964E52B34A9B400030505 /* vim */; };
857F63812A5E64F200CA4815 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 857F63802A5E64F200CA4815 /* MainMenu.xib */; }; 857F63812A5E64F200CA4815 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 857F63802A5E64F200CA4815 /* MainMenu.xib */; };
@ -59,6 +60,7 @@
A5985CD72C320C4500C57AD3 /* String+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5985CD62C320C4500C57AD3 /* String+Extension.swift */; }; A5985CD72C320C4500C57AD3 /* String+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5985CD62C320C4500C57AD3 /* String+Extension.swift */; };
A5985CD82C320C4500C57AD3 /* String+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5985CD62C320C4500C57AD3 /* String+Extension.swift */; }; A5985CD82C320C4500C57AD3 /* String+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5985CD62C320C4500C57AD3 /* String+Extension.swift */; };
A5985CE62C33060F00C57AD3 /* man in Resources */ = {isa = PBXBuildFile; fileRef = A5985CE52C33060F00C57AD3 /* man */; }; A5985CE62C33060F00C57AD3 /* man in Resources */ = {isa = PBXBuildFile; fileRef = A5985CE52C33060F00C57AD3 /* man */; };
A599CDB02CF103F60049FA26 /* NSAppearance+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A599CDAF2CF103F20049FA26 /* NSAppearance+Extension.swift */; };
A59FB5CF2AE0DB50009128F3 /* InspectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59FB5CE2AE0DB50009128F3 /* InspectorView.swift */; }; A59FB5CF2AE0DB50009128F3 /* InspectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59FB5CE2AE0DB50009128F3 /* InspectorView.swift */; };
A59FB5D12AE0DEA7009128F3 /* MetalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59FB5D02AE0DEA7009128F3 /* MetalView.swift */; }; A59FB5D12AE0DEA7009128F3 /* MetalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59FB5D02AE0DEA7009128F3 /* MetalView.swift */; };
A5A1F8852A489D6800D1E8BC /* terminfo in Resources */ = {isa = PBXBuildFile; fileRef = A5A1F8842A489D6800D1E8BC /* terminfo */; }; A5A1F8852A489D6800D1E8BC /* terminfo in Resources */ = {isa = PBXBuildFile; fileRef = A5A1F8842A489D6800D1E8BC /* terminfo */; };
@ -95,6 +97,7 @@
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
29C15B1C2CDC3B2000520DD4 /* bat */ = {isa = PBXFileReference; lastKnownFileType = folder; name = bat; path = "../zig-out/share/bat"; sourceTree = "<group>"; };
3B39CAA42B33949B00DABEB8 /* GhosttyReleaseLocal.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = GhosttyReleaseLocal.entitlements; sourceTree = "<group>"; }; 3B39CAA42B33949B00DABEB8 /* GhosttyReleaseLocal.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = GhosttyReleaseLocal.entitlements; sourceTree = "<group>"; };
55154BDF2B33911F001622DC /* ghostty */ = {isa = PBXFileReference; lastKnownFileType = folder; name = ghostty; path = "../zig-out/share/ghostty"; sourceTree = "<group>"; }; 55154BDF2B33911F001622DC /* ghostty */ = {isa = PBXFileReference; lastKnownFileType = folder; name = ghostty; path = "../zig-out/share/ghostty"; sourceTree = "<group>"; };
552964E52B34A9B400030505 /* vim */ = {isa = PBXFileReference; lastKnownFileType = folder; name = vim; path = "../zig-out/share/vim"; sourceTree = "<group>"; }; 552964E52B34A9B400030505 /* vim */ = {isa = PBXFileReference; lastKnownFileType = folder; name = vim; path = "../zig-out/share/vim"; sourceTree = "<group>"; };
@ -139,6 +142,7 @@
A59630A32AF059BB00D64628 /* Ghostty.SplitNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.SplitNode.swift; sourceTree = "<group>"; }; A59630A32AF059BB00D64628 /* Ghostty.SplitNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.SplitNode.swift; sourceTree = "<group>"; };
A5985CD62C320C4500C57AD3 /* String+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+Extension.swift"; sourceTree = "<group>"; }; A5985CD62C320C4500C57AD3 /* String+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+Extension.swift"; sourceTree = "<group>"; };
A5985CE52C33060F00C57AD3 /* man */ = {isa = PBXFileReference; lastKnownFileType = folder; name = man; path = "../zig-out/share/man"; sourceTree = "<group>"; }; A5985CE52C33060F00C57AD3 /* man */ = {isa = PBXFileReference; lastKnownFileType = folder; name = man; path = "../zig-out/share/man"; sourceTree = "<group>"; };
A599CDAF2CF103F20049FA26 /* NSAppearance+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSAppearance+Extension.swift"; sourceTree = "<group>"; };
A59FB5CE2AE0DB50009128F3 /* InspectorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InspectorView.swift; sourceTree = "<group>"; }; A59FB5CE2AE0DB50009128F3 /* InspectorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InspectorView.swift; sourceTree = "<group>"; };
A59FB5D02AE0DEA7009128F3 /* MetalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetalView.swift; sourceTree = "<group>"; }; A59FB5D02AE0DEA7009128F3 /* MetalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetalView.swift; sourceTree = "<group>"; };
A5A1F8842A489D6800D1E8BC /* terminfo */ = {isa = PBXFileReference; lastKnownFileType = folder; name = terminfo; path = "../zig-out/share/terminfo"; sourceTree = "<group>"; }; A5A1F8842A489D6800D1E8BC /* terminfo */ = {isa = PBXFileReference; lastKnownFileType = folder; name = terminfo; path = "../zig-out/share/terminfo"; sourceTree = "<group>"; };
@ -247,6 +251,7 @@
A59FB5D02AE0DEA7009128F3 /* MetalView.swift */, A59FB5D02AE0DEA7009128F3 /* MetalView.swift */,
A5CBD0552C9E65A50017A1AE /* DraggableWindowView.swift */, A5CBD0552C9E65A50017A1AE /* DraggableWindowView.swift */,
C159E81C2B66A06B00FDFE9C /* OSColor+Extension.swift */, C159E81C2B66A06B00FDFE9C /* OSColor+Extension.swift */,
A599CDAF2CF103F20049FA26 /* NSAppearance+Extension.swift */,
A52FFF5C2CAB4D05000C6A5B /* NSScreen+Extension.swift */, A52FFF5C2CAB4D05000C6A5B /* NSScreen+Extension.swift */,
C1F26EA62B738B9900404083 /* NSView+Extension.swift */, C1F26EA62B738B9900404083 /* NSView+Extension.swift */,
AEE8B3442B9AA39600260C5E /* NSPasteboard+Extension.swift */, AEE8B3442B9AA39600260C5E /* NSPasteboard+Extension.swift */,
@ -366,6 +371,7 @@
A5A1F8862A489D7400D1E8BC /* Resources */ = { A5A1F8862A489D7400D1E8BC /* Resources */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
29C15B1C2CDC3B2000520DD4 /* bat */,
55154BDF2B33911F001622DC /* ghostty */, 55154BDF2B33911F001622DC /* ghostty */,
552964E52B34A9B400030505 /* vim */, 552964E52B34A9B400030505 /* vim */,
A586167B2B7703CC009BDB1D /* fish */, A586167B2B7703CC009BDB1D /* fish */,
@ -495,7 +501,7 @@
attributes = { attributes = {
BuildIndependentTargetsInParallel = 1; BuildIndependentTargetsInParallel = 1;
LastSwiftUpdateCheck = 1520; LastSwiftUpdateCheck = 1520;
LastUpgradeCheck = 1600; LastUpgradeCheck = 1610;
TargetAttributes = { TargetAttributes = {
A5B30530299BEAAA0047F10C = { A5B30530299BEAAA0047F10C = {
CreatedOnToolsVersion = 14.2; CreatedOnToolsVersion = 14.2;
@ -538,6 +544,7 @@
A5E112932AF73E6E00C6E0C2 /* ClipboardConfirmation.xib in Resources */, A5E112932AF73E6E00C6E0C2 /* ClipboardConfirmation.xib in Resources */,
A5CDF1912AAF9A5800513312 /* ConfigurationErrors.xib in Resources */, A5CDF1912AAF9A5800513312 /* ConfigurationErrors.xib in Resources */,
857F63812A5E64F200CA4815 /* MainMenu.xib in Resources */, 857F63812A5E64F200CA4815 /* MainMenu.xib in Resources */,
29C15B1D2CDC3B2900520DD4 /* bat in Resources */,
A596309A2AEE1C6400D64628 /* Terminal.xib in Resources */, A596309A2AEE1C6400D64628 /* Terminal.xib in Resources */,
A586167C2B7703CC009BDB1D /* fish in Resources */, A586167C2B7703CC009BDB1D /* fish in Resources */,
55154BE02B33911F001622DC /* ghostty in Resources */, 55154BE02B33911F001622DC /* ghostty in Resources */,
@ -607,6 +614,7 @@
A53A6C032CCC1B7F00943E98 /* Ghostty.Action.swift in Sources */, A53A6C032CCC1B7F00943E98 /* Ghostty.Action.swift in Sources */,
A59FB5D12AE0DEA7009128F3 /* MetalView.swift in Sources */, A59FB5D12AE0DEA7009128F3 /* MetalView.swift in Sources */,
A55685E029A03A9F004303CE /* AppError.swift in Sources */, A55685E029A03A9F004303CE /* AppError.swift in Sources */,
A599CDB02CF103F60049FA26 /* NSAppearance+Extension.swift in Sources */,
A52FFF572CA90484000C6A5B /* QuickTerminalScreen.swift in Sources */, A52FFF572CA90484000C6A5B /* QuickTerminalScreen.swift in Sources */,
A5CC36132C9CD72D004D6760 /* SecureInputOverlay.swift in Sources */, A5CC36132C9CD72D004D6760 /* SecureInputOverlay.swift in Sources */,
A535B9DA299C569B0017E2E4 /* ErrorView.swift in Sources */, A535B9DA299C569B0017E2E4 /* ErrorView.swift in Sources */,
@ -648,6 +656,7 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO; ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
@ -754,6 +763,7 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO; ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
@ -815,6 +825,7 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO; ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Scheme <Scheme
LastUpgradeVersion = "1600" LastUpgradeVersion = "1610"
version = "1.7"> version = "1.7">
<BuildAction <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "YES"

View File

@ -69,6 +69,9 @@ class AppDelegate: NSObject,
/// seconds since the process was launched. /// seconds since the process was launched.
private var applicationLaunchTime: TimeInterval = 0 private var applicationLaunchTime: TimeInterval = 0
/// This is the current configuration from the Ghostty configuration that we need.
private var derivedConfig: DerivedConfig = DerivedConfig()
/// The ghostty global state. Only one per process. /// The ghostty global state. Only one per process.
let ghostty: Ghostty.App = Ghostty.App() let ghostty: Ghostty.App = Ghostty.App()
@ -92,6 +95,9 @@ class AppDelegate: NSObject,
/// makes our logic very easy. /// makes our logic very easy.
private var isVisible: Bool = true private var isVisible: Bool = true
/// The observer for the app appearance.
private var appearanceObserver: NSKeyValueObservation? = nil
override init() { override init() {
terminalManager = TerminalManager(ghostty) terminalManager = TerminalManager(ghostty)
updaterController = SPUStandardUpdaterController( updaterController = SPUStandardUpdaterController(
@ -138,7 +144,7 @@ class AppDelegate: NSObject,
menuCheckForUpdates?.action = #selector(SPUStandardUpdaterController.checkForUpdates(_:)) menuCheckForUpdates?.action = #selector(SPUStandardUpdaterController.checkForUpdates(_:))
// Initial config loading // Initial config loading
configDidReload(ghostty) ghosttyConfigDidChange(config: ghostty.config)
// Start our update checker. // Start our update checker.
updaterController.startUpdater() updaterController.startUpdater()
@ -162,6 +168,12 @@ class AppDelegate: NSObject,
name: .quickTerminalDidChangeVisibility, name: .quickTerminalDidChangeVisibility,
object: nil object: nil
) )
NotificationCenter.default.addObserver(
self,
selector: #selector(ghosttyConfigDidChange(_:)),
name: .ghosttyConfigDidChange,
object: nil
)
// Configure user notifications // Configure user notifications
let actions = [ let actions = [
@ -178,6 +190,23 @@ class AppDelegate: NSObject,
) )
]) ])
center.delegate = self center.delegate = self
// Observe our appearance so we can report the correct value to libghostty.
self.appearanceObserver = NSApplication.shared.observe(
\.effectiveAppearance,
options: [.new, .initial]
) { _, change in
guard let appearance = change.newValue else { return }
guard let app = self.ghostty.app else { return }
let scheme: ghostty_color_scheme_e
if (appearance.isDark) {
scheme = GHOSTTY_COLOR_SCHEME_DARK
} else {
scheme = GHOSTTY_COLOR_SCHEME_LIGHT
}
ghostty_app_set_color_scheme(app, scheme)
}
} }
func applicationDidBecomeActive(_ notification: Notification) { func applicationDidBecomeActive(_ notification: Notification) {
@ -188,13 +217,13 @@ class AppDelegate: NSObject,
// is possible to have other windows in a few scenarios: // is possible to have other windows in a few scenarios:
// - if we're opening a URL since `application(_:openFile:)` is called before this. // - if we're opening a URL since `application(_:openFile:)` is called before this.
// - if we're restoring from persisted state // - if we're restoring from persisted state
if terminalManager.windows.count == 0 && ghostty.config.initialWindow { if terminalManager.windows.count == 0 && derivedConfig.initialWindow {
terminalManager.newWindow() terminalManager.newWindow()
} }
} }
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
return ghostty.config.shouldQuitAfterLastWindowClosed return derivedConfig.shouldQuitAfterLastWindowClosed
} }
func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply { func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply {
@ -300,52 +329,52 @@ class AppDelegate: NSObject,
} }
/// Sync all of our menu item keyboard shortcuts with the Ghostty configuration. /// Sync all of our menu item keyboard shortcuts with the Ghostty configuration.
private func syncMenuShortcuts() { private func syncMenuShortcuts(_ config: Ghostty.Config) {
guard ghostty.readiness == .ready else { return } guard ghostty.readiness == .ready else { return }
syncMenuShortcut(action: "open_config", menuItem: self.menuOpenConfig) syncMenuShortcut(config, action: "open_config", menuItem: self.menuOpenConfig)
syncMenuShortcut(action: "reload_config", menuItem: self.menuReloadConfig) syncMenuShortcut(config, action: "reload_config", menuItem: self.menuReloadConfig)
syncMenuShortcut(action: "quit", menuItem: self.menuQuit) syncMenuShortcut(config, action: "quit", menuItem: self.menuQuit)
syncMenuShortcut(action: "new_window", menuItem: self.menuNewWindow) syncMenuShortcut(config, action: "new_window", menuItem: self.menuNewWindow)
syncMenuShortcut(action: "new_tab", menuItem: self.menuNewTab) syncMenuShortcut(config, action: "new_tab", menuItem: self.menuNewTab)
syncMenuShortcut(action: "close_surface", menuItem: self.menuClose) syncMenuShortcut(config, action: "close_surface", menuItem: self.menuClose)
syncMenuShortcut(action: "close_window", menuItem: self.menuCloseWindow) syncMenuShortcut(config, action: "close_window", menuItem: self.menuCloseWindow)
syncMenuShortcut(action: "close_all_windows", menuItem: self.menuCloseAllWindows) syncMenuShortcut(config, action: "close_all_windows", menuItem: self.menuCloseAllWindows)
syncMenuShortcut(action: "new_split:right", menuItem: self.menuSplitRight) syncMenuShortcut(config, action: "new_split:right", menuItem: self.menuSplitRight)
syncMenuShortcut(action: "new_split:down", menuItem: self.menuSplitDown) syncMenuShortcut(config, action: "new_split:down", menuItem: self.menuSplitDown)
syncMenuShortcut(action: "copy_to_clipboard", menuItem: self.menuCopy) syncMenuShortcut(config, action: "copy_to_clipboard", menuItem: self.menuCopy)
syncMenuShortcut(action: "paste_from_clipboard", menuItem: self.menuPaste) syncMenuShortcut(config, action: "paste_from_clipboard", menuItem: self.menuPaste)
syncMenuShortcut(action: "select_all", menuItem: self.menuSelectAll) syncMenuShortcut(config, action: "select_all", menuItem: self.menuSelectAll)
syncMenuShortcut(action: "toggle_split_zoom", menuItem: self.menuZoomSplit) syncMenuShortcut(config, action: "toggle_split_zoom", menuItem: self.menuZoomSplit)
syncMenuShortcut(action: "goto_split:previous", menuItem: self.menuPreviousSplit) syncMenuShortcut(config, action: "goto_split:previous", menuItem: self.menuPreviousSplit)
syncMenuShortcut(action: "goto_split:next", menuItem: self.menuNextSplit) syncMenuShortcut(config, action: "goto_split:next", menuItem: self.menuNextSplit)
syncMenuShortcut(action: "goto_split:top", menuItem: self.menuSelectSplitAbove) syncMenuShortcut(config, action: "goto_split:top", menuItem: self.menuSelectSplitAbove)
syncMenuShortcut(action: "goto_split:bottom", menuItem: self.menuSelectSplitBelow) syncMenuShortcut(config, action: "goto_split:bottom", menuItem: self.menuSelectSplitBelow)
syncMenuShortcut(action: "goto_split:left", menuItem: self.menuSelectSplitLeft) syncMenuShortcut(config, action: "goto_split:left", menuItem: self.menuSelectSplitLeft)
syncMenuShortcut(action: "goto_split:right", menuItem: self.menuSelectSplitRight) syncMenuShortcut(config, action: "goto_split:right", menuItem: self.menuSelectSplitRight)
syncMenuShortcut(action: "resize_split:up,10", menuItem: self.menuMoveSplitDividerUp) syncMenuShortcut(config, action: "resize_split:up,10", menuItem: self.menuMoveSplitDividerUp)
syncMenuShortcut(action: "resize_split:down,10", menuItem: self.menuMoveSplitDividerDown) syncMenuShortcut(config, action: "resize_split:down,10", menuItem: self.menuMoveSplitDividerDown)
syncMenuShortcut(action: "resize_split:right,10", menuItem: self.menuMoveSplitDividerRight) syncMenuShortcut(config, action: "resize_split:right,10", menuItem: self.menuMoveSplitDividerRight)
syncMenuShortcut(action: "resize_split:left,10", menuItem: self.menuMoveSplitDividerLeft) syncMenuShortcut(config, action: "resize_split:left,10", menuItem: self.menuMoveSplitDividerLeft)
syncMenuShortcut(action: "equalize_splits", menuItem: self.menuEqualizeSplits) syncMenuShortcut(config, action: "equalize_splits", menuItem: self.menuEqualizeSplits)
syncMenuShortcut(action: "increase_font_size:1", menuItem: self.menuIncreaseFontSize) syncMenuShortcut(config, action: "increase_font_size:1", menuItem: self.menuIncreaseFontSize)
syncMenuShortcut(action: "decrease_font_size:1", menuItem: self.menuDecreaseFontSize) syncMenuShortcut(config, action: "decrease_font_size:1", menuItem: self.menuDecreaseFontSize)
syncMenuShortcut(action: "reset_font_size", menuItem: self.menuResetFontSize) syncMenuShortcut(config, action: "reset_font_size", menuItem: self.menuResetFontSize)
syncMenuShortcut(action: "toggle_quick_terminal", menuItem: self.menuQuickTerminal) syncMenuShortcut(config, action: "toggle_quick_terminal", menuItem: self.menuQuickTerminal)
syncMenuShortcut(action: "toggle_visibility", menuItem: self.menuToggleVisibility) syncMenuShortcut(config, action: "toggle_visibility", menuItem: self.menuToggleVisibility)
syncMenuShortcut(action: "inspector:toggle", menuItem: self.menuTerminalInspector) syncMenuShortcut(config, action: "inspector:toggle", menuItem: self.menuTerminalInspector)
syncMenuShortcut(action: "toggle_secure_input", menuItem: self.menuSecureInput) syncMenuShortcut(config, action: "toggle_secure_input", menuItem: self.menuSecureInput)
// This menu item is NOT synced with the configuration because it disables macOS // This menu item is NOT synced with the configuration because it disables macOS
// global fullscreen keyboard shortcut. The shortcut in the Ghostty config will continue // global fullscreen keyboard shortcut. The shortcut in the Ghostty config will continue
// to work but it won't be reflected in the menu item. // to work but it won't be reflected in the menu item.
// //
// syncMenuShortcut(action: "toggle_fullscreen", menuItem: self.menuToggleFullScreen) // syncMenuShortcut(config, action: "toggle_fullscreen", menuItem: self.menuToggleFullScreen)
// Dock menu // Dock menu
reloadDockMenu() reloadDockMenu()
@ -353,9 +382,9 @@ class AppDelegate: NSObject,
/// Syncs a single menu shortcut for the given action. The action string is the same /// Syncs a single menu shortcut for the given action. The action string is the same
/// action string used for the Ghostty configuration. /// action string used for the Ghostty configuration.
private func syncMenuShortcut(action: String, menuItem: NSMenuItem?) { private func syncMenuShortcut(_ config: Ghostty.Config, action: String, menuItem: NSMenuItem?) {
guard let menu = menuItem else { return } guard let menu = menuItem else { return }
guard let equiv = ghostty.config.keyEquivalent(for: action) else { guard let equiv = config.keyEquivalent(for: action) else {
// No shortcut, clear the menu item // No shortcut, clear the menu item
menu.keyEquivalent = "" menu.keyEquivalent = ""
menu.keyEquivalentModifierMask = [] menu.keyEquivalentModifierMask = []
@ -422,6 +451,98 @@ class AppDelegate: NSObject,
self.menuQuickTerminal?.state = if (quickController.visible) { .on } else { .off } self.menuQuickTerminal?.state = if (quickController.visible) { .on } else { .off }
} }
@objc private func ghosttyConfigDidChange(_ notification: Notification) {
// We only care if the configuration is a global configuration, not a surface one.
guard notification.object == nil else { return }
// Get our managed configuration object out
guard let config = notification.userInfo?[
Notification.Name.GhosttyConfigChangeKey
] as? Ghostty.Config else { return }
ghosttyConfigDidChange(config: config)
}
private func ghosttyConfigDidChange(config: Ghostty.Config) {
// Update the config we need to store
self.derivedConfig = DerivedConfig(config)
// Depending on the "window-save-state" setting we have to set the NSQuitAlwaysKeepsWindows
// configuration. This is the only way to carefully control whether macOS invokes the
// state restoration system.
switch (config.windowSaveState) {
case "never": UserDefaults.standard.setValue(false, forKey: "NSQuitAlwaysKeepsWindows")
case "always": UserDefaults.standard.setValue(true, forKey: "NSQuitAlwaysKeepsWindows")
case "default": fallthrough
default: UserDefaults.standard.removeObject(forKey: "NSQuitAlwaysKeepsWindows")
}
// Sync our auto-update settings
updaterController.updater.automaticallyChecksForUpdates =
config.autoUpdate == .check || config.autoUpdate == .download
updaterController.updater.automaticallyDownloadsUpdates =
config.autoUpdate == .download
// Config could change keybindings, so update everything that depends on that
syncMenuShortcuts(config)
terminalManager.relabelAllTabs()
// Config could change window appearance. We wrap this in an async queue because when
// this is called as part of application launch it can deadlock with an internal
// AppKit mutex on the appearance.
DispatchQueue.main.async { self.syncAppearance(config: config) }
// If we have configuration errors, we need to show them.
let c = ConfigurationErrorsController.sharedInstance
c.errors = config.errors
if (c.errors.count > 0) {
if (c.window == nil || !c.window!.isVisible) {
c.showWindow(self)
}
}
// We need to handle our global event tap depending on if there are global
// events that we care about in Ghostty.
if (ghostty_app_has_global_keybinds(ghostty.app!)) {
if (timeSinceLaunch > 5) {
// If the process has been running for awhile we enable right away
// because no windows are likely to pop up.
GlobalEventTap.shared.enable()
} else {
// If the process just started, we wait a couple seconds to allow
// the initial windows and so on to load so our permissions dialog
// doesn't get buried.
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(2)) {
GlobalEventTap.shared.enable()
}
}
} else {
GlobalEventTap.shared.disable()
}
}
/// Sync the appearance of our app with the theme specified in the config.
private func syncAppearance(config: Ghostty.Config) {
guard let theme = config.windowTheme else { return }
switch (theme) {
case "dark":
let appearance = NSAppearance(named: .darkAqua)
NSApplication.shared.appearance = appearance
case "light":
let appearance = NSAppearance(named: .aqua)
NSApplication.shared.appearance = appearance
case "auto":
let color = OSColor(config.backgroundColor)
let appearance = NSAppearance(named: color.isLightColor ? .aqua : .darkAqua)
NSApplication.shared.appearance = appearance
default:
NSApplication.shared.appearance = nil
}
}
//MARK: - Restorable State //MARK: - Restorable State
/// We support NSSecureCoding for restorable state. Required as of macOS Sonoma (14) but a good idea anyways. /// We support NSSecureCoding for restorable state. Required as of macOS Sonoma (14) but a good idea anyways.
@ -470,88 +591,6 @@ class AppDelegate: NSObject,
return nil return nil
} }
func configDidReload(_ state: Ghostty.App) {
// Depending on the "window-save-state" setting we have to set the NSQuitAlwaysKeepsWindows
// configuration. This is the only way to carefully control whether macOS invokes the
// state restoration system.
switch (ghostty.config.windowSaveState) {
case "never": UserDefaults.standard.setValue(false, forKey: "NSQuitAlwaysKeepsWindows")
case "always": UserDefaults.standard.setValue(true, forKey: "NSQuitAlwaysKeepsWindows")
case "default": fallthrough
default: UserDefaults.standard.removeObject(forKey: "NSQuitAlwaysKeepsWindows")
}
// Sync our auto-update settings
updaterController.updater.automaticallyChecksForUpdates =
ghostty.config.autoUpdate == .check || ghostty.config.autoUpdate == .download
updaterController.updater.automaticallyDownloadsUpdates =
ghostty.config.autoUpdate == .download
// Config could change keybindings, so update everything that depends on that
syncMenuShortcuts()
terminalManager.relabelAllTabs()
// Config could change window appearance. We wrap this in an async queue because when
// this is called as part of application launch it can deadlock with an internal
// AppKit mutex on the appearance.
DispatchQueue.main.async { self.syncAppearance() }
// Update all of our windows
terminalManager.windows.forEach { window in
window.controller.configDidReload()
}
// If we have configuration errors, we need to show them.
let c = ConfigurationErrorsController.sharedInstance
c.errors = state.config.errors
if (c.errors.count > 0) {
if (c.window == nil || !c.window!.isVisible) {
c.showWindow(self)
}
}
// We need to handle our global event tap depending on if there are global
// events that we care about in Ghostty.
if (ghostty_app_has_global_keybinds(ghostty.app!)) {
if (timeSinceLaunch > 5) {
// If the process has been running for awhile we enable right away
// because no windows are likely to pop up.
GlobalEventTap.shared.enable()
} else {
// If the process just started, we wait a couple seconds to allow
// the initial windows and so on to load so our permissions dialog
// doesn't get buried.
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(2)) {
GlobalEventTap.shared.enable()
}
}
} else {
GlobalEventTap.shared.disable()
}
}
/// Sync the appearance of our app with the theme specified in the config.
private func syncAppearance() {
guard let theme = ghostty.config.windowTheme else { return }
switch (theme) {
case "dark":
let appearance = NSAppearance(named: .darkAqua)
NSApplication.shared.appearance = appearance
case "light":
let appearance = NSAppearance(named: .aqua)
NSApplication.shared.appearance = appearance
case "auto":
let color = OSColor(ghostty.config.backgroundColor)
let appearance = NSAppearance(named: color.isLightColor ? .aqua : .darkAqua)
NSApplication.shared.appearance = appearance
default:
NSApplication.shared.appearance = nil
}
}
//MARK: - Dock Menu //MARK: - Dock Menu
private func reloadDockMenu() { private func reloadDockMenu() {
@ -629,7 +668,7 @@ class AppDelegate: NSObject,
if quickController == nil { if quickController == nil {
quickController = QuickTerminalController( quickController = QuickTerminalController(
ghostty, ghostty,
position: ghostty.config.quickTerminalPosition position: derivedConfig.quickTerminalPosition
) )
} }
@ -655,4 +694,22 @@ class AppDelegate: NSObject,
isVisible.toggle() isVisible.toggle()
} }
private struct DerivedConfig {
let initialWindow: Bool
let shouldQuitAfterLastWindowClosed: Bool
let quickTerminalPosition: QuickTerminalPosition
init() {
self.initialWindow = true
self.shouldQuitAfterLastWindowClosed = false
self.quickTerminalPosition = .top
}
init(_ config: Ghostty.Config) {
self.initialWindow = config.initialWindow
self.shouldQuitAfterLastWindowClosed = config.shouldQuitAfterLastWindowClosed
self.quickTerminalPosition = config.quickTerminalPosition
}
}
} }

View File

@ -3,7 +3,7 @@ import SwiftUI
struct AboutView: View { struct AboutView: View {
@Environment(\.openURL) var openURL @Environment(\.openURL) var openURL
private let githubLink = URL(string: "https://github.com/ghostty-org/ghostty") private let githubURL = URL(string: "https://github.com/ghostty-org/ghostty")
/// Read the commit from the bundle. /// Read the commit from the bundle.
private var build: String? { Bundle.main.infoDictionary?["CFBundleVersion"] as? String } private var build: String? { Bundle.main.infoDictionary?["CFBundleVersion"] as? String }
@ -11,25 +11,6 @@ struct AboutView: View {
private var version: String? { Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String } private var version: String? { Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String }
private var copyright: String? { Bundle.main.infoDictionary?["NSHumanReadableCopyright"] as? String } private var copyright: String? { Bundle.main.infoDictionary?["NSHumanReadableCopyright"] as? String }
private var properties: [KeyValue<String>] {
let list: [KeyValue<String?>] = [
.init(key: "Version", value: version),
.init(key: "Build", value: build),
.init(key: "Commit", value: commit == "" ? nil : commit)
]
return list.compactMap {
guard let value = $0.value else { return nil }
return .init(key: $0.key, value: value)
}
}
private struct KeyValue<Value: Equatable>: Identifiable {
var id = UUID()
public let key: LocalizedStringResource
public let value: Value
}
#if os(macOS) #if os(macOS)
// This creates a background style similar to the Apple "About My Mac" Window // This creates a background style similar to the Apple "About My Mac" Window
private struct VisualEffectBackground: NSViewRepresentable { private struct VisualEffectBackground: NSViewRepresentable {
@ -80,27 +61,23 @@ struct AboutView: View {
.opacity(0.8) .opacity(0.8)
} }
.textSelection(.enabled) .textSelection(.enabled)
VStack(spacing: 2) { VStack(spacing: 2) {
ForEach(properties) { item in if let version {
HStack(spacing: 4) { PropertyRow(label: "Version", text: version)
Text(item.key)
.frame(width: 126, alignment: .trailing)
.padding(.trailing, 2)
Text(item.value)
.frame(width: 125, alignment: .leading)
.padding(.leading, 2)
.tint(.secondary)
.opacity(0.8)
} }
.font(.callout) if let build {
.textSelection(.enabled) PropertyRow(label: "Build", text: build)
.frame(maxWidth: .infinity) }
if let commit, commit != "",
let url = githubURL?.appendingPathComponent("/commits/\(commit)") {
PropertyRow(label: "Commit", text: commit, url: url)
} }
} }
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
HStack(spacing: 8) { HStack(spacing: 8) {
if let url = githubLink { if let url = githubURL {
Button("GitHub") { Button("GitHub") {
openURL(url) openURL(url)
} }
@ -127,6 +104,45 @@ struct AboutView: View {
.background(VisualEffectBackground(material: .underWindowBackground).ignoresSafeArea()) .background(VisualEffectBackground(material: .underWindowBackground).ignoresSafeArea())
#endif #endif
} }
private struct PropertyRow: View {
private let label: String
private let text: String
private let url: URL?
init(label: String, text: String, url: URL? = nil) {
self.label = label
self.text = text
self.url = url
}
@ViewBuilder private var textView: some View {
Text(text)
.frame(width: 125, alignment: .leading)
.padding(.leading, 2)
.tint(.secondary)
.opacity(0.8)
.monospaced()
}
var body: some View {
HStack(spacing: 4) {
Text(label)
.frame(width: 126, alignment: .trailing)
.padding(.trailing, 2)
if let url {
Link(destination: url) {
textView
}
} else {
textView
}
}
.font(.callout)
.textSelection(.enabled)
.frame(maxWidth: .infinity)
}
}
} }
struct AboutView_Previews: PreviewProvider { struct AboutView_Previews: PreviewProvider {

View File

@ -18,12 +18,16 @@ class QuickTerminalController: BaseTerminalController {
/// application to the front. /// application to the front.
private var previousApp: NSRunningApplication? = nil private var previousApp: NSRunningApplication? = nil
/// The configuration derived from the Ghostty config so we don't need to rely on references.
private var derivedConfig: DerivedConfig
init(_ ghostty: Ghostty.App, init(_ ghostty: Ghostty.App,
position: QuickTerminalPosition = .top, position: QuickTerminalPosition = .top,
baseConfig base: Ghostty.SurfaceConfiguration? = nil, baseConfig base: Ghostty.SurfaceConfiguration? = nil,
surfaceTree tree: Ghostty.SplitNode? = nil surfaceTree tree: Ghostty.SplitNode? = nil
) { ) {
self.position = position self.position = position
self.derivedConfig = DerivedConfig(ghostty.config)
super.init(ghostty, baseConfig: base, surfaceTree: tree) super.init(ghostty, baseConfig: base, surfaceTree: tree)
// Setup our notifications for behaviors // Setup our notifications for behaviors
@ -35,8 +39,8 @@ class QuickTerminalController: BaseTerminalController {
object: nil) object: nil)
center.addObserver( center.addObserver(
self, self,
selector: #selector(ghosttyDidReloadConfig), selector: #selector(ghosttyConfigDidChange(_:)),
name: Ghostty.Notification.ghosttyDidReloadConfig, name: .ghosttyConfigDidChange,
object: nil) object: nil)
} }
@ -64,7 +68,7 @@ class QuickTerminalController: BaseTerminalController {
window.isRestorable = false window.isRestorable = false
// Setup our configured appearance that we support. // Setup our configured appearance that we support.
syncAppearance() syncAppearance(ghostty.config)
// Setup our initial size based on our configured position // Setup our initial size based on our configured position
position.setLoaded(window) position.setLoaded(window)
@ -186,7 +190,7 @@ class QuickTerminalController: BaseTerminalController {
} }
private func animateWindowIn(window: NSWindow, from position: QuickTerminalPosition) { private func animateWindowIn(window: NSWindow, from position: QuickTerminalPosition) {
guard let screen = ghostty.config.quickTerminalScreen.screen else { return } guard let screen = derivedConfig.quickTerminalScreen.screen else { return }
// Move our window off screen to the top // Move our window off screen to the top
position.setInitial(in: window, on: screen) position.setInitial(in: window, on: screen)
@ -197,7 +201,7 @@ class QuickTerminalController: BaseTerminalController {
// Run the animation that moves our window into the proper place and makes // Run the animation that moves our window into the proper place and makes
// it visible. // it visible.
NSAnimationContext.runAnimationGroup({ context in NSAnimationContext.runAnimationGroup({ context in
context.duration = ghostty.config.quickTerminalAnimationDuration context.duration = derivedConfig.quickTerminalAnimationDuration
context.timingFunction = .init(name: .easeIn) context.timingFunction = .init(name: .easeIn)
position.setFinal(in: window.animator(), on: screen) position.setFinal(in: window.animator(), on: screen)
}, completionHandler: { }, completionHandler: {
@ -287,7 +291,7 @@ class QuickTerminalController: BaseTerminalController {
} }
NSAnimationContext.runAnimationGroup({ context in NSAnimationContext.runAnimationGroup({ context in
context.duration = ghostty.config.quickTerminalAnimationDuration context.duration = derivedConfig.quickTerminalAnimationDuration
context.timingFunction = .init(name: .easeIn) context.timingFunction = .init(name: .easeIn)
position.setInitial(in: window.animator(), on: screen) position.setInitial(in: window.animator(), on: screen)
}, completionHandler: { }, completionHandler: {
@ -297,7 +301,7 @@ class QuickTerminalController: BaseTerminalController {
}) })
} }
private func syncAppearance() { private func syncAppearance(_ config: Ghostty.Config) {
guard let window else { return } guard let window else { return }
// If our window is not visible, then delay this. This is possible specifically // If our window is not visible, then delay this. This is possible specifically
@ -306,12 +310,25 @@ class QuickTerminalController: BaseTerminalController {
// APIs such as window blur have no effect unless the window is visible. // APIs such as window blur have no effect unless the window is visible.
guard window.isVisible else { guard window.isVisible else {
// Weak window so that if the window changes or is destroyed we aren't holding a ref // Weak window so that if the window changes or is destroyed we aren't holding a ref
DispatchQueue.main.async { [weak self] in self?.syncAppearance() } DispatchQueue.main.async { [weak self] in self?.syncAppearance(config) }
return return
} }
// Terminals typically operate in sRGB color space and macOS defaults
// to "native" which is typically P3. There is a lot more resources
// covered in this GitHub issue: https://github.com/mitchellh/ghostty/pull/376
// Ghostty defaults to sRGB but this can be overridden.
switch (config.windowColorspace) {
case "display-p3":
window.colorSpace = .displayP3
case "srgb":
fallthrough
default:
window.colorSpace = .sRGB
}
// If we have window transparency then set it transparent. Otherwise set it opaque. // If we have window transparency then set it transparent. Otherwise set it opaque.
if (ghostty.config.backgroundOpacity < 1) { if (config.backgroundOpacity < 1) {
window.isOpaque = false window.isOpaque = false
// This is weird, but we don't use ".clear" because this creates a look that // This is weird, but we don't use ".clear" because this creates a look that
@ -358,8 +375,35 @@ class QuickTerminalController: BaseTerminalController {
toggleFullscreen(mode: .nonNative) toggleFullscreen(mode: .nonNative)
} }
@objc private func ghosttyDidReloadConfig(notification: SwiftUI.Notification) { @objc private func ghosttyConfigDidChange(_ notification: Notification) {
syncAppearance() // We only care if the configuration is a global configuration, not a
// surface-specific one.
guard notification.object == nil else { return }
// Get our managed configuration object out
guard let config = notification.userInfo?[
Notification.Name.GhosttyConfigChangeKey
] as? Ghostty.Config else { return }
// Update our derived config
self.derivedConfig = DerivedConfig(config)
syncAppearance(config)
}
private struct DerivedConfig {
let quickTerminalScreen: QuickTerminalScreen
let quickTerminalAnimationDuration: Double
init() {
self.quickTerminalScreen = .main
self.quickTerminalAnimationDuration = 0.2
}
init(_ config: Ghostty.Config) {
self.quickTerminalScreen = config.quickTerminalScreen
self.quickTerminalAnimationDuration = config.quickTerminalAnimationDuration
}
} }
} }

View File

@ -60,6 +60,9 @@ class BaseTerminalController: NSWindowController,
/// The previous frame information from the window /// The previous frame information from the window
private var savedFrame: SavedFrame? = nil private var savedFrame: SavedFrame? = nil
/// The configuration derived from the Ghostty config so we don't need to rely on references.
private var derivedConfig: DerivedConfig
struct SavedFrame { struct SavedFrame {
let window: NSRect let window: NSRect
let screen: NSRect let screen: NSRect
@ -74,6 +77,7 @@ class BaseTerminalController: NSWindowController,
surfaceTree tree: Ghostty.SplitNode? = nil surfaceTree tree: Ghostty.SplitNode? = nil
) { ) {
self.ghostty = ghostty self.ghostty = ghostty
self.derivedConfig = DerivedConfig(ghostty.config)
super.init(window: nil) super.init(window: nil)
@ -93,6 +97,11 @@ class BaseTerminalController: NSWindowController,
selector: #selector(didChangeScreenParametersNotification), selector: #selector(didChangeScreenParametersNotification),
name: NSApplication.didChangeScreenParametersNotification, name: NSApplication.didChangeScreenParametersNotification,
object: nil) object: nil)
center.addObserver(
self,
selector: #selector(ghosttyConfigDidChangeBase(_:)),
name: .ghosttyConfigDidChange,
object: nil)
// Listen for local events that we need to know of outside of // Listen for local events that we need to know of outside of
// single surface handlers. // single surface handlers.
@ -152,8 +161,12 @@ class BaseTerminalController: NSWindowController,
// screen then we clamp it back to within the screen. // screen then we clamp it back to within the screen.
guard let window else { return } guard let window else { return }
guard window.isVisible else { return } guard window.isVisible else { return }
guard let screen = window.screen else { return }
// We ignore fullscreen windows because macOS automatically resizes
// those back to the fullscreen bounds.
guard !window.styleMask.contains(.fullScreen) else { return }
guard let screen = window.screen else { return }
let visibleFrame = screen.visibleFrame let visibleFrame = screen.visibleFrame
var newFrame = window.frame var newFrame = window.frame
@ -187,6 +200,20 @@ class BaseTerminalController: NSWindowController,
window.setFrame(newFrame, display: true) window.setFrame(newFrame, display: true)
} }
@objc private func ghosttyConfigDidChangeBase(_ notification: Notification) {
// We only care if the configuration is a global configuration, not a
// surface-specific one.
guard notification.object == nil else { return }
// Get our managed configuration object out
guard let config = notification.userInfo?[
Notification.Name.GhosttyConfigChangeKey
] as? Ghostty.Config else { return }
// Update our derived config
self.derivedConfig = DerivedConfig(config)
}
// MARK: Local Events // MARK: Local Events
private func localEventHandler(_ event: NSEvent) -> NSEvent? { private func localEventHandler(_ event: NSEvent) -> NSEvent? {
@ -241,7 +268,7 @@ class BaseTerminalController: NSWindowController,
func pwdDidChange(to: URL?) { func pwdDidChange(to: URL?) {
guard let window else { return } guard let window else { return }
if ghostty.config.macosTitlebarProxyIcon == .visible { if derivedConfig.macosTitlebarProxyIcon == .visible {
// Use the 'to' URL directly // Use the 'to' URL directly
window.representedURL = to window.representedURL = to
} else { } else {
@ -251,7 +278,7 @@ class BaseTerminalController: NSWindowController,
func cellSizeDidChange(to: NSSize) { func cellSizeDidChange(to: NSSize) {
guard ghostty.config.windowStepResize else { return } guard derivedConfig.windowStepResize else { return }
self.window?.contentResizeIncrements = to self.window?.contentResizeIncrements = to
} }
@ -559,4 +586,19 @@ class BaseTerminalController: NSWindowController,
guard let surface = focusedSurface?.surface else { return } guard let surface = focusedSurface?.surface else { return }
ghostty.resetTerminal(surface: surface) ghostty.resetTerminal(surface: surface)
} }
private struct DerivedConfig {
let macosTitlebarProxyIcon: Ghostty.MacOSTitlebarProxyIcon
let windowStepResize: Bool
init() {
self.macosTitlebarProxyIcon = .visible
self.windowStepResize = false
}
init(_ config: Ghostty.Config) {
self.macosTitlebarProxyIcon = config.macosTitlebarProxyIcon
self.windowStepResize = config.windowStepResize
}
}
} }

View File

@ -1,6 +1,7 @@
import Foundation import Foundation
import Cocoa import Cocoa
import SwiftUI import SwiftUI
import Combine
import GhosttyKit import GhosttyKit
/// A classic, tabbed terminal experience. /// A classic, tabbed terminal experience.
@ -20,6 +21,12 @@ class TerminalController: BaseTerminalController {
/// For example, terminals executing custom scripts are not restorable. /// For example, terminals executing custom scripts are not restorable.
private var restorable: Bool = true private var restorable: Bool = true
/// The configuration derived from the Ghostty config so we don't need to rely on references.
private var derivedConfig: DerivedConfig
/// The notification cancellable for focused surface property changes.
private var surfaceAppearanceCancellables: Set<AnyCancellable> = []
init(_ ghostty: Ghostty.App, init(_ ghostty: Ghostty.App,
withBaseConfig base: Ghostty.SurfaceConfiguration? = nil, withBaseConfig base: Ghostty.SurfaceConfiguration? = nil,
withSurfaceTree tree: Ghostty.SplitNode? = nil withSurfaceTree tree: Ghostty.SplitNode? = nil
@ -31,6 +38,9 @@ class TerminalController: BaseTerminalController {
// restoration. // restoration.
self.restorable = (base?.command ?? "") == "" self.restorable = (base?.command ?? "") == ""
// Setup our initial derived config based on the current app config
self.derivedConfig = DerivedConfig(ghostty.config)
super.init(ghostty, baseConfig: base, surfaceTree: tree) super.init(ghostty, baseConfig: base, surfaceTree: tree)
// Setup our notifications for behaviors // Setup our notifications for behaviors
@ -50,6 +60,12 @@ class TerminalController: BaseTerminalController {
selector: #selector(onGotoTab), selector: #selector(onGotoTab),
name: Ghostty.Notification.ghosttyGotoTab, name: Ghostty.Notification.ghosttyGotoTab,
object: nil) object: nil)
center.addObserver(
self,
selector: #selector(ghosttyConfigDidChange(_:)),
name: .ghosttyConfigDidChange,
object: nil
)
center.addObserver( center.addObserver(
self, self,
selector: #selector(onFrameDidChange), selector: #selector(onFrameDidChange),
@ -80,10 +96,38 @@ class TerminalController: BaseTerminalController {
//MARK: - Methods //MARK: - Methods
func configDidReload() { @objc private func ghosttyConfigDidChange(_ notification: Notification) {
// Get our managed configuration object out
guard let config = notification.userInfo?[
Notification.Name.GhosttyConfigChangeKey
] as? Ghostty.Config else { return }
// If this is an app-level config update then we update some things.
if (notification.object == nil) {
// Update our derived config
self.derivedConfig = DerivedConfig(config)
guard let window = window as? TerminalWindow else { return } guard let window = window as? TerminalWindow else { return }
window.focusFollowsMouse = ghostty.config.focusFollowsMouse window.focusFollowsMouse = config.focusFollowsMouse
syncAppearance()
// If we have no surfaces in our window (is that possible?) then we update
// our window appearance based on the root config. If we have surfaces, we
// don't call this because the TODO
if surfaceTree == nil {
syncAppearance(.init(config))
}
return
}
// This is a surface-level config update. If we have the surface, we
// update our appearance based on it.
guard let surfaceView = notification.object as? Ghostty.SurfaceView else { return }
guard surfaceTree?.contains(view: surfaceView) ?? false else { return }
// We can't use surfaceView.derivedConfig because it may not be updated
// yet since it also responds to notifications.
syncAppearance(.init(config))
} }
/// Update the accessory view of each tab according to the keyboard /// Update the accessory view of each tab according to the keyboard
@ -144,28 +188,23 @@ class TerminalController: BaseTerminalController {
self.relabelTabs() self.relabelTabs()
} }
private func syncAppearance() { private func syncAppearance(_ surfaceConfig: Ghostty.SurfaceView.DerivedConfig) {
guard let window = self.window as? TerminalWindow else { return } guard let window = self.window as? TerminalWindow else { return }
// If our window is not visible, then delay this. This is possible specifically // If our window is not visible, then we do nothing. Some things such as blurring
// during state restoration but probably in other scenarios as well. To delay, // have no effect if the window is not visible. Ultimately, we'll have this called
// we just loop directly on the dispatch queue. We have to delay because some // at some point when a surface becomes focused.
// APIs such as window blur have no effect unless the window is visible. guard window.isVisible else { return }
guard window.isVisible else {
// Weak window so that if the window changes or is destroyed we aren't holding a ref
DispatchQueue.main.async { [weak self] in self?.syncAppearance() }
return
}
// Set the font for the window and tab titles. // Set the font for the window and tab titles.
if let titleFontName = ghostty.config.windowTitleFontFamily { if let titleFontName = surfaceConfig.windowTitleFontFamily {
window.titlebarFont = NSFont(name: titleFontName, size: NSFont.systemFontSize) window.titlebarFont = NSFont(name: titleFontName, size: NSFont.systemFontSize)
} else { } else {
window.titlebarFont = nil window.titlebarFont = nil
} }
// If we have window transparency then set it transparent. Otherwise set it opaque. // If we have window transparency then set it transparent. Otherwise set it opaque.
if (ghostty.config.backgroundOpacity < 1) { if (surfaceConfig.backgroundOpacity < 1) {
window.isOpaque = false window.isOpaque = false
// This is weird, but we don't use ".clear" because this creates a look that // This is weird, but we don't use ".clear" because this creates a look that
@ -179,14 +218,26 @@ class TerminalController: BaseTerminalController {
window.backgroundColor = .windowBackgroundColor window.backgroundColor = .windowBackgroundColor
} }
window.hasShadow = ghostty.config.macosWindowShadow window.hasShadow = surfaceConfig.macosWindowShadow
guard window.hasStyledTabs else { return } guard window.hasStyledTabs else { return }
// The titlebar is always updated. We don't need to worry about opacity // Our background color depends on if our focused surface borders the top or not.
// because we handle it here. // If it does, we match the focused surface. If it doesn't, we use the app
let backgroundColor = OSColor(ghostty.config.backgroundColor) // configuration.
window.titlebarColor = backgroundColor.withAlphaComponent(ghostty.config.backgroundOpacity) let backgroundColor: OSColor
if let surfaceTree {
if let focusedSurface, surfaceTree.doesBorderTop(view: focusedSurface) {
backgroundColor = OSColor(focusedSurface.backgroundColor ?? surfaceConfig.backgroundColor)
} else {
// We don't have a focused surface or our surface doesn't border the
// top. We choose to match the color of the top-left most surface.
backgroundColor = OSColor(surfaceTree.topLeft().backgroundColor ?? derivedConfig.backgroundColor)
}
} else {
backgroundColor = OSColor(self.derivedConfig.backgroundColor)
}
window.titlebarColor = backgroundColor.withAlphaComponent(surfaceConfig.backgroundOpacity)
if (window.isOpaque) { if (window.isOpaque) {
// Bg color is only synced if we have no transparency. This is because // Bg color is only synced if we have no transparency. This is because
@ -210,6 +261,12 @@ class TerminalController: BaseTerminalController {
override func windowDidLoad() { override func windowDidLoad() {
guard let window = window as? TerminalWindow else { return } guard let window = window as? TerminalWindow else { return }
// I copy this because we may change the source in the future but also because
// I regularly audit our codebase for "ghostty.config" access because generally
// you shouldn't use it. Its safe in this case because for a new window we should
// use whatever the latest app-level config is.
let config = ghostty.config
// Setting all three of these is required for restoration to work. // Setting all three of these is required for restoration to work.
window.isRestorable = restorable window.isRestorable = restorable
if (restorable) { if (restorable) {
@ -218,13 +275,13 @@ class TerminalController: BaseTerminalController {
} }
// If window decorations are disabled, remove our title // If window decorations are disabled, remove our title
if (!ghostty.config.windowDecorations) { window.styleMask.remove(.titled) } if (!config.windowDecorations) { window.styleMask.remove(.titled) }
// Terminals typically operate in sRGB color space and macOS defaults // Terminals typically operate in sRGB color space and macOS defaults
// to "native" which is typically P3. There is a lot more resources // to "native" which is typically P3. There is a lot more resources
// covered in this GitHub issue: https://github.com/mitchellh/ghostty/pull/376 // covered in this GitHub issue: https://github.com/mitchellh/ghostty/pull/376
// Ghostty defaults to sRGB but this can be overridden. // Ghostty defaults to sRGB but this can be overridden.
switch (ghostty.config.windowColorspace) { switch (config.windowColorspace) {
case "display-p3": case "display-p3":
window.colorSpace = .displayP3 window.colorSpace = .displayP3
case "srgb": case "srgb":
@ -256,30 +313,30 @@ class TerminalController: BaseTerminalController {
window.center() window.center()
// Make sure our theme is set on the window so styling is correct. // Make sure our theme is set on the window so styling is correct.
if let windowTheme = ghostty.config.windowTheme { if let windowTheme = config.windowTheme {
window.windowTheme = .init(rawValue: windowTheme) window.windowTheme = .init(rawValue: windowTheme)
} }
// Handle titlebar tabs config option. Something about what we do while setting up the // Handle titlebar tabs config option. Something about what we do while setting up the
// titlebar tabs interferes with the window restore process unless window.tabbingMode // titlebar tabs interferes with the window restore process unless window.tabbingMode
// is set to .preferred, so we set it, and switch back to automatic as soon as we can. // is set to .preferred, so we set it, and switch back to automatic as soon as we can.
if (ghostty.config.macosTitlebarStyle == "tabs") { if (config.macosTitlebarStyle == "tabs") {
window.tabbingMode = .preferred window.tabbingMode = .preferred
window.titlebarTabs = true window.titlebarTabs = true
DispatchQueue.main.async { DispatchQueue.main.async {
window.tabbingMode = .automatic window.tabbingMode = .automatic
} }
} else if (ghostty.config.macosTitlebarStyle == "transparent") { } else if (config.macosTitlebarStyle == "transparent") {
window.transparentTabs = true window.transparentTabs = true
} }
if window.hasStyledTabs { if window.hasStyledTabs {
// Set the background color of the window // Set the background color of the window
let backgroundColor = NSColor(ghostty.config.backgroundColor) let backgroundColor = NSColor(config.backgroundColor)
window.backgroundColor = backgroundColor window.backgroundColor = backgroundColor
// This makes sure our titlebar renders correctly when there is a transparent background // This makes sure our titlebar renders correctly when there is a transparent background
window.titlebarColor = backgroundColor.withAlphaComponent(ghostty.config.backgroundOpacity) window.titlebarColor = backgroundColor.withAlphaComponent(config.backgroundOpacity)
} }
// Initialize our content view to the SwiftUI root // Initialize our content view to the SwiftUI root
@ -290,7 +347,7 @@ class TerminalController: BaseTerminalController {
)) ))
// If our titlebar style is "hidden" we adjust the style appropriately // If our titlebar style is "hidden" we adjust the style appropriately
if (ghostty.config.macosTitlebarStyle == "hidden") { if (config.macosTitlebarStyle == "hidden") {
window.styleMask = [ window.styleMask = [
// We need `titled` in the mask to get the normal window frame // We need `titled` in the mask to get the normal window frame
.titled, .titled,
@ -345,10 +402,12 @@ class TerminalController: BaseTerminalController {
} }
} }
window.focusFollowsMouse = ghostty.config.focusFollowsMouse window.focusFollowsMouse = config.focusFollowsMouse
// Apply any additional appearance-related properties to the new window. // Apply any additional appearance-related properties to the new window. We
syncAppearance() // apply this based on the root config but change it later based on surface
// config (see focused surface change callback).
syncAppearance(.init(config))
} }
// Shows the "+" button in the tab bar, responds to that click. // Shows the "+" button in the tab bar, responds to that click.
@ -464,7 +523,7 @@ class TerminalController: BaseTerminalController {
// Custom toolbar-based title used when titlebar tabs are enabled. // Custom toolbar-based title used when titlebar tabs are enabled.
if let toolbar = window.toolbar as? TerminalToolbar { if let toolbar = window.toolbar as? TerminalToolbar {
if (window.titlebarTabs || ghostty.config.macosTitlebarStyle == "hidden") { if (window.titlebarTabs || derivedConfig.macosTitlebarStyle == "hidden") {
// Updating the title text as above automatically reveals the // Updating the title text as above automatically reveals the
// native title view in macOS 15.0 and above. Since we're using // native title view in macOS 15.0 and above. Since we're using
// a custom view instead, we need to re-hide it. // a custom view instead, we need to re-hide it.
@ -485,6 +544,36 @@ class TerminalController: BaseTerminalController {
window.surfaceIsZoomed = to window.surfaceIsZoomed = to
} }
override func focusedSurfaceDidChange(to: Ghostty.SurfaceView?) {
super.focusedSurfaceDidChange(to: to)
// We always cancel our event listener
surfaceAppearanceCancellables.removeAll()
// When our focus changes, we update our window appearance based on the
// currently focused surface.
guard let focusedSurface else { return }
syncAppearance(focusedSurface.derivedConfig)
// We also want to get notified of certain changes to update our appearance.
focusedSurface.$derivedConfig
.sink { [weak self, weak focusedSurface] _ in self?.syncAppearanceOnPropertyChange(focusedSurface) }
.store(in: &surfaceAppearanceCancellables)
focusedSurface.$backgroundColor
.sink { [weak self, weak focusedSurface] _ in self?.syncAppearanceOnPropertyChange(focusedSurface) }
.store(in: &surfaceAppearanceCancellables)
}
private func syncAppearanceOnPropertyChange(_ surface: Ghostty.SurfaceView?) {
guard let surface else { return }
DispatchQueue.main.async { [weak self, weak surface] in
guard let surface else { return }
guard let self else { return }
guard self.focusedSurface == surface else { return }
self.syncAppearance(surface.derivedConfig)
}
}
//MARK: - Notifications //MARK: - Notifications
@objc private func onMoveTab(notification: SwiftUI.Notification) { @objc private func onMoveTab(notification: SwiftUI.Notification) {
@ -593,4 +682,19 @@ class TerminalController: BaseTerminalController {
toggleFullscreen(mode: fullscreenMode) toggleFullscreen(mode: fullscreenMode)
} }
private struct DerivedConfig {
let backgroundColor: Color
let macosTitlebarStyle: String
init() {
self.backgroundColor = Color(NSColor.windowBackgroundColor)
self.macosTitlebarStyle = "system"
}
init(_ config: Ghostty.Config) {
self.backgroundColor = config.backgroundColor
self.macosTitlebarStyle = config.macosTitlebarStyle
}
}
} }

View File

@ -37,8 +37,12 @@ class TerminalManager {
return windows.last return windows.last
} }
/// The configuration derived from the Ghostty config so we don't need to rely on references.
private var derivedConfig: DerivedConfig
init(_ ghostty: Ghostty.App) { init(_ ghostty: Ghostty.App) {
self.ghostty = ghostty self.ghostty = ghostty
self.derivedConfig = DerivedConfig(ghostty.config)
let center = NotificationCenter.default let center = NotificationCenter.default
center.addObserver( center.addObserver(
@ -51,6 +55,11 @@ class TerminalManager {
selector: #selector(onNewWindow), selector: #selector(onNewWindow),
name: Ghostty.Notification.ghosttyNewWindow, name: Ghostty.Notification.ghosttyNewWindow,
object: nil) object: nil)
center.addObserver(
self,
selector: #selector(ghosttyConfigDidChange(_:)),
name: .ghosttyConfigDidChange,
object: nil)
} }
deinit { deinit {
@ -70,8 +79,8 @@ class TerminalManager {
if let parent = focusedSurface?.window, if let parent = focusedSurface?.window,
parent.styleMask.contains(.fullScreen) { parent.styleMask.contains(.fullScreen) {
window.toggleFullScreen(nil) window.toggleFullScreen(nil)
} else if ghostty.config.windowFullscreen { } else if derivedConfig.windowFullscreen {
switch (ghostty.config.windowFullscreenMode) { switch (derivedConfig.windowFullscreenMode) {
case .native: case .native:
// Native has to be done immediately so that our stylemask contains // Native has to be done immediately so that our stylemask contains
// fullscreen for the logic later in this method. // fullscreen for the logic later in this method.
@ -81,7 +90,7 @@ class TerminalManager {
// If we're non-native then we have to do it on a later loop // If we're non-native then we have to do it on a later loop
// so that the content view is setup. // so that the content view is setup.
DispatchQueue.main.async { DispatchQueue.main.async {
c.toggleFullscreen(mode: self.ghostty.config.windowFullscreenMode) c.toggleFullscreen(mode: self.derivedConfig.windowFullscreenMode)
} }
} }
} }
@ -159,9 +168,9 @@ class TerminalManager {
// If we have the "hidden" titlebar style we want to create new // If we have the "hidden" titlebar style we want to create new
// tabs as windows instead, so just skip adding it to the parent. // tabs as windows instead, so just skip adding it to the parent.
if (ghostty.config.macosTitlebarStyle != "hidden") { if (derivedConfig.macosTitlebarStyle != "hidden") {
// Add the window to the tab group and show it. // Add the window to the tab group and show it.
switch ghostty.config.windowNewTabPosition { switch derivedConfig.windowNewTabPosition {
case "end": case "end":
// If we already have a tab group and we want the new tab to open at the end, // If we already have a tab group and we want the new tab to open at the end,
// then we use the last window in the tab group as the parent. // then we use the last window in the tab group as the parent.
@ -227,7 +236,19 @@ class TerminalManager {
// are closing a tabbed window, we want to set the cascade point to be // are closing a tabbed window, we want to set the cascade point to be
// the next cascade point from this window. // the next cascade point from this window.
if focusedWindow != controller.window { if focusedWindow != controller.window {
// The cascadeTopLeft call below should NOT move the window. Starting with
// macOS 15, we found that specifically when used with the new window snapping
// features of macOS 15, this WOULD move the frame. So we keep track of the
// old frame and restore it if necessary. Issue:
// https://github.com/ghostty-org/ghostty/issues/2565
let oldFrame = focusedWindow.frame
Self.lastCascadePoint = focusedWindow.cascadeTopLeft(from: NSZeroPoint) Self.lastCascadePoint = focusedWindow.cascadeTopLeft(from: NSZeroPoint)
if focusedWindow.frame != oldFrame {
focusedWindow.setFrame(oldFrame, display: true)
}
return return
} }
@ -313,4 +334,39 @@ class TerminalManager {
self.newTab(to: window, withBaseConfig: config) self.newTab(to: window, withBaseConfig: config)
} }
@objc private func ghosttyConfigDidChange(_ notification: Notification) {
// We only care if the configuration is a global configuration, not a
// surface-specific one.
guard notification.object == nil else { return }
// Get our managed configuration object out
guard let config = notification.userInfo?[
Notification.Name.GhosttyConfigChangeKey
] as? Ghostty.Config else { return }
// Update our derived config
self.derivedConfig = DerivedConfig(config)
}
private struct DerivedConfig {
let windowFullscreen: Bool
let windowFullscreenMode: FullscreenMode
let macosTitlebarStyle: String
let windowNewTabPosition: String
init() {
self.windowFullscreen = false
self.windowFullscreenMode = .native
self.macosTitlebarStyle = "transparent"
self.windowNewTabPosition = ""
}
init(_ config: Ghostty.Config) {
self.windowFullscreen = config.windowFullscreen
self.windowFullscreenMode = config.windowFullscreenMode
self.macosTitlebarStyle = config.macosTitlebarStyle
self.windowNewTabPosition = config.windowNewTabPosition
}
}
} }

View File

@ -65,8 +65,10 @@ class TerminalWindowRestoration: NSObject, NSWindowRestoration {
} }
// If our configuration is "never" then we never restore the state // If our configuration is "never" then we never restore the state
// no matter what. // no matter what. Note its safe to use "ghostty.config" directly here
if (appDelegate.terminalManager.ghostty.config.windowSaveState == "never") { // because window restoration is only ever invoked on app start so we
// don't have to deal with config reloads.
if (appDelegate.ghostty.config.windowSaveState == "never") {
completionHandler(nil, nil) completionHandler(nil, nil)
return return
} }

View File

@ -50,6 +50,7 @@ struct TerminalView<ViewModel: TerminalViewModel>: View {
// Various state values sent back up from the currently focused terminals. // Various state values sent back up from the currently focused terminals.
@FocusedValue(\.ghosttySurfaceView) private var focusedSurface @FocusedValue(\.ghosttySurfaceView) private var focusedSurface
@FocusedValue(\.ghosttySurfaceTitle) private var surfaceTitle @FocusedValue(\.ghosttySurfaceTitle) private var surfaceTitle
@FocusedValue(\.ghosttySurfacePwd) private var surfacePwd
@FocusedValue(\.ghosttySurfaceZoomed) private var zoomedSplit @FocusedValue(\.ghosttySurfaceZoomed) private var zoomedSplit
@FocusedValue(\.ghosttySurfaceCellSize) private var cellSize @FocusedValue(\.ghosttySurfaceCellSize) private var cellSize
@ -66,13 +67,10 @@ struct TerminalView<ViewModel: TerminalViewModel>: View {
return title return title
} }
// The proxy icon URL for our window // The pwd of the focused surface as a URL
private var proxyIconURL: URL? { private var pwdURL: URL? {
guard let proxyURLString = focusedSurface?.pwd else { guard let surfacePwd else { return nil }
return nil return URL(fileURLWithPath: surfacePwd)
}
// Use fileURLWithPath initializer for file paths
return URL(fileURLWithPath: proxyURLString)
} }
var body: some View { var body: some View {
@ -99,7 +97,7 @@ struct TerminalView<ViewModel: TerminalViewModel>: View {
.onChange(of: title) { newValue in .onChange(of: title) { newValue in
self.delegate?.titleDidChange(to: newValue) self.delegate?.titleDidChange(to: newValue)
} }
.onChange(of: proxyIconURL) { newValue in .onChange(of: pwdURL) { newValue in
self.delegate?.pwdDidChange(to: newValue) self.delegate?.pwdDidChange(to: newValue)
} }
.onChange(of: cellSize) { newValue in .onChange(of: cellSize) { newValue in

View File

@ -1,3 +1,4 @@
import SwiftUI
import GhosttyKit import GhosttyKit
extension Ghostty { extension Ghostty {
@ -5,6 +6,33 @@ extension Ghostty {
} }
extension Ghostty.Action { extension Ghostty.Action {
struct ColorChange {
let kind: Kind
let color: Color
enum Kind {
case foreground
case background
case cursor
case palette(index: UInt8)
}
init(c: ghostty_action_color_change_s) {
switch (c.kind) {
case GHOSTTY_ACTION_COLOR_KIND_FOREGROUND:
self.kind = .foreground
case GHOSTTY_ACTION_COLOR_KIND_BACKGROUND:
self.kind = .background
case GHOSTTY_ACTION_COLOR_KIND_CURSOR:
self.kind = .cursor
default:
self.kind = .palette(index: UInt8(c.kind.rawValue))
}
self.color = Color(red: Double(c.r) / 255, green: Double(c.g) / 255, blue: Double(c.b) / 255)
}
}
struct MoveTab { struct MoveTab {
let amount: Int let amount: Int

View File

@ -3,9 +3,6 @@ import UserNotifications
import GhosttyKit import GhosttyKit
protocol GhosttyAppDelegate: AnyObject { protocol GhosttyAppDelegate: AnyObject {
/// Called when the configuration did finish reloading.
func configDidReload(_ app: Ghostty.App)
#if os(macOS) #if os(macOS)
/// Called when a callback needs access to a specific surface. This should return nil /// Called when a callback needs access to a specific surface. This should return nil
/// when the surface is no longer valid. /// when the surface is no longer valid.
@ -68,7 +65,6 @@ extension Ghostty {
supports_selection_clipboard: false, supports_selection_clipboard: false,
wakeup_cb: { userdata in App.wakeup(userdata) }, wakeup_cb: { userdata in App.wakeup(userdata) },
action_cb: { app, target, action in App.action(app!, target: target, action: action) }, action_cb: { app, target, action in App.action(app!, target: target, action: action) },
reload_config_cb: { userdata in App.reloadConfig(userdata) },
read_clipboard_cb: { userdata, loc, state in App.readClipboard(userdata, location: loc, state: state) }, read_clipboard_cb: { userdata, loc, state in App.readClipboard(userdata, location: loc, state: state) },
confirm_read_clipboard_cb: { userdata, str, state, request in App.confirmReadClipboard(userdata, string: str, state: state, request: request ) }, confirm_read_clipboard_cb: { userdata, str, state, request in App.confirmReadClipboard(userdata, string: str, state: state, request: request ) },
write_clipboard_cb: { userdata, str, loc, confirm in App.writeClipboard(userdata, string: str, location: loc, confirm: confirm) }, write_clipboard_cb: { userdata, str, loc, confirm in App.writeClipboard(userdata, string: str, location: loc, confirm: confirm) },
@ -145,9 +141,47 @@ extension Ghostty {
ghostty_app_open_config(app) ghostty_app_open_config(app)
} }
func reloadConfig() { /// Reload the configuration.
func reloadConfig(soft: Bool = false) {
guard let app = self.app else { return } guard let app = self.app else { return }
ghostty_app_reload_config(app)
// Soft updates just call with our existing config
if (soft) {
ghostty_app_update_config(app, config.config!)
return
}
// Hard or full updates have to reload the full configuration
let newConfig = Config()
guard newConfig.loaded else {
Ghostty.logger.warning("failed to reload configuration")
return
}
ghostty_app_update_config(app, newConfig.config!)
// We can only set our config after updating it so that we don't free
// memory that may still be in use
self.config = newConfig
}
func reloadConfig(surface: ghostty_surface_t, soft: Bool = false) {
// Soft updates just call with our existing config
if (soft) {
ghostty_surface_update_config(surface, config.config!)
return
}
// Hard or full updates have to reload the full configuration.
// NOTE: We never set this on self.config because this is a surface-only
// config. We free it after the call.
let newConfig = Config()
guard newConfig.loaded else {
Ghostty.logger.warning("failed to reload configuration")
return
}
ghostty_surface_update_config(surface, newConfig.config!)
} }
/// Request that the given surface is closed. This will trigger the full normal surface close event /// Request that the given surface is closed. This will trigger the full normal surface close event
@ -240,7 +274,6 @@ extension Ghostty {
static func wakeup(_ userdata: UnsafeMutableRawPointer?) {} static func wakeup(_ userdata: UnsafeMutableRawPointer?) {}
static func action(_ app: ghostty_app_t, target: ghostty_target_s, action: ghostty_action_s) {} static func action(_ app: ghostty_app_t, target: ghostty_target_s, action: ghostty_action_s) {}
static func reloadConfig(_ userdata: UnsafeMutableRawPointer?) -> ghostty_config_t? { return nil }
static func readClipboard( static func readClipboard(
_ userdata: UnsafeMutableRawPointer?, _ userdata: UnsafeMutableRawPointer?,
location: ghostty_clipboard_e, location: ghostty_clipboard_e,
@ -368,31 +401,6 @@ extension Ghostty {
) )
} }
static func reloadConfig(_ userdata: UnsafeMutableRawPointer?) -> ghostty_config_t? {
let newConfig = Config()
guard newConfig.loaded else {
AppDelegate.logger.warning("failed to reload configuration")
return nil
}
// Assign the new config. This will automatically free the old config.
// It is safe to free the old config from within this function call.
let state = Unmanaged<Self>.fromOpaque(userdata!).takeUnretainedValue()
state.config = newConfig
// If we have a delegate, notify.
if let delegate = state.delegate {
delegate.configDidReload(state)
}
// Send an event out
NotificationCenter.default.post(
name: Ghostty.Notification.ghosttyDidReloadConfig,
object: nil)
return newConfig.config
}
static func wakeup(_ userdata: UnsafeMutableRawPointer?) { static func wakeup(_ userdata: UnsafeMutableRawPointer?) {
let state = Unmanaged<App>.fromOpaque(userdata!).takeUnretainedValue() let state = Unmanaged<App>.fromOpaque(userdata!).takeUnretainedValue()
@ -488,6 +496,9 @@ extension Ghostty {
case GHOSTTY_ACTION_SET_TITLE: case GHOSTTY_ACTION_SET_TITLE:
setTitle(app, target: target, v: action.action.set_title) setTitle(app, target: target, v: action.action.set_title)
case GHOSTTY_ACTION_PWD:
pwdChanged(app, target: target, v: action.action.pwd)
case GHOSTTY_ACTION_OPEN_CONFIG: case GHOSTTY_ACTION_OPEN_CONFIG:
ghostty_config_open() ghostty_config_open()
@ -521,6 +532,15 @@ extension Ghostty {
case GHOSTTY_ACTION_KEY_SEQUENCE: case GHOSTTY_ACTION_KEY_SEQUENCE:
keySequence(app, target: target, v: action.action.key_sequence) keySequence(app, target: target, v: action.action.key_sequence)
case GHOSTTY_ACTION_CONFIG_CHANGE:
configChange(app, target: target, v: action.action.config_change)
case GHOSTTY_ACTION_RELOAD_CONFIG:
configReload(app, target: target, v: action.action.reload_config)
case GHOSTTY_ACTION_COLOR_CHANGE:
colorChange(app, target: target, change: action.action.color_change)
case GHOSTTY_ACTION_CLOSE_ALL_WINDOWS: case GHOSTTY_ACTION_CLOSE_ALL_WINDOWS:
fallthrough fallthrough
case GHOSTTY_ACTION_TOGGLE_TAB_OVERVIEW: case GHOSTTY_ACTION_TOGGLE_TAB_OVERVIEW:
@ -941,6 +961,26 @@ extension Ghostty {
} }
} }
private static func pwdChanged(
_ app: ghostty_app_t,
target: ghostty_target_s,
v: ghostty_action_pwd_s) {
switch (target.tag) {
case GHOSTTY_TARGET_APP:
Ghostty.logger.warning("pwd change does nothing with an app target")
return
case GHOSTTY_TARGET_SURFACE:
guard let surface = target.target.surface else { return }
guard let surfaceView = self.surfaceView(from: surface) else { return }
guard let pwd = String(cString: v.pwd!, encoding: .utf8) else { return }
surfaceView.pwd = pwd
default:
assertionFailure()
}
}
private static func setMouseShape( private static func setMouseShape(
_ app: ghostty_app_t, _ app: ghostty_app_t,
target: ghostty_target_s, target: ghostty_target_s,
@ -1134,6 +1174,104 @@ extension Ghostty {
} }
} }
private static func configReload(
_ app: ghostty_app_t,
target: ghostty_target_s,
v: ghostty_action_reload_config_s)
{
logger.info("config reload notification")
guard let app_ud = ghostty_app_userdata(app) else { return }
let ghostty = Unmanaged<App>.fromOpaque(app_ud).takeUnretainedValue()
switch (target.tag) {
case GHOSTTY_TARGET_APP:
ghostty.reloadConfig(soft: v.soft)
return
case GHOSTTY_TARGET_SURFACE:
guard let surface = target.target.surface else { return }
ghostty.reloadConfig(surface: surface, soft: v.soft)
default:
assertionFailure()
}
}
private static func configChange(
_ app: ghostty_app_t,
target: ghostty_target_s,
v: ghostty_action_config_change_s) {
logger.info("config change notification")
// Clone the config so we own the memory. It'd be nicer to not have to do
// this but since we async send the config out below we have to own the lifetime.
// A future improvement might be to add reference counting to config or
// something so apprt's do not have to do this.
let config = Config(clone: v.config)
switch (target.tag) {
case GHOSTTY_TARGET_APP:
// Notify the world that the app config changed
NotificationCenter.default.post(
name: .ghosttyConfigDidChange,
object: nil,
userInfo: [
SwiftUI.Notification.Name.GhosttyConfigChangeKey: config,
]
)
// We also REPLACE our app-level config when this happens. This lets
// all the various things that depend on this but are still theme specific
// such as split border color work.
guard let app_ud = ghostty_app_userdata(app) else { return }
let ghostty = Unmanaged<App>.fromOpaque(app_ud).takeUnretainedValue()
ghostty.config = config
return
case GHOSTTY_TARGET_SURFACE:
guard let surface = target.target.surface else { return }
guard let surfaceView = self.surfaceView(from: surface) else { return }
NotificationCenter.default.post(
name: .ghosttyConfigDidChange,
object: surfaceView,
userInfo: [
SwiftUI.Notification.Name.GhosttyConfigChangeKey: config,
]
)
default:
assertionFailure()
}
}
private static func colorChange(
_ app: ghostty_app_t,
target: ghostty_target_s,
change: ghostty_action_color_change_s) {
switch (target.tag) {
case GHOSTTY_TARGET_APP:
Ghostty.logger.warning("color change does nothing with an app target")
return
case GHOSTTY_TARGET_SURFACE:
guard let surface = target.target.surface else { return }
guard let surfaceView = self.surfaceView(from: surface) else { return }
NotificationCenter.default.post(
name: .ghosttyColorDidChange,
object: surfaceView,
userInfo: [
SwiftUI.Notification.Name.GhosttyColorChangeKey: Action.ColorChange(c: change)
]
)
default:
assertionFailure()
}
}
// MARK: User Notifications // MARK: User Notifications
/// Handle a received user notification. This is called when a user notification is clicked or dismissed by the user /// Handle a received user notification. This is called when a user notification is clicked or dismissed by the user

View File

@ -39,6 +39,10 @@ extension Ghostty {
} }
} }
init(clone config: ghostty_config_t) {
self.config = ghostty_config_clone(config)
}
deinit { deinit {
self.config = nil self.config = nil
} }

View File

@ -38,6 +38,16 @@ extension Ghostty {
} }
} }
func topLeft() -> SurfaceView {
switch (self) {
case .leaf(let leaf):
return leaf.surface
case .split(let container):
return container.topLeft.topLeft()
}
}
/// Returns the view that would prefer receiving focus in this tree. This is always the /// Returns the view that would prefer receiving focus in this tree. This is always the
/// top-left-most view. This is used when creating a split or closing a split to find the /// top-left-most view. This is used when creating a split or closing a split to find the
/// next view to send focus to. /// next view to send focus to.
@ -136,6 +146,24 @@ extension Ghostty {
} }
} }
/// Returns true if the surface borders the top. Assumes the view is in the tree.
func doesBorderTop(view: SurfaceView) -> Bool {
switch (self) {
case .leaf(let leaf):
return leaf.surface == view
case .split(let container):
switch (container.direction) {
case .vertical:
return container.topLeft.doesBorderTop(view: view)
case .horizontal:
return container.topLeft.doesBorderTop(view: view) ||
container.bottomRight.doesBorderTop(view: view)
}
}
}
// MARK: - Sequence // MARK: - Sequence
func makeIterator() -> IndexingIterator<[Leaf]> { func makeIterator() -> IndexingIterator<[Leaf]> {

View File

@ -206,6 +206,14 @@ extension Ghostty {
// MARK: Surface Notification // MARK: Surface Notification
extension Notification.Name { extension Notification.Name {
/// Configuration change. If the object is nil then it is app-wide. Otherwise its surface-specific.
static let ghosttyConfigDidChange = Notification.Name("com.mitchellh.ghostty.configDidChange")
static let GhosttyConfigChangeKey = ghosttyConfigDidChange.rawValue
/// Color change. Object is the surface changing.
static let ghosttyColorDidChange = Notification.Name("com.mitchellh.ghostty.ghosttyColorDidChange")
static let GhosttyColorChangeKey = ghosttyColorDidChange.rawValue
/// Goto tab. Has tab index in the userinfo. /// Goto tab. Has tab index in the userinfo.
static let ghosttyMoveTab = Notification.Name("com.mitchellh.ghostty.moveTab") static let ghosttyMoveTab = Notification.Name("com.mitchellh.ghostty.moveTab")
static let GhosttyMoveTabKey = ghosttyMoveTab.rawValue static let GhosttyMoveTabKey = ghosttyMoveTab.rawValue
@ -217,9 +225,6 @@ extension Ghostty.Notification {
/// Used to pass a configuration along when creating a new tab/window/split. /// Used to pass a configuration along when creating a new tab/window/split.
static let NewSurfaceConfigKey = "com.mitchellh.ghostty.newSurfaceConfig" static let NewSurfaceConfigKey = "com.mitchellh.ghostty.newSurfaceConfig"
/// Posted when the application configuration is reloaded.
static let ghosttyDidReloadConfig = Notification.Name("com.mitchellh.ghostty.didReloadConfig")
/// Posted when a new split is requested. The sending object will be the surface that had focus. The /// Posted when a new split is requested. The sending object will be the surface that had focus. The
/// userdata has one key "direction" with the direction to split to. /// userdata has one key "direction" with the direction to split to.
static let ghosttyNewSplit = Notification.Name("com.mitchellh.ghostty.newSplit") static let ghosttyNewSplit = Notification.Name("com.mitchellh.ghostty.newSplit")

View File

@ -59,23 +59,6 @@ extension Ghostty {
@EnvironmentObject private var ghostty: Ghostty.App @EnvironmentObject private var ghostty: Ghostty.App
#if canImport(AppKit)
// The visibility state of the mouse pointer
private var pointerVisibility: BackportVisibility {
// If our window or surface loses focus we always bring it back
if (!windowFocus || !surfaceFocus) {
return .visible
}
// If we have window focus then it depends on surface state
if (surfaceView.pointerVisible) {
return .visible
} else {
return .hidden
}
}
#endif
var body: some View { var body: some View {
let center = NotificationCenter.default let center = NotificationCenter.default
@ -92,10 +75,10 @@ extension Ghostty {
Surface(view: surfaceView, size: geo.size) Surface(view: surfaceView, size: geo.size)
.focused($surfaceFocus) .focused($surfaceFocus)
.focusedValue(\.ghosttySurfaceTitle, surfaceView.title) .focusedValue(\.ghosttySurfaceTitle, surfaceView.title)
.focusedValue(\.ghosttySurfacePwd, surfaceView.pwd)
.focusedValue(\.ghosttySurfaceView, surfaceView) .focusedValue(\.ghosttySurfaceView, surfaceView)
.focusedValue(\.ghosttySurfaceCellSize, surfaceView.cellSize) .focusedValue(\.ghosttySurfaceCellSize, surfaceView.cellSize)
#if canImport(AppKit) #if canImport(AppKit)
.backport.pointerVisibility(pointerVisibility)
.backport.pointerStyle(surfaceView.pointerStyle) .backport.pointerStyle(surfaceView.pointerStyle)
.onReceive(pubBecomeKey) { notification in .onReceive(pubBecomeKey) { notification in
guard let window = notification.object as? NSWindow else { return } guard let window = notification.object as? NSWindow else { return }
@ -171,7 +154,8 @@ extension Ghostty {
// If we have a URL from hovering a link, we show that. // If we have a URL from hovering a link, we show that.
if let url = surfaceView.hoverUrl { if let url = surfaceView.hoverUrl {
let padding: CGFloat = 3 let padding: CGFloat = 5
let cornerRadius: CGFloat = 9
ZStack { ZStack {
HStack { HStack {
Spacer() Spacer()
@ -180,7 +164,10 @@ extension Ghostty {
Text(verbatim: url) Text(verbatim: url)
.padding(.init(top: padding, leading: padding, bottom: padding, trailing: padding)) .padding(.init(top: padding, leading: padding, bottom: padding, trailing: padding))
.background(.background) .background(
UnevenRoundedRectangle(cornerRadii: .init(topLeading: cornerRadius))
.fill(.background)
)
.lineLimit(1) .lineLimit(1)
.truncationMode(.middle) .truncationMode(.middle)
.opacity(isHoveringURLLeft ? 1 : 0) .opacity(isHoveringURLLeft ? 1 : 0)
@ -193,7 +180,10 @@ extension Ghostty {
Text(verbatim: url) Text(verbatim: url)
.padding(.init(top: padding, leading: padding, bottom: padding, trailing: padding)) .padding(.init(top: padding, leading: padding, bottom: padding, trailing: padding))
.background(.background) .background(
UnevenRoundedRectangle(cornerRadii: .init(topTrailing: cornerRadius))
.fill(.background)
)
.lineLimit(1) .lineLimit(1)
.truncationMode(.middle) .truncationMode(.middle)
.opacity(isHoveringURLLeft ? 0 : 1) .opacity(isHoveringURLLeft ? 0 : 1)
@ -512,9 +502,7 @@ extension FocusedValues {
struct FocusedGhosttySurface: FocusedValueKey { struct FocusedGhosttySurface: FocusedValueKey {
typealias Value = Ghostty.SurfaceView typealias Value = Ghostty.SurfaceView
} }
}
extension FocusedValues {
var ghosttySurfaceTitle: String? { var ghosttySurfaceTitle: String? {
get { self[FocusedGhosttySurfaceTitle.self] } get { self[FocusedGhosttySurfaceTitle.self] }
set { self[FocusedGhosttySurfaceTitle.self] = newValue } set { self[FocusedGhosttySurfaceTitle.self] = newValue }
@ -523,9 +511,16 @@ extension FocusedValues {
struct FocusedGhosttySurfaceTitle: FocusedValueKey { struct FocusedGhosttySurfaceTitle: FocusedValueKey {
typealias Value = String typealias Value = String
} }
}
extension FocusedValues { var ghosttySurfacePwd: String? {
get { self[FocusedGhosttySurfacePwd.self] }
set { self[FocusedGhosttySurfacePwd.self] = newValue }
}
struct FocusedGhosttySurfacePwd: FocusedValueKey {
typealias Value = String
}
var ghosttySurfaceZoomed: Bool? { var ghosttySurfaceZoomed: Bool? {
get { self[FocusedGhosttySurfaceZoomed.self] } get { self[FocusedGhosttySurfaceZoomed.self] }
set { self[FocusedGhosttySurfaceZoomed.self] = newValue } set { self[FocusedGhosttySurfaceZoomed.self] = newValue }
@ -534,9 +529,7 @@ extension FocusedValues {
struct FocusedGhosttySurfaceZoomed: FocusedValueKey { struct FocusedGhosttySurfaceZoomed: FocusedValueKey {
typealias Value = Bool typealias Value = Bool
} }
}
extension FocusedValues {
var ghosttySurfaceCellSize: OSSize? { var ghosttySurfaceCellSize: OSSize? {
get { self[FocusedGhosttySurfaceCellSize.self] } get { self[FocusedGhosttySurfaceCellSize.self] }
set { self[FocusedGhosttySurfaceCellSize.self] = newValue } set { self[FocusedGhosttySurfaceCellSize.self] = newValue }

View File

@ -14,6 +14,10 @@ extension Ghostty {
// to the app level and it is set from there. // to the app level and it is set from there.
@Published var title: String = "👻" @Published var title: String = "👻"
// The current pwd of the surface as defined by the pty. This can be
// changed with escape codes.
@Published var pwd: String? = nil
// The cell size of this surface. This is set by the core when the // The cell size of this surface. This is set by the core when the
// surface is first created and any time the cell size changes (i.e. // surface is first created and any time the cell size changes (i.e.
// when the font size changes). This is used to allow windows to be // when the font size changes). This is used to allow windows to be
@ -42,9 +46,15 @@ extension Ghostty {
@Published var surfaceSize: ghostty_surface_size_s? = nil @Published var surfaceSize: ghostty_surface_size_s? = nil
// Whether the pointer should be visible or not // Whether the pointer should be visible or not
@Published private(set) var pointerVisible: Bool = true
@Published private(set) var pointerStyle: BackportPointerStyle = .default @Published private(set) var pointerStyle: BackportPointerStyle = .default
/// The configuration derived from the Ghostty config so we don't need to rely on references.
@Published private(set) var derivedConfig: DerivedConfig
/// The background color within the color palette of the surface. This is only set if it is
/// dynamically updated. Otherwise, the background color is the default background color.
@Published private(set) var backgroundColor: Color? = nil
// An initial size to request for a window. This will only affect // An initial size to request for a window. This will only affect
// then the view is moved to a new window. // then the view is moved to a new window.
var initialSize: NSSize? = nil var initialSize: NSSize? = nil
@ -71,17 +81,6 @@ extension Ghostty {
return ghostty_surface_needs_confirm_quit(surface) return ghostty_surface_needs_confirm_quit(surface)
} }
/// Returns the pwd of the surface if it has one.
var pwd: String? {
guard let surface = self.surface else { return nil }
let v = String(unsafeUninitializedCapacity: 1024) {
Int(ghostty_surface_pwd(surface, $0.baseAddress, UInt($0.count)))
}
if (v.count == 0) { return nil }
return v
}
// Returns the inspector instance for this surface, or nil if the // Returns the inspector instance for this surface, or nil if the
// surface has been closed. // surface has been closed.
var inspector: ghostty_inspector_t? { var inspector: ghostty_inspector_t? {
@ -122,6 +121,13 @@ extension Ghostty {
self.markedText = NSMutableAttributedString() self.markedText = NSMutableAttributedString()
self.uuid = uuid ?? .init() self.uuid = uuid ?? .init()
// Our initial config always is our application wide config.
if let appDelegate = NSApplication.shared.delegate as? AppDelegate {
self.derivedConfig = DerivedConfig(appDelegate.ghostty.config)
} else {
self.derivedConfig = DerivedConfig()
}
// Initialize with some default frame size. The important thing is that this // Initialize with some default frame size. The important thing is that this
// is non-zero so that our layer bounds are non-zero so that our renderer // is non-zero so that our layer bounds are non-zero so that our renderer
// can do SOMETHING. // can do SOMETHING.
@ -145,6 +151,16 @@ extension Ghostty {
selector: #selector(ghosttyDidEndKeySequence), selector: #selector(ghosttyDidEndKeySequence),
name: Ghostty.Notification.didEndKeySequence, name: Ghostty.Notification.didEndKeySequence,
object: self) object: self)
center.addObserver(
self,
selector: #selector(ghosttyConfigDidChange(_:)),
name: .ghosttyConfigDidChange,
object: self)
center.addObserver(
self,
selector: #selector(ghosttyColorDidChange(_:)),
name: .ghosttyColorDidChange,
object: self)
center.addObserver( center.addObserver(
self, self,
selector: #selector(windowDidChangeScreen), selector: #selector(windowDidChangeScreen),
@ -316,7 +332,11 @@ extension Ghostty {
} }
func setCursorVisibility(_ visible: Bool) { func setCursorVisibility(_ visible: Bool) {
pointerVisible = visible // Technically this action could be called anytime we want to
// change the mouse visibility but at the time of writing this
// mouse-hide-while-typing is the only use case so this is the
// preferred method.
NSCursor.setHiddenUntilMouseMoves(!visible)
} }
// MARK: - Notifications // MARK: - Notifications
@ -337,6 +357,31 @@ extension Ghostty {
keySequence = [] keySequence = []
} }
@objc private func ghosttyConfigDidChange(_ notification: SwiftUI.Notification) {
// Get our managed configuration object out
guard let config = notification.userInfo?[
SwiftUI.Notification.Name.GhosttyConfigChangeKey
] as? Ghostty.Config else { return }
// Update our derived config
self.derivedConfig = DerivedConfig(config)
}
@objc private func ghosttyColorDidChange(_ notification: SwiftUI.Notification) {
guard let change = notification.userInfo?[
SwiftUI.Notification.Name.GhosttyColorChangeKey
] as? Ghostty.Action.ColorChange else { return }
switch (change.kind) {
case .background:
self.backgroundColor = change.color
default:
// We don't do anything for the other colors yet.
break
}
}
@objc private func windowDidChangeScreen(notification: SwiftUI.Notification) { @objc private func windowDidChangeScreen(notification: SwiftUI.Notification) {
guard let window = self.window else { return } guard let window = self.window else { return }
guard let object = notification.object as? NSWindow, window == object else { return } guard let object = notification.object as? NSWindow, window == object else { return }
@ -704,12 +749,6 @@ extension Ghostty {
/// Special case handling for some control keys /// Special case handling for some control keys
override func performKeyEquivalent(with event: NSEvent) -> Bool { override func performKeyEquivalent(with event: NSEvent) -> Bool {
// Only process keys when Control is the only modifier
if (!event.modifierFlags.contains(.control) ||
!event.modifierFlags.isDisjoint(with: [.shift, .command, .option])) {
return false
}
// Only process key down events // Only process key down events
if (event.type != .keyDown) { if (event.type != .keyDown) {
return false return false
@ -722,11 +761,23 @@ extension Ghostty {
return false return false
} }
// Only process keys when Control is active. All known issues we're
// resolving happen only in this scenario. This probably isn't fully robust
// but we can broaden the scope as we find more cases.
if (!event.modifierFlags.contains(.control)) {
return false
}
let equivalent: String let equivalent: String
switch (event.charactersIgnoringModifiers) { switch (event.charactersIgnoringModifiers) {
case "/": case "/":
// Treat C-/ as C-_. We do this because C-/ makes macOS make a beep // Treat C-/ as C-_. We do this because C-/ makes macOS make a beep
// sound and we don't like the beep sound. // sound and we don't like the beep sound.
if (!event.modifierFlags.contains(.control) ||
!event.modifierFlags.isDisjoint(with: [.shift, .command, .option])) {
return false
}
equivalent = "_" equivalent = "_"
case "\r": case "\r":
@ -742,7 +793,7 @@ extension Ghostty {
let newEvent = NSEvent.keyEvent( let newEvent = NSEvent.keyEvent(
with: .keyDown, with: .keyDown,
location: event.locationInWindow, location: event.locationInWindow,
modifierFlags: .control, modifierFlags: event.modifierFlags,
timestamp: event.timestamp, timestamp: event.timestamp,
windowNumber: event.windowNumber, windowNumber: event.windowNumber,
context: nil, context: nil,
@ -1023,6 +1074,27 @@ extension Ghostty {
Ghostty.moveFocus(to: self) Ghostty.moveFocus(to: self)
} }
} }
struct DerivedConfig {
let backgroundColor: Color
let backgroundOpacity: Double
let macosWindowShadow: Bool
let windowTitleFontFamily: String?
init() {
self.backgroundColor = Color(NSColor.windowBackgroundColor)
self.backgroundOpacity = 1
self.macosWindowShadow = true
self.windowTitleFontFamily = nil
}
init(_ config: Ghostty.Config) {
self.backgroundColor = config.backgroundColor
self.backgroundOpacity = config.backgroundOpacity
self.macosWindowShadow = config.macosWindowShadow
self.windowTitleFontFamily = config.windowTitleFontFamily
}
}
} }
} }

View File

@ -12,6 +12,9 @@ extension Ghostty {
// to the app level and it is set from there. // to the app level and it is set from there.
@Published var title: String = "👻" @Published var title: String = "👻"
// The current pwd of the surface.
@Published var pwd: String? = nil
// The cell size of this surface. This is set by the core when the // The cell size of this surface. This is set by the core when the
// surface is first created and any time the cell size changes (i.e. // surface is first created and any time the cell size changes (i.e.
// when the font size changes). This is used to allow windows to be // when the font size changes). This is used to allow windows to be

View File

@ -0,0 +1,8 @@
import Cocoa
extension NSAppearance {
/// Returns true if the appearance is some kind of dark.
var isDark: Bool {
return name.rawValue.lowercased().contains("dark")
}
}

View File

@ -21,6 +21,32 @@ extension OSColor {
return (0.299 * r) + (0.587 * g) + (0.114 * b) return (0.299 * r) + (0.587 * g) + (0.114 * b)
} }
var hexString: String? {
#if canImport(AppKit)
guard let rgb = usingColorSpace(.deviceRGB) else { return nil }
let red = Int(rgb.redComponent * 255)
let green = Int(rgb.greenComponent * 255)
let blue = Int(rgb.blueComponent * 255)
return String(format: "#%02X%02X%02X", red, green, blue)
#elseif canImport(UIKit)
var red: CGFloat = 0
var green: CGFloat = 0
var blue: CGFloat = 0
var alpha: CGFloat = 0
guard self.getRed(&red, green: &green, blue: &blue, alpha: &alpha) else {
return nil
}
// Convert to 0255 range
let r = Int(red * 255)
let g = Int(green * 255)
let b = Int(blue * 255)
// Format to hexadecimal
return String(format: "#%02X%02X%02X", r, g, b)
#endif
}
func darken(by amount: CGFloat) -> OSColor { func darken(by amount: CGFloat) -> OSColor {
var h: CGFloat = 0, s: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 0 var h: CGFloat = 0, s: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 0
self.getHue(&h, saturation: &s, brightness: &b, alpha: &a) self.getHue(&h, saturation: &s, brightness: &b, alpha: &a)

View File

@ -1,3 +1,3 @@
# This file is auto-generated! check build-support/check-zig-cache-hash.sh for # This file is auto-generated! check build-support/check-zig-cache-hash.sh for
# more details. # more details.
"sha256-5LBZAExb4PJefW+M0Eo+TcoszhBdIFTGBOv6lte5L0Q=" "sha256-D1SQIlmdP9x1PDgRVOy1qJGmu9osDbuyxGOcFj646N4="

View File

@ -32,7 +32,7 @@ pub fn build(b: *std.Build) !void {
}; };
if (b.systemIntegrationOption("freetype", .{})) { if (b.systemIntegrationOption("freetype", .{})) {
lib.linkSystemLibrary2("freetype", dynamic_link_opts); lib.linkSystemLibrary2("freetype2", dynamic_link_opts);
} else { } else {
const freetype = b.dependency("freetype", .{ const freetype = b.dependency("freetype", .{
.target = target, .target = target,

View File

@ -76,7 +76,8 @@ pub fn build(b: *std.Build) !void {
}); });
if (b.systemIntegrationOption("freetype", .{})) { if (b.systemIntegrationOption("freetype", .{})) {
lib.linkSystemLibrary2("freetype", dynamic_link_opts); lib.linkSystemLibrary2("freetype2", dynamic_link_opts);
module.linkSystemLibrary("freetype2", dynamic_link_opts);
} else { } else {
lib.linkLibrary(freetype.artifact("freetype")); lib.linkLibrary(freetype.artifact("freetype"));
module.addIncludePath(freetype.builder.dependency("freetype", .{}).path("include")); module.addIncludePath(freetype.builder.dependency("freetype", .{}).path("include"));

View File

@ -12,8 +12,9 @@ const apprt = @import("apprt.zig");
const Surface = @import("Surface.zig"); const Surface = @import("Surface.zig");
const tracy = @import("tracy"); const tracy = @import("tracy");
const input = @import("input.zig"); const input = @import("input.zig");
const Config = @import("config.zig").Config; const configpkg = @import("config.zig");
const BlockingQueue = @import("./blocking_queue.zig").BlockingQueue; const Config = configpkg.Config;
const BlockingQueue = @import("datastruct/main.zig").BlockingQueue;
const renderer = @import("renderer.zig"); const renderer = @import("renderer.zig");
const font = @import("font/main.zig"); const font = @import("font/main.zig");
const internal_os = @import("os/main.zig"); const internal_os = @import("os/main.zig");
@ -66,6 +67,16 @@ font_grid_set: font.SharedGridSet,
last_notification_time: ?std.time.Instant = null, last_notification_time: ?std.time.Instant = null,
last_notification_digest: u64 = 0, last_notification_digest: u64 = 0,
/// The conditional state of the configuration. See the equivalent field
/// in the Surface struct for more information. In this case, this applies
/// to the app-level config and as a default for new surfaces.
config_conditional_state: configpkg.ConditionalState,
/// Set to false once we've created at least one surface. This
/// never goes true again. This can be used by surfaces to determine
/// if they are the first surface.
first: bool = true,
pub const CreateError = Allocator.Error || font.SharedGridSet.InitError; pub const CreateError = Allocator.Error || font.SharedGridSet.InitError;
/// Initialize the main app instance. This creates the main window, sets /// Initialize the main app instance. This creates the main window, sets
@ -89,6 +100,7 @@ pub fn create(
.mailbox = .{}, .mailbox = .{},
.quit = false, .quit = false,
.font_grid_set = font_grid_set, .font_grid_set = font_grid_set,
.config_conditional_state = .{},
}; };
errdefer app.surfaces.deinit(alloc); errdefer app.surfaces.deinit(alloc);
@ -142,11 +154,31 @@ pub fn tick(self: *App, rt_app: *apprt.App) !bool {
/// Update the configuration associated with the app. This can only be /// Update the configuration associated with the app. This can only be
/// called from the main thread. The caller owns the config memory. The /// called from the main thread. The caller owns the config memory. The
/// memory can be freed immediately when this returns. /// memory can be freed immediately when this returns.
pub fn updateConfig(self: *App, config: *const Config) !void { pub fn updateConfig(self: *App, rt_app: *apprt.App, config: *const Config) !void {
// Go through and update all of the surface configurations. // Go through and update all of the surface configurations.
for (self.surfaces.items) |surface| { for (self.surfaces.items) |surface| {
try surface.core_surface.handleMessage(.{ .change_config = config }); try surface.core_surface.handleMessage(.{ .change_config = config });
} }
// Apply our conditional state. If we fail to apply the conditional state
// then we log and attempt to move forward with the old config.
// We only apply this to the app-level config because the surface
// config applies its own conditional state.
var applied_: ?configpkg.Config = config.changeConditionalState(
self.config_conditional_state,
) catch |err| err: {
log.warn("failed to apply conditional state to config err={}", .{err});
break :err null;
};
defer if (applied_) |*c| c.deinit();
const applied: *const configpkg.Config = if (applied_) |*c| c else config;
// Notify the apprt that the app has changed configuration.
try rt_app.performAction(
.app,
.config_change,
.{ .config = applied },
);
} }
/// Add an initialized surface. This is really only for the runtime /// Add an initialized surface. This is really only for the runtime
@ -227,7 +259,6 @@ fn drainMailbox(self: *App, rt_app: *apprt.App) !void {
while (self.mailbox.pop()) |message| { while (self.mailbox.pop()) |message| {
log.debug("mailbox message={s}", .{@tagName(message)}); log.debug("mailbox message={s}", .{@tagName(message)});
switch (message) { switch (message) {
.reload_config => try self.reloadConfig(rt_app),
.open_config => try self.performAction(rt_app, .open_config), .open_config => try self.performAction(rt_app, .open_config),
.new_window => |msg| try self.newWindow(rt_app, msg), .new_window => |msg| try self.newWindow(rt_app, msg),
.close => |surface| self.closeSurface(surface), .close => |surface| self.closeSurface(surface),
@ -248,14 +279,6 @@ fn drainMailbox(self: *App, rt_app: *apprt.App) !void {
} }
} }
pub fn reloadConfig(self: *App, rt_app: *apprt.App) !void {
log.debug("reloading configuration", .{});
if (try rt_app.reloadConfig()) |new| {
log.debug("new configuration received, applying", .{});
try self.updateConfig(new);
}
}
pub fn closeSurface(self: *App, surface: *Surface) void { pub fn closeSurface(self: *App, surface: *Surface) void {
if (!self.hasSurface(surface)) return; if (!self.hasSurface(surface)) return;
surface.close(); surface.close();
@ -376,6 +399,33 @@ pub fn keyEvent(
return true; return true;
} }
/// Call to notify Ghostty that the color scheme for the app has changed.
/// "Color scheme" in this case refers to system themes such as "light/dark".
pub fn colorSchemeEvent(
self: *App,
rt_app: *apprt.App,
scheme: apprt.ColorScheme,
) !void {
const new_scheme: configpkg.ConditionalState.Theme = switch (scheme) {
.light => .light,
.dark => .dark,
};
// If our scheme didn't change, then we don't do anything.
if (self.config_conditional_state.theme == new_scheme) return;
// Setup our conditional state which has the current color theme.
self.config_conditional_state.theme = new_scheme;
// Request our configuration be reloaded because the new scheme may
// impact the colors of the app.
try rt_app.performAction(
.app,
.reload_config,
.{ .soft = true },
);
}
/// Perform a binding action. This only accepts actions that are scoped /// Perform a binding action. This only accepts actions that are scoped
/// to the app. Callers can use performAllAction to perform any action /// to the app. Callers can use performAllAction to perform any action
/// and any non-app-scoped actions will be performed on all surfaces. /// and any non-app-scoped actions will be performed on all surfaces.
@ -390,7 +440,7 @@ pub fn performAction(
.quit => self.setQuit(), .quit => self.setQuit(),
.new_window => try self.newWindow(rt_app, .{ .parent = null }), .new_window => try self.newWindow(rt_app, .{ .parent = null }),
.open_config => try rt_app.performAction(.app, .open_config, {}), .open_config => try rt_app.performAction(.app, .open_config, {}),
.reload_config => try self.reloadConfig(rt_app), .reload_config => try rt_app.performAction(.app, .reload_config, .{}),
.close_all_windows => try rt_app.performAction(.app, .close_all_windows, {}), .close_all_windows => try rt_app.performAction(.app, .close_all_windows, {}),
.toggle_quick_terminal => try rt_app.performAction(.app, .toggle_quick_terminal, {}), .toggle_quick_terminal => try rt_app.performAction(.app, .toggle_quick_terminal, {}),
.toggle_visibility => try rt_app.performAction(.app, .toggle_visibility, {}), .toggle_visibility => try rt_app.performAction(.app, .toggle_visibility, {}),
@ -450,10 +500,6 @@ fn hasSurface(self: *const App, surface: *const Surface) bool {
/// The message types that can be sent to the app thread. /// The message types that can be sent to the app thread.
pub const Message = union(enum) { pub const Message = union(enum) {
/// Reload the configuration for the entire app and propagate it to
/// all the active surfaces.
reload_config: void,
// Open the configuration file // Open the configuration file
open_config: void, open_config: void,

View File

@ -94,11 +94,6 @@ keyboard: Keyboard,
/// less important. /// less important.
pressed_key: ?input.KeyEvent = null, pressed_key: ?input.KeyEvent = null,
/// The current color scheme of the GUI element containing this surface.
/// This will default to light until the apprt sends us the actual color
/// scheme. This is used by mode 3031 and CSI 996 n.
color_scheme: apprt.ColorScheme = .light,
/// The hash value of the last keybinding trigger that we performed. This /// The hash value of the last keybinding trigger that we performed. This
/// is only set if the last key input matched a keybinding, consumed it, /// is only set if the last key input matched a keybinding, consumed it,
/// and performed it. This is used to prevent sending release/repeat events /// and performed it. This is used to prevent sending release/repeat events
@ -113,23 +108,29 @@ io_thr: std.Thread,
/// Terminal inspector /// Terminal inspector
inspector: ?*inspector.Inspector = null, inspector: ?*inspector.Inspector = null,
/// All the cached sizes since we need them at various times. /// All our sizing information.
screen_size: renderer.ScreenSize, size: renderer.Size,
grid_size: renderer.GridSize,
cell_size: renderer.CellSize,
/// Explicit padding due to configuration
padding: renderer.Padding,
/// The configuration derived from the main config. We "derive" it so that /// The configuration derived from the main config. We "derive" it so that
/// we don't have a shared pointer hanging around that we need to worry about /// we don't have a shared pointer hanging around that we need to worry about
/// the lifetime of. This makes updating config at runtime easier. /// the lifetime of. This makes updating config at runtime easier.
config: DerivedConfig, config: DerivedConfig,
/// The conditional state of the configuration. This can affect
/// how certain configurations take effect such as light/dark mode.
/// This is managed completely by Ghostty core but an apprt action
/// is sent whenever this changes.
config_conditional_state: configpkg.ConditionalState,
/// This is set to true if our IO thread notifies us our child exited. /// This is set to true if our IO thread notifies us our child exited.
/// This is used to determine if we need to confirm, hold open, etc. /// This is used to determine if we need to confirm, hold open, etc.
child_exited: bool = false, child_exited: bool = false,
/// We maintain our focus state and assume we're focused by default.
/// If we're not initially focused then apprts can call focusCallback
/// to let us know.
focused: bool = true,
/// The effect of an input event. This can be used by callers to take /// The effect of an input event. This can be used by callers to take
/// the appropriate action after an input event. For example, key /// the appropriate action after an input event. For example, key
/// input can be forwarded to the OS for further processing if it /// input can be forwarded to the OS for further processing if it
@ -324,6 +325,32 @@ const DerivedConfig = struct {
for (self.links) |*link| link.regex.deinit(); for (self.links) |*link| link.regex.deinit();
self.arena.deinit(); self.arena.deinit();
} }
fn scaledPadding(self: *const DerivedConfig, x_dpi: f32, y_dpi: f32) renderer.Padding {
const padding_top: u32 = padding_top: {
const padding_top: f32 = @floatFromInt(self.window_padding_top);
break :padding_top @intFromFloat(@floor(padding_top * y_dpi / 72));
};
const padding_bottom: u32 = padding_bottom: {
const padding_bottom: f32 = @floatFromInt(self.window_padding_bottom);
break :padding_bottom @intFromFloat(@floor(padding_bottom * y_dpi / 72));
};
const padding_left: u32 = padding_left: {
const padding_left: f32 = @floatFromInt(self.window_padding_left);
break :padding_left @intFromFloat(@floor(padding_left * x_dpi / 72));
};
const padding_right: u32 = padding_right: {
const padding_right: f32 = @floatFromInt(self.window_padding_right);
break :padding_right @intFromFloat(@floor(padding_right * x_dpi / 72));
};
return .{
.top = padding_top,
.bottom = padding_bottom,
.left = padding_left,
.right = padding_right,
};
}
}; };
/// Create a new surface. This must be called from the main thread. The /// Create a new surface. This must be called from the main thread. The
@ -332,11 +359,31 @@ const DerivedConfig = struct {
pub fn init( pub fn init(
self: *Surface, self: *Surface,
alloc: Allocator, alloc: Allocator,
config: *const configpkg.Config, config_original: *const configpkg.Config,
app: *App, app: *App,
rt_app: *apprt.runtime.App, rt_app: *apprt.runtime.App,
rt_surface: *apprt.runtime.Surface, rt_surface: *apprt.runtime.Surface,
) !void { ) !void {
// Apply our conditional state. If we fail to apply the conditional state
// then we log and attempt to move forward with the old config.
var config_: ?configpkg.Config = config_original.changeConditionalState(
app.config_conditional_state,
) catch |err| err: {
log.warn("failed to apply conditional state to config err={}", .{err});
break :err null;
};
defer if (config_) |*c| c.deinit();
// We want a config pointer for everything so we get that either
// based on our conditional state or the original config.
const config: *const configpkg.Config = if (config_) |*c| config: {
// We want to preserve our original working directory. We
// don't need to dupe memory here because termio will derive
// it. We preserve this so directory inheritance works.
c.@"working-directory" = config_original.@"working-directory";
break :config c;
} else config_original;
// Get our configuration // Get our configuration
var derived_config = try DerivedConfig.init(alloc, config); var derived_config = try DerivedConfig.init(alloc, config);
errdefer derived_config.deinit(); errdefer derived_config.deinit();
@ -373,28 +420,32 @@ pub fn init(
// Pre-calculate our initial cell size ourselves. // Pre-calculate our initial cell size ourselves.
const cell_size = font_grid.cellSize(); const cell_size = font_grid.cellSize();
// Convert our padding from points to pixels // Build our size struct which has all the sizes we need.
const padding_top: u32 = padding_top: { const size: renderer.Size = size: {
const padding_top: f32 = @floatFromInt(derived_config.window_padding_top); var size: renderer.Size = .{
break :padding_top @intFromFloat(@floor(padding_top * y_dpi / 72)); .screen = screen: {
const surface_size = try rt_surface.getSize();
break :screen .{
.width = surface_size.width,
.height = surface_size.height,
}; };
const padding_bottom: u32 = padding_bottom: { },
const padding_bottom: f32 = @floatFromInt(derived_config.window_padding_bottom);
break :padding_bottom @intFromFloat(@floor(padding_bottom * y_dpi / 72)); .cell = font_grid.cellSize(),
.padding = .{},
}; };
const padding_left: u32 = padding_left: {
const padding_left: f32 = @floatFromInt(derived_config.window_padding_left); const explicit: renderer.Padding = derived_config.scaledPadding(
break :padding_left @intFromFloat(@floor(padding_left * x_dpi / 72)); x_dpi,
}; y_dpi,
const padding_right: u32 = padding_right: { );
const padding_right: f32 = @floatFromInt(derived_config.window_padding_right); if (derived_config.window_padding_balance) {
break :padding_right @intFromFloat(@floor(padding_right * x_dpi / 72)); size.balancePadding(explicit);
}; } else {
const padding: renderer.Padding = .{ size.padding = explicit;
.top = padding_top, }
.bottom = padding_bottom,
.left = padding_left, break :size size;
.right = padding_right,
}; };
// Create our terminal grid with the initial size // Create our terminal grid with the initial size
@ -402,26 +453,12 @@ pub fn init(
var renderer_impl = try Renderer.init(alloc, .{ var renderer_impl = try Renderer.init(alloc, .{
.config = try Renderer.DerivedConfig.init(alloc, config), .config = try Renderer.DerivedConfig.init(alloc, config),
.font_grid = font_grid, .font_grid = font_grid,
.padding = .{ .size = size,
.explicit = padding,
.balance = config.@"window-padding-balance",
},
.surface_mailbox = .{ .surface = self, .app = app_mailbox }, .surface_mailbox = .{ .surface = self, .app = app_mailbox },
.rt_surface = rt_surface, .rt_surface = rt_surface,
}); });
errdefer renderer_impl.deinit(); errdefer renderer_impl.deinit();
// Calculate our grid size based on known dimensions.
const surface_size = try rt_surface.getSize();
const screen_size: renderer.ScreenSize = .{
.width = surface_size.width,
.height = surface_size.height,
};
const grid_size = renderer.GridSize.init(
screen_size.subPadding(padding),
cell_size,
);
// The mutex used to protect our renderer state. // The mutex used to protect our renderer state.
const mutex = try alloc.create(std.Thread.Mutex); const mutex = try alloc.create(std.Thread.Mutex);
mutex.* = .{}; mutex.* = .{};
@ -462,20 +499,27 @@ pub fn init(
.io = undefined, .io = undefined,
.io_thread = io_thread, .io_thread = io_thread,
.io_thr = undefined, .io_thr = undefined,
.screen_size = .{ .width = 0, .height = 0 }, .size = size,
.grid_size = .{},
.cell_size = cell_size,
.padding = padding,
.config = derived_config, .config = derived_config,
// Our conditional state is initialized to the app state. This
// lets us get the most likely correct color theme and so on.
.config_conditional_state = app.config_conditional_state,
}; };
// The command we're going to execute
const command: ?[]const u8 = if (app.first)
config.@"initial-command" orelse config.command
else
config.command;
// Start our IO implementation // Start our IO implementation
// This separate block ({}) is important because our errdefers must // This separate block ({}) is important because our errdefers must
// be scoped here to be valid. // be scoped here to be valid.
{ {
// Initialize our IO backend // Initialize our IO backend
var io_exec = try termio.Exec.init(alloc, .{ var io_exec = try termio.Exec.init(alloc, .{
.command = config.command, .command = command,
.shell_integration = config.@"shell-integration", .shell_integration = config.@"shell-integration",
.shell_integration_features = config.@"shell-integration-features", .shell_integration_features = config.@"shell-integration-features",
.working_directory = config.@"working-directory", .working_directory = config.@"working-directory",
@ -499,10 +543,7 @@ pub fn init(
errdefer io_mailbox.deinit(alloc); errdefer io_mailbox.deinit(alloc);
try termio.Termio.init(&self.io, alloc, .{ try termio.Termio.init(&self.io, alloc, .{
.grid_size = grid_size, .size = size,
.cell_size = cell_size,
.screen_size = screen_size,
.padding = padding,
.full_config = config, .full_config = config,
.config = try termio.Termio.DerivedConfig.init(alloc, config), .config = try termio.Termio.DerivedConfig.init(alloc, config),
.backend = .{ .exec = io_exec }, .backend = .{ .exec = io_exec },
@ -521,7 +562,7 @@ pub fn init(
try rt_app.performAction( try rt_app.performAction(
.{ .surface = self }, .{ .surface = self },
.cell_size, .cell_size,
.{ .width = cell_size.width, .height = cell_size.height }, .{ .width = size.cell.width, .height = size.cell.height },
); );
// Set a minimum size that is cols=10 h=4. This matches Mac's Terminal.app // Set a minimum size that is cols=10 h=4. This matches Mac's Terminal.app
@ -530,8 +571,8 @@ pub fn init(
.{ .surface = self }, .{ .surface = self },
.size_limit, .size_limit,
.{ .{
.min_width = cell_size.width * 10, .min_width = size.cell.width * 10,
.min_height = cell_size.height * 4, .min_height = size.cell.height * 4,
// No max: // No max:
.max_width = 0, .max_width = 0,
.max_height = 0, .max_height = 0,
@ -543,7 +584,7 @@ pub fn init(
// init stuff we should get rid of this. But this is required because // init stuff we should get rid of this. But this is required because
// sizeCallback does retina-aware stuff we don't do here and don't want // sizeCallback does retina-aware stuff we don't do here and don't want
// to duplicate. // to duplicate.
try self.sizeCallback(surface_size); try self.resize(self.size.screen);
// Give the renderer one more opportunity to finalize any surface // Give the renderer one more opportunity to finalize any surface
// setup on the main thread prior to spinning up the rendering thread. // setup on the main thread prior to spinning up the rendering thread.
@ -583,12 +624,12 @@ pub fn init(
// account for the padding so we get the exact correct grid size. // account for the padding so we get the exact correct grid size.
const final_width: u32 = const final_width: u32 =
@as(u32, @intFromFloat(@ceil(width_f32 / scale.x))) + @as(u32, @intFromFloat(@ceil(width_f32 / scale.x))) +
padding.left + size.padding.left +
padding.right; size.padding.right;
const final_height: u32 = const final_height: u32 =
@as(u32, @intFromFloat(@ceil(height_f32 / scale.y))) + @as(u32, @intFromFloat(@ceil(height_f32 / scale.y))) +
padding.top + size.padding.top +
padding.bottom; size.padding.bottom;
rt_app.performAction( rt_app.performAction(
.{ .surface = self }, .{ .surface = self },
@ -613,9 +654,9 @@ pub fn init(
// For xdg-terminal-exec execution we special-case and set the window // For xdg-terminal-exec execution we special-case and set the window
// title to the command being executed. This allows window managers // title to the command being executed. This allows window managers
// to set custom styling based on the command being executed. // to set custom styling based on the command being executed.
const command = config.command orelse break :xdg; const v = command orelse break :xdg;
if (command.len > 0) { if (v.len > 0) {
const title = alloc.dupeZ(u8, command) catch |err| { const title = alloc.dupeZ(u8, v) catch |err| {
log.warn( log.warn(
"error copying command for title, title will not be set err={}", "error copying command for title, title will not be set err={}",
.{err}, .{err},
@ -630,6 +671,9 @@ pub fn init(
); );
} }
} }
// We are no longer the first surface
app.first = false;
} }
pub fn deinit(self: *Surface) void { pub fn deinit(self: *Surface) void {
@ -758,7 +802,7 @@ pub fn needsConfirmQuit(self: *Surface) bool {
/// surface. /// surface.
pub fn handleMessage(self: *Surface, msg: Message) !void { pub fn handleMessage(self: *Surface, msg: Message) !void {
switch (msg) { switch (msg) {
.change_config => |config| try self.changeConfig(config), .change_config => |config| try self.updateConfig(config),
.set_title => |*v| { .set_title => |*v| {
// We ignore the message in case the title was set via config. // We ignore the message in case the title was set via config.
@ -799,6 +843,29 @@ pub fn handleMessage(self: *Surface, msg: Message) !void {
}, .unlocked); }, .unlocked);
}, },
.color_change => |change| {
// On any color change, we have to report for mode 2031
// if it is enabled.
self.reportColorScheme(false);
// Notify our apprt
try self.rt_app.performAction(
.{ .surface = self },
.color_change,
.{
.kind = switch (change.kind) {
.background => .background,
.foreground => .foreground,
.cursor => .cursor,
.palette => |v| @enumFromInt(v),
},
.r = change.color.r,
.g = change.color.g,
.b = change.color.b,
},
);
},
.set_mouse_shape => |shape| { .set_mouse_shape => |shape| {
log.debug("changing mouse shape: {}", .{shape}); log.debug("changing mouse shape: {}", .{shape});
try self.rt_app.performAction( try self.rt_app.performAction(
@ -826,6 +893,20 @@ pub fn handleMessage(self: *Surface, msg: Message) !void {
}, },
}, },
.pwd_change => |w| {
defer w.deinit();
// We always allocate for this because we need to null-terminate.
const str = try self.alloc.dupeZ(u8, w.slice());
defer self.alloc.free(str);
try self.rt_app.performAction(
.{ .surface = self },
.pwd,
.{ .pwd = str },
);
},
.close => self.close(), .close => self.close(),
// Close without confirmation. // Close without confirmation.
@ -847,7 +928,7 @@ pub fn handleMessage(self: *Surface, msg: Message) !void {
.renderer_health => |health| self.updateRendererHealth(health), .renderer_health => |health| self.updateRendererHealth(health),
.report_color_scheme => try self.reportColorScheme(), .report_color_scheme => |force| self.reportColorScheme(force),
.present_surface => try self.presentSurface(), .present_surface => try self.presentSurface(),
@ -884,9 +965,19 @@ fn passwordInput(self: *Surface, v: bool) !void {
try self.queueRender(); try self.queueRender();
} }
/// Sends a DSR response for the current color scheme to the pty. /// Sends a DSR response for the current color scheme to the pty. If
fn reportColorScheme(self: *Surface) !void { /// force is false then we only send the response if the terminal mode
const output = switch (self.color_scheme) { /// 2031 is enabled.
fn reportColorScheme(self: *Surface, force: bool) void {
if (!force) {
self.renderer_state.mutex.lock();
defer self.renderer_state.mutex.unlock();
if (!self.renderer_state.terminal.modes.get(.report_color_scheme)) {
return;
}
}
const output = switch (self.config_conditional_state.theme) {
.light => "\x1B[?997;2n", .light => "\x1B[?997;2n",
.dark => "\x1B[?997;1n", .dark => "\x1B[?997;1n",
}; };
@ -1009,8 +1100,39 @@ fn updateRendererHealth(self: *Surface, health: renderer.Health) void {
}; };
} }
/// Update our configuration at runtime. /// This should be called anytime `config_conditional_state` changes
fn changeConfig(self: *Surface, config: *const configpkg.Config) !void { /// so that the apprt can reload the configuration.
fn notifyConfigConditionalState(self: *Surface) void {
self.rt_app.performAction(
.{ .surface = self },
.reload_config,
.{ .soft = true },
) catch |err| {
log.warn("failed to notify app of config state change err={}", .{err});
};
}
/// Update our configuration at runtime. This can be called by the apprt
/// to set a surface-specific configuration that differs from the app
/// or other surfaces.
pub fn updateConfig(
self: *Surface,
original: *const configpkg.Config,
) !void {
// Apply our conditional state. If we fail to apply the conditional state
// then we log and attempt to move forward with the old config.
var config_: ?configpkg.Config = original.changeConditionalState(
self.config_conditional_state,
) catch |err| err: {
log.warn("failed to apply conditional state to config err={}", .{err});
break :err null;
};
defer if (config_) |*c| c.deinit();
// We want a config pointer for everything so we get that either
// based on our conditional state or the original config.
const config: *const configpkg.Config = if (config_) |*c| c else original;
// Update our new derived config immediately // Update our new derived config immediately
const derived = DerivedConfig.init(self.alloc, config) catch |err| { const derived = DerivedConfig.init(self.alloc, config) catch |err| {
// If the derivation fails then we just log and return. We don't // If the derivation fails then we just log and return. We don't
@ -1059,6 +1181,13 @@ fn changeConfig(self: *Surface, config: *const configpkg.Config) !void {
self.queueRender() catch |err| { self.queueRender() catch |err| {
log.warn("failed to notify renderer of config change err={}", .{err}); log.warn("failed to notify renderer of config change err={}", .{err});
}; };
// Notify the window
try self.rt_app.performAction(
.{ .surface = self },
.config_change,
.{ .config = config },
);
} }
/// Returns true if the terminal has a selection. /// Returns true if the terminal has a selection.
@ -1108,18 +1237,12 @@ pub fn selectionInfo(self: *const Surface) ?apprt.Selection {
// Our sizes are all scaled so we need to send the unscaled values back. // Our sizes are all scaled so we need to send the unscaled values back.
const content_scale = self.rt_surface.getContentScale() catch .{ .x = 1, .y = 1 }; const content_scale = self.rt_surface.getContentScale() catch .{ .x = 1, .y = 1 };
// We need to account for padding as well.
const pad = if (self.config.window_padding_balance)
renderer.Padding.balanced(self.screen_size, self.grid_size, self.cell_size)
else
self.padding;
const x: f64 = x: { const x: f64 = x: {
// Simple x * cell width gives the left // Simple x * cell width gives the left
var x: f64 = @floatFromInt(tl_coord.x * self.cell_size.width); var x: f64 = @floatFromInt(tl_coord.x * self.size.cell.width);
// Add padding // Add padding
x += @floatFromInt(pad.left); x += @floatFromInt(self.size.padding.left);
// Scale // Scale
x /= content_scale.x; x /= content_scale.x;
@ -1129,14 +1252,14 @@ pub fn selectionInfo(self: *const Surface) ?apprt.Selection {
const y: f64 = y: { const y: f64 = y: {
// Simple y * cell height gives the top // Simple y * cell height gives the top
var y: f64 = @floatFromInt(tl_coord.y * self.cell_size.height); var y: f64 = @floatFromInt(tl_coord.y * self.size.cell.height);
// We want the text baseline // We want the text baseline
y += @floatFromInt(self.cell_size.height); y += @floatFromInt(self.size.cell.height);
y -= @floatFromInt(self.font_metrics.cell_baseline); y -= @floatFromInt(self.font_metrics.cell_baseline);
// Add padding // Add padding
y += @floatFromInt(pad.top); y += @floatFromInt(self.size.padding.top);
// Scale // Scale
y /= content_scale.y; y /= content_scale.y;
@ -1177,10 +1300,10 @@ pub fn imePoint(self: *const Surface) apprt.IMEPos {
const x: f64 = x: { const x: f64 = x: {
// Simple x * cell width gives the top-left corner // Simple x * cell width gives the top-left corner
var x: f64 = @floatFromInt(cursor.x * self.cell_size.width); var x: f64 = @floatFromInt(cursor.x * self.size.cell.width);
// We want the midpoint // We want the midpoint
x += @as(f64, @floatFromInt(self.cell_size.width)) / 2; x += @as(f64, @floatFromInt(self.size.cell.width)) / 2;
// And scale it // And scale it
x /= content_scale.x; x /= content_scale.x;
@ -1190,10 +1313,10 @@ pub fn imePoint(self: *const Surface) apprt.IMEPos {
const y: f64 = y: { const y: f64 = y: {
// Simple x * cell width gives the top-left corner // Simple x * cell width gives the top-left corner
var y: f64 = @floatFromInt(cursor.y * self.cell_size.height); var y: f64 = @floatFromInt(cursor.y * self.size.cell.height);
// We want the bottom // We want the bottom
y += @floatFromInt(self.cell_size.height); y += @floatFromInt(self.size.cell.height);
// And scale it // And scale it
y /= content_scale.y; y /= content_scale.y;
@ -1319,24 +1442,12 @@ fn setSelection(self: *Surface, sel_: ?terminal.Selection) !void {
/// Change the cell size for the terminal grid. This can happen as /// Change the cell size for the terminal grid. This can happen as
/// a result of changing the font size at runtime. /// a result of changing the font size at runtime.
fn setCellSize(self: *Surface, size: renderer.CellSize) !void { fn setCellSize(self: *Surface, size: renderer.CellSize) !void {
// Update our new cell size for future calcs // Update our cell size within our size struct
self.cell_size = size; self.size.cell = size;
self.balancePaddingIfNeeded();
// Update our grid_size
self.grid_size = renderer.GridSize.init(
self.screen_size.subPadding(self.padding),
self.cell_size,
);
// Notify the terminal // Notify the terminal
self.io.queueMessage(.{ self.io.queueMessage(.{ .resize = self.size }, .unlocked);
.resize = .{
.grid_size = self.grid_size,
.cell_size = self.cell_size,
.screen_size = self.screen_size,
.padding = self.padding,
},
}, .unlocked);
// Notify the window // Notify the window
try self.rt_app.performAction( try self.rt_app.performAction(
@ -1407,41 +1518,41 @@ pub fn sizeCallback(self: *Surface, size: apprt.SurfaceSize) !void {
// Update our screen size, but only if it actually changed. And if // Update our screen size, but only if it actually changed. And if
// the screen size didn't change, then our grid size could not have // the screen size didn't change, then our grid size could not have
// changed, so we just return. // changed, so we just return.
if (self.screen_size.equals(new_screen_size)) return; if (self.size.screen.equals(new_screen_size)) return;
try self.resize(new_screen_size); try self.resize(new_screen_size);
} }
fn resize(self: *Surface, size: renderer.ScreenSize) !void { fn resize(self: *Surface, size: renderer.ScreenSize) !void {
// Save our screen size // Save our screen size
self.screen_size = size; self.size.screen = size;
self.balancePaddingIfNeeded();
// Recalculate our grid size. Because Ghostty supports fluid resizing, // Recalculate our grid size. Because Ghostty supports fluid resizing,
// its possible the grid doesn't change at all even if the screen size changes. // its possible the grid doesn't change at all even if the screen size changes.
// We have to update the IO thread no matter what because we send // We have to update the IO thread no matter what because we send
// pixel-level sizing to the subprocess. // pixel-level sizing to the subprocess.
self.grid_size = renderer.GridSize.init( const grid_size = self.size.grid();
self.screen_size.subPadding(self.padding), if (grid_size.columns < 5 and (self.size.padding.left > 0 or self.size.padding.right > 0)) {
self.cell_size,
);
if (self.grid_size.columns < 5 and (self.padding.left > 0 or self.padding.right > 0)) {
log.warn("WARNING: very small terminal grid detected with padding " ++ log.warn("WARNING: very small terminal grid detected with padding " ++
"set. Is your padding reasonable?", .{}); "set. Is your padding reasonable?", .{});
} }
if (self.grid_size.rows < 2 and (self.padding.top > 0 or self.padding.bottom > 0)) { if (grid_size.rows < 2 and (self.size.padding.top > 0 or self.size.padding.bottom > 0)) {
log.warn("WARNING: very small terminal grid detected with padding " ++ log.warn("WARNING: very small terminal grid detected with padding " ++
"set. Is your padding reasonable?", .{}); "set. Is your padding reasonable?", .{});
} }
// Mail the IO thread // Mail the IO thread
self.io.queueMessage(.{ self.io.queueMessage(.{ .resize = self.size }, .unlocked);
.resize = .{ }
.grid_size = self.grid_size,
.cell_size = self.cell_size, /// Recalculate the balanced padding if needed.
.screen_size = self.screen_size, fn balancePaddingIfNeeded(self: *Surface) void {
.padding = self.padding, if (!self.config.window_padding_balance) return;
}, const content_scale = try self.rt_surface.getContentScale();
}, .unlocked); const x_dpi = content_scale.x * font.face.default_dpi;
const y_dpi = content_scale.y * font.face.default_dpi;
self.size.balancePadding(self.config.scaledPadding(x_dpi, y_dpi));
} }
/// Called to set the preedit state for character input. Preedit is used /// Called to set the preedit state for character input. Preedit is used
@ -1972,6 +2083,10 @@ pub fn focusCallback(self: *Surface, focused: bool) !void {
crash.sentry.thread_state = self.crashThreadState(); crash.sentry.thread_state = self.crashThreadState();
defer crash.sentry.thread_state = null; defer crash.sentry.thread_state = null;
// If our focus state is the same we do nothing.
if (self.focused == focused) return;
self.focused = focused;
// Notify our render thread of the new state // Notify our render thread of the new state
_ = self.renderer_thread.mailbox.push(.{ _ = self.renderer_thread.mailbox.push(.{
.focus = focused, .focus = focused,
@ -2028,6 +2143,12 @@ pub fn focusCallback(self: *Surface, focused: bool) !void {
// Schedule render which also drains our mailbox // Schedule render which also drains our mailbox
try self.queueRender(); try self.queueRender();
// Whenever our focus changes we unhide the mouse. The mouse will be
// hidden again if the user starts typing. This helps alleviate some
// buggy behavior upstream in macOS with the mouse never becoming visible
// again when tabbing between programs (see #2525).
self.showMouse();
// Update the focus state and notify the terminal // Update the focus state and notify the terminal
{ {
self.renderer_state.mutex.lock(); self.renderer_state.mutex.lock();
@ -2090,7 +2211,8 @@ pub fn scrollCallback(
if (!scroll_mods.precision) { if (!scroll_mods.precision) {
// Calculate our magnitude of scroll. This is constant (not // Calculate our magnitude of scroll. This is constant (not
// dependent on yoff). // dependent on yoff).
const grid_rows_f64: f64 = @floatFromInt(self.grid_size.rows); const grid_size = self.size.grid();
const grid_rows_f64: f64 = @floatFromInt(grid_size.rows);
const y_delta_f64: f64 = @round((grid_rows_f64 * self.config.mouse_scroll_multiplier) / 15.0); const y_delta_f64: f64 = @round((grid_rows_f64 * self.config.mouse_scroll_multiplier) / 15.0);
const y_delta_usize: usize = @max(1, @as(usize, @intFromFloat(y_delta_f64))); const y_delta_usize: usize = @max(1, @as(usize, @intFromFloat(y_delta_f64)));
@ -2117,7 +2239,7 @@ pub fn scrollCallback(
// If the new offset is less than a single unit of scroll, we save // If the new offset is less than a single unit of scroll, we save
// the new pending value and do not scroll yet. // the new pending value and do not scroll yet.
const cell_size: f64 = @floatFromInt(self.cell_size.height); const cell_size: f64 = @floatFromInt(self.size.cell.height);
if (@abs(poff) < cell_size) { if (@abs(poff) < cell_size) {
self.mouse.pending_scroll_y = poff; self.mouse.pending_scroll_y = poff;
break :y .{}; break :y .{};
@ -2147,7 +2269,7 @@ pub fn scrollCallback(
const xoff_adjusted: f64 = xoff * self.config.mouse_scroll_multiplier; const xoff_adjusted: f64 = xoff * self.config.mouse_scroll_multiplier;
const poff: f64 = self.mouse.pending_scroll_x + xoff_adjusted; const poff: f64 = self.mouse.pending_scroll_x + xoff_adjusted;
const cell_size: f64 = @floatFromInt(self.cell_size.width); const cell_size: f64 = @floatFromInt(self.size.cell.width);
if (@abs(poff) < cell_size) { if (@abs(poff) < cell_size) {
self.mouse.pending_scroll_x = poff; self.mouse.pending_scroll_x = poff;
break :x .{}; break :x .{};
@ -2272,36 +2394,15 @@ pub fn contentScaleCallback(self: *Surface, content_scale: apprt.ContentScale) !
try self.setFontSize(size); try self.setFontSize(size);
// Update our padding which is dependent on DPI. // Update our padding which is dependent on DPI. We only do this for
self.padding = padding: { // unbalanced padding since balanced padding is not dependent on DPI.
const padding_top: u32 = padding_top: { if (!self.config.window_padding_balance) {
const padding_top: f32 = @floatFromInt(self.config.window_padding_top); self.size.padding = self.config.scaledPadding(x_dpi, y_dpi);
break :padding_top @intFromFloat(@floor(padding_top * y_dpi / 72)); }
};
const padding_bottom: u32 = padding_bottom: {
const padding_bottom: f32 = @floatFromInt(self.config.window_padding_bottom);
break :padding_bottom @intFromFloat(@floor(padding_bottom * y_dpi / 72));
};
const padding_left: u32 = padding_left: {
const padding_left: f32 = @floatFromInt(self.config.window_padding_left);
break :padding_left @intFromFloat(@floor(padding_left * x_dpi / 72));
};
const padding_right: u32 = padding_right: {
const padding_right: f32 = @floatFromInt(self.config.window_padding_right);
break :padding_right @intFromFloat(@floor(padding_right * x_dpi / 72));
};
break :padding .{
.top = padding_top,
.bottom = padding_bottom,
.left = padding_left,
.right = padding_right,
};
};
// Force a resize event because the change in padding will affect // Force a resize event because the change in padding will affect
// pixel-level changes to the renderer and viewport. // pixel-level changes to the renderer and viewport.
try self.resize(self.screen_size); try self.resize(self.size.screen);
} }
/// The type of action to report for a mouse event. /// The type of action to report for a mouse event.
@ -2340,8 +2441,8 @@ fn mouseReport(
// We always report release events no matter where they happen. // We always report release events no matter where they happen.
if (action != .release) { if (action != .release) {
const pos_out_viewport = pos_out_viewport: { const pos_out_viewport = pos_out_viewport: {
const max_x: f32 = @floatFromInt(self.screen_size.width); const max_x: f32 = @floatFromInt(self.size.screen.width);
const max_y: f32 = @floatFromInt(self.screen_size.height); const max_y: f32 = @floatFromInt(self.size.screen.height);
break :pos_out_viewport pos.x < 0 or pos.y < 0 or break :pos_out_viewport pos.x < 0 or pos.y < 0 or
pos.x > max_x or pos.y > max_y; pos.x > max_x or pos.y > max_y;
}; };
@ -2500,15 +2601,22 @@ fn mouseReport(
.sgr_pixels => { .sgr_pixels => {
// Final character to send in the CSI // Final character to send in the CSI
const final: u8 = if (action == .release) 'm' else 'M'; const final: u8 = if (action == .release) 'm' else 'M';
const adjusted = self.posAdjusted(pos.x, pos.y);
// The position has to be adjusted to the terminal space.
const coord: renderer.Coordinate.Terminal = (renderer.Coordinate{
.surface = .{
.x = pos.x,
.y = pos.y,
},
}).convert(.terminal, self.size).terminal;
// Response always is at least 4 chars, so this leaves the // Response always is at least 4 chars, so this leaves the
// remainder for numbers which are very large... // remainder for numbers which are very large...
var data: termio.Message.WriteReq.Small.Array = undefined; var data: termio.Message.WriteReq.Small.Array = undefined;
const resp = try std.fmt.bufPrint(&data, "\x1B[<{d};{d};{d}{c}", .{ const resp = try std.fmt.bufPrint(&data, "\x1B[<{d};{d};{d}{c}", .{
button_code, button_code,
@as(i32, @intFromFloat(@round(adjusted.x))), @as(i32, @intFromFloat(@round(coord.x))),
@as(i32, @intFromFloat(@round(adjusted.y))), @as(i32, @intFromFloat(@round(coord.y))),
final, final,
}); });
@ -2713,12 +2821,20 @@ pub fn mouseButtonCallback(
} }
// For left button click release we check if we are moving our cursor. // For left button click release we check if we are moving our cursor.
if (button == .left and action == .release and mods.alt) click_move: { if (button == .left and
action == .release and
mods.alt)
click_move: {
self.renderer_state.mutex.lock();
defer self.renderer_state.mutex.unlock();
// If we have a selection then we do not do click to move because
// it means that we moved our cursor while pressing the mouse button.
if (self.io.terminal.screen.selection != null) break :click_move;
// Moving always resets the click count so that we don't highlight. // Moving always resets the click count so that we don't highlight.
self.mouse.left_click_count = 0; self.mouse.left_click_count = 0;
const pin = self.mouse.left_click_pin orelse break :click_move; const pin = self.mouse.left_click_pin orelse break :click_move;
self.renderer_state.mutex.lock();
defer self.renderer_state.mutex.unlock();
try self.clickMoveCursor(pin.*); try self.clickMoveCursor(pin.*);
return true; return true;
} }
@ -2755,7 +2871,7 @@ pub fn mouseButtonCallback(
// If we move our cursor too much between clicks then we reset // If we move our cursor too much between clicks then we reset
// the multi-click state. // the multi-click state.
if (self.mouse.left_click_count > 0) { if (self.mouse.left_click_count > 0) {
const max_distance: f64 = @floatFromInt(self.cell_size.width); const max_distance: f64 = @floatFromInt(self.size.cell.width);
const distance = @sqrt( const distance = @sqrt(
std.math.pow(f64, pos.x - self.mouse.left_click_xpos, 2) + std.math.pow(f64, pos.x - self.mouse.left_click_xpos, 2) +
std.math.pow(f64, pos.y - self.mouse.left_click_ypos, 2), std.math.pow(f64, pos.y - self.mouse.left_click_ypos, 2),
@ -3067,7 +3183,7 @@ fn processLinks(self: *Surface, pos: apprt.CursorPos) !bool {
/// if there is no hyperlink. /// if there is no hyperlink.
fn osc8URI(self: *Surface, pin: terminal.Pin) ?[]const u8 { fn osc8URI(self: *Surface, pin: terminal.Pin) ?[]const u8 {
_ = self; _ = self;
const page = &pin.page.data; const page = &pin.node.data;
const cell = pin.rowAndCell().cell; const cell = pin.rowAndCell().cell;
const link_id = page.lookupHyperlink(cell) orelse return null; const link_id = page.lookupHyperlink(cell) orelse return null;
const entry = page.hyperlink_set.get(page.memory, link_id); const entry = page.hyperlink_set.get(page.memory, link_id);
@ -3255,8 +3371,8 @@ pub fn cursorPosCallback(
// We allow for a 1 pixel buffer at the top and bottom to detect // We allow for a 1 pixel buffer at the top and bottom to detect
// scroll even in full screen windows. // scroll even in full screen windows.
// Note: one day, we can change this from distance to time based if we want. // Note: one day, we can change this from distance to time based if we want.
//log.warn("CURSOR POS: {} {}", .{ pos, self.screen_size }); //log.warn("CURSOR POS: {} {}", .{ pos, self.size.screen });
const max_y: f32 = @floatFromInt(self.screen_size.height); const max_y: f32 = @floatFromInt(self.size.screen.height);
if (pos.y <= 1 or pos.y > max_y - 1) { if (pos.y <= 1 or pos.y > max_y - 1) {
const delta: isize = if (pos.y < 0) -1 else 1; const delta: isize = if (pos.y < 0) -1 else 1;
try self.io.terminal.scrollViewport(.{ .delta = delta }); try self.io.terminal.scrollViewport(.{ .delta = delta });
@ -3415,11 +3531,11 @@ fn dragLeftClickSingle(
const click_pin = self.mouse.left_click_pin.?.*; const click_pin = self.mouse.left_click_pin.?.*;
// the boundary point at which we consider selection or non-selection // the boundary point at which we consider selection or non-selection
const cell_width_f64: f64 = @floatFromInt(self.cell_size.width); const cell_width_f64: f64 = @floatFromInt(self.size.cell.width);
const cell_xboundary = cell_width_f64 * 0.6; const cell_xboundary = cell_width_f64 * 0.6;
// first xpos of the clicked cell adjusted for padding // first xpos of the clicked cell adjusted for padding
const left_padding_f64: f64 = @as(f64, @floatFromInt(self.padding.left)); const left_padding_f64: f64 = @as(f64, @floatFromInt(self.size.padding.left));
const cell_xstart = @as(f64, @floatFromInt(click_pin.x)) * cell_width_f64; const cell_xstart = @as(f64, @floatFromInt(click_pin.x)) * cell_width_f64;
const cell_start_xpos = self.mouse.left_click_xpos - cell_xstart - left_padding_f64; const cell_start_xpos = self.mouse.left_click_xpos - cell_xstart - left_padding_f64;
@ -3437,7 +3553,7 @@ fn dragLeftClickSingle(
try self.setSelection(if (selected) terminal.Selection.init( try self.setSelection(if (selected) terminal.Selection.init(
drag_pin, drag_pin,
drag_pin, drag_pin,
self.mouse.mods.ctrlOrSuper() and self.mouse.mods.alt, SurfaceMouse.isRectangleSelectState(self.mouse.mods),
) else null); ) else null);
return; return;
@ -3472,7 +3588,7 @@ fn dragLeftClickSingle(
try self.setSelection(terminal.Selection.init( try self.setSelection(terminal.Selection.init(
start, start,
drag_pin, drag_pin,
self.mouse.mods.ctrlOrSuper() and self.mouse.mods.alt, SurfaceMouse.isRectangleSelectState(self.mouse.mods),
)); ));
return; return;
} }
@ -3554,58 +3670,27 @@ pub fn colorSchemeCallback(self: *Surface, scheme: apprt.ColorScheme) !void {
crash.sentry.thread_state = self.crashThreadState(); crash.sentry.thread_state = self.crashThreadState();
defer crash.sentry.thread_state = null; defer crash.sentry.thread_state = null;
// If our scheme didn't change, then we don't do anything. const new_scheme: configpkg.ConditionalState.Theme = switch (scheme) {
if (self.color_scheme == scheme) return; .light => .light,
.dark => .dark,
};
// Set our new scheme // If our scheme didn't change, then we don't do anything.
self.color_scheme = scheme; if (self.config_conditional_state.theme == new_scheme) return;
// Setup our conditional state which has the current color theme.
self.config_conditional_state.theme = new_scheme;
self.notifyConfigConditionalState();
// If mode 2031 is on, then we report the change live. // If mode 2031 is on, then we report the change live.
const report = report: { self.reportColorScheme(false);
self.renderer_state.mutex.lock();
defer self.renderer_state.mutex.unlock();
break :report self.renderer_state.terminal.modes.get(.report_color_scheme);
};
if (report) try self.reportColorScheme();
}
pub fn posAdjusted(self: Surface, xpos: f64, ypos: f64) struct { x: f64, y: f64 } {
const pad = if (self.config.window_padding_balance)
renderer.Padding.balanced(self.screen_size, self.grid_size, self.cell_size)
else
self.padding;
return .{
.x = xpos - @as(f64, @floatFromInt(pad.left)),
.y = ypos - @as(f64, @floatFromInt(pad.top)),
};
} }
pub fn posToViewport(self: Surface, xpos: f64, ypos: f64) terminal.point.Coordinate { pub fn posToViewport(self: Surface, xpos: f64, ypos: f64) terminal.point.Coordinate {
// xpos/ypos need to be adjusted for window padding // Get our grid cell
// (i.e. "window-padding-*" settings. const coord: renderer.Coordinate = .{ .surface = .{ .x = xpos, .y = ypos } };
const adjusted = self.posAdjusted(xpos, ypos); const grid = coord.convert(.grid, self.size).grid;
return .{ .x = grid.x, .y = grid.y };
// adjusted.x and adjusted.y can be negative if while dragging, the user moves the
// mouse off the surface. Likewise, they can be larger than our surface
// width if the user drags out of the surface positively.
return .{
.x = if (adjusted.x < 0) 0 else x: {
// Our cell is the mouse divided by cell width
const cell_width: f64 = @floatFromInt(self.cell_size.width);
const x: usize = @intFromFloat(adjusted.x / cell_width);
// Can be off the screen if the user drags it out, so max
// it out on our available columns
break :x @min(x, self.grid_size.columns - 1);
},
.y = if (adjusted.y < 0) 0 else y: {
const cell_height: f64 = @floatFromInt(self.cell_size.height);
const y: usize = @intFromFloat(adjusted.y / cell_height);
break :y @min(y, self.grid_size.rows - 1);
},
};
} }
/// Scroll to the bottom of the viewport. /// Scroll to the bottom of the viewport.
@ -3843,21 +3928,21 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
}, },
.scroll_page_up => { .scroll_page_up => {
const rows: isize = @intCast(self.grid_size.rows); const rows: isize = @intCast(self.size.grid().rows);
self.io.queueMessage(.{ self.io.queueMessage(.{
.scroll_viewport = .{ .delta = -1 * rows }, .scroll_viewport = .{ .delta = -1 * rows },
}, .unlocked); }, .unlocked);
}, },
.scroll_page_down => { .scroll_page_down => {
const rows: isize = @intCast(self.grid_size.rows); const rows: isize = @intCast(self.size.grid().rows);
self.io.queueMessage(.{ self.io.queueMessage(.{
.scroll_viewport = .{ .delta = rows }, .scroll_viewport = .{ .delta = rows },
}, .unlocked); }, .unlocked);
}, },
.scroll_page_fractional => |fraction| { .scroll_page_fractional => |fraction| {
const rows: f32 = @floatFromInt(self.grid_size.rows); const rows: f32 = @floatFromInt(self.size.grid().rows);
const delta: isize = @intFromFloat(@trunc(fraction * rows)); const delta: isize = @intFromFloat(@trunc(fraction * rows));
self.io.queueMessage(.{ self.io.queueMessage(.{
.scroll_viewport = .{ .delta = delta }, .scroll_viewport = .{ .delta = delta },
@ -3927,7 +4012,7 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
.left => .left, .left => .left,
.down => .down, .down => .down,
.up => .up, .up => .up,
.auto => if (self.screen_size.width > self.screen_size.height) .auto => if (self.size.screen.width > self.size.screen.height)
.right .right
else else
.down, .down,

View File

@ -1,6 +1,7 @@
const std = @import("std"); const std = @import("std");
const assert = std.debug.assert; const assert = std.debug.assert;
const apprt = @import("../apprt.zig"); const apprt = @import("../apprt.zig");
const configpkg = @import("../config.zig");
const input = @import("../input.zig"); const input = @import("../input.zig");
const renderer = @import("../renderer.zig"); const renderer = @import("../renderer.zig");
const terminal = @import("../terminal/main.zig"); const terminal = @import("../terminal/main.zig");
@ -151,6 +152,9 @@ pub const Action = union(Key) {
/// Set the title of the target. /// Set the title of the target.
set_title: SetTitle, set_title: SetTitle,
/// The current working directory has changed for the target terminal.
pwd: Pwd,
/// Set the mouse cursor shape. /// Set the mouse cursor shape.
mouse_shape: terminal.MouseShape, mouse_shape: terminal.MouseShape,
@ -186,6 +190,33 @@ pub const Action = union(Key) {
/// key mode because other input may be ignored. /// key mode because other input may be ignored.
key_sequence: KeySequence, key_sequence: KeySequence,
/// A terminal color was changed programmatically through things
/// such as OSC 10/11.
color_change: ColorChange,
/// A request to reload the configuration. The reload request can be
/// from a user or for some internal reason. The reload request may
/// request it is a soft reload or a full reload. See the struct for
/// more documentation.
///
/// The configuration should be passed to updateConfig either at the
/// app or surface level depending on the target.
reload_config: ReloadConfig,
/// The configuration has changed. The value is a pointer to the new
/// configuration. The pointer is only valid for the duration of the
/// action and should not be stored.
///
/// This should be used by apprts to update any internal state that
/// depends on configuration for the given target (i.e. headerbar colors).
/// The apprt should copy any data it needs since the memory lifetime
/// is only valid for the duration of the action.
///
/// This allows an apprt to have config-dependent state reactively
/// change without having to store the entire configuration or poll
/// for changes.
config_change: ConfigChange,
/// Sync with: ghostty_action_tag_e /// Sync with: ghostty_action_tag_e
pub const Key = enum(c_int) { pub const Key = enum(c_int) {
new_window, new_window,
@ -211,6 +242,7 @@ pub const Action = union(Key) {
render_inspector, render_inspector,
desktop_notification, desktop_notification,
set_title, set_title,
pwd,
mouse_shape, mouse_shape,
mouse_visibility, mouse_visibility,
mouse_over_link, mouse_over_link,
@ -219,6 +251,9 @@ pub const Action = union(Key) {
quit_timer, quit_timer,
secure_input, secure_input,
key_sequence, key_sequence,
color_change,
reload_config,
config_change,
}; };
/// Sync with: ghostty_action_u /// Sync with: ghostty_action_u
@ -412,6 +447,21 @@ pub const SetTitle = struct {
} }
}; };
pub const Pwd = struct {
pwd: [:0]const u8,
// Sync with: ghostty_action_set_pwd_s
pub const C = extern struct {
pwd: [*:0]const u8,
};
pub fn cval(self: Pwd) C {
return .{
.pwd = self.pwd.ptr,
};
}
};
/// The desktop notification to show. /// The desktop notification to show.
pub const DesktopNotification = struct { pub const DesktopNotification = struct {
title: [:0]const u8, title: [:0]const u8,
@ -448,3 +498,42 @@ pub const KeySequence = union(enum) {
}; };
} }
}; };
pub const ColorChange = extern struct {
kind: ColorKind,
r: u8,
g: u8,
b: u8,
};
pub const ColorKind = enum(c_int) {
// Negative numbers indicate some named kind
foreground = -1,
background = -2,
cursor = -3,
// 0+ values indicate a palette index
_,
};
pub const ReloadConfig = extern struct {
/// A soft reload means that the configuration doesn't need to be
/// read off disk, but libghostty needs the full config again so call
/// updateConfig with it.
soft: bool = false,
};
pub const ConfigChange = struct {
config: *const configpkg.Config,
// Sync with: ghostty_action_config_change_s
pub const C = extern struct {
config: *const configpkg.Config,
};
pub fn cval(self: ConfigChange) C {
return .{
.config = self.config,
};
}
};

View File

@ -47,11 +47,6 @@ pub const App = struct {
/// Callback called to handle an action. /// Callback called to handle an action.
action: *const fn (*App, apprt.Target.C, apprt.Action.C) callconv(.C) void, action: *const fn (*App, apprt.Target.C, apprt.Action.C) callconv(.C) void,
/// Reload the configuration and return the new configuration.
/// The old configuration can be freed immediately when this is
/// called.
reload_config: *const fn (AppUD) callconv(.C) ?*const Config,
/// Read the clipboard value. The return value must be preserved /// Read the clipboard value. The return value must be preserved
/// by the host until the next call. If there is no valid clipboard /// by the host until the next call. If there is no valid clipboard
/// value then this should return null. /// value then this should return null.
@ -90,26 +85,38 @@ pub const App = struct {
}; };
core_app: *CoreApp, core_app: *CoreApp,
config: *const Config,
opts: Options, opts: Options,
keymap: input.Keymap, keymap: input.Keymap,
/// The configuration for the app. This is owned by this structure.
config: Config,
/// The keymap state is used for global keybinds only. Each surface /// The keymap state is used for global keybinds only. Each surface
/// also has its own keymap state for focused keybinds. /// also has its own keymap state for focused keybinds.
keymap_state: input.Keymap.State, keymap_state: input.Keymap.State,
pub fn init(core_app: *CoreApp, config: *const Config, opts: Options) !App { pub fn init(
core_app: *CoreApp,
config: *const Config,
opts: Options,
) !App {
// We have to clone the config.
const alloc = core_app.alloc;
var config_clone = try config.clone(alloc);
errdefer config_clone.deinit();
return .{ return .{
.core_app = core_app, .core_app = core_app,
.config = config, .config = config_clone,
.opts = opts, .opts = opts,
.keymap = try input.Keymap.init(), .keymap = try input.Keymap.init(),
.keymap_state = .{}, .keymap_state = .{},
}; };
} }
pub fn terminate(self: App) void { pub fn terminate(self: *App) void {
self.keymap.deinit(); self.keymap.deinit();
self.config.deinit();
} }
/// Returns true if there are any global keybinds in the configuration. /// Returns true if there are any global keybinds in the configuration.
@ -375,21 +382,11 @@ pub const App = struct {
} }
} }
pub fn reloadConfig(self: *App) !?*const Config { pub fn wakeup(self: *const App) void {
// Reload
if (self.opts.reload_config(self.opts.userdata)) |new| {
self.config = new;
return self.config;
}
return null;
}
pub fn wakeup(self: App) void {
self.opts.wakeup(self.opts.userdata); self.opts.wakeup(self.opts.userdata);
} }
pub fn wait(self: App) !void { pub fn wait(self: *const App) !void {
_ = self; _ = self;
} }
@ -430,6 +427,28 @@ pub const App = struct {
comptime action: apprt.Action.Key, comptime action: apprt.Action.Key,
value: apprt.Action.Value(action), value: apprt.Action.Value(action),
) !void { ) !void {
// Special case certain actions before they are sent to the
// embedded apprt.
self.performPreAction(target, action, value);
log.debug("dispatching action target={s} action={} value={}", .{
@tagName(target),
action,
value,
});
self.opts.action(
self,
target.cval(),
@unionInit(apprt.Action, @tagName(action), value).cval(),
);
}
fn performPreAction(
self: *App,
target: apprt.Target,
comptime action: apprt.Action.Key,
value: apprt.Action.Value(action),
) void {
// Special case certain actions before they are sent to the embedder // Special case certain actions before they are sent to the embedder
switch (action) { switch (action) {
.set_title => switch (target) { .set_title => switch (target) {
@ -443,19 +462,21 @@ pub const App = struct {
}, },
}, },
.config_change => switch (target) {
.surface => {},
// For app updates, we update our core config. We need to
// clone it because the caller owns the param.
.app => if (value.config.clone(self.core_app.alloc)) |config| {
self.config.deinit();
self.config = config;
} else |err| {
log.err("error updating app config err={}", .{err});
},
},
else => {}, else => {},
} }
log.debug("dispatching action target={s} action={} value={}", .{
@tagName(target),
action,
value,
});
self.opts.action(
self,
target.cval(),
@unionInit(apprt.Action, @tagName(action), value).cval(),
);
} }
}; };
@ -577,7 +598,7 @@ pub const Surface = struct {
errdefer app.core_app.deleteSurface(self); errdefer app.core_app.deleteSurface(self);
// Shallow copy the config so that we can modify it. // Shallow copy the config so that we can modify it.
var config = try apprt.surface.newConfig(app.core_app, app.config); var config = try apprt.surface.newConfig(app.core_app, &app.config);
defer config.deinit(); defer config.deinit();
// If we have a working directory from the options then we set it. // If we have a working directory from the options then we set it.
@ -1339,10 +1360,14 @@ pub const CAPI = struct {
}; };
} }
/// Reload the configuration. /// Update the configuration to the provided config. This will propagate
export fn ghostty_app_reload_config(v: *App) void { /// to all surfaces as well.
_ = v.core_app.reloadConfig(v) catch |err| { export fn ghostty_app_update_config(
log.err("error reloading config err={}", .{err}); v: *App,
config: *const Config,
) void {
v.core_app.updateConfig(v, config) catch |err| {
log.err("error updating config err={}", .{err});
return; return;
}; };
} }
@ -1357,6 +1382,22 @@ pub const CAPI = struct {
return v.hasGlobalKeybinds(); return v.hasGlobalKeybinds();
} }
/// Update the color scheme of the app.
export fn ghostty_app_set_color_scheme(v: *App, scheme_raw: c_int) void {
const scheme = std.meta.intToEnum(apprt.ColorScheme, scheme_raw) catch {
log.warn(
"invalid color scheme to ghostty_surface_set_color_scheme value={}",
.{scheme_raw},
);
return;
};
v.core_app.colorSchemeEvent(v, scheme) catch |err| {
log.err("error setting color scheme err={}", .{err});
return;
};
}
/// Returns initial surface options. /// Returns initial surface options.
export fn ghostty_surface_config_new() apprt.Surface.Options { export fn ghostty_surface_config_new() apprt.Surface.Options {
return .{}; return .{};
@ -1399,6 +1440,17 @@ pub const CAPI = struct {
return surface.newSurfaceOptions(); return surface.newSurfaceOptions();
} }
/// Update the configuration to the provided config for only this surface.
export fn ghostty_surface_update_config(
surface: *Surface,
config: *const Config,
) void {
surface.core_surface.updateConfig(config) catch |err| {
log.err("error updating config err={}", .{err});
return;
};
}
/// Returns true if the surface needs to confirm quitting. /// Returns true if the surface needs to confirm quitting.
export fn ghostty_surface_needs_confirm_quit(surface: *Surface) bool { export fn ghostty_surface_needs_confirm_quit(surface: *Surface) bool {
return surface.core_surface.needsConfirmQuit(); return surface.core_surface.needsConfirmQuit();
@ -1428,25 +1480,6 @@ pub const CAPI = struct {
return selection.len; return selection.len;
} }
/// Copies the surface working directory into the provided buffer and
/// returns the copied size. If the buffer is too small, there is no pwd,
/// or there is an error, then 0 is returned.
export fn ghostty_surface_pwd(surface: *Surface, buf: [*]u8, cap: usize) usize {
const pwd_ = surface.core_surface.pwd(global.alloc) catch |err| {
log.warn("error getting pwd err={}", .{err});
return 0;
};
const pwd = pwd_ orelse return 0;
defer global.alloc.free(pwd);
// If the buffer is too small, return no pwd.
if (pwd.len > cap) return 0;
// Copy into the buffer and return the length
@memcpy(buf[0..pwd.len], pwd);
return pwd.len;
}
/// Tell the surface that it needs to schedule a render /// Tell the surface that it needs to schedule a render
export fn ghostty_surface_refresh(surface: *Surface) void { export fn ghostty_surface_refresh(surface: *Surface) void {
surface.refresh(); surface.refresh();
@ -1466,13 +1499,14 @@ pub const CAPI = struct {
/// Return the size information a surface has. /// Return the size information a surface has.
export fn ghostty_surface_size(surface: *Surface) SurfaceSize { export fn ghostty_surface_size(surface: *Surface) SurfaceSize {
const grid_size = surface.core_surface.size.grid();
return .{ return .{
.columns = surface.core_surface.grid_size.columns, .columns = grid_size.columns,
.rows = surface.core_surface.grid_size.rows, .rows = grid_size.rows,
.width_px = surface.core_surface.screen_size.width, .width_px = surface.core_surface.size.screen.width,
.height_px = surface.core_surface.screen_size.height, .height_px = surface.core_surface.size.screen.height,
.cell_width_px = surface.core_surface.cell_size.width, .cell_width_px = surface.core_surface.size.cell.width,
.cell_height_px = surface.core_surface.cell_size.height, .cell_height_px = surface.core_surface.size.cell.height,
}; };
} }
@ -1822,7 +1856,7 @@ pub const CAPI = struct {
// This is only supported on macOS // This is only supported on macOS
if (comptime builtin.target.os.tag != .macos) return; if (comptime builtin.target.os.tag != .macos) return;
const config = app.config; const config = &app.config;
// Do nothing if we don't have background transparency enabled // Do nothing if we don't have background transparency enabled
if (config.@"background-opacity" >= 1.0) return; if (config.@"background-opacity" >= 1.0) return;

View File

@ -200,6 +200,8 @@ pub const App = struct {
}), }),
}, },
.reload_config => try self.reloadConfig(target, value),
// Unimplemented // Unimplemented
.new_split, .new_split,
.goto_split, .goto_split,
@ -223,6 +225,9 @@ pub const App = struct {
.mouse_over_link, .mouse_over_link,
.cell_size, .cell_size,
.renderer_health, .renderer_health,
.color_change,
.pwd,
.config_change,
=> log.info("unimplemented action={}", .{action}), => log.info("unimplemented action={}", .{action}),
} }
} }
@ -232,16 +237,34 @@ pub const App = struct {
/// successful return. /// successful return.
/// ///
/// The returned pointer value is only valid for a stable self pointer. /// The returned pointer value is only valid for a stable self pointer.
pub fn reloadConfig(self: *App) !?*const Config { fn reloadConfig(
self: *App,
target: apprt.action.Target,
opts: apprt.action.ReloadConfig,
) !void {
if (opts.soft) {
switch (target) {
.app => try self.app.updateConfig(self, &self.config),
.surface => |core_surface| try core_surface.updateConfig(
&self.config,
),
}
return;
}
// Load our configuration // Load our configuration
var config = try Config.load(self.app.alloc); var config = try Config.load(self.app.alloc);
errdefer config.deinit(); errdefer config.deinit();
// Call into our app to update
switch (target) {
.app => try self.app.updateConfig(self, &config),
.surface => |core_surface| try core_surface.updateConfig(&config),
}
// Update the existing config, be sure to clean up the old one. // Update the existing config, be sure to clean up the old one.
self.config.deinit(); self.config.deinit();
self.config = config; self.config = config;
return &self.config;
} }
/// Toggle the window to fullscreen mode. /// Toggle the window to fullscreen mode.

View File

@ -462,21 +462,24 @@ pub fn performAction(
.equalize_splits => self.equalizeSplits(target), .equalize_splits => self.equalizeSplits(target),
.goto_split => self.gotoSplit(target, value), .goto_split => self.gotoSplit(target, value),
.open_config => try configpkg.edit.open(self.core_app.alloc), .open_config => try configpkg.edit.open(self.core_app.alloc),
.config_change => self.configChange(value.config),
.reload_config => try self.reloadConfig(target, value),
.inspector => self.controlInspector(target, value), .inspector => self.controlInspector(target, value),
.desktop_notification => self.showDesktopNotification(target, value), .desktop_notification => self.showDesktopNotification(target, value),
.set_title => try self.setTitle(target, value), .set_title => try self.setTitle(target, value),
.pwd => try self.setPwd(target, value),
.present_terminal => self.presentTerminal(target), .present_terminal => self.presentTerminal(target),
.initial_size => try self.setInitialSize(target, value), .initial_size => try self.setInitialSize(target, value),
.mouse_visibility => self.setMouseVisibility(target, value), .mouse_visibility => self.setMouseVisibility(target, value),
.mouse_shape => try self.setMouseShape(target, value), .mouse_shape => try self.setMouseShape(target, value),
.mouse_over_link => self.setMouseOverLink(target, value), .mouse_over_link => self.setMouseOverLink(target, value),
.toggle_tab_overview => self.toggleTabOverview(target), .toggle_tab_overview => self.toggleTabOverview(target),
.toggle_split_zoom => self.toggleSplitZoom(target),
.toggle_window_decorations => self.toggleWindowDecorations(target), .toggle_window_decorations => self.toggleWindowDecorations(target),
.quit_timer => self.quitTimer(value), .quit_timer => self.quitTimer(value),
// Unimplemented // Unimplemented
.close_all_windows, .close_all_windows,
.toggle_split_zoom,
.toggle_quick_terminal, .toggle_quick_terminal,
.toggle_visibility, .toggle_visibility,
.size_limit, .size_limit,
@ -485,6 +488,7 @@ pub fn performAction(
.key_sequence, .key_sequence,
.render_inspector, .render_inspector,
.renderer_health, .renderer_health,
.color_change,
=> log.warn("unimplemented action={}", .{action}), => log.warn("unimplemented action={}", .{action}),
} }
} }
@ -670,6 +674,13 @@ fn toggleTabOverview(_: *App, target: apprt.Target) void {
} }
} }
fn toggleSplitZoom(_: *App, target: apprt.Target) void {
switch (target) {
.app => {},
.surface => |surface| surface.rt_surface.toggleSplitZoom(),
}
}
fn toggleWindowDecorations( fn toggleWindowDecorations(
_: *App, _: *App,
target: apprt.Target, target: apprt.Target,
@ -708,6 +719,17 @@ fn setTitle(
} }
} }
fn setPwd(
_: *App,
target: apprt.Target,
pwd: apprt.action.Pwd,
) !void {
switch (target) {
.app => {},
.surface => |v| try v.rt_surface.setPwd(pwd.pwd),
}
}
fn setMouseVisibility( fn setMouseVisibility(
_: *App, _: *App,
target: apprt.Target, target: apprt.Target,
@ -796,19 +818,9 @@ fn showDesktopNotification(
c.g_application_send_notification(g_app, n.body.ptr, notification); c.g_application_send_notification(g_app, n.body.ptr, notification);
} }
/// Reload the configuration. This should return the new configuration. fn configChange(self: *App, new_config: *const Config) void {
/// The old value can be freed immediately at this point assuming a _ = new_config;
/// successful return.
///
/// The returned pointer value is only valid for a stable self pointer.
pub fn reloadConfig(self: *App) !?*const Config {
// Load our configuration
var config = try Config.load(self.core_app.alloc);
errdefer config.deinit();
// Update the existing config, be sure to clean up the old one.
self.config.deinit();
self.config = config;
self.syncConfigChanges() catch |err| { self.syncConfigChanges() catch |err| {
log.warn("error handling configuration changes err={}", .{err}); log.warn("error handling configuration changes err={}", .{err});
}; };
@ -819,8 +831,36 @@ pub fn reloadConfig(self: *App) !?*const Config {
if (surface.container.window()) |window| window.onConfigReloaded(); if (surface.container.window()) |window| window.onConfigReloaded();
} }
} }
}
return &self.config; pub fn reloadConfig(
self: *App,
target: apprt.action.Target,
opts: apprt.action.ReloadConfig,
) !void {
if (opts.soft) {
switch (target) {
.app => try self.core_app.updateConfig(self, &self.config),
.surface => |core_surface| try core_surface.updateConfig(
&self.config,
),
}
return;
}
// Load our configuration
var config = try Config.load(self.core_app.alloc);
errdefer config.deinit();
// Call into our app to update
switch (target) {
.app => try self.core_app.updateConfig(self, &config),
.surface => |core_surface| try core_surface.updateConfig(&config),
}
// Update the existing config, be sure to clean up the old one.
self.config.deinit();
self.config = config;
} }
/// Call this anytime the configuration changes. /// Call this anytime the configuration changes.
@ -928,6 +968,13 @@ fn loadRuntimeCss(
\\ --headerbar-bg-color: rgb({d},{d},{d}); \\ --headerbar-bg-color: rgb({d},{d},{d});
\\ --headerbar-backdrop-color: oklab(from var(--headerbar-bg-color) calc(l * 0.9) a b / alpha); \\ --headerbar-backdrop-color: oklab(from var(--headerbar-bg-color) calc(l * 0.9) a b / alpha);
\\}} \\}}
\\windowhandle {{
\\ background-color: var(--headerbar-bg-color);
\\ color: var(--headerbar-fg-color);
\\}}
\\windowhandle:backdrop {{
\\ background-color: var(--headerbar-backdrop-color);
\\}}
, .{ , .{
headerbar_foreground.r, headerbar_foreground.r,
headerbar_foreground.g, headerbar_foreground.g,
@ -1381,9 +1428,9 @@ fn gtkActionReloadConfig(
ud: ?*anyopaque, ud: ?*anyopaque,
) callconv(.C) void { ) callconv(.C) void {
const self: *App = @ptrCast(@alignCast(ud orelse return)); const self: *App = @ptrCast(@alignCast(ud orelse return));
_ = self.core_app.mailbox.push(.{ self.reloadConfig(.app, .{}) catch |err| {
.reload_config = {}, log.err("error reloading configuration: {}", .{err});
}, .{ .forever = {} }); };
} }
fn gtkActionQuit( fn gtkActionQuit(

View File

@ -203,7 +203,7 @@ const ButtonsView = struct {
fn gtkReloadClick(_: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) void { fn gtkReloadClick(_: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) void {
const self: *ConfigErrors = @ptrCast(@alignCast(ud)); const self: *ConfigErrors = @ptrCast(@alignCast(ud));
_ = self.app.reloadConfig() catch |err| { self.app.reloadConfig(.app, .{}) catch |err| {
log.warn("error reloading config error={}", .{err}); log.warn("error reloading config error={}", .{err});
return; return;
}; };

View File

@ -94,13 +94,14 @@ fn gtkUpdate(ud: ?*anyopaque) callconv(.C) c.gboolean {
return c.FALSE; return c.FALSE;
}; };
const grid_size = surface.core_surface.size.grid();
var buf: [32]u8 = undefined; var buf: [32]u8 = undefined;
const text = std.fmt.bufPrintZ( const text = std.fmt.bufPrintZ(
&buf, &buf,
"{d}c {d}r", "{d}c {d}r",
.{ .{
surface.core_surface.grid_size.columns, grid_size.columns,
surface.core_surface.grid_size.rows, grid_size.rows,
}, },
) catch |err| { ) catch |err| {
log.err("unable to format text: {}", .{err}); log.err("unable to format text: {}", .{err});

View File

@ -77,6 +77,7 @@ pub fn init(
}); });
errdefer surface.destroy(alloc); errdefer surface.destroy(alloc);
sibling.dimSurface(); sibling.dimSurface();
sibling.setSplitZoom(false);
// Create the actual GTKPaned, attach the proper children. // Create the actual GTKPaned, attach the proper children.
const orientation: c_uint = switch (direction) { const orientation: c_uint = switch (direction) {
@ -258,7 +259,7 @@ pub fn grabFocus(self: *Split) void {
/// Update the paned children to represent the current state. /// Update the paned children to represent the current state.
/// This should be called anytime the top/left or bottom/right /// This should be called anytime the top/left or bottom/right
/// element is changed. /// element is changed.
fn updateChildren(self: *const Split) void { pub fn updateChildren(self: *const Split) void {
// We have to set both to null. If we overwrite the pane with // We have to set both to null. If we overwrite the pane with
// the same value, then GTK bugs out (the GL area unrealizes // the same value, then GTK bugs out (the GL area unrealizes
// and never rerealizes). // and never rerealizes).
@ -372,7 +373,15 @@ fn directionNext(self: *const Split, from: Side) ?struct {
} }
} }
fn removeChildren(self: *const Split) void { pub fn detachTopLeft(self: *const Split) void {
c.gtk_paned_set_start_child(@ptrCast(self.paned), null); c.gtk_paned_set_start_child(self.paned, null);
c.gtk_paned_set_end_child(@ptrCast(self.paned), null); }
pub fn detachBottomRight(self: *const Split) void {
c.gtk_paned_set_end_child(self.paned, null);
}
fn removeChildren(self: *const Split) void {
self.detachTopLeft();
self.detachBottomRight();
} }

View File

@ -308,8 +308,8 @@ pub const URLWidget = struct {
/// surface has been initialized. /// surface has been initialized.
realized: bool = false, realized: bool = false,
/// True if this surface had a parent to start with. /// The config to use to initialize a surface.
parent_surface: bool = false, init_config: InitConfig,
/// The GUI container that this surface has been attached to. This /// The GUI container that this surface has been attached to. This
/// dictates some behaviors such as new splits, etc. /// dictates some behaviors such as new splits, etc.
@ -330,6 +330,9 @@ url_widget: ?URLWidget = null,
/// The overlay that shows resizing information. /// The overlay that shows resizing information.
resize_overlay: ResizeOverlay = .{}, resize_overlay: ResizeOverlay = .{},
/// Whether or not the current surface is zoomed in (see `toggle_split_zoom`).
zoomed_in: bool = false,
/// If non-null this is the widget on the overlay which dims the surface when it is unfocused /// If non-null this is the widget on the overlay which dims the surface when it is unfocused
unfocused_widget: ?*c.GtkWidget = null, unfocused_widget: ?*c.GtkWidget = null,
@ -367,6 +370,36 @@ im_len: u7 = 0,
/// details on what this is. /// details on what this is.
cgroup_path: ?[]const u8 = null, cgroup_path: ?[]const u8 = null,
/// Configuration used for initializing the surface. We have to copy some
/// data since initialization is delayed with GTK (on realize).
pub const InitConfig = struct {
parent: bool = false,
pwd: ?[]const u8 = null,
pub fn init(
alloc: Allocator,
app: *App,
opts: Options,
) Allocator.Error!InitConfig {
const parent = opts.parent orelse return .{};
const pwd: ?[]const u8 = if (app.config.@"window-inherit-working-directory")
try parent.pwd(alloc)
else
null;
errdefer if (pwd) |p| alloc.free(p);
return .{
.parent = true,
.pwd = pwd,
};
}
pub fn deinit(self: *InitConfig, alloc: Allocator) void {
if (self.pwd) |pwd| alloc.free(pwd);
}
};
pub fn create(alloc: Allocator, app: *App, opts: Options) !*Surface { pub fn create(alloc: Allocator, app: *App, opts: Options) !*Surface {
var surface = try alloc.create(Surface); var surface = try alloc.create(Surface);
errdefer alloc.destroy(surface); errdefer alloc.destroy(surface);
@ -491,6 +524,10 @@ pub fn init(self: *Surface, app: *App, opts: Options) !void {
}; };
errdefer if (cgroup_path) |path| app.core_app.alloc.free(path); errdefer if (cgroup_path) |path| app.core_app.alloc.free(path);
// Build our initialization config
const init_config = try InitConfig.init(app.core_app.alloc, app, opts);
errdefer init_config.deinit(app.core_app.alloc);
// Build our result // Build our result
self.* = .{ self.* = .{
.app = app, .app = app,
@ -501,7 +538,7 @@ pub fn init(self: *Surface, app: *App, opts: Options) !void {
.title_text = null, .title_text = null,
.core_surface = undefined, .core_surface = undefined,
.font_size = font_size, .font_size = font_size,
.parent_surface = opts.parent != null, .init_config = init_config,
.size = .{ .width = 800, .height = 600 }, .size = .{ .width = 800, .height = 600 },
.cursor_pos = .{ .x = 0, .y = 0 }, .cursor_pos = .{ .x = 0, .y = 0 },
.im_context = im_context, .im_context = im_context,
@ -552,7 +589,11 @@ fn realize(self: *Surface) !void {
// Get our new surface config // Get our new surface config
var config = try apprt.surface.newConfig(self.app.core_app, &self.app.config); var config = try apprt.surface.newConfig(self.app.core_app, &self.app.config);
defer config.deinit(); defer config.deinit();
if (!self.parent_surface) {
if (self.init_config.pwd) |pwd| {
// If we have a working directory we want, then we force that.
config.@"working-directory" = pwd;
} else if (!self.init_config.parent) {
// A hack, see the "parent_surface" field for more information. // A hack, see the "parent_surface" field for more information.
config.@"working-directory" = self.app.config.@"working-directory"; config.@"working-directory" = self.app.config.@"working-directory";
} }
@ -580,6 +621,7 @@ fn realize(self: *Surface) !void {
} }
pub fn deinit(self: *Surface) void { pub fn deinit(self: *Surface) void {
self.init_config.deinit(self.app.core_app.alloc);
if (self.title_text) |title| self.app.core_app.alloc.free(title); if (self.title_text) |title| self.app.core_app.alloc.free(title);
// We don't allocate anything if we aren't realized. // We don't allocate anything if we aren't realized.
@ -643,6 +685,8 @@ pub fn redraw(self: *Surface) void {
/// Close this surface. /// Close this surface.
pub fn close(self: *Surface, processActive: bool) void { pub fn close(self: *Surface, processActive: bool) void {
self.setSplitZoom(false);
// If we're not part of a window hierarchy, we never confirm // If we're not part of a window hierarchy, we never confirm
// so we can just directly remove ourselves and exit. // so we can just directly remove ourselves and exit.
const window = self.container.window() orelse { const window = self.container.window() orelse {
@ -791,7 +835,16 @@ pub fn setInitialWindowSize(self: *const Surface, width: u32, height: u32) !void
} }
pub fn grabFocus(self: *Surface) void { pub fn grabFocus(self: *Surface) void {
if (self.container.tab()) |tab| tab.focus_child = self; if (self.container.tab()) |tab| {
// If any other surface was focused and zoomed in, set it to non zoomed in
// so that self can grab focus.
if (tab.focus_child) |focus_child| {
if (focus_child.zoomed_in and focus_child != self) {
focus_child.setSplitZoom(false);
}
}
tab.focus_child = self;
}
const widget = @as(*c.GtkWidget, @ptrCast(self.gl_area)); const widget = @as(*c.GtkWidget, @ptrCast(self.gl_area));
_ = c.gtk_widget_grab_focus(widget); _ = c.gtk_widget_grab_focus(widget);
@ -801,7 +854,7 @@ pub fn grabFocus(self: *Surface) void {
fn updateTitleLabels(self: *Surface) void { fn updateTitleLabels(self: *Surface) void {
// If we have no title, then we have nothing to update. // If we have no title, then we have nothing to update.
const title = self.title_text orelse return; const title = self.getTitle() orelse return;
// If we have a tab and are the focused child, then we have to update the tab // If we have a tab and are the focused child, then we have to update the tab
if (self.container.tab()) |tab| { if (self.container.tab()) |tab| {
@ -822,9 +875,19 @@ fn updateTitleLabels(self: *Surface) void {
} }
} }
const zoom_title_prefix = "🔍 ";
pub fn setTitle(self: *Surface, slice: [:0]const u8) !void { pub fn setTitle(self: *Surface, slice: [:0]const u8) !void {
const alloc = self.app.core_app.alloc; const alloc = self.app.core_app.alloc;
const copy = try alloc.dupeZ(u8, slice);
// Always allocate with the "🔍 " at the beginning and slice accordingly
// is the surface is zoomed in or not.
const copy: [:0]const u8 = copy: {
const new_title = try alloc.allocSentinel(u8, zoom_title_prefix.len + slice.len, 0);
@memcpy(new_title[0..zoom_title_prefix.len], zoom_title_prefix);
@memcpy(new_title[zoom_title_prefix.len..], slice);
break :copy new_title;
};
errdefer alloc.free(copy); errdefer alloc.free(copy);
if (self.title_text) |old| alloc.free(old); if (self.title_text) |old| alloc.free(old);
@ -834,7 +897,21 @@ pub fn setTitle(self: *Surface, slice: [:0]const u8) !void {
} }
pub fn getTitle(self: *Surface) ?[:0]const u8 { pub fn getTitle(self: *Surface) ?[:0]const u8 {
return self.title_text; if (self.title_text) |title_text| {
return if (self.zoomed_in)
title_text
else
title_text[zoom_title_prefix.len..];
}
return null;
}
pub fn setPwd(self: *Surface, pwd: [:0]const u8) !void {
// If we have a tab and are the focused child, then we have to update the tab
if (self.container.tab()) |tab| {
tab.setTooltipText(pwd);
}
} }
pub fn setMouseShape( pub fn setMouseShape(
@ -1875,3 +1952,41 @@ pub fn present(self: *Surface) void {
self.grabFocus(); self.grabFocus();
} }
fn detachFromSplit(self: *Surface) void {
const split = self.container.split() orelse return;
switch (self.container.splitSide() orelse unreachable) {
.top_left => split.detachTopLeft(),
.bottom_right => split.detachBottomRight(),
}
}
fn attachToSplit(self: *Surface) void {
const split = self.container.split() orelse return;
split.updateChildren();
}
pub fn setSplitZoom(self: *Surface, new_split_zoom: bool) void {
if (new_split_zoom == self.zoomed_in) return;
const tab = self.container.tab() orelse return;
const tab_widget = tab.elem.widget();
const surface_widget = self.primaryWidget();
if (new_split_zoom) {
self.detachFromSplit();
c.gtk_box_remove(tab.box, tab_widget);
c.gtk_box_append(tab.box, surface_widget);
} else {
c.gtk_box_remove(tab.box, surface_widget);
self.attachToSplit();
c.gtk_box_append(tab.box, tab_widget);
}
self.zoomed_in = new_split_zoom;
self.grabFocus();
}
pub fn toggleSplitZoom(self: *Surface) void {
self.setSplitZoom(!self.zoomed_in);
}

View File

@ -112,6 +112,10 @@ pub fn setLabelText(self: *Tab, title: [:0]const u8) void {
self.window.notebook.setTabLabel(self, title); self.window.notebook.setTabLabel(self, title);
} }
pub fn setTooltipText(self: *Tab, tooltip: [:0]const u8) void {
self.window.notebook.setTabTooltip(self, tooltip);
}
/// Remove this tab from the window. /// Remove this tab from the window.
pub fn remove(self: *Tab) void { pub fn remove(self: *Tab) void {
self.window.closeTab(self); self.window.closeTab(self);

View File

@ -92,13 +92,9 @@ pub fn init(self: *Window, app: *App) !void {
break :window window; break :window window;
} }
}; };
errdefer c.gtk_window_destroy(@ptrCast(window));
const gtk_window: *c.GtkWindow = @ptrCast(window); const gtk_window: *c.GtkWindow = @ptrCast(window);
errdefer if (self.isAdwWindow()) {
c.adw_application_window_destroy(window);
} else {
c.gtk_application_window_destroy(gtk_window);
};
self.window = gtk_window; self.window = gtk_window;
c.gtk_window_set_title(gtk_window, "Ghostty"); c.gtk_window_set_title(gtk_window, "Ghostty");
c.gtk_window_set_default_size(gtk_window, 1000, 600); c.gtk_window_set_default_size(gtk_window, 1000, 600);

View File

@ -12,7 +12,7 @@ const Config = @import("../../config.zig").Config;
/// This must be `inline` so that the comptime check noops conditional /// This must be `inline` so that the comptime check noops conditional
/// paths that are not enabled. /// paths that are not enabled.
pub inline fn enabled(config: *const Config) bool { pub inline fn enabled(config: *const Config) bool {
return build_options.libadwaita and return build_options.adwaita and
config.@"gtk-adwaita"; config.@"gtk-adwaita";
} }
@ -30,7 +30,7 @@ pub fn versionAtLeast(
comptime minor: u16, comptime minor: u16,
comptime micro: u16, comptime micro: u16,
) bool { ) bool {
if (comptime !build_options.libadwaita) return false; if (comptime !build_options.adwaita) return false;
// If our header has lower versions than the given version, // If our header has lower versions than the given version,
// we can return false immediately. This prevents us from // we can return false immediately. This prevents us from

View File

@ -1,7 +1,7 @@
/// Imported C API directly from header files /// Imported C API directly from header files
pub const c = @cImport({ pub const c = @cImport({
@cInclude("gtk/gtk.h"); @cInclude("gtk/gtk.h");
if (@import("build_options").libadwaita) { if (@import("build_options").adwaita) {
@cInclude("libadwaita-1/adwaita.h"); @cInclude("libadwaita-1/adwaita.h");
} }

View File

@ -223,6 +223,17 @@ pub const Notebook = union(enum) {
} }
} }
pub fn setTabTooltip(self: Notebook, tab: *Tab, tooltip: [:0]const u8) void {
switch (self) {
.adw_tab_view => |tab_view| {
if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable;
const page = c.adw_tab_view_get_page(tab_view, @ptrCast(tab.box));
c.adw_tab_page_set_tooltip(page, tooltip.ptr);
},
.gtk_notebook => c.gtk_widget_set_tooltip_text(@ptrCast(@alignCast(tab.label_text)), tooltip.ptr),
}
}
fn newTabInsertPosition(self: Notebook, tab: *Tab) c_int { fn newTabInsertPosition(self: Notebook, tab: *Tab) c_int {
const numPages = self.nPages(); const numPages = self.nPages();
return switch (tab.window.app.config.@"window-new-tab-position") { return switch (tab.window.app.config.@"window-new-tab-position") {

View File

@ -58,8 +58,10 @@ pub const Message = union(enum) {
/// Health status change for the renderer. /// Health status change for the renderer.
renderer_health: renderer.Health, renderer_health: renderer.Health,
/// Report the color scheme /// Report the color scheme. The bool parameter is whether to force or not.
report_color_scheme: void, /// If force is true, the color scheme should be reported even if mode
/// 2031 is not set.
report_color_scheme: bool,
/// Tell the surface to present itself to the user. This may require raising /// Tell the surface to present itself to the user. This may require raising
/// a window and switching tabs. /// a window and switching tabs.
@ -70,6 +72,15 @@ pub const Message = union(enum) {
/// unless the surface exits. /// unless the surface exits.
password_input: bool, password_input: bool,
/// A terminal color was changed using OSC sequences.
color_change: struct {
kind: terminal.osc.Command.ColorKind,
color: terminal.color.RGB,
},
/// The terminal has reported a change in the working directory.
pwd_change: WriteReq,
pub const ReportTitleStyle = enum { pub const ReportTitleStyle = enum {
csi_21_t, csi_21_t,

View File

@ -12,7 +12,7 @@ pub const fish_completions = comptimeGenerateFishCompletions();
fn comptimeGenerateFishCompletions() []const u8 { fn comptimeGenerateFishCompletions() []const u8 {
comptime { comptime {
@setEvalBranchQuota(17000); @setEvalBranchQuota(18000);
var counter = std.io.countingWriter(std.io.null_writer); var counter = std.io.countingWriter(std.io.null_writer);
try writeFishCompletions(&counter.writer()); try writeFishCompletions(&counter.writer());
@ -53,7 +53,7 @@ fn writeFishCompletions(writer: anytype) !void {
if (std.mem.startsWith(u8, field.name, "font-family")) if (std.mem.startsWith(u8, field.name, "font-family"))
try writer.writeAll(" -f -a \"(ghostty +list-fonts | grep '^[A-Z]')\"") try writer.writeAll(" -f -a \"(ghostty +list-fonts | grep '^[A-Z]')\"")
else if (std.mem.eql(u8, "theme", field.name)) else if (std.mem.eql(u8, "theme", field.name))
try writer.writeAll(" -f -a \"(ghostty +list-themes)\"") try writer.writeAll(" -f -a \"(ghostty +list-themes | sed -E 's/^(.*) \\(.*\\$/\\1/')\"")
else if (std.mem.eql(u8, "working-directory", field.name)) else if (std.mem.eql(u8, "working-directory", field.name))
try writer.writeAll(" -f -k -a \"(__fish_complete_directories)\"") try writer.writeAll(" -f -k -a \"(__fish_complete_directories)\"")
else { else {

View File

@ -26,7 +26,7 @@ pub fn genConfig(writer: anytype, cli: bool) !void {
\\ \\
); );
@setEvalBranchQuota(2000); @setEvalBranchQuota(3000);
inline for (@typeInfo(Config).Struct.fields) |field| { inline for (@typeInfo(Config).Struct.fields) |field| {
if (field.name[0] == '_') continue; if (field.name[0] == '_') continue;

View File

@ -21,7 +21,7 @@ const WasmTarget = @import("os/wasm/target.zig").Target;
pub const BuildConfig = struct { pub const BuildConfig = struct {
version: std.SemanticVersion = .{ .major = 0, .minor = 0, .patch = 0 }, version: std.SemanticVersion = .{ .major = 0, .minor = 0, .patch = 0 },
flatpak: bool = false, flatpak: bool = false,
libadwaita: bool = false, adwaita: bool = false,
app_runtime: apprt.Runtime = .none, app_runtime: apprt.Runtime = .none,
renderer: rendererpkg.Impl = .opengl, renderer: rendererpkg.Impl = .opengl,
font_backend: font.Backend = .freetype, font_backend: font.Backend = .freetype,
@ -40,7 +40,7 @@ pub const BuildConfig = struct {
// We need to break these down individual because addOption doesn't // We need to break these down individual because addOption doesn't
// support all types. // support all types.
step.addOption(bool, "flatpak", self.flatpak); step.addOption(bool, "flatpak", self.flatpak);
step.addOption(bool, "libadwaita", self.libadwaita); step.addOption(bool, "adwaita", self.adwaita);
step.addOption(apprt.Runtime, "app_runtime", self.app_runtime); step.addOption(apprt.Runtime, "app_runtime", self.app_runtime);
step.addOption(font.Backend, "font_backend", self.font_backend); step.addOption(font.Backend, "font_backend", self.font_backend);
step.addOption(rendererpkg.Impl, "renderer", self.renderer); step.addOption(rendererpkg.Impl, "renderer", self.renderer);
@ -67,7 +67,7 @@ pub const BuildConfig = struct {
return .{ return .{
.version = options.app_version, .version = options.app_version,
.flatpak = options.flatpak, .flatpak = options.flatpak,
.libadwaita = options.libadwaita, .adwaita = options.adwaita,
.app_runtime = std.meta.stringToEnum(apprt.Runtime, @tagName(options.app_runtime)).?, .app_runtime = std.meta.stringToEnum(apprt.Runtime, @tagName(options.app_runtime)).?,
.font_backend = std.meta.stringToEnum(font.Backend, @tagName(options.font_backend)).?, .font_backend = std.meta.stringToEnum(font.Backend, @tagName(options.font_backend)).?,
.renderer = std.meta.stringToEnum(rendererpkg.Impl, @tagName(options.renderer)).?, .renderer = std.meta.stringToEnum(rendererpkg.Impl, @tagName(options.renderer)).?,

View File

@ -71,6 +71,13 @@ pub const Action = enum {
var pending_help: bool = false; var pending_help: bool = false;
var pending: ?Action = null; var pending: ?Action = null;
while (iter.next()) |arg| { while (iter.next()) |arg| {
// If we see a "-e" and we haven't seen a command yet, then
// we are done looking for commands. This special case enables
// `ghostty -e ghostty +command`. If we've seen a command we
// still want to keep looking because
// `ghostty +command -e +command` is invalid.
if (std.mem.eql(u8, arg, "-e") and pending == null) return null;
// Special case, --version always outputs the version no // Special case, --version always outputs the version no
// matter what, no matter what other args exist. // matter what, no matter what other args exist.
if (std.mem.eql(u8, arg, "--version")) return .version; if (std.mem.eql(u8, arg, "--version")) return .version;
@ -240,3 +247,30 @@ test "parse action plus" {
try testing.expect(action.? == .version); try testing.expect(action.? == .version);
} }
} }
test "parse action plus ignores -e" {
const testing = std.testing;
const alloc = testing.allocator;
{
var iter = try std.process.ArgIteratorGeneral(.{}).init(
alloc,
"--a=42 -e +version",
);
defer iter.deinit();
const action = try Action.detectIter(&iter);
try testing.expect(action == null);
}
{
var iter = try std.process.ArgIteratorGeneral(.{}).init(
alloc,
"+list-fonts --a=42 -e +version",
);
defer iter.deinit();
try testing.expectError(
Action.Error.MultipleActions,
Action.detectIter(&iter),
);
}
}

View File

@ -13,7 +13,7 @@ const DiagnosticList = diags.DiagnosticList;
// `--long value`? Not currently allowed. // `--long value`? Not currently allowed.
// For trimming // For trimming
const whitespace = " \t"; pub const whitespace = " \t";
/// The base errors for arg parsing. Additional errors can be returned due /// The base errors for arg parsing. Additional errors can be returned due
/// to type-specific parsing but these are always possible. /// to type-specific parsing but these are always possible.
@ -132,8 +132,8 @@ pub fn parse(
// track more error messages. // track more error messages.
error.OutOfMemory => return err, error.OutOfMemory => return err,
error.InvalidField => "unknown field", error.InvalidField => "unknown field",
error.ValueRequired => "value required", error.ValueRequired => formatValueRequired(T, arena_alloc, key) catch "value required",
error.InvalidValue => "invalid value", error.InvalidValue => formatInvalidValue(T, arena_alloc, key, value) catch "invalid value",
else => try std.fmt.allocPrintZ( else => try std.fmt.allocPrintZ(
arena_alloc, arena_alloc,
"unknown error {}", "unknown error {}",
@ -151,6 +151,54 @@ pub fn parse(
} }
} }
fn formatValueRequired(
comptime T: type,
arena_alloc: std.mem.Allocator,
key: []const u8,
) std.mem.Allocator.Error![:0]const u8 {
var buf = std.ArrayList(u8).init(arena_alloc);
errdefer buf.deinit();
const writer = buf.writer();
try writer.print("value required", .{});
try formatValues(T, key, writer);
try writer.writeByte(0);
return buf.items[0 .. buf.items.len - 1 :0];
}
fn formatInvalidValue(
comptime T: type,
arena_alloc: std.mem.Allocator,
key: []const u8,
value: ?[]const u8,
) std.mem.Allocator.Error![:0]const u8 {
var buf = std.ArrayList(u8).init(arena_alloc);
errdefer buf.deinit();
const writer = buf.writer();
try writer.print("invalid value \"{?s}\"", .{value});
try formatValues(T, key, writer);
try writer.writeByte(0);
return buf.items[0 .. buf.items.len - 1 :0];
}
fn formatValues(comptime T: type, key: []const u8, writer: anytype) std.mem.Allocator.Error!void {
const typeinfo = @typeInfo(T);
inline for (typeinfo.Struct.fields) |f| {
if (std.mem.eql(u8, key, f.name)) {
switch (@typeInfo(f.type)) {
.Enum => |e| {
try writer.print(", valid values are: ", .{});
inline for (e.fields, 0..) |field, i| {
if (i != 0) try writer.print(", ", .{});
try writer.print("{s}", .{field.name});
}
},
else => {},
}
break;
}
}
}
/// Returns true if this type can track diagnostics. /// Returns true if this type can track diagnostics.
fn canTrackDiags(comptime T: type) bool { fn canTrackDiags(comptime T: type) bool {
return @hasField(T, "_diagnostics"); return @hasField(T, "_diagnostics");
@ -161,7 +209,7 @@ fn canTrackDiags(comptime T: type) bool {
/// This may result in allocations. The allocations can only be freed by freeing /// This may result in allocations. The allocations can only be freed by freeing
/// all the memory associated with alloc. It is expected that alloc points to /// all the memory associated with alloc. It is expected that alloc points to
/// an arena. /// an arena.
fn parseIntoField( pub fn parseIntoField(
comptime T: type, comptime T: type,
alloc: Allocator, alloc: Allocator,
dst: *T, dst: *T,
@ -202,10 +250,46 @@ fn parseIntoField(
1 => @field(dst, field.name) = try Field.parseCLI(value), 1 => @field(dst, field.name) = try Field.parseCLI(value),
// 2 arg = (self, input) => void // 2 arg = (self, input) => void
2 => try @field(dst, field.name).parseCLI(value), 2 => switch (@typeInfo(field.type)) {
.Struct,
.Union,
.Enum,
=> try @field(dst, field.name).parseCLI(value),
// If the field is optional and set, then we use
// the pointer value directly into it. If its not
// set we need to create a new instance.
.Optional => if (@field(dst, field.name)) |*v| {
try v.parseCLI(value);
} else {
// Note: you cannot do @field(dst, name) = undefined
// because this causes the value to be "null"
// in ReleaseFast modes.
var tmp: Field = undefined;
try tmp.parseCLI(value);
@field(dst, field.name) = tmp;
},
else => @compileError("unexpected field type"),
},
// 3 arg = (self, alloc, input) => void // 3 arg = (self, alloc, input) => void
3 => try @field(dst, field.name).parseCLI(alloc, value), 3 => switch (@typeInfo(field.type)) {
.Struct,
.Union,
.Enum,
=> try @field(dst, field.name).parseCLI(alloc, value),
.Optional => if (@field(dst, field.name)) |*v| {
try v.parseCLI(alloc, value);
} else {
var tmp: Field = undefined;
try tmp.parseCLI(alloc, value);
@field(dst, field.name) = tmp;
},
else => @compileError("unexpected field type"),
},
else => @compileError("parseCLI invalid argument count"), else => @compileError("parseCLI invalid argument count"),
} }
@ -262,8 +346,9 @@ fn parseIntoField(
value orelse return error.ValueRequired, value orelse return error.ValueRequired,
) orelse return error.InvalidValue, ) orelse return error.InvalidValue,
.Struct => try parsePackedStruct( .Struct => try parseStruct(
Field, Field,
alloc,
value orelse return error.ValueRequired, value orelse return error.ValueRequired,
), ),
@ -330,9 +415,79 @@ fn parseTaggedUnion(comptime T: type, alloc: Allocator, v: []const u8) !T {
return error.InvalidValue; return error.InvalidValue;
} }
fn parseStruct(comptime T: type, alloc: Allocator, v: []const u8) !T {
return switch (@typeInfo(T).Struct.layout) {
.auto => parseAutoStruct(T, alloc, v),
.@"packed" => parsePackedStruct(T, v),
else => @compileError("unsupported struct layout"),
};
}
pub fn parseAutoStruct(comptime T: type, alloc: Allocator, v: []const u8) !T {
const info = @typeInfo(T).Struct;
comptime assert(info.layout == .auto);
// We start our result as undefined so we don't get an error for required
// fields. We track required fields below and we validate that we set them
// all at the bottom of this function (in addition to setting defaults for
// optionals).
var result: T = undefined;
// Keep track of which fields were set so we can error if a required
// field was not set.
const FieldSet = std.StaticBitSet(info.fields.len);
var fields_set: FieldSet = FieldSet.initEmpty();
// We split each value by ","
var iter = std.mem.splitSequence(u8, v, ",");
loop: while (iter.next()) |entry| {
// Find the key/value, trimming whitespace. The value may be quoted
// which we strip the quotes from.
const idx = mem.indexOf(u8, entry, ":") orelse return error.InvalidValue;
const key = std.mem.trim(u8, entry[0..idx], whitespace);
const value = value: {
var value = std.mem.trim(u8, entry[idx + 1 ..], whitespace);
// Detect a quoted string.
if (value.len >= 2 and
value[0] == '"' and
value[value.len - 1] == '"')
{
// Trim quotes since our CLI args processor expects
// quotes to already be gone.
value = value[1 .. value.len - 1];
}
break :value value;
};
inline for (info.fields, 0..) |field, i| {
if (std.mem.eql(u8, field.name, key)) {
try parseIntoField(T, alloc, &result, key, value);
fields_set.set(i);
continue :loop;
}
}
// No field matched
return error.InvalidValue;
}
// Ensure all required fields are set
inline for (info.fields, 0..) |field, i| {
if (!fields_set.isSet(i)) {
const default_ptr = field.default_value orelse return error.InvalidValue;
const typed_ptr: *const field.type = @alignCast(@ptrCast(default_ptr));
@field(result, field.name) = typed_ptr.*;
}
}
return result;
}
fn parsePackedStruct(comptime T: type, v: []const u8) !T { fn parsePackedStruct(comptime T: type, v: []const u8) !T {
const info = @typeInfo(T).Struct; const info = @typeInfo(T).Struct;
assert(info.layout == .@"packed"); comptime assert(info.layout == .@"packed");
var result: T = .{}; var result: T = .{};
@ -799,6 +954,62 @@ test "parseIntoField: struct with parse func" {
try testing.expectEqual(@as([]const u8, "HELLO!"), data.a.v); try testing.expectEqual(@as([]const u8, "HELLO!"), data.a.v);
} }
test "parseIntoField: optional struct with parse func" {
const testing = std.testing;
var arena = ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
var data: struct {
a: ?struct {
const Self = @This();
v: []const u8,
pub fn parseCLI(self: *Self, _: Allocator, value: ?[]const u8) !void {
_ = value;
self.* = .{ .v = "HELLO!" };
}
} = null,
} = .{};
try parseIntoField(@TypeOf(data), alloc, &data, "a", "42");
try testing.expectEqual(@as([]const u8, "HELLO!"), data.a.?.v);
}
test "parseIntoField: struct with basic fields" {
const testing = std.testing;
var arena = ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
var data: struct {
value: struct {
a: []const u8,
b: u32,
c: u8 = 12,
} = undefined,
} = .{};
// Set required fields
try parseIntoField(@TypeOf(data), alloc, &data, "value", "a:hello,b:42");
try testing.expectEqualStrings("hello", data.value.a);
try testing.expectEqual(42, data.value.b);
try testing.expectEqual(12, data.value.c);
// Set all fields
try parseIntoField(@TypeOf(data), alloc, &data, "value", "a:world,b:84,c:24");
try testing.expectEqualStrings("world", data.value.a);
try testing.expectEqual(84, data.value.b);
try testing.expectEqual(24, data.value.c);
// Missing require dfield
try testing.expectError(
error.InvalidValue,
parseIntoField(@TypeOf(data), alloc, &data, "value", "a:hello"),
);
}
test "parseIntoField: tagged union" { test "parseIntoField: tagged union" {
const testing = std.testing; const testing = std.testing;
var arena = ArenaAllocator.init(testing.allocator); var arena = ArenaAllocator.init(testing.allocator);

View File

@ -111,7 +111,7 @@ fn prettyPrint(alloc: Allocator, keybinds: Config.Keybinds) !u8 {
// align things nicely // align things nicely
var iter = keybinds.set.bindings.iterator(); var iter = keybinds.set.bindings.iterator();
var bindings = std.ArrayList(Binding).init(alloc); var bindings = std.ArrayList(Binding).init(alloc);
var widest_key: usize = 0; var widest_key: u16 = 0;
var buf: [64]u8 = undefined; var buf: [64]u8 = undefined;
while (iter.next()) |bind| { while (iter.next()) |bind| {
const action = switch (bind.value_ptr.*) { const action = switch (bind.value_ptr.*) {
@ -134,7 +134,7 @@ fn prettyPrint(alloc: Allocator, keybinds: Config.Keybinds) !u8 {
const alt_style: vaxis.Style = .{ .fg = .{ .index = 3 } }; const alt_style: vaxis.Style = .{ .fg = .{ .index = 3 } };
const shift_style: vaxis.Style = .{ .fg = .{ .index = 4 } }; const shift_style: vaxis.Style = .{ .fg = .{ .index = 4 } };
var longest_col: usize = 0; var longest_col: u16 = 0;
// Print the list // Print the list
for (bindings.items) |bind| { for (bindings.items) |bind| {
@ -143,20 +143,20 @@ fn prettyPrint(alloc: Allocator, keybinds: Config.Keybinds) !u8 {
var result: vaxis.Window.PrintResult = .{ .col = 0, .row = 0, .overflow = false }; var result: vaxis.Window.PrintResult = .{ .col = 0, .row = 0, .overflow = false };
const trigger = bind.trigger; const trigger = bind.trigger;
if (trigger.mods.super) { if (trigger.mods.super) {
result = try win.printSegment(.{ .text = "super", .style = super_style }, .{ .col_offset = result.col }); result = win.printSegment(.{ .text = "super", .style = super_style }, .{ .col_offset = result.col });
result = try win.printSegment(.{ .text = " + " }, .{ .col_offset = result.col }); result = win.printSegment(.{ .text = " + " }, .{ .col_offset = result.col });
} }
if (trigger.mods.ctrl) { if (trigger.mods.ctrl) {
result = try win.printSegment(.{ .text = "ctrl ", .style = ctrl_style }, .{ .col_offset = result.col }); result = win.printSegment(.{ .text = "ctrl ", .style = ctrl_style }, .{ .col_offset = result.col });
result = try win.printSegment(.{ .text = " + " }, .{ .col_offset = result.col }); result = win.printSegment(.{ .text = " + " }, .{ .col_offset = result.col });
} }
if (trigger.mods.alt) { if (trigger.mods.alt) {
result = try win.printSegment(.{ .text = "alt ", .style = alt_style }, .{ .col_offset = result.col }); result = win.printSegment(.{ .text = "alt ", .style = alt_style }, .{ .col_offset = result.col });
result = try win.printSegment(.{ .text = " + " }, .{ .col_offset = result.col }); result = win.printSegment(.{ .text = " + " }, .{ .col_offset = result.col });
} }
if (trigger.mods.shift) { if (trigger.mods.shift) {
result = try win.printSegment(.{ .text = "shift", .style = shift_style }, .{ .col_offset = result.col }); result = win.printSegment(.{ .text = "shift", .style = shift_style }, .{ .col_offset = result.col });
result = try win.printSegment(.{ .text = " + " }, .{ .col_offset = result.col }); result = win.printSegment(.{ .text = " + " }, .{ .col_offset = result.col });
} }
const key = switch (trigger.key) { const key = switch (trigger.key) {
@ -166,20 +166,20 @@ fn prettyPrint(alloc: Allocator, keybinds: Config.Keybinds) !u8 {
}; };
// We don't track the key print because we index the action off the *widest* key so we get // We don't track the key print because we index the action off the *widest* key so we get
// nice alignment no matter what was printed for mods // nice alignment no matter what was printed for mods
_ = try win.printSegment(.{ .text = key }, .{ .col_offset = result.col }); _ = win.printSegment(.{ .text = key }, .{ .col_offset = result.col });
if (longest_col < result.col) longest_col = result.col; if (longest_col < result.col) longest_col = result.col;
const action = try std.fmt.allocPrint(alloc, "{}", .{bind.action}); const action = try std.fmt.allocPrint(alloc, "{}", .{bind.action});
// If our action has an argument, we print the argument in a different color // If our action has an argument, we print the argument in a different color
if (std.mem.indexOfScalar(u8, action, ':')) |idx| { if (std.mem.indexOfScalar(u8, action, ':')) |idx| {
_ = try win.print(&.{ _ = win.print(&.{
.{ .text = action[0..idx] }, .{ .text = action[0..idx] },
.{ .text = action[idx .. idx + 1], .style = .{ .dim = true } }, .{ .text = action[idx .. idx + 1], .style = .{ .dim = true } },
.{ .text = action[idx + 1 ..], .style = .{ .fg = .{ .index = 5 } } }, .{ .text = action[idx + 1 ..], .style = .{ .fg = .{ .index = 5 } } },
}, .{ .col_offset = longest_col + widest_key + 2 }); }, .{ .col_offset = longest_col + widest_key + 2 });
} else { } else {
_ = try win.printSegment(.{ .text = action }, .{ .col_offset = longest_col + widest_key + 2 }); _ = win.printSegment(.{ .text = action }, .{ .col_offset = longest_col + widest_key + 2 });
} }
try vx.prettyPrint(writer); try vx.prettyPrint(writer);
} }

View File

@ -256,7 +256,10 @@ const Preview = struct {
fn updateFiltered(self: *Preview) !void { fn updateFiltered(self: *Preview) !void {
const relative = self.current -| self.window; const relative = self.current -| self.window;
const selected = self.themes[self.filtered.items[self.current]].theme; var selected: []const u8 = undefined;
if (self.filtered.items.len > 0) {
selected = self.themes[self.filtered.items[self.current]].theme;
}
const hash_algorithm = std.hash.Wyhash; const hash_algorithm = std.hash.Wyhash;
@ -289,9 +292,11 @@ const Preview = struct {
while (it.next()) |token| try tokens.append(token); while (it.next()) |token| try tokens.append(token);
for (self.themes, 0..) |*theme, i| { for (self.themes, 0..) |*theme, i| {
theme.rank = zf.rank(theme.theme, tokens.items, false, true); theme.rank = zf.rank(theme.theme, tokens.items, .{
if (theme.rank) |_| .to_lower = true,
try self.filtered.append(i); .plain = true,
});
if (theme.rank != null) try self.filtered.append(i);
} }
} else { } else {
for (self.themes, 0..) |*theme, i| { for (self.themes, 0..) |*theme, i| {
@ -382,8 +387,8 @@ const Preview = struct {
self.tty.anyWriter(), self.tty.anyWriter(),
self.themes[self.filtered.items[self.current]].theme, self.themes[self.filtered.items[self.current]].theme,
alloc, alloc,
); )
if (key.matches('c', .{ .shift = true })) else if (key.matches('c', .{ .shift = true }))
try self.vx.copyToSystemClipboard( try self.vx.copyToSystemClipboard(
self.tty.anyWriter(), self.tty.anyWriter(),
self.themes[self.filtered.items[self.current]].path, self.themes[self.filtered.items[self.current]].path,
@ -498,8 +503,8 @@ const Preview = struct {
const theme_list = win.child(.{ const theme_list = win.child(.{
.x_off = 0, .x_off = 0,
.y_off = 0, .y_off = 0,
.width = .{ .limit = 32 }, .width = 32,
.height = .{ .limit = win.height }, .height = win.height,
}); });
var highlight: ?usize = null; var highlight: ?usize = null;
@ -541,7 +546,8 @@ const Preview = struct {
theme_list.fill(.{ .style = self.ui_standard() }); theme_list.fill(.{ .style = self.ui_standard() });
for (0..theme_list.height) |row| { for (0..theme_list.height) |row_capture| {
const row: u16 = @intCast(row_capture);
const index = self.window + row; const index = self.window + row;
if (index >= self.filtered.items.len) break; if (index >= self.filtered.items.len) break;
@ -554,7 +560,7 @@ const Preview = struct {
}; };
if (style == .selected) { if (style == .selected) {
_ = try theme_list.printSegment( _ = theme_list.printSegment(
.{ .{
.text = " ", .text = " ",
.style = self.ui_selected(), .style = self.ui_selected(),
@ -565,7 +571,7 @@ const Preview = struct {
}, },
); );
} }
_ = try theme_list.printSegment( _ = theme_list.printSegment(
.{ .{
.text = theme.theme, .text = theme.theme,
.style = switch (style) { .style = switch (style) {
@ -584,8 +590,9 @@ const Preview = struct {
); );
if (style == .selected) { if (style == .selected) {
if (theme.theme.len < theme_list.width - 4) { if (theme.theme.len < theme_list.width - 4) {
for (2 + theme.theme.len..theme_list.width - 2) |i| for (2 + theme.theme.len..theme_list.width - 2) |i_capture| {
_ = try theme_list.printSegment( const i: u16 = @intCast(i_capture);
_ = theme_list.printSegment(
.{ .{
.text = " ", .text = " ",
.style = self.ui_selected(), .style = self.ui_selected(),
@ -596,7 +603,8 @@ const Preview = struct {
}, },
); );
} }
_ = try theme_list.printSegment( }
_ = theme_list.printSegment(
.{ .{
.text = " ", .text = " ",
.style = self.ui_selected(), .style = self.ui_selected(),
@ -623,12 +631,8 @@ const Preview = struct {
.{ .{
.x_off = win.width / 2 -| width / 2, .x_off = win.width / 2 -| width / 2,
.y_off = win.height / 2 -| height / 2, .y_off = win.height / 2 -| height / 2,
.width = .{ .width = width,
.limit = width, .height = height,
},
.height = .{
.limit = height,
},
.border = .{ .border = .{
.where = .all, .where = .all,
.style = self.ui_standard(), .style = self.ui_standard(),
@ -658,8 +662,9 @@ const Preview = struct {
.{ .keys = "", .help = "Close search window." }, .{ .keys = "", .help = "Close search window." },
}; };
for (key_help, 0..) |help, i| { for (key_help, 0..) |help, captured_i| {
_ = try child.printSegment( const i: u16 = @intCast(captured_i);
_ = child.printSegment(
.{ .{
.text = help.keys, .text = help.keys,
.style = self.ui_standard(), .style = self.ui_standard(),
@ -669,7 +674,7 @@ const Preview = struct {
.col_offset = 2, .col_offset = 2,
}, },
); );
_ = try child.printSegment( _ = child.printSegment(
.{ .{
.text = "", .text = "",
.style = self.ui_standard(), .style = self.ui_standard(),
@ -679,7 +684,7 @@ const Preview = struct {
.col_offset = 15, .col_offset = 15,
}, },
); );
_ = try child.printSegment( _ = child.printSegment(
.{ .{
.text = help.help, .text = help.help,
.style = self.ui_standard(), .style = self.ui_standard(),
@ -695,12 +700,8 @@ const Preview = struct {
const child = win.child(.{ const child = win.child(.{
.x_off = 20, .x_off = 20,
.y_off = win.height - 5, .y_off = win.height - 5,
.width = .{ .width = win.width - 40,
.limit = win.width - 40, .height = 3,
},
.height = .{
.limit = 3,
},
.border = .{ .border = .{
.where = .all, .where = .all,
.style = self.ui_standard(), .style = self.ui_standard(),
@ -712,44 +713,45 @@ const Preview = struct {
} }
} }
pub fn drawPreview(self: *Preview, alloc: std.mem.Allocator, win: vaxis.Window, x_off: usize) !void { pub fn drawPreview(self: *Preview, alloc: std.mem.Allocator, win: vaxis.Window, x_off_unconverted: i17) !void {
const width = win.width - x_off; const x_off: u16 = @intCast(x_off_unconverted);
const width: u16 = win.width - x_off;
if (self.filtered.items.len > 0) {
const theme = self.themes[self.filtered.items[self.current]]; const theme = self.themes[self.filtered.items[self.current]];
var config = try Config.default(alloc); var config = try Config.default(alloc);
defer config.deinit(); defer config.deinit();
config.loadFile(config._arena.?.allocator(), theme.path) catch |err| { config.loadFile(config._arena.?.allocator(), theme.path) catch |err| {
const theme_path_len: u16 = @intCast(theme.path.len);
const child = win.child( const child = win.child(
.{ .{
.x_off = x_off, .x_off = x_off,
.y_off = 0, .y_off = 0,
.width = .{ .width = width,
.limit = width, .height = win.height,
},
.height = .{
.limit = win.height,
},
}, },
); );
child.fill(.{ .style = self.ui_standard() }); child.fill(.{ .style = self.ui_standard() });
const middle = child.height / 2; const middle = child.height / 2;
{ {
const text = try std.fmt.allocPrint(alloc, "Unable to open {s} from:", .{theme.theme}); const text = try std.fmt.allocPrint(alloc, "Unable to open {s} from:", .{theme.theme});
_ = try child.printSegment( const text_len: u16 = @intCast(text.len);
_ = child.printSegment(
.{ .{
.text = text, .text = text,
.style = self.ui_err(), .style = self.ui_err(),
}, },
.{ .{
.row_offset = middle -| 1, .row_offset = middle -| 1,
.col_offset = child.width / 2 -| text.len / 2, .col_offset = child.width / 2 -| text_len / 2,
}, },
); );
} }
{ {
_ = try child.printSegment( _ = child.printSegment(
.{ .{
.text = theme.path, .text = theme.path,
.style = self.ui_err(), .style = self.ui_err(),
@ -759,27 +761,28 @@ const Preview = struct {
}, },
.{ .{
.row_offset = middle, .row_offset = middle,
.col_offset = child.width / 2 -| theme.path.len / 2, .col_offset = child.width / 2 -| theme_path_len / 2,
}, },
); );
} }
{ {
const text = try std.fmt.allocPrint(alloc, "{}", .{err}); const text = try std.fmt.allocPrint(alloc, "{}", .{err});
_ = try child.printSegment( const text_len: u16 = @intCast(text.len);
_ = child.printSegment(
.{ .{
.text = text, .text = text,
.style = self.ui_err(), .style = self.ui_err(),
}, },
.{ .{
.row_offset = middle + 1, .row_offset = middle + 1,
.col_offset = child.width / 2 -| text.len / 2, .col_offset = child.width / 2 -| text_len / 2,
}, },
); );
} }
return; return;
}; };
var next_start: usize = 0; var next_start: u16 = 0;
const fg: vaxis.Color = .{ const fg: vaxis.Color = .{
.rgb = [_]u8{ .rgb = [_]u8{
@ -842,20 +845,18 @@ const Preview = struct {
}; };
{ {
const theme_len: u16 = @intCast(theme.theme.len);
const theme_path_len: u16 = @intCast(theme.path.len);
const child = win.child( const child = win.child(
.{ .{
.x_off = x_off, .x_off = x_off,
.y_off = next_start, .y_off = next_start,
.width = .{ .width = width,
.limit = width, .height = 4,
},
.height = .{
.limit = 4,
},
}, },
); );
child.fill(.{ .style = standard }); child.fill(.{ .style = standard });
_ = try child.printSegment( _ = child.printSegment(
.{ .{
.text = theme.theme, .text = theme.theme,
.style = standard_bold_italic, .style = standard_bold_italic,
@ -865,10 +866,10 @@ const Preview = struct {
}, },
.{ .{
.row_offset = 1, .row_offset = 1,
.col_offset = child.width / 2 -| theme.theme.len / 2, .col_offset = child.width / 2 -| theme_len / 2,
}, },
); );
_ = try child.printSegment( _ = child.printSegment(
.{ .{
.text = theme.path, .text = theme.path,
.style = standard, .style = standard,
@ -878,7 +879,7 @@ const Preview = struct {
}, },
.{ .{
.row_offset = 2, .row_offset = 2,
.col_offset = child.width / 2 -| theme.path.len / 2, .col_offset = child.width / 2 -| theme_path_len / 2,
.wrap = .none, .wrap = .none,
}, },
); );
@ -886,37 +887,36 @@ const Preview = struct {
} }
if (config._diagnostics.items().len > 0) { if (config._diagnostics.items().len > 0) {
const diagnostic_items_len: u16 = @intCast(config._diagnostics.items().len);
const child = win.child( const child = win.child(
.{ .{
.x_off = x_off, .x_off = x_off,
.y_off = next_start, .y_off = next_start,
.width = .{ .width = width,
.limit = width, .height = if (config._diagnostics.items().len == 0) 0 else 2 + diagnostic_items_len,
},
.height = .{
.limit = if (config._diagnostics.items().len == 0) 0 else 2 + config._diagnostics.items().len,
},
}, },
); );
{ {
const text = "Problems were encountered trying to load the theme:"; const text = "Problems were encountered trying to load the theme:";
_ = try child.printSegment( const text_len: u16 = @intCast(text.len);
_ = child.printSegment(
.{ .{
.text = text, .text = text,
.style = self.ui_err(), .style = self.ui_err(),
}, },
.{ .{
.row_offset = 0, .row_offset = 0,
.col_offset = child.width / 2 -| (text.len / 2), .col_offset = child.width / 2 -| (text_len / 2),
}, },
); );
} }
var buf = std.ArrayList(u8).init(alloc); var buf = std.ArrayList(u8).init(alloc);
defer buf.deinit(); defer buf.deinit();
for (config._diagnostics.items(), 0..) |diag, i| { for (config._diagnostics.items(), 0..) |diag, captured_i| {
const i: u16 = @intCast(captured_i);
try diag.write(buf.writer()); try diag.write(buf.writer());
_ = try child.printSegment( _ = child.printSegment(
.{ .{
.text = buf.items, .text = buf.items,
.style = self.ui_err(), .style = self.ui_err(),
@ -934,24 +934,21 @@ const Preview = struct {
const child = win.child(.{ const child = win.child(.{
.x_off = x_off, .x_off = x_off,
.y_off = next_start, .y_off = next_start,
.width = .{ .width = width,
.limit = width, .height = 6,
},
.height = .{
.limit = 6,
},
}); });
child.fill(.{ .style = standard }); child.fill(.{ .style = standard });
for (0..16) |i| { for (0..16) |captured_i| {
const i: u16 = @intCast(captured_i);
const r = i / 8; const r = i / 8;
const c = i % 8; const c = i % 8;
const text = if (self.hex) const text = if (self.hex)
try std.fmt.allocPrint(alloc, " {x:0>2}", .{i}) try std.fmt.allocPrint(alloc, " {x:0>2}", .{i})
else else
try std.fmt.allocPrint(alloc, "{d:3}", .{i}); try std.fmt.allocPrint(alloc, "{d:3}", .{i});
_ = try child.printSegment( _ = child.printSegment(
.{ .{
.text = text, .text = text,
.style = standard, .style = standard,
@ -961,7 +958,7 @@ const Preview = struct {
.col_offset = c * 8, .col_offset = c * 8,
}, },
); );
_ = try child.printSegment( _ = child.printSegment(
.{ .{
.text = "████", .text = "████",
.style = .{ .style = .{
@ -974,7 +971,7 @@ const Preview = struct {
.col_offset = 4 + c * 8, .col_offset = 4 + c * 8,
}, },
); );
_ = try child.printSegment( _ = child.printSegment(
.{ .{
.text = "████", .text = "████",
.style = .{ .style = .{
@ -995,12 +992,8 @@ const Preview = struct {
.{ .{
.x_off = x_off, .x_off = x_off,
.y_off = next_start, .y_off = next_start,
.width = .{ .width = width,
.limit = width, .height = 24,
},
.height = .{
.limit = 24,
},
}, },
); );
const bold: vaxis.Style = .{ const bold: vaxis.Style = .{
@ -1050,7 +1043,7 @@ const Preview = struct {
.bg = bg, .bg = bg,
}; };
child.fill(.{ .style = standard }); child.fill(.{ .style = standard });
_ = try child.print( _ = child.print(
&.{ &.{
.{ .text = "", .style = color2 }, .{ .text = "", .style = color2 },
.{ .text = " ", .style = standard }, .{ .text = " ", .style = standard },
@ -1064,7 +1057,7 @@ const Preview = struct {
}, },
); );
{ {
_ = try child.print( _ = child.print(
&.{ &.{
.{ .{
.text = "───────┬", .text = "───────┬",
@ -1077,8 +1070,9 @@ const Preview = struct {
}, },
); );
if (child.width > 10) { if (child.width > 10) {
for (10..child.width) |col| { for (10..child.width) |captured_col| {
_ = try child.print( const col: u16 = @intCast(captured_col);
_ = child.print(
&.{ &.{
.{ .{
.text = "", .text = "",
@ -1093,7 +1087,7 @@ const Preview = struct {
} }
} }
} }
_ = try child.print( _ = child.print(
&.{ &.{
.{ .{
.text = "", .text = "",
@ -1116,7 +1110,7 @@ const Preview = struct {
}, },
); );
{ {
_ = try child.print( _ = child.print(
&.{ &.{
.{ .{
.text = "───────┼", .text = "───────┼",
@ -1129,8 +1123,9 @@ const Preview = struct {
}, },
); );
if (child.width > 10) { if (child.width > 10) {
for (10..child.width) |col| { for (10..child.width) |captured_col| {
_ = try child.print( const col: u16 = @intCast(captured_col);
_ = child.print(
&.{ &.{
.{ .{
.text = "", .text = "",
@ -1145,7 +1140,7 @@ const Preview = struct {
} }
} }
} }
_ = try child.print( _ = child.print(
&.{ &.{
.{ .text = " 1 │ ", .style = color238 }, .{ .text = " 1 │ ", .style = color238 },
.{ .text = "const", .style = color5 }, .{ .text = "const", .style = color5 },
@ -1160,7 +1155,7 @@ const Preview = struct {
.col_offset = 2, .col_offset = 2,
}, },
); );
_ = try child.print( _ = child.print(
&.{ &.{
.{ .text = " 2 │", .style = color238 }, .{ .text = " 2 │", .style = color238 },
}, },
@ -1169,7 +1164,7 @@ const Preview = struct {
.col_offset = 2, .col_offset = 2,
}, },
); );
_ = try child.print( _ = child.print(
&.{ &.{
.{ .text = " 3 │ ", .style = color238 }, .{ .text = " 3 │ ", .style = color238 },
.{ .text = "pub ", .style = color5 }, .{ .text = "pub ", .style = color5 },
@ -1185,7 +1180,7 @@ const Preview = struct {
.col_offset = 2, .col_offset = 2,
}, },
); );
_ = try child.print( _ = child.print(
&.{ &.{
.{ .text = " 4 │ ", .style = color238 }, .{ .text = " 4 │ ", .style = color238 },
.{ .text = "const ", .style = color5 }, .{ .text = "const ", .style = color5 },
@ -1198,7 +1193,7 @@ const Preview = struct {
.col_offset = 2, .col_offset = 2,
}, },
); );
_ = try child.print( _ = child.print(
&.{ &.{
.{ .text = " 5 │ ", .style = color238 }, .{ .text = " 5 │ ", .style = color238 },
.{ .text = "var ", .style = color5 }, .{ .text = "var ", .style = color5 },
@ -1213,7 +1208,7 @@ const Preview = struct {
.col_offset = 2, .col_offset = 2,
}, },
); );
_ = try child.print( _ = child.print(
&.{ &.{
.{ .text = " 6 │ ", .style = color238 }, .{ .text = " 6 │ ", .style = color238 },
.{ .text = "while ", .style = color5 }, .{ .text = "while ", .style = color5 },
@ -1230,7 +1225,7 @@ const Preview = struct {
.col_offset = 2, .col_offset = 2,
}, },
); );
_ = try child.print( _ = child.print(
&.{ &.{
.{ .text = " 7 │ ", .style = color238 }, .{ .text = " 7 │ ", .style = color238 },
.{ .text = "if ", .style = color5 }, .{ .text = "if ", .style = color5 },
@ -1246,7 +1241,7 @@ const Preview = struct {
.col_offset = 2, .col_offset = 2,
}, },
); );
_ = try child.print( _ = child.print(
&.{ &.{
.{ .text = " 8 │ ", .style = color238 }, .{ .text = " 8 │ ", .style = color238 },
.{ .text = "try ", .style = color5 }, .{ .text = "try ", .style = color5 },
@ -1261,7 +1256,7 @@ const Preview = struct {
.col_offset = 2, .col_offset = 2,
}, },
); );
_ = try child.print( _ = child.print(
&.{ &.{
.{ .text = " 9 │ ", .style = color238 }, .{ .text = " 9 │ ", .style = color238 },
.{ .text = "} ", .style = standard }, .{ .text = "} ", .style = standard },
@ -1278,7 +1273,7 @@ const Preview = struct {
.col_offset = 2, .col_offset = 2,
}, },
); );
_ = try child.print( _ = child.print(
&.{ &.{
.{ .text = " 10 │ ", .style = color238 }, .{ .text = " 10 │ ", .style = color238 },
.{ .text = "try ", .style = color5 }, .{ .text = "try ", .style = color5 },
@ -1293,7 +1288,7 @@ const Preview = struct {
.col_offset = 2, .col_offset = 2,
}, },
); );
_ = try child.print( _ = child.print(
&.{ &.{
.{ .text = " 11 │ ", .style = color238 }, .{ .text = " 11 │ ", .style = color238 },
.{ .text = "} ", .style = standard }, .{ .text = "} ", .style = standard },
@ -1310,7 +1305,7 @@ const Preview = struct {
.col_offset = 2, .col_offset = 2,
}, },
); );
_ = try child.print( _ = child.print(
&.{ &.{
.{ .text = " 12 │ ", .style = color238 }, .{ .text = " 12 │ ", .style = color238 },
.{ .text = "try ", .style = color5 }, .{ .text = "try ", .style = color5 },
@ -1325,7 +1320,7 @@ const Preview = struct {
.col_offset = 2, .col_offset = 2,
}, },
); );
_ = try child.print( _ = child.print(
&.{ &.{
.{ .text = " 13 │ ", .style = color238 }, .{ .text = " 13 │ ", .style = color238 },
.{ .text = "} ", .style = standard }, .{ .text = "} ", .style = standard },
@ -1337,7 +1332,7 @@ const Preview = struct {
.col_offset = 2, .col_offset = 2,
}, },
); );
_ = try child.print( _ = child.print(
&.{ &.{
.{ .text = " 14 │ ", .style = color238 }, .{ .text = " 14 │ ", .style = color238 },
.{ .text = "try ", .style = color5 }, .{ .text = "try ", .style = color5 },
@ -1352,7 +1347,7 @@ const Preview = struct {
.col_offset = 2, .col_offset = 2,
}, },
); );
_ = try child.print( _ = child.print(
&.{ &.{
.{ .text = " 15 │ ", .style = color238 }, .{ .text = " 15 │ ", .style = color238 },
.{ .text = "}", .style = standard }, .{ .text = "}", .style = standard },
@ -1362,7 +1357,7 @@ const Preview = struct {
.col_offset = 2, .col_offset = 2,
}, },
); );
_ = try child.print( _ = child.print(
&.{ &.{
.{ .text = " 16 │ ", .style = color238 }, .{ .text = " 16 │ ", .style = color238 },
.{ .text = "}", .style = standard }, .{ .text = "}", .style = standard },
@ -1372,7 +1367,7 @@ const Preview = struct {
.col_offset = 2, .col_offset = 2,
}, },
); );
_ = try child.print( _ = child.print(
&.{ &.{
.{ .text = " 17 │ ", .style = color238 }, .{ .text = " 17 │ ", .style = color238 },
.{ .text = "}", .style = standard }, .{ .text = "}", .style = standard },
@ -1383,7 +1378,7 @@ const Preview = struct {
}, },
); );
{ {
_ = try child.print( _ = child.print(
&.{ &.{
.{ .{
.text = "───────┴", .text = "───────┴",
@ -1396,8 +1391,9 @@ const Preview = struct {
}, },
); );
if (child.width > 10) { if (child.width > 10) {
for (10..child.width) |col| { for (10..child.width) |captured_col| {
_ = try child.print( const col: u16 = @intCast(captured_col);
_ = child.print(
&.{ &.{
.{ .{
.text = "", .text = "",
@ -1412,7 +1408,7 @@ const Preview = struct {
} }
} }
} }
_ = try child.print( _ = child.print(
&.{ &.{
.{ .text = "ghostty ", .style = color6 }, .{ .text = "ghostty ", .style = color6 },
.{ .text = "on ", .style = standard }, .{ .text = "on ", .style = standard },
@ -1428,7 +1424,7 @@ const Preview = struct {
.col_offset = 2, .col_offset = 2,
}, },
); );
_ = try child.print( _ = child.print(
&.{ &.{
.{ .text = "", .style = color4 }, .{ .text = "", .style = color4 },
.{ .text = "at ", .style = standard }, .{ .text = "at ", .style = standard },
@ -1447,23 +1443,20 @@ const Preview = struct {
.{ .{
.x_off = x_off, .x_off = x_off,
.y_off = next_start, .y_off = next_start,
.width = .{ .width = width,
.limit = width, .height = win.height - next_start,
},
.height = .{
.limit = win.height - next_start,
},
}, },
); );
child.fill(.{ .style = standard }); child.fill(.{ .style = standard });
var it = std.mem.splitAny(u8, lorem_ipsum, " \n"); var it = std.mem.splitAny(u8, lorem_ipsum, " \n");
var row: usize = 1; var row: u16 = 1;
var col: usize = 2; var col: u16 = 2;
while (row < child.height) { while (row < child.height) {
const word = it.next() orelse line: { const word = it.next() orelse line: {
it.reset(); it.reset();
break :line it.next() orelse unreachable; break :line it.next() orelse unreachable;
}; };
const word_len: u16 = @intCast(word.len);
if (col + word.len > child.width) { if (col + word.len > child.width) {
row += 1; row += 1;
col = 2; col = 2;
@ -1480,7 +1473,7 @@ const Preview = struct {
if (std.mem.eql(u8, "odio", word)) break :style standard_curly_underline; if (std.mem.eql(u8, "odio", word)) break :style standard_curly_underline;
break :style standard; break :style standard;
}; };
_ = try child.printSegment( _ = child.printSegment(
.{ .{
.text = word, .text = word,
.style = style, .style = style,
@ -1490,9 +1483,28 @@ const Preview = struct {
.col_offset = col, .col_offset = col,
}, },
); );
col += word.len + 1; col += word_len + 1;
} }
} }
} else {
const child = win.child(.{
.x_off = 0,
.y_off = 0,
.width = win.width,
.height = win.height,
});
child.fill(.{
.style = self.ui_standard(),
});
_ = child.printSegment(.{
.text = "No theme found!",
.style = self.ui_standard(),
}, .{
.row_offset = win.height / 2 - 1,
.col_offset = win.width / 2 - 7,
});
}
} }
}; };

View File

@ -42,7 +42,7 @@ pub fn run(alloc: Allocator) !u8 {
gtk.gtk_get_minor_version(), gtk.gtk_get_minor_version(),
gtk.gtk_get_micro_version(), gtk.gtk_get_micro_version(),
}); });
if (comptime build_options.libadwaita) { if (comptime build_options.adwaita) {
try stdout.print(" - libadwaita : enabled\n", .{}); try stdout.print(" - libadwaita : enabled\n", .{});
try stdout.print(" build : {s}\n", .{ try stdout.print(" build : {s}\n", .{
gtk.ADW_VERSION_S, gtk.ADW_VERSION_S,

View File

@ -2,10 +2,12 @@ const builtin = @import("builtin");
const formatter = @import("config/formatter.zig"); const formatter = @import("config/formatter.zig");
pub const Config = @import("config/Config.zig"); pub const Config = @import("config/Config.zig");
pub const conditional = @import("config/conditional.zig");
pub const string = @import("config/string.zig"); pub const string = @import("config/string.zig");
pub const edit = @import("config/edit.zig"); pub const edit = @import("config/edit.zig");
pub const url = @import("config/url.zig"); pub const url = @import("config/url.zig");
pub const ConditionalState = conditional.State;
pub const FileFormatter = formatter.FileFormatter; pub const FileFormatter = formatter.FileFormatter;
pub const entryFormatter = formatter.entryFormatter; pub const entryFormatter = formatter.entryFormatter;
pub const formatEntry = formatter.formatEntry; pub const formatEntry = formatter.formatEntry;
@ -16,6 +18,7 @@ pub const CopyOnSelect = Config.CopyOnSelect;
pub const CustomShaderAnimation = Config.CustomShaderAnimation; pub const CustomShaderAnimation = Config.CustomShaderAnimation;
pub const FontSyntheticStyle = Config.FontSyntheticStyle; pub const FontSyntheticStyle = Config.FontSyntheticStyle;
pub const FontStyle = Config.FontStyle; pub const FontStyle = Config.FontStyle;
pub const FreetypeLoadFlags = Config.FreetypeLoadFlags;
pub const Keybinds = Config.Keybinds; pub const Keybinds = Config.Keybinds;
pub const MouseShiftCapture = Config.MouseShiftCapture; pub const MouseShiftCapture = Config.MouseShiftCapture;
pub const NonNativeFullscreen = Config.NonNativeFullscreen; pub const NonNativeFullscreen = Config.NonNativeFullscreen;

View File

@ -19,6 +19,7 @@ export fn ghostty_config_new() ?*Config {
result.* = Config.default(global.alloc) catch |err| { result.* = Config.default(global.alloc) catch |err| {
log.err("error creating config err={}", .{err}); log.err("error creating config err={}", .{err});
global.alloc.destroy(result);
return null; return null;
}; };
@ -32,6 +33,22 @@ export fn ghostty_config_free(ptr: ?*Config) void {
} }
} }
/// Deep clone the configuration.
export fn ghostty_config_clone(self: *Config) ?*Config {
const result = global.alloc.create(Config) catch |err| {
log.err("error allocating config err={}", .{err});
return null;
};
result.* = self.clone(global.alloc) catch |err| {
log.err("error cloning config err={}", .{err});
global.alloc.destroy(result);
return null;
};
return result;
}
/// Load the configuration from the CLI args. /// Load the configuration from the CLI args.
export fn ghostty_config_load_cli_args(self: *Config) void { export fn ghostty_config_load_cli_args(self: *Config) void {
self.loadCliArgs(global.alloc) catch |err| { self.loadCliArgs(global.alloc) catch |err| {

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,95 @@
const std = @import("std");
const builtin = @import("builtin");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
/// Conditionals in Ghostty configuration are based on a static, typed
/// state of the world instead of a dynamic key-value set. This simplifies
/// the implementation, allows for better type checking, and enables a
/// typed C API.
pub const State = struct {
/// The theme of the underlying OS desktop environment.
theme: Theme = .light,
/// The target OS of the current build.
os: std.Target.Os.Tag = builtin.target.os.tag,
pub const Theme = enum { light, dark };
/// Tests the conditional against the state and returns true if it matches.
pub fn match(self: State, cond: Conditional) bool {
switch (cond.key) {
inline else => |tag| {
// The raw value of the state field.
const raw = @field(self, @tagName(tag));
// Since all values are enums currently then we can just
// do this. If we introduce non-enum state values then this
// will be a compile error and we should fix here.
const value: []const u8 = @tagName(raw);
return switch (cond.op) {
.eq => std.mem.eql(u8, value, cond.value),
.ne => !std.mem.eql(u8, value, cond.value),
};
},
}
}
};
/// An enum of the available conditional configuration keys.
pub const Key = key: {
const stateInfo = @typeInfo(State).Struct;
var fields: [stateInfo.fields.len]std.builtin.Type.EnumField = undefined;
for (stateInfo.fields, 0..) |field, i| fields[i] = .{
.name = field.name,
.value = i,
};
break :key @Type(.{ .Enum = .{
.tag_type = std.math.IntFittingRange(0, fields.len - 1),
.fields = &fields,
.decls = &.{},
.is_exhaustive = true,
} });
};
/// A single conditional that can be true or false.
pub const Conditional = struct {
key: Key,
op: Op,
value: []const u8,
pub const Op = enum { eq, ne };
pub fn clone(
self: Conditional,
alloc: Allocator,
) Allocator.Error!Conditional {
return .{
.key = self.key,
.op = self.op,
.value = try alloc.dupe(u8, self.value),
};
}
};
test "conditional enum match" {
const testing = std.testing;
const state: State = .{ .theme = .dark };
try testing.expect(state.match(.{
.key = .theme,
.op = .eq,
.value = "dark",
}));
try testing.expect(!state.match(.{
.key = .theme,
.op = .ne,
.value = "dark",
}));
try testing.expect(state.match(.{
.key = .theme,
.op = .ne,
.value = "light",
}));
}

View File

@ -0,0 +1,51 @@
const std = @import("std");
const Config = @import("Config.zig");
const Template = struct {
const header =
\\%YAML 1.2
\\---
\\# See http://www.sublimetext.com/docs/syntax.html
\\name: Ghostty Config
\\file_extensions:
\\ - ghostty
\\scope: source.ghostty
\\
\\contexts:
\\ main:
\\ # Comments
\\ - match: '^\s*#.*$'
\\ scope: comment.line.number-sign.ghostty
\\
\\ # Keywords
\\ - match: '\b(
;
const footer =
\\)\b'
\\ scope: keyword.other.ghostty
\\
;
};
/// Check if a field is internal (starts with underscore)
fn isInternal(name: []const u8) bool {
return name.len > 0 and name[0] == '_';
}
/// Generate keywords from Config fields
fn generateKeywords() []const u8 {
@setEvalBranchQuota(5000);
var keywords: []const u8 = "";
const config_fields = @typeInfo(Config).Struct.fields;
for (config_fields) |field| {
if (isInternal(field.name)) continue;
if (keywords.len > 0) keywords = keywords ++ "|";
keywords = keywords ++ field.name;
}
return keywords;
}
/// Complete Sublime syntax file content
pub const syntax = Template.header ++ generateKeywords() ++ Template.footer;

1
src/config/testdata/theme_dark vendored Normal file
View File

@ -0,0 +1 @@
background = #EEEEEE

1
src/config/testdata/theme_light vendored Normal file
View File

@ -0,0 +1 @@
background = #FFFFFF

2
src/config/testdata/theme_simple vendored Normal file
View File

@ -0,0 +1,2 @@
# A simple theme
background = #123ABC

View File

@ -112,7 +112,6 @@ pub fn open(
path: []const u8, path: []const u8,
file: std.fs.File, file: std.fs.File,
} { } {
// Absolute themes are loaded a different path. // Absolute themes are loaded a different path.
if (std.fs.path.isAbsolute(theme)) { if (std.fs.path.isAbsolute(theme)) {
const file: std.fs.File = try openAbsolute( const file: std.fs.File = try openAbsolute(

View File

@ -24,7 +24,7 @@ const oni = @import("oniguruma");
/// handling them well requires a non-regex approach. /// handling them well requires a non-regex approach.
pub const regex = pub const regex =
"(?:" ++ url_schemes ++ "(?:" ++ url_schemes ++
\\)(?:[\w\-.~:/?#\[\]@!$&*+,;=%]+(?:\(\w*\))?)+(?<!\.) \\)(?:[\w\-.~:/?#@!$&*+,;=%]+(?:[\(\[]\w*[\)\]])?)+(?<![,.])
; ;
const url_schemes = const url_schemes =
\\https?://|mailto:|ftp://|file:|ssh:|git://|ssh://|tel:|magnet:|ipfs://|ipns://|gemini://|gopher://|news: \\https?://|mailto:|ftp://|file:|ssh:|git://|ssh://|tel:|magnet:|ipfs://|ipns://|gemini://|gopher://|news:
@ -70,6 +70,10 @@ test "url regex" {
.input = "Link period https://example.com. More text.", .input = "Link period https://example.com. More text.",
.expect = "https://example.com", .expect = "https://example.com",
}, },
.{
.input = "Link trailing colon https://example.com, more text.",
.expect = "https://example.com",
},
.{ .{
.input = "Link in double quotes \"https://example.com\" and more", .input = "Link in double quotes \"https://example.com\" and more",
.expect = "https://example.com", .expect = "https://example.com",
@ -112,6 +116,11 @@ test "url regex" {
.input = "square brackets https://example.com/[foo] and more", .input = "square brackets https://example.com/[foo] and more",
.expect = "https://example.com/[foo]", .expect = "https://example.com/[foo]",
}, },
// square bracket following url
.{
.input = "[13]:TooManyStatements: TempFile#assign_temp_file_to_entity has approx 7 statements [https://example.com/docs/Too-Many-Statements.md]",
.expect = "https://example.com/docs/Too-Many-Statements.md",
},
// remaining URL schemes tests // remaining URL schemes tests
.{ .{
.input = "match ftp://example.com ftp links", .input = "match ftp://example.com ftp links",

View File

@ -70,7 +70,7 @@ fn writeSyntax(writer: anytype) !void {
try writer.writeAll( try writer.writeAll(
\\ \\
\\ \\
\\syn match ghosttyConfigComment /#.*/ contains=@Spell \\syn match ghosttyConfigComment /^\s*#.*/ contains=@Spell
\\ \\
\\hi def link ghosttyConfigComment Comment \\hi def link ghosttyConfigComment Comment
\\hi def link ghosttyConfigKeyword Keyword \\hi def link ghosttyConfigKeyword Keyword

View File

@ -177,29 +177,30 @@ fn beforeSend(
const obj = sentry.Value.initObject(); const obj = sentry.Value.initObject();
errdefer obj.decref(); errdefer obj.decref();
const surface = thr_state.surface; const surface = thr_state.surface;
const grid_size = surface.size.grid();
obj.set( obj.set(
"screen-width", "screen-width",
sentry.Value.initInt32(std.math.cast(i32, surface.screen_size.width) orelse -1), sentry.Value.initInt32(std.math.cast(i32, surface.size.screen.width) orelse -1),
); );
obj.set( obj.set(
"screen-height", "screen-height",
sentry.Value.initInt32(std.math.cast(i32, surface.screen_size.height) orelse -1), sentry.Value.initInt32(std.math.cast(i32, surface.size.screen.height) orelse -1),
); );
obj.set( obj.set(
"grid-columns", "grid-columns",
sentry.Value.initInt32(std.math.cast(i32, surface.grid_size.columns) orelse -1), sentry.Value.initInt32(std.math.cast(i32, grid_size.columns) orelse -1),
); );
obj.set( obj.set(
"grid-rows", "grid-rows",
sentry.Value.initInt32(std.math.cast(i32, surface.grid_size.rows) orelse -1), sentry.Value.initInt32(std.math.cast(i32, grid_size.rows) orelse -1),
); );
obj.set( obj.set(
"cell-width", "cell-width",
sentry.Value.initInt32(std.math.cast(i32, surface.cell_size.width) orelse -1), sentry.Value.initInt32(std.math.cast(i32, surface.size.cell.width) orelse -1),
); );
obj.set( obj.set(
"cell-height", "cell-height",
sentry.Value.initInt32(std.math.cast(i32, surface.cell_size.height) orelse -1), sentry.Value.initInt32(std.math.cast(i32, surface.size.cell.height) orelse -1),
); );
contexts.set("Dimensions", obj); contexts.set("Dimensions", obj);

View File

@ -1,4 +1,4 @@
const fastmem = @import("./fastmem.zig"); const fastmem = @import("../fastmem.zig");
const std = @import("std"); const std = @import("std");
const assert = std.debug.assert; const assert = std.debug.assert;

View File

@ -1,7 +1,7 @@
const std = @import("std"); const std = @import("std");
const assert = std.debug.assert; const assert = std.debug.assert;
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const fastmem = @import("fastmem.zig"); const fastmem = @import("../fastmem.zig");
/// Returns a circular buffer containing type T. /// Returns a circular buffer containing type T.
pub fn CircBuf(comptime T: type, comptime default: T) type { pub fn CircBuf(comptime T: type, comptime default: T) type {

View File

@ -0,0 +1,181 @@
const std = @import("std");
const testing = std.testing;
/// An intrusive doubly-linked list. The type T must have a "next" and "prev"
/// field pointing to itself.
///
/// This is an adaptation of the DoublyLinkedList from the Zig standard
/// library, which is MIT licensed. I've removed functionality that I don't
/// need.
pub fn DoublyLinkedList(comptime T: type) type {
return struct {
const Self = @This();
/// The type of the node in the list. This makes it easy to get the
/// node type from the list type.
pub const Node = T;
first: ?*Node = null,
last: ?*Node = null,
/// Insert a new node after an existing one.
///
/// Arguments:
/// node: Pointer to a node in the list.
/// new_node: Pointer to the new node to insert.
pub fn insertAfter(list: *Self, node: *Node, new_node: *Node) void {
new_node.prev = node;
if (node.next) |next_node| {
// Intermediate node.
new_node.next = next_node;
next_node.prev = new_node;
} else {
// Last element of the list.
new_node.next = null;
list.last = new_node;
}
node.next = new_node;
}
/// Insert a new node before an existing one.
///
/// Arguments:
/// node: Pointer to a node in the list.
/// new_node: Pointer to the new node to insert.
pub fn insertBefore(list: *Self, node: *Node, new_node: *Node) void {
new_node.next = node;
if (node.prev) |prev_node| {
// Intermediate node.
new_node.prev = prev_node;
prev_node.next = new_node;
} else {
// First element of the list.
new_node.prev = null;
list.first = new_node;
}
node.prev = new_node;
}
/// Insert a new node at the end of the list.
///
/// Arguments:
/// new_node: Pointer to the new node to insert.
pub fn append(list: *Self, new_node: *Node) void {
if (list.last) |last| {
// Insert after last.
list.insertAfter(last, new_node);
} else {
// Empty list.
list.prepend(new_node);
}
}
/// Insert a new node at the beginning of the list.
///
/// Arguments:
/// new_node: Pointer to the new node to insert.
pub fn prepend(list: *Self, new_node: *Node) void {
if (list.first) |first| {
// Insert before first.
list.insertBefore(first, new_node);
} else {
// Empty list.
list.first = new_node;
list.last = new_node;
new_node.prev = null;
new_node.next = null;
}
}
/// Remove a node from the list.
///
/// Arguments:
/// node: Pointer to the node to be removed.
pub fn remove(list: *Self, node: *Node) void {
if (node.prev) |prev_node| {
// Intermediate node.
prev_node.next = node.next;
} else {
// First element of the list.
list.first = node.next;
}
if (node.next) |next_node| {
// Intermediate node.
next_node.prev = node.prev;
} else {
// Last element of the list.
list.last = node.prev;
}
}
/// Remove and return the last node in the list.
///
/// Returns:
/// A pointer to the last node in the list.
pub fn pop(list: *Self) ?*Node {
const last = list.last orelse return null;
list.remove(last);
return last;
}
/// Remove and return the first node in the list.
///
/// Returns:
/// A pointer to the first node in the list.
pub fn popFirst(list: *Self) ?*Node {
const first = list.first orelse return null;
list.remove(first);
return first;
}
};
}
test "basic DoublyLinkedList test" {
const Node = struct {
data: u32,
prev: ?*@This() = null,
next: ?*@This() = null,
};
const L = DoublyLinkedList(Node);
var list: L = .{};
var one: Node = .{ .data = 1 };
var two: Node = .{ .data = 2 };
var three: Node = .{ .data = 3 };
var four: Node = .{ .data = 4 };
var five: Node = .{ .data = 5 };
list.append(&two); // {2}
list.append(&five); // {2, 5}
list.prepend(&one); // {1, 2, 5}
list.insertBefore(&five, &four); // {1, 2, 4, 5}
list.insertAfter(&two, &three); // {1, 2, 3, 4, 5}
// Traverse forwards.
{
var it = list.first;
var index: u32 = 1;
while (it) |node| : (it = node.next) {
try testing.expect(node.data == index);
index += 1;
}
}
// Traverse backwards.
{
var it = list.last;
var index: u32 = 1;
while (it) |node| : (it = node.prev) {
try testing.expect(node.data == (6 - index));
index += 1;
}
}
_ = list.popFirst(); // {2, 3, 4, 5}
_ = list.pop(); // {2, 3, 4}
list.remove(&three); // {2, 4}
try testing.expect(list.first.?.data == 2);
try testing.expect(list.last.?.data == 4);
}

19
src/datastruct/main.zig Normal file
View File

@ -0,0 +1,19 @@
//! The datastruct package contains data structures or anything closely
//! related to data structures.
const blocking_queue = @import("blocking_queue.zig");
const cache_table = @import("cache_table.zig");
const circ_buf = @import("circ_buf.zig");
const intrusive_linked_list = @import("intrusive_linked_list.zig");
const segmented_pool = @import("segmented_pool.zig");
pub const lru = @import("lru.zig");
pub const BlockingQueue = blocking_queue.BlockingQueue;
pub const CacheTable = cache_table.CacheTable;
pub const CircBuf = circ_buf.CircBuf;
pub const IntrusiveDoublyLinkedList = intrusive_linked_list.DoublyLinkedList;
pub const SegmentedPool = segmented_pool.SegmentedPool;
test {
@import("std").testing.refAllDecls(@This());
}

View File

@ -452,6 +452,12 @@ pub const LoadOptions = struct {
/// for this is owned by the user and is not freed by the collection. /// for this is owned by the user and is not freed by the collection.
metric_modifiers: Metrics.ModifierSet = .{}, metric_modifiers: Metrics.ModifierSet = .{},
/// Freetype Load Flags to use when loading glyphs. This is a list of
/// bitfield constants that controls operations to perform during glyph
/// loading. Only a subset is exposed for configuration, for the whole set
/// of flags see `pkg.freetype.face.LoadFlags`.
freetype_load_flags: font.face.FreetypeLoadFlags = font.face.freetype_load_flags_default,
pub fn deinit(self: *LoadOptions, alloc: Allocator) void { pub fn deinit(self: *LoadOptions, alloc: Allocator) void {
_ = self; _ = self;
_ = alloc; _ = alloc;
@ -462,6 +468,7 @@ pub const LoadOptions = struct {
return .{ return .{
.size = self.size, .size = self.size,
.metric_modifiers = &self.metric_modifiers, .metric_modifiers = &self.metric_modifiers,
.freetype_load_flags = self.freetype_load_flags,
}; };
} }
}; };

View File

@ -57,6 +57,11 @@ pub const CoreText = struct {
/// The initialized font /// The initialized font
font: *macos.text.Font, font: *macos.text.Font,
/// Variations to apply to this font. We apply the variations to the
/// search descriptor but sometimes when the font collection is
/// made the variation axes are reset so we have to reapply them.
variations: []const font.face.Variation,
pub fn deinit(self: *CoreText) void { pub fn deinit(self: *CoreText) void {
self.font.release(); self.font.release();
self.* = undefined; self.* = undefined;
@ -194,7 +199,10 @@ fn loadCoreText(
) !Face { ) !Face {
_ = lib; _ = lib;
const ct = self.ct.?; const ct = self.ct.?;
return try Face.initFontCopy(ct.font, opts); var face = try Face.initFontCopy(ct.font, opts);
errdefer face.deinit();
try face.setVariations(ct.variations, opts);
return face;
} }
fn loadCoreTextFreetype( fn loadCoreTextFreetype(
@ -236,43 +244,7 @@ fn loadCoreTextFreetype(
//std.log.warn("path={s}", .{path_slice}); //std.log.warn("path={s}", .{path_slice});
var face = try Face.initFile(lib, buf[0..path_slice.len :0], 0, opts); var face = try Face.initFile(lib, buf[0..path_slice.len :0], 0, opts);
errdefer face.deinit(); errdefer face.deinit();
try face.setVariations(ct.variations, opts);
// If our ct font has variations, apply them to the face.
if (ct.font.copyAttribute(.variation)) |variations| vars: {
defer variations.release();
if (variations.getCount() == 0) break :vars;
// This configuration is just used for testing so we don't want to
// have to pass a full allocator through so use the stack. We
// shouldn't have a lot of variations and if we do we should use
// another mechanism.
//
// On macOS the default stack size for a thread is 512KB and the main
// thread gets megabytes so 16KB is a safe stack allocation.
var data: [1024 * 16]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(&data);
const alloc = fba.allocator();
var face_vars = std.ArrayList(font.face.Variation).init(alloc);
const kav = try variations.getKeysAndValues(alloc);
for (kav.keys, kav.values) |key, value| {
const num: *const macos.foundation.Number = @ptrCast(key.?);
const val: *const macos.foundation.Number = @ptrCast(value.?);
var num_i32: i32 = undefined;
if (!num.getValue(.sint32, &num_i32)) continue;
var val_f64: f64 = undefined;
if (!val.getValue(.float64, &val_f64)) continue;
try face_vars.append(.{
.id = @bitCast(num_i32),
.value = val_f64,
});
}
try face.setVariations(face_vars.items, opts);
}
return face; return face;
} }

View File

@ -168,6 +168,7 @@ fn collection(
.library = self.font_lib, .library = self.font_lib,
.size = size, .size = size,
.metric_modifiers = key.metric_modifiers, .metric_modifiers = key.metric_modifiers,
.freetype_load_flags = key.freetype_load_flags,
}; };
var c = Collection.init(); var c = Collection.init();
@ -427,6 +428,7 @@ pub const DerivedConfig = struct {
@"adjust-strikethrough-position": ?Metrics.Modifier, @"adjust-strikethrough-position": ?Metrics.Modifier,
@"adjust-strikethrough-thickness": ?Metrics.Modifier, @"adjust-strikethrough-thickness": ?Metrics.Modifier,
@"adjust-cursor-thickness": ?Metrics.Modifier, @"adjust-cursor-thickness": ?Metrics.Modifier,
@"freetype-load-flags": font.face.FreetypeLoadFlags,
/// Initialize a DerivedConfig. The config should be either a /// Initialize a DerivedConfig. The config should be either a
/// config.Config or another DerivedConfig to clone from. /// config.Config or another DerivedConfig to clone from.
@ -461,6 +463,7 @@ pub const DerivedConfig = struct {
.@"adjust-strikethrough-position" = config.@"adjust-strikethrough-position", .@"adjust-strikethrough-position" = config.@"adjust-strikethrough-position",
.@"adjust-strikethrough-thickness" = config.@"adjust-strikethrough-thickness", .@"adjust-strikethrough-thickness" = config.@"adjust-strikethrough-thickness",
.@"adjust-cursor-thickness" = config.@"adjust-cursor-thickness", .@"adjust-cursor-thickness" = config.@"adjust-cursor-thickness",
.@"freetype-load-flags" = if (font.face.FreetypeLoadFlags != void) config.@"freetype-load-flags" else {},
// This must be last so the arena contains all our allocations // This must be last so the arena contains all our allocations
// from above since Zig does assignment in order. // from above since Zig does assignment in order.
@ -500,6 +503,10 @@ pub const Key = struct {
/// font grid. /// font grid.
font_size: DesiredSize = .{ .points = 12 }, font_size: DesiredSize = .{ .points = 12 },
/// The freetype load flags configuration, only non-void if the
/// freetype backend is enabled.
freetype_load_flags: font.face.FreetypeLoadFlags = font.face.freetype_load_flags_default,
const style_offsets_len = std.enums.directEnumArrayLen(Style, 0); const style_offsets_len = std.enums.directEnumArrayLen(Style, 0);
const StyleOffsets = [style_offsets_len]usize; const StyleOffsets = [style_offsets_len]usize;
@ -618,6 +625,10 @@ pub const Key = struct {
.codepoint_map = codepoint_map, .codepoint_map = codepoint_map,
.metric_modifiers = metric_modifiers, .metric_modifiers = metric_modifiers,
.font_size = font_size, .font_size = font_size,
.freetype_load_flags = if (font.face.FreetypeLoadFlags != void)
config.@"freetype-load-flags"
else
font.face.freetype_load_flags_default,
}; };
} }
@ -647,6 +658,7 @@ pub const Key = struct {
for (self.descriptors) |d| d.hash(hasher); for (self.descriptors) |d| d.hash(hasher);
self.codepoint_map.hash(hasher); self.codepoint_map.hash(hasher);
autoHash(hasher, self.metric_modifiers.count()); autoHash(hasher, self.metric_modifiers.count());
autoHash(hasher, self.freetype_load_flags);
if (self.metric_modifiers.count() > 0) { if (self.metric_modifiers.count() > 0) {
inline for (@typeInfo(Metrics.Key).Enum.fields) |field| { inline for (@typeInfo(Metrics.Key).Enum.fields) |field| {
const key = @field(Metrics.Key, field.name); const key = @field(Metrics.Key, field.name);

View File

@ -79,7 +79,7 @@ pub const Descriptor = struct {
// This is not correct, but we don't currently depend on the // This is not correct, but we don't currently depend on the
// hash value being different based on decimal values of variations. // hash value being different based on decimal values of variations.
autoHash(hasher, @as(u64, @intFromFloat(variation.value))); autoHash(hasher, @as(i64, @intFromFloat(variation.value)));
} }
} }
@ -235,21 +235,7 @@ pub const Descriptor = struct {
); );
} }
// Build our descriptor from attrs return try macos.text.FontDescriptor.createWithAttributes(@ptrCast(attrs));
var desc = try macos.text.FontDescriptor.createWithAttributes(@ptrCast(attrs));
errdefer desc.release();
// Variations are built by copying the descriptor. I don't know a way
// to set it on attrs directly.
for (self.variations) |v| {
const id = try macos.foundation.Number.create(.int, @ptrCast(&v.id));
defer id.release();
const next = try desc.createCopyWithVariation(id, v.value);
desc.release();
desc = next;
}
return desc;
} }
}; };
@ -384,6 +370,7 @@ pub const CoreText = struct {
return DiscoverIterator{ return DiscoverIterator{
.alloc = alloc, .alloc = alloc,
.list = zig_list, .list = zig_list,
.variations = desc.variations,
.i = 0, .i = 0,
}; };
} }
@ -420,6 +407,7 @@ pub const CoreText = struct {
return DiscoverIterator{ return DiscoverIterator{
.alloc = alloc, .alloc = alloc,
.list = list, .list = list,
.variations = desc.variations,
.i = 0, .i = 0,
}; };
} }
@ -443,6 +431,7 @@ pub const CoreText = struct {
return DiscoverIterator{ return DiscoverIterator{
.alloc = alloc, .alloc = alloc,
.list = list, .list = list,
.variations = desc.variations,
.i = 0, .i = 0,
}; };
} }
@ -682,30 +671,29 @@ pub const CoreText = struct {
break :style .unmatched; break :style .unmatched;
defer style.release(); defer style.release();
// If we have a specific desired style, attempt to search for that. // Get our style string
if (desc.style) |desired_style| {
var buf: [128]u8 = undefined; var buf: [128]u8 = undefined;
const style_str = style.cstring(&buf, .utf8) orelse break :style .unmatched; const style_str = style.cstring(&buf, .utf8) orelse break :style .unmatched;
// If we have a specific desired style, attempt to search for that.
if (desc.style) |desired_style| {
// Matching style string gets highest score // Matching style string gets highest score
if (std.mem.eql(u8, desired_style, style_str)) break :style .match; if (std.mem.eql(u8, desired_style, style_str)) break :style .match;
} else if (!desc.bold and !desc.italic) {
// Otherwise the score is based on the length of the style string.
// Shorter styles are scored higher.
break :style @enumFromInt(100 -| style_str.len);
}
// If we do not, and we have no symbolic traits, then we try // If we do not, and we have no symbolic traits, then we try
// to find "regular" (or no style). If we have symbolic traits // to find "regular" (or no style). If we have symbolic traits
// we do nothing but we can improve scoring by taking that into // we do nothing but we can improve scoring by taking that into
// account, too. // account, too.
if (!desc.bold and !desc.italic) { if (std.mem.eql(u8, "Regular", style_str)) {
var buf: [128]u8 = undefined; break :style .match;
const style_str = style.cstring(&buf, .utf8) orelse break :style .unmatched; }
if (std.mem.eql(u8, "Regular", style_str)) break :style .match;
} }
break :style .unmatched; // Otherwise the score is based on the length of the style string.
// Shorter styles are scored higher. This is a heuristic that
// if we don't have a desired style then shorter tends to be
// more often the "regular" style.
break :style @enumFromInt(100 -| style_str.len);
}; };
score_acc.traits = traits: { score_acc.traits = traits: {
@ -721,6 +709,7 @@ pub const CoreText = struct {
pub const DiscoverIterator = struct { pub const DiscoverIterator = struct {
alloc: Allocator, alloc: Allocator,
list: []const *macos.text.FontDescriptor, list: []const *macos.text.FontDescriptor,
variations: []const Variation,
i: usize, i: usize,
pub fn deinit(self: *DiscoverIterator) void { pub fn deinit(self: *DiscoverIterator) void {
@ -756,7 +745,10 @@ pub const CoreText = struct {
defer self.i += 1; defer self.i += 1;
return DeferredFace{ return DeferredFace{
.ct = .{ .font = font }, .ct = .{
.font = font,
.variations = self.variations,
},
}; };
} }
}; };

View File

@ -14,6 +14,7 @@ pub const emoji = @embedFile("res/NotoColorEmoji.ttf");
pub const emoji_text = @embedFile("res/NotoEmoji-Regular.ttf"); pub const emoji_text = @embedFile("res/NotoEmoji-Regular.ttf");
/// Fonts with general properties /// Fonts with general properties
pub const arabic = @embedFile("res/KawkabMono-Regular.ttf");
pub const variable = @embedFile("res/Lilex-VF.ttf"); pub const variable = @embedFile("res/Lilex-VF.ttf");
/// Font with nerd fonts embedded. /// Font with nerd fonts embedded.

View File

@ -2,6 +2,7 @@ const std = @import("std");
const builtin = @import("builtin"); const builtin = @import("builtin");
const options = @import("main.zig").options; const options = @import("main.zig").options;
pub const Metrics = @import("face/Metrics.zig"); pub const Metrics = @import("face/Metrics.zig");
const config = @import("../config.zig");
const freetype = @import("face/freetype.zig"); const freetype = @import("face/freetype.zig");
const coretext = @import("face/coretext.zig"); const coretext = @import("face/coretext.zig");
pub const web_canvas = @import("face/web_canvas.zig"); pub const web_canvas = @import("face/web_canvas.zig");
@ -26,10 +27,19 @@ pub const Face = switch (options.backend) {
/// using whatever platform method you can. /// using whatever platform method you can.
pub const default_dpi = if (builtin.os.tag == .macos) 72 else 96; pub const default_dpi = if (builtin.os.tag == .macos) 72 else 96;
/// These are the flags to customize how freetype loads fonts. This is
/// only non-void if the freetype backend is enabled.
pub const FreetypeLoadFlags = if (options.backend.hasFreetype())
config.FreetypeLoadFlags
else
void;
pub const freetype_load_flags_default = if (FreetypeLoadFlags != void) .{} else {};
/// Options for initializing a font face. /// Options for initializing a font face.
pub const Options = struct { pub const Options = struct {
size: DesiredSize, size: DesiredSize,
metric_modifiers: ?*const Metrics.ModifierSet = null, metric_modifiers: ?*const Metrics.ModifierSet = null,
freetype_load_flags: FreetypeLoadFlags = freetype_load_flags_default,
}; };
/// The desired size for loading a font. /// The desired size for loading a font.

View File

@ -229,6 +229,9 @@ pub const Face = struct {
vs: []const font.face.Variation, vs: []const font.face.Variation,
opts: font.face.Options, opts: font.face.Options,
) !void { ) !void {
// If we have no variations, we don't need to do anything.
if (vs.len == 0) return;
// Create a new font descriptor with all the variations set. // Create a new font descriptor with all the variations set.
var desc = self.font.copyDescriptor(); var desc = self.font.copyDescriptor();
defer desc.release(); defer desc.release();

View File

@ -18,10 +18,16 @@ const Library = font.Library;
const convert = @import("freetype_convert.zig"); const convert = @import("freetype_convert.zig");
const fastmem = @import("../../fastmem.zig"); const fastmem = @import("../../fastmem.zig");
const quirks = @import("../../quirks.zig"); const quirks = @import("../../quirks.zig");
const config = @import("../../config.zig");
const log = std.log.scoped(.font_face); const log = std.log.scoped(.font_face);
pub const Face = struct { pub const Face = struct {
comptime {
// If we have the freetype backend, we should have load flags.
assert(font.face.FreetypeLoadFlags != void);
}
/// Our freetype library /// Our freetype library
lib: freetype.Library, lib: freetype.Library,
@ -34,6 +40,9 @@ pub const Face = struct {
/// Metrics for this font face. These are useful for renderers. /// Metrics for this font face. These are useful for renderers.
metrics: font.face.Metrics, metrics: font.face.Metrics,
/// Freetype load flags for this font face.
load_flags: font.face.FreetypeLoadFlags,
/// Set quirks.disableDefaultFontFeatures /// Set quirks.disableDefaultFontFeatures
quirks_disable_default_font_features: bool = false, quirks_disable_default_font_features: bool = false,
@ -77,6 +86,7 @@ pub const Face = struct {
.face = face, .face = face,
.hb_font = hb_font, .hb_font = hb_font,
.metrics = calcMetrics(face, opts.metric_modifiers), .metrics = calcMetrics(face, opts.metric_modifiers),
.load_flags = opts.freetype_load_flags,
}; };
result.quirks_disable_default_font_features = quirks.disableDefaultFontFeatures(&result); result.quirks_disable_default_font_features = quirks.disableDefaultFontFeatures(&result);
@ -319,6 +329,12 @@ pub const Face = struct {
// This must be enabled for color faces though because those are // This must be enabled for color faces though because those are
// often colored bitmaps, which we support. // often colored bitmaps, which we support.
.no_bitmap = !self.face.hasColor(), .no_bitmap = !self.face.hasColor(),
// use options from config
.no_hinting = !self.load_flags.hinting,
.force_autohint = !self.load_flags.@"force-autohint",
.monochrome = !self.load_flags.monochrome,
.no_autohint = !self.load_flags.autohint,
}); });
const glyph = self.face.handle.*.glyph; const glyph = self.face.handle.*.glyph;

Binary file not shown.

19
src/font/res/MIT.txt Normal file
View File

@ -0,0 +1,19 @@
MIT License
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

91
src/font/res/OFL.txt Normal file
View File

@ -0,0 +1,91 @@
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://openfontlicense.org
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

32
src/font/res/README.md Normal file
View File

@ -0,0 +1,32 @@
# Fonts and Licenses
This project uses several fonts which fall under the SIL Open Font License (OFL-1.1) and MIT License:
- Code New Roman (OFL-1.1)
- [© 2014 Sam Radian. All Rights Reserved.](https://github.com/chrissimpkins/codeface/blob/master/fonts/code-new-roman/license.txt)
- Geist Mono (OFL-1.1)
- [Copyright (c) 2023 Vercel, in collaboration with basement.studio](https://github.com/vercel/geist-font/blob/main/LICENSE.txt)
- Inconsolata (OFL-1.1)
- [Copyright 2006 The Inconsolata Project Authors](https://github.com/google/fonts/blob/main/ofl/inconsolata/OFL.txt)
- JetBrains Mono (OFL-1.1)
- [Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono)](https://github.com/JetBrains/JetBrainsMono/blob/master/OFL.txt)
- JuliaMono (OFL-1.1)
- [Copyright (c) 2020 - 2023, cormullion
with Reserved Font Name JuliaMono.](https://github.com/cormullion/juliamono/blob/master/LICENSE)
- Kawkab Mono (OFL-1.1)
- [Copyright (c) 2015, Abdullah Arif (abdullah.a@gmail.com). Copyright 2010, 2012, 2014 Adobe Systems Incorporated (http://www.adobe.com/), with Reserved Font Name 'Source'. All Rights Reserved. Source is a trademark of Adobe Systems Incorporated in the United States and/or other countries.
](https://github.com/aiaf/kawkab-mono/blob/master/OFL.txt)
- Lilex (OFL-1.1)
- [Copyright 2019 The Lilex Project Authors (https://github.com/mishamyrt/Lilex)](https://github.com/mishamyrt/Lilex/blob/master/OFL.txt)
- Monaspace Neon (OFL-1.1)
- [Copyright (c) 2023, GitHub https://github.com/githubnext/monaspace
with Reserved Font Name "Monaspace", including subfamilies: "Argon", "Neon", "Xenon", "Radon", and "Krypton"](https://github.com/githubnext/monaspace/blob/main/LICENSE)
- Noto Emoji (OFL-1.1)
- [Copyright 2013 Google LLC](https://github.com/googlefonts/noto-emoji/blob/main/LICENSE)
- Cozette (MIT)
- [Copyright (c) 2020, Slavfox](https://github.com/slavfox/Cozette/blob/main/LICENSE)
A full copy of the OFL license can be found at [OFL.txt](./OFL.txt).
An accompanying FAQ is also available at <https://openfontlicense.org/>.
A full copy of the MIT license can be found at [MIT.txt](./MIT.txt).

View File

@ -14,7 +14,7 @@ const std = @import("std");
const assert = std.debug.assert; const assert = std.debug.assert;
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const font = @import("../main.zig"); const font = @import("../main.zig");
const CacheTable = @import("../../cache_table.zig").CacheTable; const CacheTable = @import("../../datastruct/main.zig").CacheTable;
const log = std.log.scoped(.font_shaper_cache); const log = std.log.scoped(.font_shaper_cache);

View File

@ -190,6 +190,11 @@ pub const Shaper = struct {
// Reset the buffer for our current run // Reset the buffer for our current run
self.shaper.hb_buf.reset(); self.shaper.hb_buf.reset();
self.shaper.hb_buf.setContentType(.unicode); self.shaper.hb_buf.setContentType(.unicode);
// We don't support RTL text because RTL in terminals is messy.
// Its something we want to improve. For now, we force LTR because
// our renderers assume a strictly increasing X value.
self.shaper.hb_buf.setDirection(.ltr);
} }
pub fn addCodepoint(self: RunIteratorHook, cp: u32, cluster: u32) !void { pub fn addCodepoint(self: RunIteratorHook, cp: u32, cluster: u32) !void {
@ -453,6 +458,46 @@ test "shape monaspace ligs" {
} }
} }
// Ghostty doesn't currently support RTL and our renderers assume
// that cells are in strict LTR order. This means that we need to
// force RTL text to be LTR for rendering. This test ensures that
// we are correctly forcing RTL text to be LTR.
test "shape arabic forced LTR" {
const testing = std.testing;
const alloc = testing.allocator;
var testdata = try testShaperWithFont(alloc, .arabic);
defer testdata.deinit();
var screen = try terminal.Screen.init(alloc, 120, 30, 0);
defer screen.deinit();
try screen.testWriteString(@embedFile("testdata/arabic.txt"));
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
null,
);
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
try testing.expectEqual(@as(usize, 25), run.cells);
const cells = try shaper.shape(run);
try testing.expectEqual(@as(usize, 25), cells.len);
var x: u16 = cells[0].x;
for (cells[1..]) |cell| {
try testing.expectEqual(x + 1, cell.x);
x = cell.x;
}
}
try testing.expectEqual(@as(usize, 1), count);
}
test "shape emoji width" { test "shape emoji width" {
const testing = std.testing; const testing = std.testing;
const alloc = testing.allocator; const alloc = testing.allocator;
@ -1146,6 +1191,7 @@ const TestShaper = struct {
const TestFont = enum { const TestFont = enum {
inconsolata, inconsolata,
monaspace_neon, monaspace_neon,
arabic,
}; };
/// Helper to return a fully initialized shaper. /// Helper to return a fully initialized shaper.
@ -1159,6 +1205,7 @@ fn testShaperWithFont(alloc: Allocator, font_req: TestFont) !TestShaper {
const testFont = switch (font_req) { const testFont = switch (font_req) {
.inconsolata => font.embedded.inconsolata, .inconsolata => font.embedded.inconsolata,
.monaspace_neon => font.embedded.monaspace_neon, .monaspace_neon => font.embedded.monaspace_neon,
.arabic => font.embedded.arabic,
}; };
var lib = try Library.init(); var lib = try Library.init();

3
src/font/shaper/testdata/arabic.txt vendored Normal file
View File

@ -0,0 +1,3 @@
غريبه لاني عربي أبا عن جد
واتكلم الانجليزية بطلاقة اكثر من ٢٥ سنه
ومع هذا اجد العربيه افضل لان فيها الكثير من المفردات الاكثر دقه بالوصف

View File

@ -79,6 +79,17 @@ const Quads = packed struct(u4) {
br: bool = false, br: bool = false,
}; };
/// Specification of a branch drawing node, which consists of a
/// circle which is either empty or filled, and lines connecting
/// optionally between the circle and each of the 4 edges.
const BranchNode = packed struct(u5) {
up: bool = false,
right: bool = false,
down: bool = false,
left: bool = false,
filled: bool = false,
};
/// Alignment of a figure within a cell /// Alignment of a figure within a cell
const Alignment = struct { const Alignment = struct {
horizontal: enum { horizontal: enum {
@ -474,14 +485,14 @@ fn draw(self: Box, alloc: Allocator, canvas: *font.sprite.Canvas, cp: u32) !void
// '╬' // '╬'
0x256c => self.draw_lines(canvas, .{ .up = .double, .down = .double, .left = .double, .right = .double }), 0x256c => self.draw_lines(canvas, .{ .up = .double, .down = .double, .left = .double, .right = .double }),
// '╭' // '╭'
0x256d => try self.draw_light_arc(canvas, .br), 0x256d => try self.draw_arc(canvas, .br, .light),
// '╮' // '╮'
0x256e => try self.draw_light_arc(canvas, .bl), 0x256e => try self.draw_arc(canvas, .bl, .light),
// '╯' // '╯'
0x256f => try self.draw_light_arc(canvas, .tl), 0x256f => try self.draw_arc(canvas, .tl, .light),
// '╰' // '╰'
0x2570 => try self.draw_light_arc(canvas, .tr), 0x2570 => try self.draw_arc(canvas, .tr, .light),
// '' // ''
0x2571 => self.draw_light_diagonal_upper_right_to_lower_left(canvas), 0x2571 => self.draw_light_diagonal_upper_right_to_lower_left(canvas),
// '╲' // '╲'
@ -1302,6 +1313,330 @@ fn draw(self: Box, alloc: Allocator, canvas: *font.sprite.Canvas, cp: u32) !void
// '🯯' // '🯯'
0x1fbef => self.draw_circle(canvas, Alignment.top_left, true), 0x1fbef => self.draw_circle(canvas, Alignment.top_left, true),
// (Below:)
// Branch drawing character set, used for drawing git-like
// graphs in the terminal. Originally implemented in Kitty.
// Ref:
// - https://github.com/kovidgoyal/kitty/pull/7681
// - https://github.com/kovidgoyal/kitty/pull/7805
// NOTE: Kitty is GPL licensed, and its code was not referenced
// for these characters, only the loose specification of
// the character set in the pull request descriptions.
//
// TODO(qwerasd): This should be in another file, but really the
// general organization of the sprite font code
// needs to be reworked eventually.
//
//
//
//
//
// ''
0x0f5d0 => self.hline_middle(canvas, .light),
// ''
0x0f5d1 => self.vline_middle(canvas, .light),
// ''
0x0f5d2 => self.draw_fading_line(canvas, .right, .light),
// ''
0x0f5d3 => self.draw_fading_line(canvas, .left, .light),
// ''
0x0f5d4 => self.draw_fading_line(canvas, .bottom, .light),
// ''
0x0f5d5 => self.draw_fading_line(canvas, .top, .light),
// ''
0x0f5d6 => try self.draw_arc(canvas, .br, .light),
// ''
0x0f5d7 => try self.draw_arc(canvas, .bl, .light),
// ''
0x0f5d8 => try self.draw_arc(canvas, .tr, .light),
// ''
0x0f5d9 => try self.draw_arc(canvas, .tl, .light),
// ''
0x0f5da => {
self.vline_middle(canvas, .light);
try self.draw_arc(canvas, .tr, .light);
},
// ''
0x0f5db => {
self.vline_middle(canvas, .light);
try self.draw_arc(canvas, .br, .light);
},
// ''
0x0f5dc => {
try self.draw_arc(canvas, .tr, .light);
try self.draw_arc(canvas, .br, .light);
},
// ''
0x0f5dd => {
self.vline_middle(canvas, .light);
try self.draw_arc(canvas, .tl, .light);
},
// ''
0x0f5de => {
self.vline_middle(canvas, .light);
try self.draw_arc(canvas, .bl, .light);
},
// ''
0x0f5df => {
try self.draw_arc(canvas, .tl, .light);
try self.draw_arc(canvas, .bl, .light);
},
// ''
0x0f5e0 => {
try self.draw_arc(canvas, .bl, .light);
self.hline_middle(canvas, .light);
},
// ''
0x0f5e1 => {
try self.draw_arc(canvas, .br, .light);
self.hline_middle(canvas, .light);
},
// ''
0x0f5e2 => {
try self.draw_arc(canvas, .br, .light);
try self.draw_arc(canvas, .bl, .light);
},
// ''
0x0f5e3 => {
try self.draw_arc(canvas, .tl, .light);
self.hline_middle(canvas, .light);
},
// ''
0x0f5e4 => {
try self.draw_arc(canvas, .tr, .light);
self.hline_middle(canvas, .light);
},
// ''
0x0f5e5 => {
try self.draw_arc(canvas, .tr, .light);
try self.draw_arc(canvas, .tl, .light);
},
// ''
0x0f5e6 => {
self.vline_middle(canvas, .light);
try self.draw_arc(canvas, .tl, .light);
try self.draw_arc(canvas, .tr, .light);
},
// ''
0x0f5e7 => {
self.vline_middle(canvas, .light);
try self.draw_arc(canvas, .bl, .light);
try self.draw_arc(canvas, .br, .light);
},
// ''
0x0f5e8 => {
self.hline_middle(canvas, .light);
try self.draw_arc(canvas, .bl, .light);
try self.draw_arc(canvas, .tl, .light);
},
// ''
0x0f5e9 => {
self.hline_middle(canvas, .light);
try self.draw_arc(canvas, .tr, .light);
try self.draw_arc(canvas, .br, .light);
},
// ''
0x0f5ea => {
self.vline_middle(canvas, .light);
try self.draw_arc(canvas, .tl, .light);
try self.draw_arc(canvas, .br, .light);
},
// ''
0x0f5eb => {
self.vline_middle(canvas, .light);
try self.draw_arc(canvas, .tr, .light);
try self.draw_arc(canvas, .bl, .light);
},
// ''
0x0f5ec => {
self.hline_middle(canvas, .light);
try self.draw_arc(canvas, .tl, .light);
try self.draw_arc(canvas, .br, .light);
},
// ''
0x0f5ed => {
self.hline_middle(canvas, .light);
try self.draw_arc(canvas, .tr, .light);
try self.draw_arc(canvas, .bl, .light);
},
// ''
0x0f5ee => self.draw_branch_node(canvas, .{ .filled = true }, .light),
// ''
0x0f5ef => self.draw_branch_node(canvas, .{}, .light),
// ''
0x0f5f0 => self.draw_branch_node(canvas, .{
.right = true,
.filled = true,
}, .light),
// ''
0x0f5f1 => self.draw_branch_node(canvas, .{
.right = true,
}, .light),
// ''
0x0f5f2 => self.draw_branch_node(canvas, .{
.left = true,
.filled = true,
}, .light),
// ''
0x0f5f3 => self.draw_branch_node(canvas, .{
.left = true,
}, .light),
// ''
0x0f5f4 => self.draw_branch_node(canvas, .{
.left = true,
.right = true,
.filled = true,
}, .light),
// ''
0x0f5f5 => self.draw_branch_node(canvas, .{
.left = true,
.right = true,
}, .light),
// ''
0x0f5f6 => self.draw_branch_node(canvas, .{
.down = true,
.filled = true,
}, .light),
// ''
0x0f5f7 => self.draw_branch_node(canvas, .{
.down = true,
}, .light),
// ''
0x0f5f8 => self.draw_branch_node(canvas, .{
.up = true,
.filled = true,
}, .light),
// ''
0x0f5f9 => self.draw_branch_node(canvas, .{
.up = true,
}, .light),
// ''
0x0f5fa => self.draw_branch_node(canvas, .{
.up = true,
.down = true,
.filled = true,
}, .light),
// ''
0x0f5fb => self.draw_branch_node(canvas, .{
.up = true,
.down = true,
}, .light),
// ''
0x0f5fc => self.draw_branch_node(canvas, .{
.right = true,
.down = true,
.filled = true,
}, .light),
// ''
0x0f5fd => self.draw_branch_node(canvas, .{
.right = true,
.down = true,
}, .light),
// ''
0x0f5fe => self.draw_branch_node(canvas, .{
.left = true,
.down = true,
.filled = true,
}, .light),
// ''
0x0f5ff => self.draw_branch_node(canvas, .{
.left = true,
.down = true,
}, .light),
// ''
0x0f600 => self.draw_branch_node(canvas, .{
.up = true,
.right = true,
.filled = true,
}, .light),
// ''
0x0f601 => self.draw_branch_node(canvas, .{
.up = true,
.right = true,
}, .light),
// ''
0x0f602 => self.draw_branch_node(canvas, .{
.up = true,
.left = true,
.filled = true,
}, .light),
// ''
0x0f603 => self.draw_branch_node(canvas, .{
.up = true,
.left = true,
}, .light),
// ''
0x0f604 => self.draw_branch_node(canvas, .{
.up = true,
.down = true,
.right = true,
.filled = true,
}, .light),
// ''
0x0f605 => self.draw_branch_node(canvas, .{
.up = true,
.down = true,
.right = true,
}, .light),
// ''
0x0f606 => self.draw_branch_node(canvas, .{
.up = true,
.down = true,
.left = true,
.filled = true,
}, .light),
// ''
0x0f607 => self.draw_branch_node(canvas, .{
.up = true,
.down = true,
.left = true,
}, .light),
// ''
0x0f608 => self.draw_branch_node(canvas, .{
.down = true,
.left = true,
.right = true,
.filled = true,
}, .light),
// ''
0x0f609 => self.draw_branch_node(canvas, .{
.down = true,
.left = true,
.right = true,
}, .light),
// ''
0x0f60a => self.draw_branch_node(canvas, .{
.up = true,
.left = true,
.right = true,
.filled = true,
}, .light),
// ''
0x0f60b => self.draw_branch_node(canvas, .{
.up = true,
.left = true,
.right = true,
}, .light),
// ''
0x0f60c => self.draw_branch_node(canvas, .{
.up = true,
.down = true,
.left = true,
.right = true,
.filled = true,
}, .light),
// ''
0x0f60d => self.draw_branch_node(canvas, .{
.up = true,
.down = true,
.left = true,
.right = true,
}, .light),
// Not official box characters but special characters we hide // Not official box characters but special characters we hide
// in the high bits of a unicode codepoint. // in the high bits of a unicode codepoint.
@intFromEnum(Sprite.cursor_rect) => self.draw_cursor_rect(canvas), @intFromEnum(Sprite.cursor_rect) => self.draw_cursor_rect(canvas),
@ -1793,6 +2128,135 @@ fn draw_cell_diagonal(
) catch {}; ) catch {};
} }
fn draw_fading_line(
self: Box,
canvas: *font.sprite.Canvas,
comptime to: Edge,
comptime thickness: Thickness,
) void {
const thick_px = thickness.height(self.thickness);
const float_width: f64 = @floatFromInt(self.width);
const float_height: f64 = @floatFromInt(self.height);
// Top of horizontal strokes
const h_top = (self.height -| thick_px) / 2;
// Bottom of horizontal strokes
const h_bottom = h_top +| thick_px;
// Left of vertical strokes
const v_left = (self.width -| thick_px) / 2;
// Right of vertical strokes
const v_right = v_left +| thick_px;
// If we're fading to the top or left, we start with 0.0
// and increment up as we progress, otherwise we start
// at 255.0 and increment down (negative).
var color: f64 = switch (to) {
.top, .left => 0.0,
.bottom, .right => 255.0,
};
const inc: f64 = 255.0 / switch (to) {
.top => float_height,
.bottom => -float_height,
.left => float_width,
.right => -float_width,
};
switch (to) {
.top, .bottom => {
for (0..self.height) |y| {
for (v_left..v_right) |x| {
canvas.pixel(
@intCast(x),
@intCast(y),
@enumFromInt(@as(u8, @intFromFloat(@round(color)))),
);
}
color += inc;
}
},
.left, .right => {
for (0..self.width) |x| {
for (h_top..h_bottom) |y| {
canvas.pixel(
@intCast(x),
@intCast(y),
@enumFromInt(@as(u8, @intFromFloat(@round(color)))),
);
}
color += inc;
}
},
}
}
fn draw_branch_node(
self: Box,
canvas: *font.sprite.Canvas,
node: BranchNode,
comptime thickness: Thickness,
) void {
const thick_px = thickness.height(self.thickness);
const float_width: f64 = @floatFromInt(self.width);
const float_height: f64 = @floatFromInt(self.height);
const float_thick: f64 = @floatFromInt(thick_px);
// Top of horizontal strokes
const h_top = (self.height -| thick_px) / 2;
// Bottom of horizontal strokes
const h_bottom = h_top +| thick_px;
// Left of vertical strokes
const v_left = (self.width -| thick_px) / 2;
// Right of vertical strokes
const v_right = v_left +| thick_px;
// We calculate the center of the circle this way
// to ensure it aligns with box drawing characters
// since the lines are sometimes off center to
// make sure they aren't split between pixels.
const cx: f64 = @as(f64, @floatFromInt(v_left)) + float_thick / 2;
const cy: f64 = @as(f64, @floatFromInt(h_top)) + float_thick / 2;
// The radius needs to be the smallest distance from the center to an edge.
const r: f64 = @min(
@min(cx, cy),
@min(float_width - cx, float_height - cy),
);
var ctx: z2d.Context = .{
.surface = canvas.sfc,
.pattern = .{
.opaque_pattern = .{
.pixel = .{ .alpha8 = .{ .a = @intFromEnum(Shade.on) } },
},
},
.line_width = float_thick,
};
var path = z2d.Path.init(canvas.alloc);
defer path.deinit();
// These @intFromFloat casts shouldn't ever fail since r can never
// be greater than cx or cy, so when subtracting it from them the
// result can never be negative.
if (node.up)
self.rect(canvas, v_left, 0, v_right, @intFromFloat(@ceil(cy - r)));
if (node.right)
self.rect(canvas, @intFromFloat(@floor(cx + r)), h_top, self.width, h_bottom);
if (node.down)
self.rect(canvas, v_left, @intFromFloat(@floor(cy + r)), v_right, self.height);
if (node.left)
self.rect(canvas, 0, h_top, @intFromFloat(@ceil(cx - r)), h_bottom);
if (node.filled) {
path.arc(cx, cy, r, 0, std.math.pi * 2, false, null) catch return;
path.close() catch return;
ctx.fill(canvas.alloc, path) catch return;
} else {
path.arc(cx, cy, r - float_thick / 2, 0, std.math.pi * 2, false, null) catch return;
path.close() catch return;
ctx.stroke(canvas.alloc, path) catch return;
}
}
fn draw_circle( fn draw_circle(
self: Box, self: Box,
canvas: *font.sprite.Canvas, canvas: *font.sprite.Canvas,
@ -2118,12 +2582,13 @@ fn draw_edge_triangle(
try ctx.fill(canvas.alloc, path); try ctx.fill(canvas.alloc, path);
} }
fn draw_light_arc( fn draw_arc(
self: Box, self: Box,
canvas: *font.sprite.Canvas, canvas: *font.sprite.Canvas,
comptime corner: Corner, comptime corner: Corner,
comptime thickness: Thickness,
) !void { ) !void {
const thick_px = Thickness.light.height(self.thickness); const thick_px = thickness.height(self.thickness);
const float_width: f64 = @floatFromInt(self.width); const float_width: f64 = @floatFromInt(self.width);
const float_height: f64 = @floatFromInt(self.height); const float_height: f64 = @floatFromInt(self.height);
const float_thick: f64 = @floatFromInt(thick_px); const float_thick: f64 = @floatFromInt(thick_px);
@ -2534,6 +2999,32 @@ fn testRenderAll(self: Box, alloc: Allocator, atlas: *font.Atlas) !void {
else => {}, else => {},
} }
} }
// Branch drawing character set, used for drawing git-like
// graphs in the terminal. Originally implemented in Kitty.
// Ref:
// - https://github.com/kovidgoyal/kitty/pull/7681
// - https://github.com/kovidgoyal/kitty/pull/7805
// NOTE: Kitty is GPL licensed, and its code was not referenced
// for these characters, only the loose specification of
// the character set in the pull request descriptions.
//
// TODO(qwerasd): This should be in another file, but really the
// general organization of the sprite font code
// needs to be reworked eventually.
//
//
//
//
//
cp = 0xf5d0;
while (cp <= 0xf60d) : (cp += 1) {
_ = try self.renderGlyph(
alloc,
atlas,
cp,
);
}
} }
test "render all sprites" { test "render all sprites" {

View File

@ -263,6 +263,21 @@ const Kind = enum {
0x1FBCE...0x1FBEF, 0x1FBCE...0x1FBEF,
=> .box, => .box,
// Branch drawing character set, used for drawing git-like
// graphs in the terminal. Originally implemented in Kitty.
// Ref:
// - https://github.com/kovidgoyal/kitty/pull/7681
// - https://github.com/kovidgoyal/kitty/pull/7805
// NOTE: Kitty is GPL licensed, and its code was not referenced
// for these characters, only the loose specification of
// the character set in the pull request descriptions.
//
//
//
//
//
0xF5D0...0xF60D => .box,
// Powerline fonts // Powerline fonts
0xE0B0, 0xE0B0,
0xE0B4, 0xE0B4,

Binary file not shown.

View File

@ -317,8 +317,7 @@ pub const Action = union(enum) {
/// Focus on a split in a given direction. /// Focus on a split in a given direction.
goto_split: SplitFocusDirection, goto_split: SplitFocusDirection,
/// zoom/unzoom the current split. This is currently only supported /// zoom/unzoom the current split.
/// on macOS. Contributions welcome for other platforms.
toggle_split_zoom: void, toggle_split_zoom: void,
/// Resize the current split by moving the split divider in the given /// Resize the current split by moving the split divider in the given
@ -422,6 +421,8 @@ pub const Action = union(enum) {
/// ///
crash: CrashThread, crash: CrashThread,
pub const Key = @typeInfo(Action).Union.tag_type.?;
pub const CrashThread = enum { pub const CrashThread = enum {
main, main,
io, io,
@ -431,6 +432,16 @@ pub const Action = union(enum) {
pub const CursorKey = struct { pub const CursorKey = struct {
normal: []const u8, normal: []const u8,
application: []const u8, application: []const u8,
pub fn clone(
self: CursorKey,
alloc: Allocator,
) Allocator.Error!CursorKey {
return .{
.normal = try alloc.dupe(u8, self.normal),
.application = try alloc.dupe(u8, self.application),
};
}
}; };
pub const AdjustSelection = enum { pub const AdjustSelection = enum {
@ -774,6 +785,50 @@ pub const Action = union(enum) {
} }
} }
/// Clone this action with the given allocator. The allocator
/// should be an arena-style allocator since fine-grained
/// deallocation is not possible.
pub fn clone(self: Action, alloc: Allocator) Allocator.Error!Action {
return switch (self) {
inline else => |value, tag| @unionInit(
Action,
@tagName(tag),
try cloneValue(alloc, value),
),
};
}
fn cloneValue(
alloc: Allocator,
value: anytype,
) Allocator.Error!@TypeOf(value) {
return switch (@typeInfo(@TypeOf(value))) {
.Void,
.Int,
.Float,
.Enum,
=> value,
.Pointer => |info| slice: {
comptime assert(info.size == .Slice);
break :slice try alloc.dupe(
info.child,
value,
);
},
.Struct => |info| if (info.is_tuple)
value
else
try value.clone(alloc),
else => {
@compileLog(@TypeOf(value));
@compileError("unexpected type");
},
};
}
/// Returns a hash code that can be used to uniquely identify this /// Returns a hash code that can be used to uniquely identify this
/// action. /// action.
pub fn hash(self: Action) u64 { pub fn hash(self: Action) u64 {
@ -1102,6 +1157,16 @@ pub const Set = struct {
action: Action, action: Action,
flags: Flags, flags: Flags,
pub fn clone(
self: Leaf,
alloc: Allocator,
) Allocator.Error!Leaf {
return .{
.action = try self.action.clone(alloc),
.flags = self.flags,
};
}
pub fn hash(self: Leaf) u64 { pub fn hash(self: Leaf) u64 {
var hasher = std.hash.Wyhash.init(0); var hasher = std.hash.Wyhash.init(0);
self.action.hash(&hasher); self.action.hash(&hasher);
@ -1391,8 +1456,9 @@ pub const Set = struct {
// If we have any leaders we need to clone them. // If we have any leaders we need to clone them.
var it = result.bindings.iterator(); var it = result.bindings.iterator();
while (it.next()) |entry| switch (entry.value_ptr.*) { while (it.next()) |entry| switch (entry.value_ptr.*) {
// No data to clone // Leaves could have data to clone (i.e. text actions
.leaf => {}, // contain allocated strings).
.leaf => |*s| s.* = try s.clone(alloc),
// Must be deep cloned. // Must be deep cloned.
.leader => |*s| { .leader => |*s| {
@ -2129,3 +2195,22 @@ test "set: consumed state" {
try testing.expect(s.get(.{ .key = .{ .translated = .a } }).?.value_ptr.* == .leaf); try testing.expect(s.get(.{ .key = .{ .translated = .a } }).?.value_ptr.* == .leaf);
try testing.expect(s.get(.{ .key = .{ .translated = .a } }).?.value_ptr.*.leaf.flags.consumed); try testing.expect(s.get(.{ .key = .{ .translated = .a } }).?.value_ptr.*.leaf.flags.consumed);
} }
test "Action: clone" {
const testing = std.testing;
var arena = std.heap.ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
{
var a: Action = .ignore;
const b = try a.clone(alloc);
try testing.expect(b == .ignore);
}
{
var a: Action = .{ .text = "foo" };
const b = try a.clone(alloc);
try testing.expect(b == .text);
}
}

View File

@ -11,6 +11,7 @@ const cimgui = @import("cimgui");
const Surface = @import("../Surface.zig"); const Surface = @import("../Surface.zig");
const font = @import("../font/main.zig"); const font = @import("../font/main.zig");
const input = @import("../input.zig"); const input = @import("../input.zig");
const renderer = @import("../renderer.zig");
const terminal = @import("../terminal/main.zig"); const terminal = @import("../terminal/main.zig");
const inspector = @import("main.zig"); const inspector = @import("main.zig");
@ -641,8 +642,8 @@ fn renderSizeWindow(self: *Inspector) void {
_ = cimgui.c.igTableSetColumnIndex(1); _ = cimgui.c.igTableSetColumnIndex(1);
cimgui.c.igText( cimgui.c.igText(
"%dpx x %dpx", "%dpx x %dpx",
self.surface.screen_size.width, self.surface.size.screen.width,
self.surface.screen_size.height, self.surface.size.screen.height,
); );
} }
} }
@ -656,10 +657,11 @@ fn renderSizeWindow(self: *Inspector) void {
} }
{ {
_ = cimgui.c.igTableSetColumnIndex(1); _ = cimgui.c.igTableSetColumnIndex(1);
const grid_size = self.surface.size.grid();
cimgui.c.igText( cimgui.c.igText(
"%dc x %dr", "%dc x %dr",
self.surface.grid_size.columns, grid_size.columns,
self.surface.grid_size.rows, grid_size.rows,
); );
} }
} }
@ -675,8 +677,8 @@ fn renderSizeWindow(self: *Inspector) void {
_ = cimgui.c.igTableSetColumnIndex(1); _ = cimgui.c.igTableSetColumnIndex(1);
cimgui.c.igText( cimgui.c.igText(
"%dpx x %dpx", "%dpx x %dpx",
self.surface.cell_size.width, self.surface.size.cell.width,
self.surface.cell_size.height, self.surface.size.cell.height,
); );
} }
} }
@ -692,10 +694,10 @@ fn renderSizeWindow(self: *Inspector) void {
_ = cimgui.c.igTableSetColumnIndex(1); _ = cimgui.c.igTableSetColumnIndex(1);
cimgui.c.igText( cimgui.c.igText(
"T=%d B=%d L=%d R=%d px", "T=%d B=%d L=%d R=%d px",
self.surface.padding.top, self.surface.size.padding.top,
self.surface.padding.bottom, self.surface.size.padding.bottom,
self.surface.padding.left, self.surface.size.padding.left,
self.surface.padding.right, self.surface.size.padding.right,
); );
} }
} }
@ -785,7 +787,13 @@ fn renderSizeWindow(self: *Inspector) void {
} }
{ {
const adjusted = self.surface.posAdjusted(self.mouse.last_xpos, self.mouse.last_ypos); const coord: renderer.Coordinate.Terminal = (renderer.Coordinate{
.surface = .{
.x = self.mouse.last_xpos,
.y = self.mouse.last_ypos,
},
}).convert(.terminal, self.surface.size).terminal;
cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0);
{ {
_ = cimgui.c.igTableSetColumnIndex(0); _ = cimgui.c.igTableSetColumnIndex(0);
@ -795,8 +803,8 @@ fn renderSizeWindow(self: *Inspector) void {
_ = cimgui.c.igTableSetColumnIndex(1); _ = cimgui.c.igTableSetColumnIndex(1);
cimgui.c.igText( cimgui.c.igText(
"(%dpx, %dpx)", "(%dpx, %dpx)",
@as(i64, @intFromFloat(adjusted.x)), @as(i64, @intFromFloat(coord.x)),
@as(i64, @intFromFloat(adjusted.y)), @as(i64, @intFromFloat(coord.y)),
); );
} }
} }

View File

@ -1,7 +1,7 @@
const std = @import("std"); const std = @import("std");
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const input = @import("../input.zig"); const input = @import("../input.zig");
const CircBuf = @import("../circ_buf.zig").CircBuf; const CircBuf = @import("../datastruct/main.zig").CircBuf;
const cimgui = @import("cimgui"); const cimgui = @import("cimgui");
/// Circular buffer of key events. /// Circular buffer of key events.

View File

@ -2,7 +2,7 @@ const std = @import("std");
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const cimgui = @import("cimgui"); const cimgui = @import("cimgui");
const terminal = @import("../terminal/main.zig"); const terminal = @import("../terminal/main.zig");
const CircBuf = @import("../circ_buf.zig").CircBuf; const CircBuf = @import("../datastruct/main.zig").CircBuf;
const Surface = @import("../Surface.zig"); const Surface = @import("../Surface.zig");
/// The stream handler for our inspector. /// The stream handler for our inspector.

View File

@ -168,7 +168,6 @@ pub const std_options: std.Options = .{
}; };
test { test {
_ = @import("circ_buf.zig");
_ = @import("pty.zig"); _ = @import("pty.zig");
_ = @import("Command.zig"); _ = @import("Command.zig");
_ = @import("font/main.zig"); _ = @import("font/main.zig");
@ -180,17 +179,11 @@ test {
_ = @import("surface_mouse.zig"); _ = @import("surface_mouse.zig");
// Libraries // Libraries
_ = @import("segmented_pool.zig");
_ = @import("crash/main.zig"); _ = @import("crash/main.zig");
_ = @import("datastruct/main.zig");
_ = @import("inspector/main.zig"); _ = @import("inspector/main.zig");
_ = @import("terminal/main.zig"); _ = @import("terminal/main.zig");
_ = @import("terminfo/main.zig"); _ = @import("terminfo/main.zig");
_ = @import("simd/main.zig"); _ = @import("simd/main.zig");
_ = @import("unicode/main.zig"); _ = @import("unicode/main.zig");
// TODO
_ = @import("blocking_queue.zig");
_ = @import("cache_table.zig");
_ = @import("config.zig");
_ = @import("lru.zig");
} }

View File

@ -9,7 +9,7 @@ const builtin = @import("builtin");
const xev = @import("xev"); const xev = @import("xev");
const macos = @import("macos"); const macos = @import("macos");
const BlockingQueue = @import("../blocking_queue.zig").BlockingQueue; const BlockingQueue = @import("../datastruct/main.zig").BlockingQueue;
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const log = std.log.scoped(.cf_release_thread); const log = std.log.scoped(.cf_release_thread);

View File

@ -3,12 +3,6 @@ const builtin = @import("builtin");
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const posix = std.posix; const posix = std.posix;
/// The separator used in environment variables such as PATH.
pub const PATH_SEP = switch (builtin.os.tag) {
.windows => ";",
else => ":",
};
/// Append a value to an environment variable such as PATH. /// Append a value to an environment variable such as PATH.
/// The returned value is always allocated so it must be freed. /// The returned value is always allocated so it must be freed.
pub fn appendEnv( pub fn appendEnv(
@ -33,9 +27,9 @@ pub fn appendEnvAlways(
current: []const u8, current: []const u8,
value: []const u8, value: []const u8,
) ![]u8 { ) ![]u8 {
return try std.fmt.allocPrint(alloc, "{s}{s}{s}", .{ return try std.fmt.allocPrint(alloc, "{s}{c}{s}", .{
current, current,
PATH_SEP, std.fs.path.delimiter,
value, value,
}); });
} }

Some files were not shown because too many files have changed in this diff Show More