mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-08-02 14:57:31 +03:00
Merge branch 'ghostty-org:main' into main
This commit is contained in:
12
.github/workflows/release-tip.yml
vendored
12
.github/workflows/release-tip.yml
vendored
@ -158,6 +158,9 @@ jobs:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
|
||||
- name: XCode Select
|
||||
run: sudo xcode-select -s /Applications/Xcode_16.0.app
|
||||
|
||||
# Setup Sparkle
|
||||
- name: Setup Sparkle
|
||||
env:
|
||||
@ -187,7 +190,6 @@ jobs:
|
||||
- name: Build Ghostty.app
|
||||
run: |
|
||||
cd macos
|
||||
sudo xcode-select -s /Applications/Xcode_16.0.app
|
||||
xcodebuild -target Ghostty -configuration Release
|
||||
|
||||
# We inject the "build number" as simply the number of commits since HEAD.
|
||||
@ -342,6 +344,9 @@ jobs:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
|
||||
- name: XCode Select
|
||||
run: sudo xcode-select -s /Applications/Xcode_16.0.app
|
||||
|
||||
# Setup Sparkle
|
||||
- name: Setup Sparkle
|
||||
env:
|
||||
@ -371,7 +376,6 @@ jobs:
|
||||
- name: Build Ghostty.app
|
||||
run: |
|
||||
cd macos
|
||||
sudo xcode-select -s /Applications/Xcode_16.0.app
|
||||
xcodebuild -target Ghostty -configuration Release
|
||||
|
||||
# We inject the "build number" as simply the number of commits since HEAD.
|
||||
@ -514,6 +518,9 @@ jobs:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
|
||||
- name: XCode Select
|
||||
run: sudo xcode-select -s /Applications/Xcode_16.0.app
|
||||
|
||||
# Setup Sparkle
|
||||
- name: Setup Sparkle
|
||||
env:
|
||||
@ -543,7 +550,6 @@ jobs:
|
||||
- name: Build Ghostty.app
|
||||
run: |
|
||||
cd macos
|
||||
sudo xcode-select -s /Applications/Xcode_16.0.app
|
||||
xcodebuild -target Ghostty -configuration Release
|
||||
|
||||
# We inject the "build number" as simply the number of commits since HEAD.
|
||||
|
10
.github/workflows/test.yml
vendored
10
.github/workflows/test.yml
vendored
@ -196,6 +196,9 @@ jobs:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
|
||||
- name: XCode Select
|
||||
run: sudo xcode-select -s /Applications/Xcode_16.0.app
|
||||
|
||||
- name: Test All
|
||||
run: |
|
||||
# OpenGL
|
||||
@ -324,10 +327,10 @@ jobs:
|
||||
run: nix develop -c zig build -Dapp-runtime=none test
|
||||
|
||||
- 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)
|
||||
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
|
||||
run: nix develop -c zig build -Dapp-runtime=glfw
|
||||
@ -352,6 +355,9 @@ jobs:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
|
||||
- name: XCode Select
|
||||
run: sudo xcode-select -s /Applications/Xcode_16.0.app
|
||||
|
||||
- name: test
|
||||
run: nix develop -c zig build test
|
||||
|
||||
|
79
CONTRIBUTING.md
Normal file
79
CONTRIBUTING.md
Normal 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.
|
@ -80,7 +80,9 @@ relevant to package maintainers:
|
||||
|
||||
- `--system`: The path to the offline cache directory. This disables
|
||||
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
|
||||
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.
|
||||
This avoids building for newer CPU features that may not be available on
|
||||
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.
|
||||
|
15
README.md
15
README.md
@ -63,7 +63,7 @@ placed at `$XDG_CONFIG_HOME/ghostty/config`, which defaults to
|
||||
|
||||
The file format is documented below as an example:
|
||||
|
||||
```
|
||||
```ini
|
||||
# The syntax is "key = value". The whitespace around the equals doesn't matter.
|
||||
background = 282c34
|
||||
foreground= ffffff
|
||||
@ -375,9 +375,9 @@ test cases.
|
||||
|
||||
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/))
|
||||
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
|
||||
(1) standards, if available, (2) xterm, if the feature exists, (3)
|
||||
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`
|
||||
# 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, ... }: {
|
||||
|
1
TODO.md
1
TODO.md
@ -19,4 +19,3 @@ Mac:
|
||||
Major Features:
|
||||
|
||||
- Bell
|
||||
- Sixels: https://saitoha.github.io/libsixel/
|
||||
|
76
build.zig
76
build.zig
@ -10,6 +10,7 @@ const font = @import("src/font/main.zig");
|
||||
const renderer = @import("src/renderer.zig");
|
||||
const terminfo = @import("src/terminfo/main.zig");
|
||||
const config_vim = @import("src/config/vim.zig");
|
||||
const config_sublime_syntax = @import("src/config/sublime_syntax.zig");
|
||||
const fish_completions = @import("src/build/fish_completions.zig");
|
||||
const build_config = @import("src/build_config.zig");
|
||||
const BuildConfig = build_config.BuildConfig;
|
||||
@ -56,6 +57,11 @@ pub fn build(b: *std.Build) !void {
|
||||
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;
|
||||
|
||||
// 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.",
|
||||
) orelse renderer.Impl.default(target.result, wasm_target);
|
||||
|
||||
config.libadwaita = b.option(
|
||||
config.adwaita = b.option(
|
||||
bool,
|
||||
"gtk-libadwaita",
|
||||
"Enables the use of libadwaita when using the gtk rendering backend.",
|
||||
"gtk-adwaita",
|
||||
"Enables the use of Adwaita when using the GTK rendering backend.",
|
||||
) 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 u8,
|
||||
"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 (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.
|
||||
const path = Command.expandPath(b.allocator, "pandoc") catch
|
||||
break :emit_docs false;
|
||||
@ -281,6 +296,9 @@ pub fn build(b: *std.Build) !void {
|
||||
|
||||
// Exe
|
||||
if (exe_) |exe| {
|
||||
// Set PIE if requested
|
||||
if (pie) exe.pie = true;
|
||||
|
||||
// Add the shared dependencies
|
||||
_ = 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
|
||||
if (emit_docs) {
|
||||
try buildDocumentation(b, config);
|
||||
@ -975,7 +1025,7 @@ fn addDeps(
|
||||
|
||||
if (b.systemIntegrationOption("freetype", .{})) {
|
||||
step.linkSystemLibrary2("bzip2", dynamic_link_opts);
|
||||
step.linkSystemLibrary2("freetype", dynamic_link_opts);
|
||||
step.linkSystemLibrary2("freetype2", dynamic_link_opts);
|
||||
} else {
|
||||
step.linkLibrary(freetype_dep.artifact("freetype"));
|
||||
try static_libs.append(freetype_dep.artifact("freetype").getEmittedBin());
|
||||
@ -1054,7 +1104,8 @@ fn addDeps(
|
||||
});
|
||||
step.root_module.addImport("oniguruma", oniguruma_dep.module("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 {
|
||||
step.linkLibrary(oniguruma_dep.artifact("oniguruma"));
|
||||
try static_libs.append(oniguruma_dep.artifact("oniguruma").getEmittedBin());
|
||||
@ -1068,6 +1119,7 @@ fn addDeps(
|
||||
step.root_module.addImport("glslang", glslang_dep.module("glslang"));
|
||||
if (b.systemIntegrationOption("glslang", .{})) {
|
||||
step.linkSystemLibrary2("glslang", dynamic_link_opts);
|
||||
step.linkSystemLibrary2("glslang-default-resource-limits", dynamic_link_opts);
|
||||
} else {
|
||||
step.linkLibrary(glslang_dep.artifact("glslang"));
|
||||
try static_libs.append(glslang_dep.artifact("glslang").getEmittedBin());
|
||||
@ -1190,8 +1242,6 @@ fn addDeps(
|
||||
step.root_module.addImport("vaxis", b.dependency("vaxis", .{
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
.libxev = false,
|
||||
.images = false,
|
||||
}).module("vaxis"));
|
||||
step.root_module.addImport("wuffs", b.dependency("wuffs", .{
|
||||
.target = target,
|
||||
@ -1213,6 +1263,7 @@ fn addDeps(
|
||||
step.root_module.addImport("zf", b.dependency("zf", .{
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
.with_tui = false,
|
||||
}).module("zf"));
|
||||
|
||||
// Mac Stuff
|
||||
@ -1226,14 +1277,7 @@ fn addDeps(
|
||||
.optimize = optimize,
|
||||
});
|
||||
|
||||
// This is a bit of a hack that should probably be fixed upstream
|
||||
// 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("objc", objc_dep.module("objc"));
|
||||
step.root_module.addImport("macos", macos_dep.module("macos"));
|
||||
step.linkLibrary(macos_dep.artifact("macos"));
|
||||
try static_libs.append(macos_dep.artifact("macos").getEmittedBin());
|
||||
@ -1294,7 +1338,7 @@ fn addDeps(
|
||||
|
||||
.gtk => {
|
||||
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");
|
||||
|
@ -14,8 +14,8 @@
|
||||
.lazy = true,
|
||||
},
|
||||
.zig_objc = .{
|
||||
.url = "https://github.com/mitchellh/zig-objc/archive/fe5ac419530cf800294369d996133fe9cd067aec.tar.gz",
|
||||
.hash = "122034b3e15d582d8d101a7713e5f13c872b8b8eb6d9cb47515b8e34ee75e122630d",
|
||||
.url = "https://github.com/mitchellh/zig-objc/archive/9b8ba849b0f58fe207ecd6ab7c147af55b17556e.tar.gz",
|
||||
.hash = "1220e17e64ef0ef561b3e4b9f3a96a2494285f2ec31c097721bf8c8677ec4415c634",
|
||||
},
|
||||
.zig_js = .{
|
||||
.url = "https://github.com/mitchellh/zig-js/archive/d0b8b0a57c52fbc89f9d9fecba75ca29da7dd7d1.tar.gz",
|
||||
@ -49,16 +49,16 @@
|
||||
// Other
|
||||
.apple_sdk = .{ .path = "./pkg/apple-sdk" },
|
||||
.iterm2_themes = .{
|
||||
.url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/b4a9c4d.tar.gz",
|
||||
.hash = "122056fbb29863ec1678b7954fb76b1533ad8c581a34577c1b2efe419e29e05596df",
|
||||
.url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/80543b14552b7c9fef88fad826552e6ac5632abe.tar.gz",
|
||||
.hash = "1220217ae916146a4c598f8ba5bfff0ff940335d00572e337f20b4accf24fa2ca4fc",
|
||||
},
|
||||
.vaxis = .{
|
||||
.url = "git+https://github.com/rockorager/libvaxis?ref=main#a1b43d24653670d612b91f0855b165e6c987b809",
|
||||
.hash = "1220e4d6fc82c487178339422887fdfd5094b15c242fe31ad596c4b2fcdc60ef667f",
|
||||
.url = "git+https://github.com/rockorager/libvaxis/?ref=main#6d729a2dc3b934818dffe06d2ba3ce02841ed74b",
|
||||
.hash = "12200df4ebeaed45de26cb2c9f3b6f3746d8013b604e035dae658f86f586c8c91d2f",
|
||||
},
|
||||
.zf = .{
|
||||
.url = "git+https://github.com/natecraddock/zf.git?ref=main#bb27a917c3513785c6a91f0b1c10002a5029cacc",
|
||||
.hash = "1220a74107c7f153a2f809e41c7fa7e8dbf75c91043e39fad998247804e5edac2cc8",
|
||||
.url = "git+https://github.com/natecraddock/zf/?ref=main#ed99ca18b02dda052e20ba467e90b623c04690dd",
|
||||
.hash = "1220edc3b8d8bedbb50555947987e5e8e2f93871ca3c8e8d4cc8f1377c15b5dd35e8",
|
||||
},
|
||||
.z2d = .{
|
||||
.url = "git+https://github.com/vancluever/z2d?ref=main#285a796eb9c25a2389f087d008f0e60faf0b8eda",
|
||||
|
@ -430,6 +430,11 @@ typedef struct {
|
||||
const char* title;
|
||||
} ghostty_action_set_title_s;
|
||||
|
||||
// apprt.action.Pwd.C
|
||||
typedef struct {
|
||||
const char* pwd;
|
||||
} ghostty_action_pwd_s;
|
||||
|
||||
// terminal.MouseShape
|
||||
typedef enum {
|
||||
GHOSTTY_MOUSE_SHAPE_DEFAULT,
|
||||
@ -512,6 +517,31 @@ typedef struct {
|
||||
ghostty_input_trigger_s trigger;
|
||||
} 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
|
||||
typedef enum {
|
||||
GHOSTTY_ACTION_NEW_WINDOW,
|
||||
@ -537,6 +567,7 @@ typedef enum {
|
||||
GHOSTTY_ACTION_RENDER_INSPECTOR,
|
||||
GHOSTTY_ACTION_DESKTOP_NOTIFICATION,
|
||||
GHOSTTY_ACTION_SET_TITLE,
|
||||
GHOSTTY_ACTION_PWD,
|
||||
GHOSTTY_ACTION_MOUSE_SHAPE,
|
||||
GHOSTTY_ACTION_MOUSE_VISIBILITY,
|
||||
GHOSTTY_ACTION_MOUSE_OVER_LINK,
|
||||
@ -545,6 +576,9 @@ typedef enum {
|
||||
GHOSTTY_ACTION_QUIT_TIMER,
|
||||
GHOSTTY_ACTION_SECURE_INPUT,
|
||||
GHOSTTY_ACTION_KEY_SEQUENCE,
|
||||
GHOSTTY_ACTION_COLOR_CHANGE,
|
||||
GHOSTTY_ACTION_RELOAD_CONFIG,
|
||||
GHOSTTY_ACTION_CONFIG_CHANGE,
|
||||
} ghostty_action_tag_e;
|
||||
|
||||
typedef union {
|
||||
@ -560,6 +594,7 @@ typedef union {
|
||||
ghostty_action_inspector_e inspector;
|
||||
ghostty_action_desktop_notification_s desktop_notification;
|
||||
ghostty_action_set_title_s set_title;
|
||||
ghostty_action_pwd_s pwd;
|
||||
ghostty_action_mouse_shape_e mouse_shape;
|
||||
ghostty_action_mouse_visibility_e mouse_visibility;
|
||||
ghostty_action_mouse_over_link_s mouse_over_link;
|
||||
@ -567,6 +602,9 @@ typedef union {
|
||||
ghostty_action_quit_timer_e quit_timer;
|
||||
ghostty_action_secure_input_e secure_input;
|
||||
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;
|
||||
|
||||
typedef struct {
|
||||
@ -575,7 +613,6 @@ typedef struct {
|
||||
} ghostty_action_s;
|
||||
|
||||
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*,
|
||||
ghostty_clipboard_e,
|
||||
void*);
|
||||
@ -598,7 +635,6 @@ typedef struct {
|
||||
bool supports_selection_clipboard;
|
||||
ghostty_runtime_wakeup_cb wakeup_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_confirm_read_clipboard_cb confirm_read_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();
|
||||
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_default_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);
|
||||
void ghostty_app_keyboard_changed(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_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();
|
||||
|
||||
@ -646,6 +684,7 @@ void ghostty_surface_free(ghostty_surface_t);
|
||||
void* ghostty_surface_userdata(ghostty_surface_t);
|
||||
ghostty_app_t ghostty_surface_app(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);
|
||||
void ghostty_surface_refresh(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*,
|
||||
void*,
|
||||
bool);
|
||||
uintptr_t ghostty_surface_pwd(ghostty_surface_t, char*, uintptr_t);
|
||||
bool ghostty_surface_has_selection(ghostty_surface_t);
|
||||
uintptr_t ghostty_surface_selection(ghostty_surface_t, char*, uintptr_t);
|
||||
|
||||
|
@ -7,6 +7,7 @@
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
29C15B1D2CDC3B2900520DD4 /* bat in Resources */ = {isa = PBXBuildFile; fileRef = 29C15B1C2CDC3B2000520DD4 /* bat */; };
|
||||
55154BE02B33911F001622DC /* ghostty in Resources */ = {isa = PBXBuildFile; fileRef = 55154BDF2B33911F001622DC /* ghostty */; };
|
||||
552964E62B34A9B400030505 /* vim in Resources */ = {isa = PBXBuildFile; fileRef = 552964E52B34A9B400030505 /* vim */; };
|
||||
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 */; };
|
||||
A5985CD82C320C4500C57AD3 /* String+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5985CD62C320C4500C57AD3 /* String+Extension.swift */; };
|
||||
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 */; };
|
||||
A59FB5D12AE0DEA7009128F3 /* MetalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59FB5D02AE0DEA7009128F3 /* MetalView.swift */; };
|
||||
A5A1F8852A489D6800D1E8BC /* terminfo in Resources */ = {isa = PBXBuildFile; fileRef = A5A1F8842A489D6800D1E8BC /* terminfo */; };
|
||||
@ -95,6 +97,7 @@
|
||||
/* End PBXBuildFile 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>"; };
|
||||
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>"; };
|
||||
@ -139,6 +142,7 @@
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@ -247,6 +251,7 @@
|
||||
A59FB5D02AE0DEA7009128F3 /* MetalView.swift */,
|
||||
A5CBD0552C9E65A50017A1AE /* DraggableWindowView.swift */,
|
||||
C159E81C2B66A06B00FDFE9C /* OSColor+Extension.swift */,
|
||||
A599CDAF2CF103F20049FA26 /* NSAppearance+Extension.swift */,
|
||||
A52FFF5C2CAB4D05000C6A5B /* NSScreen+Extension.swift */,
|
||||
C1F26EA62B738B9900404083 /* NSView+Extension.swift */,
|
||||
AEE8B3442B9AA39600260C5E /* NSPasteboard+Extension.swift */,
|
||||
@ -366,6 +371,7 @@
|
||||
A5A1F8862A489D7400D1E8BC /* Resources */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
29C15B1C2CDC3B2000520DD4 /* bat */,
|
||||
55154BDF2B33911F001622DC /* ghostty */,
|
||||
552964E52B34A9B400030505 /* vim */,
|
||||
A586167B2B7703CC009BDB1D /* fish */,
|
||||
@ -495,7 +501,7 @@
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = 1;
|
||||
LastSwiftUpdateCheck = 1520;
|
||||
LastUpgradeCheck = 1600;
|
||||
LastUpgradeCheck = 1610;
|
||||
TargetAttributes = {
|
||||
A5B30530299BEAAA0047F10C = {
|
||||
CreatedOnToolsVersion = 14.2;
|
||||
@ -538,6 +544,7 @@
|
||||
A5E112932AF73E6E00C6E0C2 /* ClipboardConfirmation.xib in Resources */,
|
||||
A5CDF1912AAF9A5800513312 /* ConfigurationErrors.xib in Resources */,
|
||||
857F63812A5E64F200CA4815 /* MainMenu.xib in Resources */,
|
||||
29C15B1D2CDC3B2900520DD4 /* bat in Resources */,
|
||||
A596309A2AEE1C6400D64628 /* Terminal.xib in Resources */,
|
||||
A586167C2B7703CC009BDB1D /* fish in Resources */,
|
||||
55154BE02B33911F001622DC /* ghostty in Resources */,
|
||||
@ -607,6 +614,7 @@
|
||||
A53A6C032CCC1B7F00943E98 /* Ghostty.Action.swift in Sources */,
|
||||
A59FB5D12AE0DEA7009128F3 /* MetalView.swift in Sources */,
|
||||
A55685E029A03A9F004303CE /* AppError.swift in Sources */,
|
||||
A599CDB02CF103F60049FA26 /* NSAppearance+Extension.swift in Sources */,
|
||||
A52FFF572CA90484000C6A5B /* QuickTerminalScreen.swift in Sources */,
|
||||
A5CC36132C9CD72D004D6760 /* SecureInputOverlay.swift in Sources */,
|
||||
A535B9DA299C569B0017E2E4 /* ErrorView.swift in Sources */,
|
||||
@ -648,6 +656,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
@ -754,6 +763,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
@ -815,6 +825,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
|
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1600"
|
||||
LastUpgradeVersion = "1610"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
@ -69,6 +69,9 @@ class AppDelegate: NSObject,
|
||||
/// seconds since the process was launched.
|
||||
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.
|
||||
let ghostty: Ghostty.App = Ghostty.App()
|
||||
|
||||
@ -92,6 +95,9 @@ class AppDelegate: NSObject,
|
||||
/// makes our logic very easy.
|
||||
private var isVisible: Bool = true
|
||||
|
||||
/// The observer for the app appearance.
|
||||
private var appearanceObserver: NSKeyValueObservation? = nil
|
||||
|
||||
override init() {
|
||||
terminalManager = TerminalManager(ghostty)
|
||||
updaterController = SPUStandardUpdaterController(
|
||||
@ -138,7 +144,7 @@ class AppDelegate: NSObject,
|
||||
menuCheckForUpdates?.action = #selector(SPUStandardUpdaterController.checkForUpdates(_:))
|
||||
|
||||
// Initial config loading
|
||||
configDidReload(ghostty)
|
||||
ghosttyConfigDidChange(config: ghostty.config)
|
||||
|
||||
// Start our update checker.
|
||||
updaterController.startUpdater()
|
||||
@ -162,6 +168,12 @@ class AppDelegate: NSObject,
|
||||
name: .quickTerminalDidChangeVisibility,
|
||||
object: nil
|
||||
)
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(ghosttyConfigDidChange(_:)),
|
||||
name: .ghosttyConfigDidChange,
|
||||
object: nil
|
||||
)
|
||||
|
||||
// Configure user notifications
|
||||
let actions = [
|
||||
@ -178,6 +190,23 @@ class AppDelegate: NSObject,
|
||||
)
|
||||
])
|
||||
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) {
|
||||
@ -188,13 +217,13 @@ class AppDelegate: NSObject,
|
||||
// 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 restoring from persisted state
|
||||
if terminalManager.windows.count == 0 && ghostty.config.initialWindow {
|
||||
if terminalManager.windows.count == 0 && derivedConfig.initialWindow {
|
||||
terminalManager.newWindow()
|
||||
}
|
||||
}
|
||||
|
||||
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
||||
return ghostty.config.shouldQuitAfterLastWindowClosed
|
||||
return derivedConfig.shouldQuitAfterLastWindowClosed
|
||||
}
|
||||
|
||||
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.
|
||||
private func syncMenuShortcuts() {
|
||||
private func syncMenuShortcuts(_ config: Ghostty.Config) {
|
||||
guard ghostty.readiness == .ready else { return }
|
||||
|
||||
syncMenuShortcut(action: "open_config", menuItem: self.menuOpenConfig)
|
||||
syncMenuShortcut(action: "reload_config", menuItem: self.menuReloadConfig)
|
||||
syncMenuShortcut(action: "quit", menuItem: self.menuQuit)
|
||||
syncMenuShortcut(config, action: "open_config", menuItem: self.menuOpenConfig)
|
||||
syncMenuShortcut(config, action: "reload_config", menuItem: self.menuReloadConfig)
|
||||
syncMenuShortcut(config, action: "quit", menuItem: self.menuQuit)
|
||||
|
||||
syncMenuShortcut(action: "new_window", menuItem: self.menuNewWindow)
|
||||
syncMenuShortcut(action: "new_tab", menuItem: self.menuNewTab)
|
||||
syncMenuShortcut(action: "close_surface", menuItem: self.menuClose)
|
||||
syncMenuShortcut(action: "close_window", menuItem: self.menuCloseWindow)
|
||||
syncMenuShortcut(action: "close_all_windows", menuItem: self.menuCloseAllWindows)
|
||||
syncMenuShortcut(action: "new_split:right", menuItem: self.menuSplitRight)
|
||||
syncMenuShortcut(action: "new_split:down", menuItem: self.menuSplitDown)
|
||||
syncMenuShortcut(config, action: "new_window", menuItem: self.menuNewWindow)
|
||||
syncMenuShortcut(config, action: "new_tab", menuItem: self.menuNewTab)
|
||||
syncMenuShortcut(config, action: "close_surface", menuItem: self.menuClose)
|
||||
syncMenuShortcut(config, action: "close_window", menuItem: self.menuCloseWindow)
|
||||
syncMenuShortcut(config, action: "close_all_windows", menuItem: self.menuCloseAllWindows)
|
||||
syncMenuShortcut(config, action: "new_split:right", menuItem: self.menuSplitRight)
|
||||
syncMenuShortcut(config, action: "new_split:down", menuItem: self.menuSplitDown)
|
||||
|
||||
syncMenuShortcut(action: "copy_to_clipboard", menuItem: self.menuCopy)
|
||||
syncMenuShortcut(action: "paste_from_clipboard", menuItem: self.menuPaste)
|
||||
syncMenuShortcut(action: "select_all", menuItem: self.menuSelectAll)
|
||||
syncMenuShortcut(config, action: "copy_to_clipboard", menuItem: self.menuCopy)
|
||||
syncMenuShortcut(config, action: "paste_from_clipboard", menuItem: self.menuPaste)
|
||||
syncMenuShortcut(config, action: "select_all", menuItem: self.menuSelectAll)
|
||||
|
||||
syncMenuShortcut(action: "toggle_split_zoom", menuItem: self.menuZoomSplit)
|
||||
syncMenuShortcut(action: "goto_split:previous", menuItem: self.menuPreviousSplit)
|
||||
syncMenuShortcut(action: "goto_split:next", menuItem: self.menuNextSplit)
|
||||
syncMenuShortcut(action: "goto_split:top", menuItem: self.menuSelectSplitAbove)
|
||||
syncMenuShortcut(action: "goto_split:bottom", menuItem: self.menuSelectSplitBelow)
|
||||
syncMenuShortcut(action: "goto_split:left", menuItem: self.menuSelectSplitLeft)
|
||||
syncMenuShortcut(action: "goto_split:right", menuItem: self.menuSelectSplitRight)
|
||||
syncMenuShortcut(action: "resize_split:up,10", menuItem: self.menuMoveSplitDividerUp)
|
||||
syncMenuShortcut(action: "resize_split:down,10", menuItem: self.menuMoveSplitDividerDown)
|
||||
syncMenuShortcut(action: "resize_split:right,10", menuItem: self.menuMoveSplitDividerRight)
|
||||
syncMenuShortcut(action: "resize_split:left,10", menuItem: self.menuMoveSplitDividerLeft)
|
||||
syncMenuShortcut(action: "equalize_splits", menuItem: self.menuEqualizeSplits)
|
||||
syncMenuShortcut(config, action: "toggle_split_zoom", menuItem: self.menuZoomSplit)
|
||||
syncMenuShortcut(config, action: "goto_split:previous", menuItem: self.menuPreviousSplit)
|
||||
syncMenuShortcut(config, action: "goto_split:next", menuItem: self.menuNextSplit)
|
||||
syncMenuShortcut(config, action: "goto_split:top", menuItem: self.menuSelectSplitAbove)
|
||||
syncMenuShortcut(config, action: "goto_split:bottom", menuItem: self.menuSelectSplitBelow)
|
||||
syncMenuShortcut(config, action: "goto_split:left", menuItem: self.menuSelectSplitLeft)
|
||||
syncMenuShortcut(config, action: "goto_split:right", menuItem: self.menuSelectSplitRight)
|
||||
syncMenuShortcut(config, action: "resize_split:up,10", menuItem: self.menuMoveSplitDividerUp)
|
||||
syncMenuShortcut(config, action: "resize_split:down,10", menuItem: self.menuMoveSplitDividerDown)
|
||||
syncMenuShortcut(config, action: "resize_split:right,10", menuItem: self.menuMoveSplitDividerRight)
|
||||
syncMenuShortcut(config, action: "resize_split:left,10", menuItem: self.menuMoveSplitDividerLeft)
|
||||
syncMenuShortcut(config, action: "equalize_splits", menuItem: self.menuEqualizeSplits)
|
||||
|
||||
syncMenuShortcut(action: "increase_font_size:1", menuItem: self.menuIncreaseFontSize)
|
||||
syncMenuShortcut(action: "decrease_font_size:1", menuItem: self.menuDecreaseFontSize)
|
||||
syncMenuShortcut(action: "reset_font_size", menuItem: self.menuResetFontSize)
|
||||
syncMenuShortcut(action: "toggle_quick_terminal", menuItem: self.menuQuickTerminal)
|
||||
syncMenuShortcut(action: "toggle_visibility", menuItem: self.menuToggleVisibility)
|
||||
syncMenuShortcut(action: "inspector:toggle", menuItem: self.menuTerminalInspector)
|
||||
syncMenuShortcut(config, action: "increase_font_size:1", menuItem: self.menuIncreaseFontSize)
|
||||
syncMenuShortcut(config, action: "decrease_font_size:1", menuItem: self.menuDecreaseFontSize)
|
||||
syncMenuShortcut(config, action: "reset_font_size", menuItem: self.menuResetFontSize)
|
||||
syncMenuShortcut(config, action: "toggle_quick_terminal", menuItem: self.menuQuickTerminal)
|
||||
syncMenuShortcut(config, action: "toggle_visibility", menuItem: self.menuToggleVisibility)
|
||||
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
|
||||
// global fullscreen keyboard shortcut. The shortcut in the Ghostty config will continue
|
||||
// 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
|
||||
reloadDockMenu()
|
||||
@ -353,9 +382,9 @@ class AppDelegate: NSObject,
|
||||
|
||||
/// Syncs a single menu shortcut for the given action. The action string is the same
|
||||
/// 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 equiv = ghostty.config.keyEquivalent(for: action) else {
|
||||
guard let equiv = config.keyEquivalent(for: action) else {
|
||||
// No shortcut, clear the menu item
|
||||
menu.keyEquivalent = ""
|
||||
menu.keyEquivalentModifierMask = []
|
||||
@ -422,6 +451,98 @@ class AppDelegate: NSObject,
|
||||
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
|
||||
|
||||
/// 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
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
private func reloadDockMenu() {
|
||||
@ -629,7 +668,7 @@ class AppDelegate: NSObject,
|
||||
if quickController == nil {
|
||||
quickController = QuickTerminalController(
|
||||
ghostty,
|
||||
position: ghostty.config.quickTerminalPosition
|
||||
position: derivedConfig.quickTerminalPosition
|
||||
)
|
||||
}
|
||||
|
||||
@ -655,4 +694,22 @@ class AppDelegate: NSObject,
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ import SwiftUI
|
||||
struct AboutView: View {
|
||||
@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.
|
||||
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 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)
|
||||
// This creates a background style similar to the Apple "About My Mac" Window
|
||||
private struct VisualEffectBackground: NSViewRepresentable {
|
||||
@ -80,27 +61,23 @@ struct AboutView: View {
|
||||
.opacity(0.8)
|
||||
}
|
||||
.textSelection(.enabled)
|
||||
|
||||
VStack(spacing: 2) {
|
||||
ForEach(properties) { item in
|
||||
HStack(spacing: 4) {
|
||||
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)
|
||||
.textSelection(.enabled)
|
||||
.frame(maxWidth: .infinity)
|
||||
if let version {
|
||||
PropertyRow(label: "Version", text: version)
|
||||
}
|
||||
if let build {
|
||||
PropertyRow(label: "Build", text: build)
|
||||
}
|
||||
if let commit, commit != "",
|
||||
let url = githubURL?.appendingPathComponent("/commits/\(commit)") {
|
||||
PropertyRow(label: "Commit", text: commit, url: url)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
|
||||
HStack(spacing: 8) {
|
||||
if let url = githubLink {
|
||||
if let url = githubURL {
|
||||
Button("GitHub") {
|
||||
openURL(url)
|
||||
}
|
||||
@ -127,6 +104,45 @@ struct AboutView: View {
|
||||
.background(VisualEffectBackground(material: .underWindowBackground).ignoresSafeArea())
|
||||
#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 {
|
||||
|
@ -18,12 +18,16 @@ class QuickTerminalController: BaseTerminalController {
|
||||
/// application to the front.
|
||||
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,
|
||||
position: QuickTerminalPosition = .top,
|
||||
baseConfig base: Ghostty.SurfaceConfiguration? = nil,
|
||||
surfaceTree tree: Ghostty.SplitNode? = nil
|
||||
) {
|
||||
self.position = position
|
||||
self.derivedConfig = DerivedConfig(ghostty.config)
|
||||
super.init(ghostty, baseConfig: base, surfaceTree: tree)
|
||||
|
||||
// Setup our notifications for behaviors
|
||||
@ -35,8 +39,8 @@ class QuickTerminalController: BaseTerminalController {
|
||||
object: nil)
|
||||
center.addObserver(
|
||||
self,
|
||||
selector: #selector(ghosttyDidReloadConfig),
|
||||
name: Ghostty.Notification.ghosttyDidReloadConfig,
|
||||
selector: #selector(ghosttyConfigDidChange(_:)),
|
||||
name: .ghosttyConfigDidChange,
|
||||
object: nil)
|
||||
}
|
||||
|
||||
@ -64,7 +68,7 @@ class QuickTerminalController: BaseTerminalController {
|
||||
window.isRestorable = false
|
||||
|
||||
// Setup our configured appearance that we support.
|
||||
syncAppearance()
|
||||
syncAppearance(ghostty.config)
|
||||
|
||||
// Setup our initial size based on our configured position
|
||||
position.setLoaded(window)
|
||||
@ -186,7 +190,7 @@ class QuickTerminalController: BaseTerminalController {
|
||||
}
|
||||
|
||||
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
|
||||
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
|
||||
// it visible.
|
||||
NSAnimationContext.runAnimationGroup({ context in
|
||||
context.duration = ghostty.config.quickTerminalAnimationDuration
|
||||
context.duration = derivedConfig.quickTerminalAnimationDuration
|
||||
context.timingFunction = .init(name: .easeIn)
|
||||
position.setFinal(in: window.animator(), on: screen)
|
||||
}, completionHandler: {
|
||||
@ -287,7 +291,7 @@ class QuickTerminalController: BaseTerminalController {
|
||||
}
|
||||
|
||||
NSAnimationContext.runAnimationGroup({ context in
|
||||
context.duration = ghostty.config.quickTerminalAnimationDuration
|
||||
context.duration = derivedConfig.quickTerminalAnimationDuration
|
||||
context.timingFunction = .init(name: .easeIn)
|
||||
position.setInitial(in: window.animator(), on: screen)
|
||||
}, completionHandler: {
|
||||
@ -297,7 +301,7 @@ class QuickTerminalController: BaseTerminalController {
|
||||
})
|
||||
}
|
||||
|
||||
private func syncAppearance() {
|
||||
private func syncAppearance(_ config: Ghostty.Config) {
|
||||
guard let window else { return }
|
||||
|
||||
// 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.
|
||||
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() }
|
||||
DispatchQueue.main.async { [weak self] in self?.syncAppearance(config) }
|
||||
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 (ghostty.config.backgroundOpacity < 1) {
|
||||
if (config.backgroundOpacity < 1) {
|
||||
window.isOpaque = false
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
@objc private func ghosttyDidReloadConfig(notification: SwiftUI.Notification) {
|
||||
syncAppearance()
|
||||
@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)
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -60,6 +60,9 @@ class BaseTerminalController: NSWindowController,
|
||||
/// The previous frame information from the window
|
||||
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 {
|
||||
let window: NSRect
|
||||
let screen: NSRect
|
||||
@ -74,6 +77,7 @@ class BaseTerminalController: NSWindowController,
|
||||
surfaceTree tree: Ghostty.SplitNode? = nil
|
||||
) {
|
||||
self.ghostty = ghostty
|
||||
self.derivedConfig = DerivedConfig(ghostty.config)
|
||||
|
||||
super.init(window: nil)
|
||||
|
||||
@ -93,6 +97,11 @@ class BaseTerminalController: NSWindowController,
|
||||
selector: #selector(didChangeScreenParametersNotification),
|
||||
name: NSApplication.didChangeScreenParametersNotification,
|
||||
object: nil)
|
||||
center.addObserver(
|
||||
self,
|
||||
selector: #selector(ghosttyConfigDidChangeBase(_:)),
|
||||
name: .ghosttyConfigDidChange,
|
||||
object: nil)
|
||||
|
||||
// Listen for local events that we need to know of outside of
|
||||
// single surface handlers.
|
||||
@ -146,14 +155,18 @@ class BaseTerminalController: NSWindowController,
|
||||
}
|
||||
|
||||
// MARK: Notifications
|
||||
|
||||
|
||||
@objc private func didChangeScreenParametersNotification(_ notification: Notification) {
|
||||
// If we have a window that is visible and it is outside the bounds of the
|
||||
// screen then we clamp it back to within the screen.
|
||||
guard let window 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
|
||||
var newFrame = window.frame
|
||||
|
||||
@ -187,6 +200,20 @@ class BaseTerminalController: NSWindowController,
|
||||
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
|
||||
|
||||
private func localEventHandler(_ event: NSEvent) -> NSEvent? {
|
||||
@ -241,7 +268,7 @@ class BaseTerminalController: NSWindowController,
|
||||
func pwdDidChange(to: URL?) {
|
||||
guard let window else { return }
|
||||
|
||||
if ghostty.config.macosTitlebarProxyIcon == .visible {
|
||||
if derivedConfig.macosTitlebarProxyIcon == .visible {
|
||||
// Use the 'to' URL directly
|
||||
window.representedURL = to
|
||||
} else {
|
||||
@ -251,7 +278,7 @@ class BaseTerminalController: NSWindowController,
|
||||
|
||||
|
||||
func cellSizeDidChange(to: NSSize) {
|
||||
guard ghostty.config.windowStepResize else { return }
|
||||
guard derivedConfig.windowStepResize else { return }
|
||||
self.window?.contentResizeIncrements = to
|
||||
}
|
||||
|
||||
@ -559,4 +586,19 @@ class BaseTerminalController: NSWindowController,
|
||||
guard let surface = focusedSurface?.surface else { return }
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import Foundation
|
||||
import Cocoa
|
||||
import SwiftUI
|
||||
import Combine
|
||||
import GhosttyKit
|
||||
|
||||
/// A classic, tabbed terminal experience.
|
||||
@ -20,6 +21,12 @@ class TerminalController: BaseTerminalController {
|
||||
/// For example, terminals executing custom scripts are not restorable.
|
||||
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,
|
||||
withBaseConfig base: Ghostty.SurfaceConfiguration? = nil,
|
||||
withSurfaceTree tree: Ghostty.SplitNode? = nil
|
||||
@ -31,6 +38,9 @@ class TerminalController: BaseTerminalController {
|
||||
// restoration.
|
||||
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)
|
||||
|
||||
// Setup our notifications for behaviors
|
||||
@ -50,6 +60,12 @@ class TerminalController: BaseTerminalController {
|
||||
selector: #selector(onGotoTab),
|
||||
name: Ghostty.Notification.ghosttyGotoTab,
|
||||
object: nil)
|
||||
center.addObserver(
|
||||
self,
|
||||
selector: #selector(ghosttyConfigDidChange(_:)),
|
||||
name: .ghosttyConfigDidChange,
|
||||
object: nil
|
||||
)
|
||||
center.addObserver(
|
||||
self,
|
||||
selector: #selector(onFrameDidChange),
|
||||
@ -80,10 +96,38 @@ class TerminalController: BaseTerminalController {
|
||||
|
||||
//MARK: - Methods
|
||||
|
||||
func configDidReload() {
|
||||
guard let window = window as? TerminalWindow else { return }
|
||||
window.focusFollowsMouse = ghostty.config.focusFollowsMouse
|
||||
syncAppearance()
|
||||
@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 }
|
||||
window.focusFollowsMouse = config.focusFollowsMouse
|
||||
|
||||
// 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
|
||||
@ -144,28 +188,23 @@ class TerminalController: BaseTerminalController {
|
||||
self.relabelTabs()
|
||||
}
|
||||
|
||||
private func syncAppearance() {
|
||||
private func syncAppearance(_ surfaceConfig: Ghostty.SurfaceView.DerivedConfig) {
|
||||
guard let window = self.window as? TerminalWindow else { return }
|
||||
|
||||
// If our window is not visible, then delay this. This is possible specifically
|
||||
// during state restoration but probably in other scenarios as well. To delay,
|
||||
// we just loop directly on the dispatch queue. We have to delay because some
|
||||
// APIs such as window blur have no effect unless the window is visible.
|
||||
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
|
||||
}
|
||||
// If our window is not visible, then we do nothing. Some things such as blurring
|
||||
// have no effect if the window is not visible. Ultimately, we'll have this called
|
||||
// at some point when a surface becomes focused.
|
||||
guard window.isVisible else { return }
|
||||
|
||||
// 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)
|
||||
} else {
|
||||
window.titlebarFont = nil
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
// 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.hasShadow = ghostty.config.macosWindowShadow
|
||||
window.hasShadow = surfaceConfig.macosWindowShadow
|
||||
|
||||
guard window.hasStyledTabs else { return }
|
||||
|
||||
// The titlebar is always updated. We don't need to worry about opacity
|
||||
// because we handle it here.
|
||||
let backgroundColor = OSColor(ghostty.config.backgroundColor)
|
||||
window.titlebarColor = backgroundColor.withAlphaComponent(ghostty.config.backgroundOpacity)
|
||||
// Our background color depends on if our focused surface borders the top or not.
|
||||
// If it does, we match the focused surface. If it doesn't, we use the app
|
||||
// configuration.
|
||||
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) {
|
||||
// Bg color is only synced if we have no transparency. This is because
|
||||
@ -210,6 +261,12 @@ class TerminalController: BaseTerminalController {
|
||||
override func windowDidLoad() {
|
||||
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.
|
||||
window.isRestorable = restorable
|
||||
if (restorable) {
|
||||
@ -218,13 +275,13 @@ class TerminalController: BaseTerminalController {
|
||||
}
|
||||
|
||||
// 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
|
||||
// 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 (ghostty.config.windowColorspace) {
|
||||
switch (config.windowColorspace) {
|
||||
case "display-p3":
|
||||
window.colorSpace = .displayP3
|
||||
case "srgb":
|
||||
@ -256,30 +313,30 @@ class TerminalController: BaseTerminalController {
|
||||
window.center()
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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
|
||||
// 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.titlebarTabs = true
|
||||
DispatchQueue.main.async {
|
||||
window.tabbingMode = .automatic
|
||||
}
|
||||
} else if (ghostty.config.macosTitlebarStyle == "transparent") {
|
||||
} else if (config.macosTitlebarStyle == "transparent") {
|
||||
window.transparentTabs = true
|
||||
}
|
||||
|
||||
if window.hasStyledTabs {
|
||||
// Set the background color of the window
|
||||
let backgroundColor = NSColor(ghostty.config.backgroundColor)
|
||||
let backgroundColor = NSColor(config.backgroundColor)
|
||||
window.backgroundColor = backgroundColor
|
||||
|
||||
// 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
|
||||
@ -290,7 +347,7 @@ class TerminalController: BaseTerminalController {
|
||||
))
|
||||
|
||||
// If our titlebar style is "hidden" we adjust the style appropriately
|
||||
if (ghostty.config.macosTitlebarStyle == "hidden") {
|
||||
if (config.macosTitlebarStyle == "hidden") {
|
||||
window.styleMask = [
|
||||
// We need `titled` in the mask to get the normal window frame
|
||||
.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.
|
||||
syncAppearance()
|
||||
// Apply any additional appearance-related properties to the new window. We
|
||||
// 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.
|
||||
@ -464,7 +523,7 @@ class TerminalController: BaseTerminalController {
|
||||
|
||||
// Custom toolbar-based title used when titlebar tabs are enabled.
|
||||
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
|
||||
// native title view in macOS 15.0 and above. Since we're using
|
||||
// a custom view instead, we need to re-hide it.
|
||||
@ -485,6 +544,36 @@ class TerminalController: BaseTerminalController {
|
||||
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
|
||||
|
||||
@objc private func onMoveTab(notification: SwiftUI.Notification) {
|
||||
@ -593,4 +682,19 @@ class TerminalController: BaseTerminalController {
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -37,8 +37,12 @@ class TerminalManager {
|
||||
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) {
|
||||
self.ghostty = ghostty
|
||||
self.derivedConfig = DerivedConfig(ghostty.config)
|
||||
|
||||
let center = NotificationCenter.default
|
||||
center.addObserver(
|
||||
@ -51,6 +55,11 @@ class TerminalManager {
|
||||
selector: #selector(onNewWindow),
|
||||
name: Ghostty.Notification.ghosttyNewWindow,
|
||||
object: nil)
|
||||
center.addObserver(
|
||||
self,
|
||||
selector: #selector(ghosttyConfigDidChange(_:)),
|
||||
name: .ghosttyConfigDidChange,
|
||||
object: nil)
|
||||
}
|
||||
|
||||
deinit {
|
||||
@ -70,8 +79,8 @@ class TerminalManager {
|
||||
if let parent = focusedSurface?.window,
|
||||
parent.styleMask.contains(.fullScreen) {
|
||||
window.toggleFullScreen(nil)
|
||||
} else if ghostty.config.windowFullscreen {
|
||||
switch (ghostty.config.windowFullscreenMode) {
|
||||
} else if derivedConfig.windowFullscreen {
|
||||
switch (derivedConfig.windowFullscreenMode) {
|
||||
case .native:
|
||||
// Native has to be done immediately so that our stylemask contains
|
||||
// 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
|
||||
// so that the content view is setup.
|
||||
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
|
||||
// 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.
|
||||
switch ghostty.config.windowNewTabPosition {
|
||||
switch derivedConfig.windowNewTabPosition {
|
||||
case "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.
|
||||
@ -227,7 +236,19 @@ class TerminalManager {
|
||||
// are closing a tabbed window, we want to set the cascade point to be
|
||||
// the next cascade point from this 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)
|
||||
|
||||
if focusedWindow.frame != oldFrame {
|
||||
focusedWindow.setFrame(oldFrame, display: true)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@ -313,4 +334,39 @@ class TerminalManager {
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -65,8 +65,10 @@ class TerminalWindowRestoration: NSObject, NSWindowRestoration {
|
||||
}
|
||||
|
||||
// If our configuration is "never" then we never restore the state
|
||||
// no matter what.
|
||||
if (appDelegate.terminalManager.ghostty.config.windowSaveState == "never") {
|
||||
// no matter what. Note its safe to use "ghostty.config" directly here
|
||||
// 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)
|
||||
return
|
||||
}
|
||||
|
@ -50,6 +50,7 @@ struct TerminalView<ViewModel: TerminalViewModel>: View {
|
||||
// Various state values sent back up from the currently focused terminals.
|
||||
@FocusedValue(\.ghosttySurfaceView) private var focusedSurface
|
||||
@FocusedValue(\.ghosttySurfaceTitle) private var surfaceTitle
|
||||
@FocusedValue(\.ghosttySurfacePwd) private var surfacePwd
|
||||
@FocusedValue(\.ghosttySurfaceZoomed) private var zoomedSplit
|
||||
@FocusedValue(\.ghosttySurfaceCellSize) private var cellSize
|
||||
|
||||
@ -65,14 +66,11 @@ struct TerminalView<ViewModel: TerminalViewModel>: View {
|
||||
|
||||
return title
|
||||
}
|
||||
|
||||
// The proxy icon URL for our window
|
||||
private var proxyIconURL: URL? {
|
||||
guard let proxyURLString = focusedSurface?.pwd else {
|
||||
return nil
|
||||
}
|
||||
// Use fileURLWithPath initializer for file paths
|
||||
return URL(fileURLWithPath: proxyURLString)
|
||||
|
||||
// The pwd of the focused surface as a URL
|
||||
private var pwdURL: URL? {
|
||||
guard let surfacePwd else { return nil }
|
||||
return URL(fileURLWithPath: surfacePwd)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
@ -88,7 +86,7 @@ struct TerminalView<ViewModel: TerminalViewModel>: View {
|
||||
if (Ghostty.info.mode == GHOSTTY_BUILD_MODE_DEBUG || Ghostty.info.mode == GHOSTTY_BUILD_MODE_RELEASE_SAFE) {
|
||||
DebugBuildWarningView()
|
||||
}
|
||||
|
||||
|
||||
Ghostty.TerminalSplit(node: $viewModel.surfaceTree)
|
||||
.environmentObject(ghostty)
|
||||
.focused($focused)
|
||||
@ -99,7 +97,7 @@ struct TerminalView<ViewModel: TerminalViewModel>: View {
|
||||
.onChange(of: title) { newValue in
|
||||
self.delegate?.titleDidChange(to: newValue)
|
||||
}
|
||||
.onChange(of: proxyIconURL) { newValue in
|
||||
.onChange(of: pwdURL) { newValue in
|
||||
self.delegate?.pwdDidChange(to: newValue)
|
||||
}
|
||||
.onChange(of: cellSize) { newValue in
|
||||
|
@ -1,3 +1,4 @@
|
||||
import SwiftUI
|
||||
import GhosttyKit
|
||||
|
||||
extension Ghostty {
|
||||
@ -5,6 +6,33 @@ extension Ghostty {
|
||||
}
|
||||
|
||||
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 {
|
||||
let amount: Int
|
||||
|
||||
|
@ -3,9 +3,6 @@ import UserNotifications
|
||||
import GhosttyKit
|
||||
|
||||
protocol GhosttyAppDelegate: AnyObject {
|
||||
/// Called when the configuration did finish reloading.
|
||||
func configDidReload(_ app: Ghostty.App)
|
||||
|
||||
#if os(macOS)
|
||||
/// Called when a callback needs access to a specific surface. This should return nil
|
||||
/// when the surface is no longer valid.
|
||||
@ -68,7 +65,6 @@ extension Ghostty {
|
||||
supports_selection_clipboard: false,
|
||||
wakeup_cb: { userdata in App.wakeup(userdata) },
|
||||
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) },
|
||||
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) },
|
||||
@ -145,9 +141,47 @@ extension Ghostty {
|
||||
ghostty_app_open_config(app)
|
||||
}
|
||||
|
||||
func reloadConfig() {
|
||||
/// Reload the configuration.
|
||||
func reloadConfig(soft: Bool = false) {
|
||||
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
|
||||
@ -240,7 +274,6 @@ extension Ghostty {
|
||||
|
||||
static func wakeup(_ userdata: UnsafeMutableRawPointer?) {}
|
||||
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(
|
||||
_ userdata: UnsafeMutableRawPointer?,
|
||||
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?) {
|
||||
let state = Unmanaged<App>.fromOpaque(userdata!).takeUnretainedValue()
|
||||
|
||||
@ -488,6 +496,9 @@ extension Ghostty {
|
||||
case GHOSTTY_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:
|
||||
ghostty_config_open()
|
||||
|
||||
@ -521,6 +532,15 @@ extension Ghostty {
|
||||
case GHOSTTY_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:
|
||||
fallthrough
|
||||
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(
|
||||
_ app: ghostty_app_t,
|
||||
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
|
||||
|
||||
/// Handle a received user notification. This is called when a user notification is clicked or dismissed by the user
|
||||
|
@ -39,6 +39,10 @@ extension Ghostty {
|
||||
}
|
||||
}
|
||||
|
||||
init(clone config: ghostty_config_t) {
|
||||
self.config = ghostty_config_clone(config)
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.config = nil
|
||||
}
|
||||
|
@ -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
|
||||
/// top-left-most view. This is used when creating a split or closing a split to find the
|
||||
/// 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
|
||||
|
||||
func makeIterator() -> IndexingIterator<[Leaf]> {
|
||||
|
@ -206,6 +206,14 @@ extension Ghostty {
|
||||
// MARK: Surface Notification
|
||||
|
||||
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.
|
||||
static let ghosttyMoveTab = Notification.Name("com.mitchellh.ghostty.moveTab")
|
||||
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.
|
||||
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
|
||||
/// userdata has one key "direction" with the direction to split to.
|
||||
static let ghosttyNewSplit = Notification.Name("com.mitchellh.ghostty.newSplit")
|
||||
|
@ -59,23 +59,6 @@ extension Ghostty {
|
||||
|
||||
@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 {
|
||||
let center = NotificationCenter.default
|
||||
|
||||
@ -92,10 +75,10 @@ extension Ghostty {
|
||||
Surface(view: surfaceView, size: geo.size)
|
||||
.focused($surfaceFocus)
|
||||
.focusedValue(\.ghosttySurfaceTitle, surfaceView.title)
|
||||
.focusedValue(\.ghosttySurfacePwd, surfaceView.pwd)
|
||||
.focusedValue(\.ghosttySurfaceView, surfaceView)
|
||||
.focusedValue(\.ghosttySurfaceCellSize, surfaceView.cellSize)
|
||||
#if canImport(AppKit)
|
||||
.backport.pointerVisibility(pointerVisibility)
|
||||
.backport.pointerStyle(surfaceView.pointerStyle)
|
||||
.onReceive(pubBecomeKey) { notification in
|
||||
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 let url = surfaceView.hoverUrl {
|
||||
let padding: CGFloat = 3
|
||||
let padding: CGFloat = 5
|
||||
let cornerRadius: CGFloat = 9
|
||||
ZStack {
|
||||
HStack {
|
||||
Spacer()
|
||||
@ -180,7 +164,10 @@ extension Ghostty {
|
||||
|
||||
Text(verbatim: url)
|
||||
.padding(.init(top: padding, leading: padding, bottom: padding, trailing: padding))
|
||||
.background(.background)
|
||||
.background(
|
||||
UnevenRoundedRectangle(cornerRadii: .init(topLeading: cornerRadius))
|
||||
.fill(.background)
|
||||
)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.middle)
|
||||
.opacity(isHoveringURLLeft ? 1 : 0)
|
||||
@ -193,7 +180,10 @@ extension Ghostty {
|
||||
|
||||
Text(verbatim: url)
|
||||
.padding(.init(top: padding, leading: padding, bottom: padding, trailing: padding))
|
||||
.background(.background)
|
||||
.background(
|
||||
UnevenRoundedRectangle(cornerRadii: .init(topTrailing: cornerRadius))
|
||||
.fill(.background)
|
||||
)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.middle)
|
||||
.opacity(isHoveringURLLeft ? 0 : 1)
|
||||
@ -512,9 +502,7 @@ extension FocusedValues {
|
||||
struct FocusedGhosttySurface: FocusedValueKey {
|
||||
typealias Value = Ghostty.SurfaceView
|
||||
}
|
||||
}
|
||||
|
||||
extension FocusedValues {
|
||||
var ghosttySurfaceTitle: String? {
|
||||
get { self[FocusedGhosttySurfaceTitle.self] }
|
||||
set { self[FocusedGhosttySurfaceTitle.self] = newValue }
|
||||
@ -523,9 +511,16 @@ extension FocusedValues {
|
||||
struct FocusedGhosttySurfaceTitle: FocusedValueKey {
|
||||
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? {
|
||||
get { self[FocusedGhosttySurfaceZoomed.self] }
|
||||
set { self[FocusedGhosttySurfaceZoomed.self] = newValue }
|
||||
@ -534,9 +529,7 @@ extension FocusedValues {
|
||||
struct FocusedGhosttySurfaceZoomed: FocusedValueKey {
|
||||
typealias Value = Bool
|
||||
}
|
||||
}
|
||||
|
||||
extension FocusedValues {
|
||||
var ghosttySurfaceCellSize: OSSize? {
|
||||
get { self[FocusedGhosttySurfaceCellSize.self] }
|
||||
set { self[FocusedGhosttySurfaceCellSize.self] = newValue }
|
||||
|
@ -14,6 +14,10 @@ extension Ghostty {
|
||||
// to the app level and it is set from there.
|
||||
@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
|
||||
// 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
|
||||
@ -42,9 +46,15 @@ extension Ghostty {
|
||||
@Published var surfaceSize: ghostty_surface_size_s? = nil
|
||||
|
||||
// Whether the pointer should be visible or not
|
||||
@Published private(set) var pointerVisible: Bool = true
|
||||
@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
|
||||
// then the view is moved to a new window.
|
||||
var initialSize: NSSize? = nil
|
||||
@ -71,17 +81,6 @@ extension Ghostty {
|
||||
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
|
||||
// surface has been closed.
|
||||
var inspector: ghostty_inspector_t? {
|
||||
@ -122,6 +121,13 @@ extension Ghostty {
|
||||
self.markedText = NSMutableAttributedString()
|
||||
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
|
||||
// is non-zero so that our layer bounds are non-zero so that our renderer
|
||||
// can do SOMETHING.
|
||||
@ -145,6 +151,16 @@ extension Ghostty {
|
||||
selector: #selector(ghosttyDidEndKeySequence),
|
||||
name: Ghostty.Notification.didEndKeySequence,
|
||||
object: self)
|
||||
center.addObserver(
|
||||
self,
|
||||
selector: #selector(ghosttyConfigDidChange(_:)),
|
||||
name: .ghosttyConfigDidChange,
|
||||
object: self)
|
||||
center.addObserver(
|
||||
self,
|
||||
selector: #selector(ghosttyColorDidChange(_:)),
|
||||
name: .ghosttyColorDidChange,
|
||||
object: self)
|
||||
center.addObserver(
|
||||
self,
|
||||
selector: #selector(windowDidChangeScreen),
|
||||
@ -316,7 +332,11 @@ extension Ghostty {
|
||||
}
|
||||
|
||||
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
|
||||
@ -337,6 +357,31 @@ extension Ghostty {
|
||||
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) {
|
||||
guard let window = self.window 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
|
||||
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
|
||||
if (event.type != .keyDown) {
|
||||
return false
|
||||
@ -722,11 +761,23 @@ extension Ghostty {
|
||||
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
|
||||
switch (event.charactersIgnoringModifiers) {
|
||||
case "/":
|
||||
// Treat C-/ as C-_. We do this because C-/ makes macOS make a beep
|
||||
// sound and we don't like the beep sound.
|
||||
if (!event.modifierFlags.contains(.control) ||
|
||||
!event.modifierFlags.isDisjoint(with: [.shift, .command, .option])) {
|
||||
return false
|
||||
}
|
||||
|
||||
equivalent = "_"
|
||||
|
||||
case "\r":
|
||||
@ -742,7 +793,7 @@ extension Ghostty {
|
||||
let newEvent = NSEvent.keyEvent(
|
||||
with: .keyDown,
|
||||
location: event.locationInWindow,
|
||||
modifierFlags: .control,
|
||||
modifierFlags: event.modifierFlags,
|
||||
timestamp: event.timestamp,
|
||||
windowNumber: event.windowNumber,
|
||||
context: nil,
|
||||
@ -1023,6 +1074,27 @@ extension Ghostty {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -12,6 +12,9 @@ extension Ghostty {
|
||||
// to the app level and it is set from there.
|
||||
@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
|
||||
// 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
|
||||
|
8
macos/Sources/Helpers/NSAppearance+Extension.swift
Normal file
8
macos/Sources/Helpers/NSAppearance+Extension.swift
Normal 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")
|
||||
}
|
||||
}
|
@ -21,6 +21,32 @@ extension OSColor {
|
||||
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 0–255 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 {
|
||||
var h: CGFloat = 0, s: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 0
|
||||
self.getHue(&h, saturation: &s, brightness: &b, alpha: &a)
|
||||
|
@ -1,3 +1,3 @@
|
||||
# This file is auto-generated! check build-support/check-zig-cache-hash.sh for
|
||||
# more details.
|
||||
"sha256-5LBZAExb4PJefW+M0Eo+TcoszhBdIFTGBOv6lte5L0Q="
|
||||
"sha256-D1SQIlmdP9x1PDgRVOy1qJGmu9osDbuyxGOcFj646N4="
|
||||
|
@ -32,7 +32,7 @@ pub fn build(b: *std.Build) !void {
|
||||
};
|
||||
|
||||
if (b.systemIntegrationOption("freetype", .{})) {
|
||||
lib.linkSystemLibrary2("freetype", dynamic_link_opts);
|
||||
lib.linkSystemLibrary2("freetype2", dynamic_link_opts);
|
||||
} else {
|
||||
const freetype = b.dependency("freetype", .{
|
||||
.target = target,
|
||||
|
@ -76,7 +76,8 @@ pub fn build(b: *std.Build) !void {
|
||||
});
|
||||
|
||||
if (b.systemIntegrationOption("freetype", .{})) {
|
||||
lib.linkSystemLibrary2("freetype", dynamic_link_opts);
|
||||
lib.linkSystemLibrary2("freetype2", dynamic_link_opts);
|
||||
module.linkSystemLibrary("freetype2", dynamic_link_opts);
|
||||
} else {
|
||||
lib.linkLibrary(freetype.artifact("freetype"));
|
||||
module.addIncludePath(freetype.builder.dependency("freetype", .{}).path("include"));
|
||||
|
80
src/App.zig
80
src/App.zig
@ -12,8 +12,9 @@ const apprt = @import("apprt.zig");
|
||||
const Surface = @import("Surface.zig");
|
||||
const tracy = @import("tracy");
|
||||
const input = @import("input.zig");
|
||||
const Config = @import("config.zig").Config;
|
||||
const BlockingQueue = @import("./blocking_queue.zig").BlockingQueue;
|
||||
const configpkg = @import("config.zig");
|
||||
const Config = configpkg.Config;
|
||||
const BlockingQueue = @import("datastruct/main.zig").BlockingQueue;
|
||||
const renderer = @import("renderer.zig");
|
||||
const font = @import("font/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_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;
|
||||
|
||||
/// Initialize the main app instance. This creates the main window, sets
|
||||
@ -89,6 +100,7 @@ pub fn create(
|
||||
.mailbox = .{},
|
||||
.quit = false,
|
||||
.font_grid_set = font_grid_set,
|
||||
.config_conditional_state = .{},
|
||||
};
|
||||
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
|
||||
/// called from the main thread. The caller owns the config memory. The
|
||||
/// 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.
|
||||
for (self.surfaces.items) |surface| {
|
||||
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
|
||||
@ -227,7 +259,6 @@ fn drainMailbox(self: *App, rt_app: *apprt.App) !void {
|
||||
while (self.mailbox.pop()) |message| {
|
||||
log.debug("mailbox message={s}", .{@tagName(message)});
|
||||
switch (message) {
|
||||
.reload_config => try self.reloadConfig(rt_app),
|
||||
.open_config => try self.performAction(rt_app, .open_config),
|
||||
.new_window => |msg| try self.newWindow(rt_app, msg),
|
||||
.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 {
|
||||
if (!self.hasSurface(surface)) return;
|
||||
surface.close();
|
||||
@ -376,6 +399,33 @@ pub fn keyEvent(
|
||||
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
|
||||
/// to the app. Callers can use performAllAction to perform any action
|
||||
/// and any non-app-scoped actions will be performed on all surfaces.
|
||||
@ -390,7 +440,7 @@ pub fn performAction(
|
||||
.quit => self.setQuit(),
|
||||
.new_window => try self.newWindow(rt_app, .{ .parent = null }),
|
||||
.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, {}),
|
||||
.toggle_quick_terminal => try rt_app.performAction(.app, .toggle_quick_terminal, {}),
|
||||
.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.
|
||||
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_config: void,
|
||||
|
||||
|
525
src/Surface.zig
525
src/Surface.zig
@ -94,11 +94,6 @@ keyboard: Keyboard,
|
||||
/// less important.
|
||||
pressed_key: ?input.KeyEvent = null,
|
||||
|
||||
/// The current color scheme of the GUI element containing this surface.
|
||||
/// This will default to light until the apprt sends us the actual color
|
||||
/// scheme. This is used by mode 3031 and CSI 996 n.
|
||||
color_scheme: apprt.ColorScheme = .light,
|
||||
|
||||
/// The hash value of the last keybinding trigger that we performed. This
|
||||
/// is only set if the last key input matched a keybinding, consumed it,
|
||||
/// and performed it. This is used to prevent sending release/repeat events
|
||||
@ -113,23 +108,29 @@ io_thr: std.Thread,
|
||||
/// Terminal inspector
|
||||
inspector: ?*inspector.Inspector = null,
|
||||
|
||||
/// All the cached sizes since we need them at various times.
|
||||
screen_size: renderer.ScreenSize,
|
||||
grid_size: renderer.GridSize,
|
||||
cell_size: renderer.CellSize,
|
||||
|
||||
/// Explicit padding due to configuration
|
||||
padding: renderer.Padding,
|
||||
/// All our sizing information.
|
||||
size: renderer.Size,
|
||||
|
||||
/// 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
|
||||
/// the lifetime of. This makes updating config at runtime easier.
|
||||
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 used to determine if we need to confirm, hold open, etc.
|
||||
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 appropriate action after an input event. For example, key
|
||||
/// 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();
|
||||
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
|
||||
@ -332,11 +359,31 @@ const DerivedConfig = struct {
|
||||
pub fn init(
|
||||
self: *Surface,
|
||||
alloc: Allocator,
|
||||
config: *const configpkg.Config,
|
||||
config_original: *const configpkg.Config,
|
||||
app: *App,
|
||||
rt_app: *apprt.runtime.App,
|
||||
rt_surface: *apprt.runtime.Surface,
|
||||
) !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
|
||||
var derived_config = try DerivedConfig.init(alloc, config);
|
||||
errdefer derived_config.deinit();
|
||||
@ -373,28 +420,32 @@ pub fn init(
|
||||
// Pre-calculate our initial cell size ourselves.
|
||||
const cell_size = font_grid.cellSize();
|
||||
|
||||
// Convert our padding from points to pixels
|
||||
const padding_top: u32 = padding_top: {
|
||||
const padding_top: f32 = @floatFromInt(derived_config.window_padding_top);
|
||||
break :padding_top @intFromFloat(@floor(padding_top * y_dpi / 72));
|
||||
};
|
||||
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));
|
||||
};
|
||||
const padding_left: u32 = padding_left: {
|
||||
const padding_left: f32 = @floatFromInt(derived_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(derived_config.window_padding_right);
|
||||
break :padding_right @intFromFloat(@floor(padding_right * x_dpi / 72));
|
||||
};
|
||||
const padding: renderer.Padding = .{
|
||||
.top = padding_top,
|
||||
.bottom = padding_bottom,
|
||||
.left = padding_left,
|
||||
.right = padding_right,
|
||||
// Build our size struct which has all the sizes we need.
|
||||
const size: renderer.Size = size: {
|
||||
var size: renderer.Size = .{
|
||||
.screen = screen: {
|
||||
const surface_size = try rt_surface.getSize();
|
||||
break :screen .{
|
||||
.width = surface_size.width,
|
||||
.height = surface_size.height,
|
||||
};
|
||||
},
|
||||
|
||||
.cell = font_grid.cellSize(),
|
||||
.padding = .{},
|
||||
};
|
||||
|
||||
const explicit: renderer.Padding = derived_config.scaledPadding(
|
||||
x_dpi,
|
||||
y_dpi,
|
||||
);
|
||||
if (derived_config.window_padding_balance) {
|
||||
size.balancePadding(explicit);
|
||||
} else {
|
||||
size.padding = explicit;
|
||||
}
|
||||
|
||||
break :size size;
|
||||
};
|
||||
|
||||
// Create our terminal grid with the initial size
|
||||
@ -402,26 +453,12 @@ pub fn init(
|
||||
var renderer_impl = try Renderer.init(alloc, .{
|
||||
.config = try Renderer.DerivedConfig.init(alloc, config),
|
||||
.font_grid = font_grid,
|
||||
.padding = .{
|
||||
.explicit = padding,
|
||||
.balance = config.@"window-padding-balance",
|
||||
},
|
||||
.size = size,
|
||||
.surface_mailbox = .{ .surface = self, .app = app_mailbox },
|
||||
.rt_surface = rt_surface,
|
||||
});
|
||||
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.
|
||||
const mutex = try alloc.create(std.Thread.Mutex);
|
||||
mutex.* = .{};
|
||||
@ -462,20 +499,27 @@ pub fn init(
|
||||
.io = undefined,
|
||||
.io_thread = io_thread,
|
||||
.io_thr = undefined,
|
||||
.screen_size = .{ .width = 0, .height = 0 },
|
||||
.grid_size = .{},
|
||||
.cell_size = cell_size,
|
||||
.padding = padding,
|
||||
.size = size,
|
||||
.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
|
||||
// This separate block ({}) is important because our errdefers must
|
||||
// be scoped here to be valid.
|
||||
{
|
||||
// Initialize our IO backend
|
||||
var io_exec = try termio.Exec.init(alloc, .{
|
||||
.command = config.command,
|
||||
.command = command,
|
||||
.shell_integration = config.@"shell-integration",
|
||||
.shell_integration_features = config.@"shell-integration-features",
|
||||
.working_directory = config.@"working-directory",
|
||||
@ -499,10 +543,7 @@ pub fn init(
|
||||
errdefer io_mailbox.deinit(alloc);
|
||||
|
||||
try termio.Termio.init(&self.io, alloc, .{
|
||||
.grid_size = grid_size,
|
||||
.cell_size = cell_size,
|
||||
.screen_size = screen_size,
|
||||
.padding = padding,
|
||||
.size = size,
|
||||
.full_config = config,
|
||||
.config = try termio.Termio.DerivedConfig.init(alloc, config),
|
||||
.backend = .{ .exec = io_exec },
|
||||
@ -521,7 +562,7 @@ pub fn init(
|
||||
try rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.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
|
||||
@ -530,8 +571,8 @@ pub fn init(
|
||||
.{ .surface = self },
|
||||
.size_limit,
|
||||
.{
|
||||
.min_width = cell_size.width * 10,
|
||||
.min_height = cell_size.height * 4,
|
||||
.min_width = size.cell.width * 10,
|
||||
.min_height = size.cell.height * 4,
|
||||
// No max:
|
||||
.max_width = 0,
|
||||
.max_height = 0,
|
||||
@ -543,7 +584,7 @@ pub fn init(
|
||||
// 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
|
||||
// to duplicate.
|
||||
try self.sizeCallback(surface_size);
|
||||
try self.resize(self.size.screen);
|
||||
|
||||
// Give the renderer one more opportunity to finalize any surface
|
||||
// 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.
|
||||
const final_width: u32 =
|
||||
@as(u32, @intFromFloat(@ceil(width_f32 / scale.x))) +
|
||||
padding.left +
|
||||
padding.right;
|
||||
size.padding.left +
|
||||
size.padding.right;
|
||||
const final_height: u32 =
|
||||
@as(u32, @intFromFloat(@ceil(height_f32 / scale.y))) +
|
||||
padding.top +
|
||||
padding.bottom;
|
||||
size.padding.top +
|
||||
size.padding.bottom;
|
||||
|
||||
rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
@ -613,9 +654,9 @@ pub fn init(
|
||||
// For xdg-terminal-exec execution we special-case and set the window
|
||||
// title to the command being executed. This allows window managers
|
||||
// to set custom styling based on the command being executed.
|
||||
const command = config.command orelse break :xdg;
|
||||
if (command.len > 0) {
|
||||
const title = alloc.dupeZ(u8, command) catch |err| {
|
||||
const v = command orelse break :xdg;
|
||||
if (v.len > 0) {
|
||||
const title = alloc.dupeZ(u8, v) catch |err| {
|
||||
log.warn(
|
||||
"error copying command for title, title will not be set 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 {
|
||||
@ -758,7 +802,7 @@ pub fn needsConfirmQuit(self: *Surface) bool {
|
||||
/// surface.
|
||||
pub fn handleMessage(self: *Surface, msg: Message) !void {
|
||||
switch (msg) {
|
||||
.change_config => |config| try self.changeConfig(config),
|
||||
.change_config => |config| try self.updateConfig(config),
|
||||
|
||||
.set_title => |*v| {
|
||||
// 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);
|
||||
},
|
||||
|
||||
.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| {
|
||||
log.debug("changing mouse shape: {}", .{shape});
|
||||
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 without confirmation.
|
||||
@ -847,7 +928,7 @@ pub fn handleMessage(self: *Surface, msg: Message) !void {
|
||||
|
||||
.renderer_health => |health| self.updateRendererHealth(health),
|
||||
|
||||
.report_color_scheme => try self.reportColorScheme(),
|
||||
.report_color_scheme => |force| self.reportColorScheme(force),
|
||||
|
||||
.present_surface => try self.presentSurface(),
|
||||
|
||||
@ -884,9 +965,19 @@ fn passwordInput(self: *Surface, v: bool) !void {
|
||||
try self.queueRender();
|
||||
}
|
||||
|
||||
/// Sends a DSR response for the current color scheme to the pty.
|
||||
fn reportColorScheme(self: *Surface) !void {
|
||||
const output = switch (self.color_scheme) {
|
||||
/// Sends a DSR response for the current color scheme to the pty. If
|
||||
/// force is false then we only send the response if the terminal mode
|
||||
/// 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",
|
||||
.dark => "\x1B[?997;1n",
|
||||
};
|
||||
@ -1009,8 +1100,39 @@ fn updateRendererHealth(self: *Surface, health: renderer.Health) void {
|
||||
};
|
||||
}
|
||||
|
||||
/// Update our configuration at runtime.
|
||||
fn changeConfig(self: *Surface, config: *const configpkg.Config) !void {
|
||||
/// This should be called anytime `config_conditional_state` changes
|
||||
/// 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
|
||||
const derived = DerivedConfig.init(self.alloc, config) catch |err| {
|
||||
// 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| {
|
||||
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.
|
||||
@ -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.
|
||||
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: {
|
||||
// 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
|
||||
x += @floatFromInt(pad.left);
|
||||
x += @floatFromInt(self.size.padding.left);
|
||||
|
||||
// Scale
|
||||
x /= content_scale.x;
|
||||
@ -1129,14 +1252,14 @@ pub fn selectionInfo(self: *const Surface) ?apprt.Selection {
|
||||
|
||||
const y: f64 = y: {
|
||||
// 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
|
||||
y += @floatFromInt(self.cell_size.height);
|
||||
y += @floatFromInt(self.size.cell.height);
|
||||
y -= @floatFromInt(self.font_metrics.cell_baseline);
|
||||
|
||||
// Add padding
|
||||
y += @floatFromInt(pad.top);
|
||||
y += @floatFromInt(self.size.padding.top);
|
||||
|
||||
// Scale
|
||||
y /= content_scale.y;
|
||||
@ -1177,10 +1300,10 @@ pub fn imePoint(self: *const Surface) apprt.IMEPos {
|
||||
|
||||
const x: f64 = x: {
|
||||
// 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
|
||||
x += @as(f64, @floatFromInt(self.cell_size.width)) / 2;
|
||||
x += @as(f64, @floatFromInt(self.size.cell.width)) / 2;
|
||||
|
||||
// And scale it
|
||||
x /= content_scale.x;
|
||||
@ -1190,10 +1313,10 @@ pub fn imePoint(self: *const Surface) apprt.IMEPos {
|
||||
|
||||
const y: f64 = y: {
|
||||
// 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
|
||||
y += @floatFromInt(self.cell_size.height);
|
||||
y += @floatFromInt(self.size.cell.height);
|
||||
|
||||
// And scale it
|
||||
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
|
||||
/// a result of changing the font size at runtime.
|
||||
fn setCellSize(self: *Surface, size: renderer.CellSize) !void {
|
||||
// Update our new cell size for future calcs
|
||||
self.cell_size = size;
|
||||
|
||||
// Update our grid_size
|
||||
self.grid_size = renderer.GridSize.init(
|
||||
self.screen_size.subPadding(self.padding),
|
||||
self.cell_size,
|
||||
);
|
||||
// Update our cell size within our size struct
|
||||
self.size.cell = size;
|
||||
self.balancePaddingIfNeeded();
|
||||
|
||||
// Notify the terminal
|
||||
self.io.queueMessage(.{
|
||||
.resize = .{
|
||||
.grid_size = self.grid_size,
|
||||
.cell_size = self.cell_size,
|
||||
.screen_size = self.screen_size,
|
||||
.padding = self.padding,
|
||||
},
|
||||
}, .unlocked);
|
||||
self.io.queueMessage(.{ .resize = self.size }, .unlocked);
|
||||
|
||||
// Notify the window
|
||||
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
|
||||
// the screen size didn't change, then our grid size could not have
|
||||
// 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);
|
||||
}
|
||||
|
||||
fn resize(self: *Surface, size: renderer.ScreenSize) !void {
|
||||
// Save our screen size
|
||||
self.screen_size = size;
|
||||
self.size.screen = size;
|
||||
self.balancePaddingIfNeeded();
|
||||
|
||||
// Recalculate our grid size. Because Ghostty supports fluid resizing,
|
||||
// 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
|
||||
// pixel-level sizing to the subprocess.
|
||||
self.grid_size = renderer.GridSize.init(
|
||||
self.screen_size.subPadding(self.padding),
|
||||
self.cell_size,
|
||||
);
|
||||
if (self.grid_size.columns < 5 and (self.padding.left > 0 or self.padding.right > 0)) {
|
||||
const grid_size = self.size.grid();
|
||||
if (grid_size.columns < 5 and (self.size.padding.left > 0 or self.size.padding.right > 0)) {
|
||||
log.warn("WARNING: very small terminal grid detected with padding " ++
|
||||
"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 " ++
|
||||
"set. Is your padding reasonable?", .{});
|
||||
}
|
||||
|
||||
// Mail the IO thread
|
||||
self.io.queueMessage(.{
|
||||
.resize = .{
|
||||
.grid_size = self.grid_size,
|
||||
.cell_size = self.cell_size,
|
||||
.screen_size = self.screen_size,
|
||||
.padding = self.padding,
|
||||
},
|
||||
}, .unlocked);
|
||||
self.io.queueMessage(.{ .resize = self.size }, .unlocked);
|
||||
}
|
||||
|
||||
/// Recalculate the balanced padding if needed.
|
||||
fn balancePaddingIfNeeded(self: *Surface) void {
|
||||
if (!self.config.window_padding_balance) return;
|
||||
const content_scale = try self.rt_surface.getContentScale();
|
||||
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
|
||||
@ -1972,6 +2083,10 @@ pub fn focusCallback(self: *Surface, focused: bool) !void {
|
||||
crash.sentry.thread_state = self.crashThreadState();
|
||||
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
|
||||
_ = self.renderer_thread.mailbox.push(.{
|
||||
.focus = focused,
|
||||
@ -2028,6 +2143,12 @@ pub fn focusCallback(self: *Surface, focused: bool) !void {
|
||||
// Schedule render which also drains our mailbox
|
||||
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
|
||||
{
|
||||
self.renderer_state.mutex.lock();
|
||||
@ -2090,7 +2211,8 @@ pub fn scrollCallback(
|
||||
if (!scroll_mods.precision) {
|
||||
// Calculate our magnitude of scroll. This is constant (not
|
||||
// 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_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
|
||||
// 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) {
|
||||
self.mouse.pending_scroll_y = poff;
|
||||
break :y .{};
|
||||
@ -2147,7 +2269,7 @@ pub fn scrollCallback(
|
||||
|
||||
const xoff_adjusted: f64 = xoff * self.config.mouse_scroll_multiplier;
|
||||
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) {
|
||||
self.mouse.pending_scroll_x = poff;
|
||||
break :x .{};
|
||||
@ -2272,36 +2394,15 @@ pub fn contentScaleCallback(self: *Surface, content_scale: apprt.ContentScale) !
|
||||
|
||||
try self.setFontSize(size);
|
||||
|
||||
// Update our padding which is dependent on DPI.
|
||||
self.padding = padding: {
|
||||
const padding_top: u32 = padding_top: {
|
||||
const padding_top: f32 = @floatFromInt(self.config.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.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,
|
||||
};
|
||||
};
|
||||
// Update our padding which is dependent on DPI. We only do this for
|
||||
// unbalanced padding since balanced padding is not dependent on DPI.
|
||||
if (!self.config.window_padding_balance) {
|
||||
self.size.padding = self.config.scaledPadding(x_dpi, y_dpi);
|
||||
}
|
||||
|
||||
// Force a resize event because the change in padding will affect
|
||||
// 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.
|
||||
@ -2340,8 +2441,8 @@ fn mouseReport(
|
||||
// We always report release events no matter where they happen.
|
||||
if (action != .release) {
|
||||
const pos_out_viewport = pos_out_viewport: {
|
||||
const max_x: f32 = @floatFromInt(self.screen_size.width);
|
||||
const max_y: f32 = @floatFromInt(self.screen_size.height);
|
||||
const max_x: f32 = @floatFromInt(self.size.screen.width);
|
||||
const max_y: f32 = @floatFromInt(self.size.screen.height);
|
||||
break :pos_out_viewport pos.x < 0 or pos.y < 0 or
|
||||
pos.x > max_x or pos.y > max_y;
|
||||
};
|
||||
@ -2500,15 +2601,22 @@ fn mouseReport(
|
||||
.sgr_pixels => {
|
||||
// Final character to send in the CSI
|
||||
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
|
||||
// remainder for numbers which are very large...
|
||||
var data: termio.Message.WriteReq.Small.Array = undefined;
|
||||
const resp = try std.fmt.bufPrint(&data, "\x1B[<{d};{d};{d}{c}", .{
|
||||
button_code,
|
||||
@as(i32, @intFromFloat(@round(adjusted.x))),
|
||||
@as(i32, @intFromFloat(@round(adjusted.y))),
|
||||
@as(i32, @intFromFloat(@round(coord.x))),
|
||||
@as(i32, @intFromFloat(@round(coord.y))),
|
||||
final,
|
||||
});
|
||||
|
||||
@ -2713,12 +2821,20 @@ pub fn mouseButtonCallback(
|
||||
}
|
||||
|
||||
// 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.
|
||||
self.mouse.left_click_count = 0;
|
||||
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.*);
|
||||
return true;
|
||||
}
|
||||
@ -2755,7 +2871,7 @@ pub fn mouseButtonCallback(
|
||||
// If we move our cursor too much between clicks then we reset
|
||||
// the multi-click state.
|
||||
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(
|
||||
std.math.pow(f64, pos.x - self.mouse.left_click_xpos, 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.
|
||||
fn osc8URI(self: *Surface, pin: terminal.Pin) ?[]const u8 {
|
||||
_ = self;
|
||||
const page = &pin.page.data;
|
||||
const page = &pin.node.data;
|
||||
const cell = pin.rowAndCell().cell;
|
||||
const link_id = page.lookupHyperlink(cell) orelse return null;
|
||||
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
|
||||
// scroll even in full screen windows.
|
||||
// Note: one day, we can change this from distance to time based if we want.
|
||||
//log.warn("CURSOR POS: {} {}", .{ pos, self.screen_size });
|
||||
const max_y: f32 = @floatFromInt(self.screen_size.height);
|
||||
//log.warn("CURSOR POS: {} {}", .{ pos, self.size.screen });
|
||||
const max_y: f32 = @floatFromInt(self.size.screen.height);
|
||||
if (pos.y <= 1 or pos.y > max_y - 1) {
|
||||
const delta: isize = if (pos.y < 0) -1 else 1;
|
||||
try self.io.terminal.scrollViewport(.{ .delta = delta });
|
||||
@ -3415,11 +3531,11 @@ fn dragLeftClickSingle(
|
||||
const click_pin = self.mouse.left_click_pin.?.*;
|
||||
|
||||
// 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;
|
||||
|
||||
// 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_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(
|
||||
drag_pin,
|
||||
drag_pin,
|
||||
self.mouse.mods.ctrlOrSuper() and self.mouse.mods.alt,
|
||||
SurfaceMouse.isRectangleSelectState(self.mouse.mods),
|
||||
) else null);
|
||||
|
||||
return;
|
||||
@ -3472,7 +3588,7 @@ fn dragLeftClickSingle(
|
||||
try self.setSelection(terminal.Selection.init(
|
||||
start,
|
||||
drag_pin,
|
||||
self.mouse.mods.ctrlOrSuper() and self.mouse.mods.alt,
|
||||
SurfaceMouse.isRectangleSelectState(self.mouse.mods),
|
||||
));
|
||||
return;
|
||||
}
|
||||
@ -3554,58 +3670,27 @@ pub fn colorSchemeCallback(self: *Surface, scheme: apprt.ColorScheme) !void {
|
||||
crash.sentry.thread_state = self.crashThreadState();
|
||||
defer crash.sentry.thread_state = null;
|
||||
|
||||
// If our scheme didn't change, then we don't do anything.
|
||||
if (self.color_scheme == scheme) return;
|
||||
const new_scheme: configpkg.ConditionalState.Theme = switch (scheme) {
|
||||
.light => .light,
|
||||
.dark => .dark,
|
||||
};
|
||||
|
||||
// Set our new scheme
|
||||
self.color_scheme = scheme;
|
||||
// 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;
|
||||
self.notifyConfigConditionalState();
|
||||
|
||||
// If mode 2031 is on, then we report the change live.
|
||||
const report = report: {
|
||||
self.renderer_state.mutex.lock();
|
||||
defer self.renderer_state.mutex.unlock();
|
||||
break :report self.renderer_state.terminal.modes.get(.report_color_scheme);
|
||||
};
|
||||
if (report) try self.reportColorScheme();
|
||||
}
|
||||
|
||||
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)),
|
||||
};
|
||||
self.reportColorScheme(false);
|
||||
}
|
||||
|
||||
pub fn posToViewport(self: Surface, xpos: f64, ypos: f64) terminal.point.Coordinate {
|
||||
// xpos/ypos need to be adjusted for window padding
|
||||
// (i.e. "window-padding-*" settings.
|
||||
const adjusted = self.posAdjusted(xpos, ypos);
|
||||
|
||||
// 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);
|
||||
},
|
||||
};
|
||||
// Get our grid cell
|
||||
const coord: renderer.Coordinate = .{ .surface = .{ .x = xpos, .y = ypos } };
|
||||
const grid = coord.convert(.grid, self.size).grid;
|
||||
return .{ .x = grid.x, .y = grid.y };
|
||||
}
|
||||
|
||||
/// Scroll to the bottom of the viewport.
|
||||
@ -3843,21 +3928,21 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
|
||||
},
|
||||
|
||||
.scroll_page_up => {
|
||||
const rows: isize = @intCast(self.grid_size.rows);
|
||||
const rows: isize = @intCast(self.size.grid().rows);
|
||||
self.io.queueMessage(.{
|
||||
.scroll_viewport = .{ .delta = -1 * rows },
|
||||
}, .unlocked);
|
||||
},
|
||||
|
||||
.scroll_page_down => {
|
||||
const rows: isize = @intCast(self.grid_size.rows);
|
||||
const rows: isize = @intCast(self.size.grid().rows);
|
||||
self.io.queueMessage(.{
|
||||
.scroll_viewport = .{ .delta = rows },
|
||||
}, .unlocked);
|
||||
},
|
||||
|
||||
.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));
|
||||
self.io.queueMessage(.{
|
||||
.scroll_viewport = .{ .delta = delta },
|
||||
@ -3927,7 +4012,7 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
|
||||
.left => .left,
|
||||
.down => .down,
|
||||
.up => .up,
|
||||
.auto => if (self.screen_size.width > self.screen_size.height)
|
||||
.auto => if (self.size.screen.width > self.size.screen.height)
|
||||
.right
|
||||
else
|
||||
.down,
|
||||
|
@ -1,6 +1,7 @@
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const apprt = @import("../apprt.zig");
|
||||
const configpkg = @import("../config.zig");
|
||||
const input = @import("../input.zig");
|
||||
const renderer = @import("../renderer.zig");
|
||||
const terminal = @import("../terminal/main.zig");
|
||||
@ -151,6 +152,9 @@ pub const Action = union(Key) {
|
||||
/// Set the title of the target.
|
||||
set_title: SetTitle,
|
||||
|
||||
/// The current working directory has changed for the target terminal.
|
||||
pwd: Pwd,
|
||||
|
||||
/// Set the mouse cursor shape.
|
||||
mouse_shape: terminal.MouseShape,
|
||||
|
||||
@ -186,6 +190,33 @@ pub const Action = union(Key) {
|
||||
/// key mode because other input may be ignored.
|
||||
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
|
||||
pub const Key = enum(c_int) {
|
||||
new_window,
|
||||
@ -211,6 +242,7 @@ pub const Action = union(Key) {
|
||||
render_inspector,
|
||||
desktop_notification,
|
||||
set_title,
|
||||
pwd,
|
||||
mouse_shape,
|
||||
mouse_visibility,
|
||||
mouse_over_link,
|
||||
@ -219,6 +251,9 @@ pub const Action = union(Key) {
|
||||
quit_timer,
|
||||
secure_input,
|
||||
key_sequence,
|
||||
color_change,
|
||||
reload_config,
|
||||
config_change,
|
||||
};
|
||||
|
||||
/// 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.
|
||||
pub const DesktopNotification = struct {
|
||||
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,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
@ -47,11 +47,6 @@ pub const App = struct {
|
||||
/// Callback called to handle an action.
|
||||
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
|
||||
/// by the host until the next call. If there is no valid clipboard
|
||||
/// value then this should return null.
|
||||
@ -90,26 +85,38 @@ pub const App = struct {
|
||||
};
|
||||
|
||||
core_app: *CoreApp,
|
||||
config: *const Config,
|
||||
opts: Options,
|
||||
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
|
||||
/// also has its own keymap state for focused keybinds.
|
||||
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 .{
|
||||
.core_app = core_app,
|
||||
.config = config,
|
||||
.config = config_clone,
|
||||
.opts = opts,
|
||||
.keymap = try input.Keymap.init(),
|
||||
.keymap_state = .{},
|
||||
};
|
||||
}
|
||||
|
||||
pub fn terminate(self: App) void {
|
||||
pub fn terminate(self: *App) void {
|
||||
self.keymap.deinit();
|
||||
self.config.deinit();
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
// Reload
|
||||
if (self.opts.reload_config(self.opts.userdata)) |new| {
|
||||
self.config = new;
|
||||
return self.config;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn wakeup(self: App) void {
|
||||
pub fn wakeup(self: *const App) void {
|
||||
self.opts.wakeup(self.opts.userdata);
|
||||
}
|
||||
|
||||
pub fn wait(self: App) !void {
|
||||
pub fn wait(self: *const App) !void {
|
||||
_ = self;
|
||||
}
|
||||
|
||||
@ -430,6 +427,28 @@ pub const App = struct {
|
||||
comptime action: apprt.Action.Key,
|
||||
value: apprt.Action.Value(action),
|
||||
) !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
|
||||
switch (action) {
|
||||
.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 => {},
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
// 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();
|
||||
|
||||
// If we have a working directory from the options then we set it.
|
||||
@ -1339,10 +1360,14 @@ pub const CAPI = struct {
|
||||
};
|
||||
}
|
||||
|
||||
/// Reload the configuration.
|
||||
export fn ghostty_app_reload_config(v: *App) void {
|
||||
_ = v.core_app.reloadConfig(v) catch |err| {
|
||||
log.err("error reloading config err={}", .{err});
|
||||
/// Update the configuration to the provided config. This will propagate
|
||||
/// to all surfaces as well.
|
||||
export fn ghostty_app_update_config(
|
||||
v: *App,
|
||||
config: *const Config,
|
||||
) void {
|
||||
v.core_app.updateConfig(v, config) catch |err| {
|
||||
log.err("error updating config err={}", .{err});
|
||||
return;
|
||||
};
|
||||
}
|
||||
@ -1357,6 +1382,22 @@ pub const CAPI = struct {
|
||||
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.
|
||||
export fn ghostty_surface_config_new() apprt.Surface.Options {
|
||||
return .{};
|
||||
@ -1399,6 +1440,17 @@ pub const CAPI = struct {
|
||||
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.
|
||||
export fn ghostty_surface_needs_confirm_quit(surface: *Surface) bool {
|
||||
return surface.core_surface.needsConfirmQuit();
|
||||
@ -1428,25 +1480,6 @@ pub const CAPI = struct {
|
||||
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
|
||||
export fn ghostty_surface_refresh(surface: *Surface) void {
|
||||
surface.refresh();
|
||||
@ -1466,13 +1499,14 @@ pub const CAPI = struct {
|
||||
|
||||
/// Return the size information a surface has.
|
||||
export fn ghostty_surface_size(surface: *Surface) SurfaceSize {
|
||||
const grid_size = surface.core_surface.size.grid();
|
||||
return .{
|
||||
.columns = surface.core_surface.grid_size.columns,
|
||||
.rows = surface.core_surface.grid_size.rows,
|
||||
.width_px = surface.core_surface.screen_size.width,
|
||||
.height_px = surface.core_surface.screen_size.height,
|
||||
.cell_width_px = surface.core_surface.cell_size.width,
|
||||
.cell_height_px = surface.core_surface.cell_size.height,
|
||||
.columns = grid_size.columns,
|
||||
.rows = grid_size.rows,
|
||||
.width_px = surface.core_surface.size.screen.width,
|
||||
.height_px = surface.core_surface.size.screen.height,
|
||||
.cell_width_px = surface.core_surface.size.cell.width,
|
||||
.cell_height_px = surface.core_surface.size.cell.height,
|
||||
};
|
||||
}
|
||||
|
||||
@ -1822,7 +1856,7 @@ pub const CAPI = struct {
|
||||
// This is only supported on macOS
|
||||
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
|
||||
if (config.@"background-opacity" >= 1.0) return;
|
||||
|
@ -200,6 +200,8 @@ pub const App = struct {
|
||||
}),
|
||||
},
|
||||
|
||||
.reload_config => try self.reloadConfig(target, value),
|
||||
|
||||
// Unimplemented
|
||||
.new_split,
|
||||
.goto_split,
|
||||
@ -223,6 +225,9 @@ pub const App = struct {
|
||||
.mouse_over_link,
|
||||
.cell_size,
|
||||
.renderer_health,
|
||||
.color_change,
|
||||
.pwd,
|
||||
.config_change,
|
||||
=> log.info("unimplemented action={}", .{action}),
|
||||
}
|
||||
}
|
||||
@ -232,16 +237,34 @@ pub const App = struct {
|
||||
/// successful return.
|
||||
///
|
||||
/// 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
|
||||
var config = try Config.load(self.app.alloc);
|
||||
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.
|
||||
self.config.deinit();
|
||||
self.config = config;
|
||||
|
||||
return &self.config;
|
||||
}
|
||||
|
||||
/// Toggle the window to fullscreen mode.
|
||||
|
@ -462,21 +462,24 @@ pub fn performAction(
|
||||
.equalize_splits => self.equalizeSplits(target),
|
||||
.goto_split => self.gotoSplit(target, value),
|
||||
.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),
|
||||
.desktop_notification => self.showDesktopNotification(target, value),
|
||||
.set_title => try self.setTitle(target, value),
|
||||
.pwd => try self.setPwd(target, value),
|
||||
.present_terminal => self.presentTerminal(target),
|
||||
.initial_size => try self.setInitialSize(target, value),
|
||||
.mouse_visibility => self.setMouseVisibility(target, value),
|
||||
.mouse_shape => try self.setMouseShape(target, value),
|
||||
.mouse_over_link => self.setMouseOverLink(target, value),
|
||||
.toggle_tab_overview => self.toggleTabOverview(target),
|
||||
.toggle_split_zoom => self.toggleSplitZoom(target),
|
||||
.toggle_window_decorations => self.toggleWindowDecorations(target),
|
||||
.quit_timer => self.quitTimer(value),
|
||||
|
||||
// Unimplemented
|
||||
.close_all_windows,
|
||||
.toggle_split_zoom,
|
||||
.toggle_quick_terminal,
|
||||
.toggle_visibility,
|
||||
.size_limit,
|
||||
@ -485,6 +488,7 @@ pub fn performAction(
|
||||
.key_sequence,
|
||||
.render_inspector,
|
||||
.renderer_health,
|
||||
.color_change,
|
||||
=> 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(
|
||||
_: *App,
|
||||
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(
|
||||
_: *App,
|
||||
target: apprt.Target,
|
||||
@ -796,19 +818,9 @@ fn showDesktopNotification(
|
||||
c.g_application_send_notification(g_app, n.body.ptr, notification);
|
||||
}
|
||||
|
||||
/// Reload the configuration. This should return the new configuration.
|
||||
/// The old value can be freed immediately at this point assuming a
|
||||
/// 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();
|
||||
fn configChange(self: *App, new_config: *const Config) void {
|
||||
_ = new_config;
|
||||
|
||||
// Update the existing config, be sure to clean up the old one.
|
||||
self.config.deinit();
|
||||
self.config = config;
|
||||
self.syncConfigChanges() catch |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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
@ -928,6 +968,13 @@ fn loadRuntimeCss(
|
||||
\\ --headerbar-bg-color: rgb({d},{d},{d});
|
||||
\\ --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.g,
|
||||
@ -1381,9 +1428,9 @@ fn gtkActionReloadConfig(
|
||||
ud: ?*anyopaque,
|
||||
) callconv(.C) void {
|
||||
const self: *App = @ptrCast(@alignCast(ud orelse return));
|
||||
_ = self.core_app.mailbox.push(.{
|
||||
.reload_config = {},
|
||||
}, .{ .forever = {} });
|
||||
self.reloadConfig(.app, .{}) catch |err| {
|
||||
log.err("error reloading configuration: {}", .{err});
|
||||
};
|
||||
}
|
||||
|
||||
fn gtkActionQuit(
|
||||
|
@ -203,7 +203,7 @@ const ButtonsView = struct {
|
||||
|
||||
fn gtkReloadClick(_: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) void {
|
||||
const self: *ConfigErrors = @ptrCast(@alignCast(ud));
|
||||
_ = self.app.reloadConfig() catch |err| {
|
||||
self.app.reloadConfig(.app, .{}) catch |err| {
|
||||
log.warn("error reloading config error={}", .{err});
|
||||
return;
|
||||
};
|
||||
|
@ -94,13 +94,14 @@ fn gtkUpdate(ud: ?*anyopaque) callconv(.C) c.gboolean {
|
||||
return c.FALSE;
|
||||
};
|
||||
|
||||
const grid_size = surface.core_surface.size.grid();
|
||||
var buf: [32]u8 = undefined;
|
||||
const text = std.fmt.bufPrintZ(
|
||||
&buf,
|
||||
"{d}c ⨯ {d}r",
|
||||
.{
|
||||
surface.core_surface.grid_size.columns,
|
||||
surface.core_surface.grid_size.rows,
|
||||
grid_size.columns,
|
||||
grid_size.rows,
|
||||
},
|
||||
) catch |err| {
|
||||
log.err("unable to format text: {}", .{err});
|
||||
|
@ -77,6 +77,7 @@ pub fn init(
|
||||
});
|
||||
errdefer surface.destroy(alloc);
|
||||
sibling.dimSurface();
|
||||
sibling.setSplitZoom(false);
|
||||
|
||||
// Create the actual GTKPaned, attach the proper children.
|
||||
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.
|
||||
/// This should be called anytime the top/left or bottom/right
|
||||
/// 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
|
||||
// the same value, then GTK bugs out (the GL area unrealizes
|
||||
// and never rerealizes).
|
||||
@ -372,7 +373,15 @@ fn directionNext(self: *const Split, from: Side) ?struct {
|
||||
}
|
||||
}
|
||||
|
||||
fn removeChildren(self: *const Split) void {
|
||||
c.gtk_paned_set_start_child(@ptrCast(self.paned), null);
|
||||
c.gtk_paned_set_end_child(@ptrCast(self.paned), null);
|
||||
pub fn detachTopLeft(self: *const Split) void {
|
||||
c.gtk_paned_set_start_child(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();
|
||||
}
|
||||
|
@ -308,8 +308,8 @@ pub const URLWidget = struct {
|
||||
/// surface has been initialized.
|
||||
realized: bool = false,
|
||||
|
||||
/// True if this surface had a parent to start with.
|
||||
parent_surface: bool = false,
|
||||
/// The config to use to initialize a surface.
|
||||
init_config: InitConfig,
|
||||
|
||||
/// The GUI container that this surface has been attached to. This
|
||||
/// dictates some behaviors such as new splits, etc.
|
||||
@ -330,6 +330,9 @@ url_widget: ?URLWidget = null,
|
||||
/// The overlay that shows resizing information.
|
||||
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
|
||||
unfocused_widget: ?*c.GtkWidget = null,
|
||||
|
||||
@ -367,6 +370,36 @@ im_len: u7 = 0,
|
||||
/// details on what this is.
|
||||
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 {
|
||||
var surface = try alloc.create(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);
|
||||
|
||||
// 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
|
||||
self.* = .{
|
||||
.app = app,
|
||||
@ -501,7 +538,7 @@ pub fn init(self: *Surface, app: *App, opts: Options) !void {
|
||||
.title_text = null,
|
||||
.core_surface = undefined,
|
||||
.font_size = font_size,
|
||||
.parent_surface = opts.parent != null,
|
||||
.init_config = init_config,
|
||||
.size = .{ .width = 800, .height = 600 },
|
||||
.cursor_pos = .{ .x = 0, .y = 0 },
|
||||
.im_context = im_context,
|
||||
@ -552,7 +589,11 @@ fn realize(self: *Surface) !void {
|
||||
// Get our new surface config
|
||||
var config = try apprt.surface.newConfig(self.app.core_app, &self.app.config);
|
||||
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.
|
||||
config.@"working-directory" = self.app.config.@"working-directory";
|
||||
}
|
||||
@ -580,6 +621,7 @@ fn realize(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);
|
||||
|
||||
// We don't allocate anything if we aren't realized.
|
||||
@ -643,6 +685,8 @@ pub fn redraw(self: *Surface) void {
|
||||
|
||||
/// Close this surface.
|
||||
pub fn close(self: *Surface, processActive: bool) void {
|
||||
self.setSplitZoom(false);
|
||||
|
||||
// If we're not part of a window hierarchy, we never confirm
|
||||
// so we can just directly remove ourselves and exit.
|
||||
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 {
|
||||
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));
|
||||
_ = c.gtk_widget_grab_focus(widget);
|
||||
@ -801,7 +854,7 @@ pub fn grabFocus(self: *Surface) void {
|
||||
|
||||
fn updateTitleLabels(self: *Surface) void {
|
||||
// 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 (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 {
|
||||
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);
|
||||
|
||||
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 {
|
||||
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(
|
||||
@ -1875,3 +1952,41 @@ pub fn present(self: *Surface) void {
|
||||
|
||||
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);
|
||||
}
|
||||
|
@ -112,6 +112,10 @@ pub fn setLabelText(self: *Tab, title: [:0]const u8) void {
|
||||
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.
|
||||
pub fn remove(self: *Tab) void {
|
||||
self.window.closeTab(self);
|
||||
|
@ -92,13 +92,9 @@ pub fn init(self: *Window, app: *App) !void {
|
||||
break :window window;
|
||||
}
|
||||
};
|
||||
errdefer c.gtk_window_destroy(@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;
|
||||
c.gtk_window_set_title(gtk_window, "Ghostty");
|
||||
c.gtk_window_set_default_size(gtk_window, 1000, 600);
|
||||
|
@ -12,7 +12,7 @@ const Config = @import("../../config.zig").Config;
|
||||
/// This must be `inline` so that the comptime check noops conditional
|
||||
/// paths that are not enabled.
|
||||
pub inline fn enabled(config: *const Config) bool {
|
||||
return build_options.libadwaita and
|
||||
return build_options.adwaita and
|
||||
config.@"gtk-adwaita";
|
||||
}
|
||||
|
||||
@ -30,7 +30,7 @@ pub fn versionAtLeast(
|
||||
comptime minor: u16,
|
||||
comptime micro: u16,
|
||||
) 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,
|
||||
// we can return false immediately. This prevents us from
|
||||
|
@ -1,7 +1,7 @@
|
||||
/// Imported C API directly from header files
|
||||
pub const c = @cImport({
|
||||
@cInclude("gtk/gtk.h");
|
||||
if (@import("build_options").libadwaita) {
|
||||
if (@import("build_options").adwaita) {
|
||||
@cInclude("libadwaita-1/adwaita.h");
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
const numPages = self.nPages();
|
||||
return switch (tab.window.app.config.@"window-new-tab-position") {
|
||||
|
@ -58,8 +58,10 @@ pub const Message = union(enum) {
|
||||
/// Health status change for the renderer.
|
||||
renderer_health: renderer.Health,
|
||||
|
||||
/// Report the color scheme
|
||||
report_color_scheme: void,
|
||||
/// Report the color scheme. The bool parameter is whether to force or not.
|
||||
/// 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
|
||||
/// a window and switching tabs.
|
||||
@ -70,6 +72,15 @@ pub const Message = union(enum) {
|
||||
/// unless the surface exits.
|
||||
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 {
|
||||
csi_21_t,
|
||||
|
||||
|
@ -12,7 +12,7 @@ pub const fish_completions = comptimeGenerateFishCompletions();
|
||||
|
||||
fn comptimeGenerateFishCompletions() []const u8 {
|
||||
comptime {
|
||||
@setEvalBranchQuota(17000);
|
||||
@setEvalBranchQuota(18000);
|
||||
var counter = std.io.countingWriter(std.io.null_writer);
|
||||
try writeFishCompletions(&counter.writer());
|
||||
|
||||
@ -53,7 +53,7 @@ fn writeFishCompletions(writer: anytype) !void {
|
||||
if (std.mem.startsWith(u8, field.name, "font-family"))
|
||||
try writer.writeAll(" -f -a \"(ghostty +list-fonts | grep '^[A-Z]')\"")
|
||||
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))
|
||||
try writer.writeAll(" -f -k -a \"(__fish_complete_directories)\"")
|
||||
else {
|
||||
|
@ -26,7 +26,7 @@ pub fn genConfig(writer: anytype, cli: bool) !void {
|
||||
\\
|
||||
);
|
||||
|
||||
@setEvalBranchQuota(2000);
|
||||
@setEvalBranchQuota(3000);
|
||||
inline for (@typeInfo(Config).Struct.fields) |field| {
|
||||
if (field.name[0] == '_') continue;
|
||||
|
||||
|
@ -21,7 +21,7 @@ const WasmTarget = @import("os/wasm/target.zig").Target;
|
||||
pub const BuildConfig = struct {
|
||||
version: std.SemanticVersion = .{ .major = 0, .minor = 0, .patch = 0 },
|
||||
flatpak: bool = false,
|
||||
libadwaita: bool = false,
|
||||
adwaita: bool = false,
|
||||
app_runtime: apprt.Runtime = .none,
|
||||
renderer: rendererpkg.Impl = .opengl,
|
||||
font_backend: font.Backend = .freetype,
|
||||
@ -40,7 +40,7 @@ pub const BuildConfig = struct {
|
||||
// We need to break these down individual because addOption doesn't
|
||||
// support all types.
|
||||
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(font.Backend, "font_backend", self.font_backend);
|
||||
step.addOption(rendererpkg.Impl, "renderer", self.renderer);
|
||||
@ -67,7 +67,7 @@ pub const BuildConfig = struct {
|
||||
return .{
|
||||
.version = options.app_version,
|
||||
.flatpak = options.flatpak,
|
||||
.libadwaita = options.libadwaita,
|
||||
.adwaita = options.adwaita,
|
||||
.app_runtime = std.meta.stringToEnum(apprt.Runtime, @tagName(options.app_runtime)).?,
|
||||
.font_backend = std.meta.stringToEnum(font.Backend, @tagName(options.font_backend)).?,
|
||||
.renderer = std.meta.stringToEnum(rendererpkg.Impl, @tagName(options.renderer)).?,
|
||||
|
@ -71,6 +71,13 @@ pub const Action = enum {
|
||||
var pending_help: bool = false;
|
||||
var pending: ?Action = null;
|
||||
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
|
||||
// matter what, no matter what other args exist.
|
||||
if (std.mem.eql(u8, arg, "--version")) return .version;
|
||||
@ -240,3 +247,30 @@ test "parse action plus" {
|
||||
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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
227
src/cli/args.zig
227
src/cli/args.zig
@ -13,7 +13,7 @@ const DiagnosticList = diags.DiagnosticList;
|
||||
// `--long value`? Not currently allowed.
|
||||
|
||||
// For trimming
|
||||
const whitespace = " \t";
|
||||
pub const whitespace = " \t";
|
||||
|
||||
/// The base errors for arg parsing. Additional errors can be returned due
|
||||
/// to type-specific parsing but these are always possible.
|
||||
@ -132,8 +132,8 @@ pub fn parse(
|
||||
// track more error messages.
|
||||
error.OutOfMemory => return err,
|
||||
error.InvalidField => "unknown field",
|
||||
error.ValueRequired => "value required",
|
||||
error.InvalidValue => "invalid value",
|
||||
error.ValueRequired => formatValueRequired(T, arena_alloc, key) catch "value required",
|
||||
error.InvalidValue => formatInvalidValue(T, arena_alloc, key, value) catch "invalid value",
|
||||
else => try std.fmt.allocPrintZ(
|
||||
arena_alloc,
|
||||
"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.
|
||||
fn canTrackDiags(comptime T: type) bool {
|
||||
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
|
||||
/// all the memory associated with alloc. It is expected that alloc points to
|
||||
/// an arena.
|
||||
fn parseIntoField(
|
||||
pub fn parseIntoField(
|
||||
comptime T: type,
|
||||
alloc: Allocator,
|
||||
dst: *T,
|
||||
@ -202,10 +250,46 @@ fn parseIntoField(
|
||||
1 => @field(dst, field.name) = try Field.parseCLI(value),
|
||||
|
||||
// 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 => 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"),
|
||||
}
|
||||
@ -262,8 +346,9 @@ fn parseIntoField(
|
||||
value orelse return error.ValueRequired,
|
||||
) orelse return error.InvalidValue,
|
||||
|
||||
.Struct => try parsePackedStruct(
|
||||
.Struct => try parseStruct(
|
||||
Field,
|
||||
alloc,
|
||||
value orelse return error.ValueRequired,
|
||||
),
|
||||
|
||||
@ -330,9 +415,79 @@ fn parseTaggedUnion(comptime T: type, alloc: Allocator, v: []const u8) !T {
|
||||
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 {
|
||||
const info = @typeInfo(T).Struct;
|
||||
assert(info.layout == .@"packed");
|
||||
comptime assert(info.layout == .@"packed");
|
||||
|
||||
var result: T = .{};
|
||||
|
||||
@ -799,6 +954,62 @@ test "parseIntoField: struct with parse func" {
|
||||
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" {
|
||||
const testing = std.testing;
|
||||
var arena = ArenaAllocator.init(testing.allocator);
|
||||
|
@ -111,7 +111,7 @@ fn prettyPrint(alloc: Allocator, keybinds: Config.Keybinds) !u8 {
|
||||
// align things nicely
|
||||
var iter = keybinds.set.bindings.iterator();
|
||||
var bindings = std.ArrayList(Binding).init(alloc);
|
||||
var widest_key: usize = 0;
|
||||
var widest_key: u16 = 0;
|
||||
var buf: [64]u8 = undefined;
|
||||
while (iter.next()) |bind| {
|
||||
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 shift_style: vaxis.Style = .{ .fg = .{ .index = 4 } };
|
||||
|
||||
var longest_col: usize = 0;
|
||||
var longest_col: u16 = 0;
|
||||
|
||||
// Print the list
|
||||
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 };
|
||||
const trigger = bind.trigger;
|
||||
if (trigger.mods.super) {
|
||||
result = try win.printSegment(.{ .text = "super", .style = super_style }, .{ .col_offset = result.col });
|
||||
result = try win.printSegment(.{ .text = " + " }, .{ .col_offset = result.col });
|
||||
result = win.printSegment(.{ .text = "super", .style = super_style }, .{ .col_offset = result.col });
|
||||
result = win.printSegment(.{ .text = " + " }, .{ .col_offset = result.col });
|
||||
}
|
||||
if (trigger.mods.ctrl) {
|
||||
result = try win.printSegment(.{ .text = "ctrl ", .style = ctrl_style }, .{ .col_offset = result.col });
|
||||
result = try win.printSegment(.{ .text = " + " }, .{ .col_offset = result.col });
|
||||
result = win.printSegment(.{ .text = "ctrl ", .style = ctrl_style }, .{ .col_offset = result.col });
|
||||
result = win.printSegment(.{ .text = " + " }, .{ .col_offset = result.col });
|
||||
}
|
||||
if (trigger.mods.alt) {
|
||||
result = try win.printSegment(.{ .text = "alt ", .style = alt_style }, .{ .col_offset = result.col });
|
||||
result = try win.printSegment(.{ .text = " + " }, .{ .col_offset = result.col });
|
||||
result = win.printSegment(.{ .text = "alt ", .style = alt_style }, .{ .col_offset = result.col });
|
||||
result = win.printSegment(.{ .text = " + " }, .{ .col_offset = result.col });
|
||||
}
|
||||
if (trigger.mods.shift) {
|
||||
result = try win.printSegment(.{ .text = "shift", .style = shift_style }, .{ .col_offset = result.col });
|
||||
result = try win.printSegment(.{ .text = " + " }, .{ .col_offset = result.col });
|
||||
result = win.printSegment(.{ .text = "shift", .style = shift_style }, .{ .col_offset = result.col });
|
||||
result = win.printSegment(.{ .text = " + " }, .{ .col_offset = result.col });
|
||||
}
|
||||
|
||||
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
|
||||
// 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;
|
||||
|
||||
const action = try std.fmt.allocPrint(alloc, "{}", .{bind.action});
|
||||
// If our action has an argument, we print the argument in a different color
|
||||
if (std.mem.indexOfScalar(u8, action, ':')) |idx| {
|
||||
_ = try win.print(&.{
|
||||
_ = win.print(&.{
|
||||
.{ .text = action[0..idx] },
|
||||
.{ .text = action[idx .. idx + 1], .style = .{ .dim = true } },
|
||||
.{ .text = action[idx + 1 ..], .style = .{ .fg = .{ .index = 5 } } },
|
||||
}, .{ .col_offset = longest_col + widest_key + 2 });
|
||||
} 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);
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -42,7 +42,7 @@ pub fn run(alloc: Allocator) !u8 {
|
||||
gtk.gtk_get_minor_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(" build : {s}\n", .{
|
||||
gtk.ADW_VERSION_S,
|
||||
|
@ -2,10 +2,12 @@ const builtin = @import("builtin");
|
||||
|
||||
const formatter = @import("config/formatter.zig");
|
||||
pub const Config = @import("config/Config.zig");
|
||||
pub const conditional = @import("config/conditional.zig");
|
||||
pub const string = @import("config/string.zig");
|
||||
pub const edit = @import("config/edit.zig");
|
||||
pub const url = @import("config/url.zig");
|
||||
|
||||
pub const ConditionalState = conditional.State;
|
||||
pub const FileFormatter = formatter.FileFormatter;
|
||||
pub const entryFormatter = formatter.entryFormatter;
|
||||
pub const formatEntry = formatter.formatEntry;
|
||||
@ -16,6 +18,7 @@ pub const CopyOnSelect = Config.CopyOnSelect;
|
||||
pub const CustomShaderAnimation = Config.CustomShaderAnimation;
|
||||
pub const FontSyntheticStyle = Config.FontSyntheticStyle;
|
||||
pub const FontStyle = Config.FontStyle;
|
||||
pub const FreetypeLoadFlags = Config.FreetypeLoadFlags;
|
||||
pub const Keybinds = Config.Keybinds;
|
||||
pub const MouseShiftCapture = Config.MouseShiftCapture;
|
||||
pub const NonNativeFullscreen = Config.NonNativeFullscreen;
|
||||
|
@ -19,6 +19,7 @@ export fn ghostty_config_new() ?*Config {
|
||||
|
||||
result.* = Config.default(global.alloc) catch |err| {
|
||||
log.err("error creating config err={}", .{err});
|
||||
global.alloc.destroy(result);
|
||||
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.
|
||||
export fn ghostty_config_load_cli_args(self: *Config) void {
|
||||
self.loadCliArgs(global.alloc) catch |err| {
|
||||
|
File diff suppressed because it is too large
Load Diff
95
src/config/conditional.zig
Normal file
95
src/config/conditional.zig
Normal 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",
|
||||
}));
|
||||
}
|
51
src/config/sublime_syntax.zig
Normal file
51
src/config/sublime_syntax.zig
Normal 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
1
src/config/testdata/theme_dark
vendored
Normal file
@ -0,0 +1 @@
|
||||
background = #EEEEEE
|
1
src/config/testdata/theme_light
vendored
Normal file
1
src/config/testdata/theme_light
vendored
Normal file
@ -0,0 +1 @@
|
||||
background = #FFFFFF
|
2
src/config/testdata/theme_simple
vendored
Normal file
2
src/config/testdata/theme_simple
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
# A simple theme
|
||||
background = #123ABC
|
@ -112,7 +112,6 @@ pub fn open(
|
||||
path: []const u8,
|
||||
file: std.fs.File,
|
||||
} {
|
||||
|
||||
// Absolute themes are loaded a different path.
|
||||
if (std.fs.path.isAbsolute(theme)) {
|
||||
const file: std.fs.File = try openAbsolute(
|
||||
|
@ -24,7 +24,7 @@ const oni = @import("oniguruma");
|
||||
/// handling them well requires a non-regex approach.
|
||||
pub const regex =
|
||||
"(?:" ++ url_schemes ++
|
||||
\\)(?:[\w\-.~:/?#\[\]@!$&*+,;=%]+(?:\(\w*\))?)+(?<!\.)
|
||||
\\)(?:[\w\-.~:/?#@!$&*+,;=%]+(?:[\(\[]\w*[\)\]])?)+(?<![,.])
|
||||
;
|
||||
const url_schemes =
|
||||
\\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.",
|
||||
.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",
|
||||
.expect = "https://example.com",
|
||||
@ -112,6 +116,11 @@ test "url regex" {
|
||||
.input = "square brackets https://example.com/[foo] and more",
|
||||
.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
|
||||
.{
|
||||
.input = "match ftp://example.com ftp links",
|
||||
|
@ -70,7 +70,7 @@ fn writeSyntax(writer: anytype) !void {
|
||||
try writer.writeAll(
|
||||
\\
|
||||
\\
|
||||
\\syn match ghosttyConfigComment /#.*/ contains=@Spell
|
||||
\\syn match ghosttyConfigComment /^\s*#.*/ contains=@Spell
|
||||
\\
|
||||
\\hi def link ghosttyConfigComment Comment
|
||||
\\hi def link ghosttyConfigKeyword Keyword
|
||||
|
@ -177,29 +177,30 @@ fn beforeSend(
|
||||
const obj = sentry.Value.initObject();
|
||||
errdefer obj.decref();
|
||||
const surface = thr_state.surface;
|
||||
const grid_size = surface.size.grid();
|
||||
obj.set(
|
||||
"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(
|
||||
"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(
|
||||
"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(
|
||||
"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(
|
||||
"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(
|
||||
"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);
|
||||
|
@ -1,4 +1,4 @@
|
||||
const fastmem = @import("./fastmem.zig");
|
||||
const fastmem = @import("../fastmem.zig");
|
||||
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
@ -1,7 +1,7 @@
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const fastmem = @import("fastmem.zig");
|
||||
const fastmem = @import("../fastmem.zig");
|
||||
|
||||
/// Returns a circular buffer containing type T.
|
||||
pub fn CircBuf(comptime T: type, comptime default: T) type {
|
181
src/datastruct/intrusive_linked_list.zig
Normal file
181
src/datastruct/intrusive_linked_list.zig
Normal 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
19
src/datastruct/main.zig
Normal 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());
|
||||
}
|
@ -452,6 +452,12 @@ pub const LoadOptions = struct {
|
||||
/// for this is owned by the user and is not freed by the collection.
|
||||
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 {
|
||||
_ = self;
|
||||
_ = alloc;
|
||||
@ -462,6 +468,7 @@ pub const LoadOptions = struct {
|
||||
return .{
|
||||
.size = self.size,
|
||||
.metric_modifiers = &self.metric_modifiers,
|
||||
.freetype_load_flags = self.freetype_load_flags,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
@ -57,6 +57,11 @@ pub const CoreText = struct {
|
||||
/// The initialized 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 {
|
||||
self.font.release();
|
||||
self.* = undefined;
|
||||
@ -194,7 +199,10 @@ fn loadCoreText(
|
||||
) !Face {
|
||||
_ = lib;
|
||||
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(
|
||||
@ -236,43 +244,7 @@ fn loadCoreTextFreetype(
|
||||
//std.log.warn("path={s}", .{path_slice});
|
||||
var face = try Face.initFile(lib, buf[0..path_slice.len :0], 0, opts);
|
||||
errdefer face.deinit();
|
||||
|
||||
// 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);
|
||||
}
|
||||
try face.setVariations(ct.variations, opts);
|
||||
|
||||
return face;
|
||||
}
|
||||
|
@ -168,6 +168,7 @@ fn collection(
|
||||
.library = self.font_lib,
|
||||
.size = size,
|
||||
.metric_modifiers = key.metric_modifiers,
|
||||
.freetype_load_flags = key.freetype_load_flags,
|
||||
};
|
||||
|
||||
var c = Collection.init();
|
||||
@ -427,6 +428,7 @@ pub const DerivedConfig = struct {
|
||||
@"adjust-strikethrough-position": ?Metrics.Modifier,
|
||||
@"adjust-strikethrough-thickness": ?Metrics.Modifier,
|
||||
@"adjust-cursor-thickness": ?Metrics.Modifier,
|
||||
@"freetype-load-flags": font.face.FreetypeLoadFlags,
|
||||
|
||||
/// Initialize a DerivedConfig. The config should be either a
|
||||
/// 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-thickness" = config.@"adjust-strikethrough-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
|
||||
// from above since Zig does assignment in order.
|
||||
@ -500,6 +503,10 @@ pub const Key = struct {
|
||||
/// font grid.
|
||||
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 StyleOffsets = [style_offsets_len]usize;
|
||||
|
||||
@ -618,6 +625,10 @@ pub const Key = struct {
|
||||
.codepoint_map = codepoint_map,
|
||||
.metric_modifiers = metric_modifiers,
|
||||
.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);
|
||||
self.codepoint_map.hash(hasher);
|
||||
autoHash(hasher, self.metric_modifiers.count());
|
||||
autoHash(hasher, self.freetype_load_flags);
|
||||
if (self.metric_modifiers.count() > 0) {
|
||||
inline for (@typeInfo(Metrics.Key).Enum.fields) |field| {
|
||||
const key = @field(Metrics.Key, field.name);
|
||||
|
@ -79,7 +79,7 @@ pub const Descriptor = struct {
|
||||
|
||||
// This is not correct, but we don't currently depend on the
|
||||
// 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
|
||||
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;
|
||||
return try macos.text.FontDescriptor.createWithAttributes(@ptrCast(attrs));
|
||||
}
|
||||
};
|
||||
|
||||
@ -384,6 +370,7 @@ pub const CoreText = struct {
|
||||
return DiscoverIterator{
|
||||
.alloc = alloc,
|
||||
.list = zig_list,
|
||||
.variations = desc.variations,
|
||||
.i = 0,
|
||||
};
|
||||
}
|
||||
@ -420,6 +407,7 @@ pub const CoreText = struct {
|
||||
return DiscoverIterator{
|
||||
.alloc = alloc,
|
||||
.list = list,
|
||||
.variations = desc.variations,
|
||||
.i = 0,
|
||||
};
|
||||
}
|
||||
@ -443,6 +431,7 @@ pub const CoreText = struct {
|
||||
return DiscoverIterator{
|
||||
.alloc = alloc,
|
||||
.list = list,
|
||||
.variations = desc.variations,
|
||||
.i = 0,
|
||||
};
|
||||
}
|
||||
@ -682,30 +671,29 @@ pub const CoreText = struct {
|
||||
break :style .unmatched;
|
||||
defer style.release();
|
||||
|
||||
// Get our style string
|
||||
var buf: [128]u8 = undefined;
|
||||
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| {
|
||||
var buf: [128]u8 = undefined;
|
||||
const style_str = style.cstring(&buf, .utf8) orelse break :style .unmatched;
|
||||
|
||||
// Matching style string gets highest score
|
||||
if (std.mem.eql(u8, desired_style, style_str)) break :style .match;
|
||||
|
||||
// Otherwise the score is based on the length of the style string.
|
||||
// Shorter styles are scored higher.
|
||||
break :style @enumFromInt(100 -| style_str.len);
|
||||
} else if (!desc.bold and !desc.italic) {
|
||||
// If we do not, and we have no symbolic traits, then we try
|
||||
// to find "regular" (or no style). If we have symbolic traits
|
||||
// we do nothing but we can improve scoring by taking that into
|
||||
// account, too.
|
||||
if (std.mem.eql(u8, "Regular", style_str)) {
|
||||
break :style .match;
|
||||
}
|
||||
}
|
||||
|
||||
// If we do not, and we have no symbolic traits, then we try
|
||||
// to find "regular" (or no style). If we have symbolic traits
|
||||
// we do nothing but we can improve scoring by taking that into
|
||||
// account, too.
|
||||
if (!desc.bold and !desc.italic) {
|
||||
var buf: [128]u8 = undefined;
|
||||
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: {
|
||||
@ -721,6 +709,7 @@ pub const CoreText = struct {
|
||||
pub const DiscoverIterator = struct {
|
||||
alloc: Allocator,
|
||||
list: []const *macos.text.FontDescriptor,
|
||||
variations: []const Variation,
|
||||
i: usize,
|
||||
|
||||
pub fn deinit(self: *DiscoverIterator) void {
|
||||
@ -756,7 +745,10 @@ pub const CoreText = struct {
|
||||
defer self.i += 1;
|
||||
|
||||
return DeferredFace{
|
||||
.ct = .{ .font = font },
|
||||
.ct = .{
|
||||
.font = font,
|
||||
.variations = self.variations,
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
@ -14,6 +14,7 @@ pub const emoji = @embedFile("res/NotoColorEmoji.ttf");
|
||||
pub const emoji_text = @embedFile("res/NotoEmoji-Regular.ttf");
|
||||
|
||||
/// Fonts with general properties
|
||||
pub const arabic = @embedFile("res/KawkabMono-Regular.ttf");
|
||||
pub const variable = @embedFile("res/Lilex-VF.ttf");
|
||||
|
||||
/// Font with nerd fonts embedded.
|
||||
|
@ -2,6 +2,7 @@ const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const options = @import("main.zig").options;
|
||||
pub const Metrics = @import("face/Metrics.zig");
|
||||
const config = @import("../config.zig");
|
||||
const freetype = @import("face/freetype.zig");
|
||||
const coretext = @import("face/coretext.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.
|
||||
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.
|
||||
pub const Options = struct {
|
||||
size: DesiredSize,
|
||||
metric_modifiers: ?*const Metrics.ModifierSet = null,
|
||||
freetype_load_flags: FreetypeLoadFlags = freetype_load_flags_default,
|
||||
};
|
||||
|
||||
/// The desired size for loading a font.
|
||||
|
@ -229,6 +229,9 @@ pub const Face = struct {
|
||||
vs: []const font.face.Variation,
|
||||
opts: font.face.Options,
|
||||
) !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.
|
||||
var desc = self.font.copyDescriptor();
|
||||
defer desc.release();
|
||||
|
@ -18,10 +18,16 @@ const Library = font.Library;
|
||||
const convert = @import("freetype_convert.zig");
|
||||
const fastmem = @import("../../fastmem.zig");
|
||||
const quirks = @import("../../quirks.zig");
|
||||
const config = @import("../../config.zig");
|
||||
|
||||
const log = std.log.scoped(.font_face);
|
||||
|
||||
pub const Face = struct {
|
||||
comptime {
|
||||
// If we have the freetype backend, we should have load flags.
|
||||
assert(font.face.FreetypeLoadFlags != void);
|
||||
}
|
||||
|
||||
/// Our freetype library
|
||||
lib: freetype.Library,
|
||||
|
||||
@ -34,6 +40,9 @@ pub const Face = struct {
|
||||
/// Metrics for this font face. These are useful for renderers.
|
||||
metrics: font.face.Metrics,
|
||||
|
||||
/// Freetype load flags for this font face.
|
||||
load_flags: font.face.FreetypeLoadFlags,
|
||||
|
||||
/// Set quirks.disableDefaultFontFeatures
|
||||
quirks_disable_default_font_features: bool = false,
|
||||
|
||||
@ -77,6 +86,7 @@ pub const Face = struct {
|
||||
.face = face,
|
||||
.hb_font = hb_font,
|
||||
.metrics = calcMetrics(face, opts.metric_modifiers),
|
||||
.load_flags = opts.freetype_load_flags,
|
||||
};
|
||||
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
|
||||
// often colored bitmaps, which we support.
|
||||
.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;
|
||||
|
||||
|
BIN
src/font/res/KawkabMono-Regular.ttf
Normal file
BIN
src/font/res/KawkabMono-Regular.ttf
Normal file
Binary file not shown.
19
src/font/res/MIT.txt
Normal file
19
src/font/res/MIT.txt
Normal 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
91
src/font/res/OFL.txt
Normal 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
32
src/font/res/README.md
Normal 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).
|
@ -14,7 +14,7 @@ const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
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);
|
||||
|
||||
|
@ -190,6 +190,11 @@ pub const Shaper = struct {
|
||||
// Reset the buffer for our current run
|
||||
self.shaper.hb_buf.reset();
|
||||
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 {
|
||||
@ -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" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
@ -1146,6 +1191,7 @@ const TestShaper = struct {
|
||||
const TestFont = enum {
|
||||
inconsolata,
|
||||
monaspace_neon,
|
||||
arabic,
|
||||
};
|
||||
|
||||
/// Helper to return a fully initialized shaper.
|
||||
@ -1159,6 +1205,7 @@ fn testShaperWithFont(alloc: Allocator, font_req: TestFont) !TestShaper {
|
||||
const testFont = switch (font_req) {
|
||||
.inconsolata => font.embedded.inconsolata,
|
||||
.monaspace_neon => font.embedded.monaspace_neon,
|
||||
.arabic => font.embedded.arabic,
|
||||
};
|
||||
|
||||
var lib = try Library.init();
|
||||
|
3
src/font/shaper/testdata/arabic.txt
vendored
Normal file
3
src/font/shaper/testdata/arabic.txt
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
غريبه لاني عربي أبا عن جد
|
||||
واتكلم الانجليزية بطلاقة اكثر من ٢٥ سنه
|
||||
ومع هذا اجد العربيه افضل لان فيها الكثير من المفردات الاكثر دقه بالوصف
|
@ -79,6 +79,17 @@ const Quads = packed struct(u4) {
|
||||
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
|
||||
const Alignment = struct {
|
||||
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 }),
|
||||
// '╭'
|
||||
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),
|
||||
// '╲'
|
||||
@ -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),
|
||||
|
||||
// (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
|
||||
// in the high bits of a unicode codepoint.
|
||||
@intFromEnum(Sprite.cursor_rect) => self.draw_cursor_rect(canvas),
|
||||
@ -1793,6 +2128,135 @@ fn draw_cell_diagonal(
|
||||
) 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(
|
||||
self: Box,
|
||||
canvas: *font.sprite.Canvas,
|
||||
@ -2118,12 +2582,13 @@ fn draw_edge_triangle(
|
||||
try ctx.fill(canvas.alloc, path);
|
||||
}
|
||||
|
||||
fn draw_light_arc(
|
||||
fn draw_arc(
|
||||
self: Box,
|
||||
canvas: *font.sprite.Canvas,
|
||||
comptime corner: Corner,
|
||||
comptime thickness: Thickness,
|
||||
) !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_height: f64 = @floatFromInt(self.height);
|
||||
const float_thick: f64 = @floatFromInt(thick_px);
|
||||
@ -2534,6 +2999,32 @@ fn testRenderAll(self: Box, alloc: Allocator, atlas: *font.Atlas) !void {
|
||||
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" {
|
||||
|
@ -263,6 +263,21 @@ const Kind = enum {
|
||||
0x1FBCE...0x1FBEF,
|
||||
=> .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
|
||||
0xE0B0,
|
||||
0xE0B4,
|
||||
|
BIN
src/font/sprite/testdata/Box.ppm
vendored
BIN
src/font/sprite/testdata/Box.ppm
vendored
Binary file not shown.
@ -317,8 +317,7 @@ pub const Action = union(enum) {
|
||||
/// Focus on a split in a given direction.
|
||||
goto_split: SplitFocusDirection,
|
||||
|
||||
/// zoom/unzoom the current split. This is currently only supported
|
||||
/// on macOS. Contributions welcome for other platforms.
|
||||
/// zoom/unzoom the current split.
|
||||
toggle_split_zoom: void,
|
||||
|
||||
/// Resize the current split by moving the split divider in the given
|
||||
@ -422,6 +421,8 @@ pub const Action = union(enum) {
|
||||
///
|
||||
crash: CrashThread,
|
||||
|
||||
pub const Key = @typeInfo(Action).Union.tag_type.?;
|
||||
|
||||
pub const CrashThread = enum {
|
||||
main,
|
||||
io,
|
||||
@ -431,6 +432,16 @@ pub const Action = union(enum) {
|
||||
pub const CursorKey = struct {
|
||||
normal: []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 {
|
||||
@ -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
|
||||
/// action.
|
||||
pub fn hash(self: Action) u64 {
|
||||
@ -1102,6 +1157,16 @@ pub const Set = struct {
|
||||
action: Action,
|
||||
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 {
|
||||
var hasher = std.hash.Wyhash.init(0);
|
||||
self.action.hash(&hasher);
|
||||
@ -1391,8 +1456,9 @@ pub const Set = struct {
|
||||
// If we have any leaders we need to clone them.
|
||||
var it = result.bindings.iterator();
|
||||
while (it.next()) |entry| switch (entry.value_ptr.*) {
|
||||
// No data to clone
|
||||
.leaf => {},
|
||||
// Leaves could have data to clone (i.e. text actions
|
||||
// contain allocated strings).
|
||||
.leaf => |*s| s.* = try s.clone(alloc),
|
||||
|
||||
// Must be deep cloned.
|
||||
.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.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);
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ const cimgui = @import("cimgui");
|
||||
const Surface = @import("../Surface.zig");
|
||||
const font = @import("../font/main.zig");
|
||||
const input = @import("../input.zig");
|
||||
const renderer = @import("../renderer.zig");
|
||||
const terminal = @import("../terminal/main.zig");
|
||||
const inspector = @import("main.zig");
|
||||
|
||||
@ -641,8 +642,8 @@ fn renderSizeWindow(self: *Inspector) void {
|
||||
_ = cimgui.c.igTableSetColumnIndex(1);
|
||||
cimgui.c.igText(
|
||||
"%dpx x %dpx",
|
||||
self.surface.screen_size.width,
|
||||
self.surface.screen_size.height,
|
||||
self.surface.size.screen.width,
|
||||
self.surface.size.screen.height,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -656,10 +657,11 @@ fn renderSizeWindow(self: *Inspector) void {
|
||||
}
|
||||
{
|
||||
_ = cimgui.c.igTableSetColumnIndex(1);
|
||||
const grid_size = self.surface.size.grid();
|
||||
cimgui.c.igText(
|
||||
"%dc x %dr",
|
||||
self.surface.grid_size.columns,
|
||||
self.surface.grid_size.rows,
|
||||
grid_size.columns,
|
||||
grid_size.rows,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -675,8 +677,8 @@ fn renderSizeWindow(self: *Inspector) void {
|
||||
_ = cimgui.c.igTableSetColumnIndex(1);
|
||||
cimgui.c.igText(
|
||||
"%dpx x %dpx",
|
||||
self.surface.cell_size.width,
|
||||
self.surface.cell_size.height,
|
||||
self.surface.size.cell.width,
|
||||
self.surface.size.cell.height,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -692,10 +694,10 @@ fn renderSizeWindow(self: *Inspector) void {
|
||||
_ = cimgui.c.igTableSetColumnIndex(1);
|
||||
cimgui.c.igText(
|
||||
"T=%d B=%d L=%d R=%d px",
|
||||
self.surface.padding.top,
|
||||
self.surface.padding.bottom,
|
||||
self.surface.padding.left,
|
||||
self.surface.padding.right,
|
||||
self.surface.size.padding.top,
|
||||
self.surface.size.padding.bottom,
|
||||
self.surface.size.padding.left,
|
||||
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.igTableSetColumnIndex(0);
|
||||
@ -795,8 +803,8 @@ fn renderSizeWindow(self: *Inspector) void {
|
||||
_ = cimgui.c.igTableSetColumnIndex(1);
|
||||
cimgui.c.igText(
|
||||
"(%dpx, %dpx)",
|
||||
@as(i64, @intFromFloat(adjusted.x)),
|
||||
@as(i64, @intFromFloat(adjusted.y)),
|
||||
@as(i64, @intFromFloat(coord.x)),
|
||||
@as(i64, @intFromFloat(coord.y)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const input = @import("../input.zig");
|
||||
const CircBuf = @import("../circ_buf.zig").CircBuf;
|
||||
const CircBuf = @import("../datastruct/main.zig").CircBuf;
|
||||
const cimgui = @import("cimgui");
|
||||
|
||||
/// Circular buffer of key events.
|
||||
|
@ -2,7 +2,7 @@ const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const cimgui = @import("cimgui");
|
||||
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");
|
||||
|
||||
/// The stream handler for our inspector.
|
||||
|
@ -168,7 +168,6 @@ pub const std_options: std.Options = .{
|
||||
};
|
||||
|
||||
test {
|
||||
_ = @import("circ_buf.zig");
|
||||
_ = @import("pty.zig");
|
||||
_ = @import("Command.zig");
|
||||
_ = @import("font/main.zig");
|
||||
@ -180,17 +179,11 @@ test {
|
||||
_ = @import("surface_mouse.zig");
|
||||
|
||||
// Libraries
|
||||
_ = @import("segmented_pool.zig");
|
||||
_ = @import("crash/main.zig");
|
||||
_ = @import("datastruct/main.zig");
|
||||
_ = @import("inspector/main.zig");
|
||||
_ = @import("terminal/main.zig");
|
||||
_ = @import("terminfo/main.zig");
|
||||
_ = @import("simd/main.zig");
|
||||
_ = @import("unicode/main.zig");
|
||||
|
||||
// TODO
|
||||
_ = @import("blocking_queue.zig");
|
||||
_ = @import("cache_table.zig");
|
||||
_ = @import("config.zig");
|
||||
_ = @import("lru.zig");
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ const builtin = @import("builtin");
|
||||
const xev = @import("xev");
|
||||
const macos = @import("macos");
|
||||
|
||||
const BlockingQueue = @import("../blocking_queue.zig").BlockingQueue;
|
||||
const BlockingQueue = @import("../datastruct/main.zig").BlockingQueue;
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
const log = std.log.scoped(.cf_release_thread);
|
||||
|
@ -3,12 +3,6 @@ const builtin = @import("builtin");
|
||||
const Allocator = std.mem.Allocator;
|
||||
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.
|
||||
/// The returned value is always allocated so it must be freed.
|
||||
pub fn appendEnv(
|
||||
@ -33,9 +27,9 @@ pub fn appendEnvAlways(
|
||||
current: []const u8,
|
||||
value: []const u8,
|
||||
) ![]u8 {
|
||||
return try std.fmt.allocPrint(alloc, "{s}{s}{s}", .{
|
||||
return try std.fmt.allocPrint(alloc, "{s}{c}{s}", .{
|
||||
current,
|
||||
PATH_SEP,
|
||||
std.fs.path.delimiter,
|
||||
value,
|
||||
});
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user