mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-04-17 13:02:42 +00:00
Merge branch 'main' into fix_2271
This commit is contained in:
4
.github/workflows/nix.yml
vendored
4
.github/workflows/nix.yml
vendored
@@ -11,13 +11,13 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@v1.1.7
|
||||
uses: namespacelabs/nscloud-cache-action@v1.1.8
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
- name: Setup Nix
|
||||
uses: cachix/install-nix-action@V28
|
||||
uses: cachix/install-nix-action@v29
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@v15
|
||||
|
||||
14
.github/workflows/release-pr.yml
vendored
14
.github/workflows/release-pr.yml
vendored
@@ -57,7 +57,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@V28
|
||||
- uses: cachix/install-nix-action@v29
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@v15
|
||||
@@ -92,7 +92,10 @@ jobs:
|
||||
# codesigning. IMPORTANT: this must NOT run in a Nix environment.
|
||||
# Nix breaks xcodebuild so this has to be run outside.
|
||||
- name: Build Ghostty.app
|
||||
run: cd macos && xcodebuild -target Ghostty -configuration Release
|
||||
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.
|
||||
# This will be a monotonically always increasing build number that we use.
|
||||
@@ -205,7 +208,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@V28
|
||||
- uses: cachix/install-nix-action@v29
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@v15
|
||||
@@ -240,7 +243,10 @@ jobs:
|
||||
# codesigning. IMPORTANT: this must NOT run in a Nix environment.
|
||||
# Nix breaks xcodebuild so this has to be run outside.
|
||||
- name: Build Ghostty.app
|
||||
run: cd macos && xcodebuild -target Ghostty -configuration Release
|
||||
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.
|
||||
# This will be a monotonically always increasing build number that we use.
|
||||
|
||||
21
.github/workflows/release-tip.yml
vendored
21
.github/workflows/release-tip.yml
vendored
@@ -105,7 +105,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@V28
|
||||
- uses: cachix/install-nix-action@v29
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@v15
|
||||
@@ -140,7 +140,10 @@ jobs:
|
||||
# codesigning. IMPORTANT: this must NOT run in a Nix environment.
|
||||
# Nix breaks xcodebuild so this has to be run outside.
|
||||
- name: Build Ghostty.app
|
||||
run: cd macos && xcodebuild -target Ghostty -configuration Release
|
||||
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.
|
||||
# This will be a monotonically always increasing build number that we use.
|
||||
@@ -286,7 +289,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@V28
|
||||
- uses: cachix/install-nix-action@v29
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@v15
|
||||
@@ -321,7 +324,10 @@ jobs:
|
||||
# codesigning. IMPORTANT: this must NOT run in a Nix environment.
|
||||
# Nix breaks xcodebuild so this has to be run outside.
|
||||
- name: Build Ghostty.app
|
||||
run: cd macos && xcodebuild -target Ghostty -configuration Release
|
||||
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.
|
||||
# This will be a monotonically always increasing build number that we use.
|
||||
@@ -455,7 +461,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@V28
|
||||
- uses: cachix/install-nix-action@v29
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@v15
|
||||
@@ -490,7 +496,10 @@ jobs:
|
||||
# codesigning. IMPORTANT: this must NOT run in a Nix environment.
|
||||
# Nix breaks xcodebuild so this has to be run outside.
|
||||
- name: Build Ghostty.app
|
||||
run: cd macos && xcodebuild -target Ghostty -configuration Release
|
||||
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.
|
||||
# This will be a monotonically always increasing build number that we use.
|
||||
|
||||
41
.github/workflows/test.yml
vendored
41
.github/workflows/test.yml
vendored
@@ -32,14 +32,14 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@v1.1.7
|
||||
uses: namespacelabs/nscloud-cache-action@v1.1.8
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@V28
|
||||
- uses: cachix/install-nix-action@v29
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@v15
|
||||
@@ -64,14 +64,14 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@v1.1.7
|
||||
uses: namespacelabs/nscloud-cache-action@v1.1.8
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@V28
|
||||
- uses: cachix/install-nix-action@v29
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@v15
|
||||
@@ -93,14 +93,14 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@v1.1.7
|
||||
uses: namespacelabs/nscloud-cache-action@v1.1.8
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@V28
|
||||
- uses: cachix/install-nix-action@v29
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@v15
|
||||
@@ -126,14 +126,14 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@v1.1.7
|
||||
uses: namespacelabs/nscloud-cache-action@v1.1.8
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@V28
|
||||
- uses: cachix/install-nix-action@v29
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@v15
|
||||
@@ -152,7 +152,7 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@V28
|
||||
- uses: cachix/install-nix-action@v29
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@v15
|
||||
@@ -160,6 +160,9 @@ jobs:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
|
||||
- name: XCode Select
|
||||
run: sudo xcode-select -s /Applications/Xcode_16.0.app
|
||||
|
||||
# GhosttyKit is the framework that is built from Zig for our native
|
||||
# Mac app to access.
|
||||
- name: Build GhosttyKit
|
||||
@@ -185,7 +188,7 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@V28
|
||||
- uses: cachix/install-nix-action@v29
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@v15
|
||||
@@ -302,14 +305,14 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@v1.1.7
|
||||
uses: namespacelabs/nscloud-cache-action@v1.1.8
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@V28
|
||||
- uses: cachix/install-nix-action@v29
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@v15
|
||||
@@ -340,7 +343,7 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@V28
|
||||
- uses: cachix/install-nix-action@v29
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@v15
|
||||
@@ -361,12 +364,12 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4 # Check out repo so we can lint it
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@v1.1.7
|
||||
uses: namespacelabs/nscloud-cache-action@v1.1.8
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
- uses: cachix/install-nix-action@V28
|
||||
- uses: cachix/install-nix-action@v29
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@v15
|
||||
@@ -388,12 +391,12 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4 # Check out repo so we can lint it
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@v1.1.7
|
||||
uses: namespacelabs/nscloud-cache-action@v1.1.8
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
- uses: cachix/install-nix-action@V28
|
||||
- uses: cachix/install-nix-action@v29
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@v15
|
||||
@@ -415,12 +418,12 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4 # Check out repo so we can lint it
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@v1.1.7
|
||||
uses: namespacelabs/nscloud-cache-action@v1.1.8
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
- uses: cachix/install-nix-action@V28
|
||||
- uses: cachix/install-nix-action@v29
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@v15
|
||||
|
||||
@@ -1038,12 +1038,15 @@ fn addDeps(
|
||||
.optimize = optimize,
|
||||
.libxev = false,
|
||||
.images = false,
|
||||
.text_input = false,
|
||||
});
|
||||
const wuffs_dep = b.dependency("wuffs", .{
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
const zf_dep = b.dependency("zf", .{
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
|
||||
// Wasm we do manually since it is such a different build.
|
||||
if (step.rootModuleTarget().cpu.arch == .wasm32) {
|
||||
@@ -1130,6 +1133,7 @@ fn addDeps(
|
||||
step.root_module.addImport("ziglyph", ziglyph_dep.module("ziglyph"));
|
||||
step.root_module.addImport("vaxis", vaxis_dep.module("vaxis"));
|
||||
step.root_module.addImport("wuffs", wuffs_dep.module("wuffs"));
|
||||
step.root_module.addImport("zf", zf_dep.module("zf"));
|
||||
|
||||
// Mac Stuff
|
||||
if (step.rootModuleTarget().isDarwin()) {
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
.dependencies = .{
|
||||
// Zig libs
|
||||
.libxev = .{
|
||||
.url = "https://github.com/mitchellh/libxev/archive/43c7e4b3308f359e5b758db2d824d7c447f4ed3f.tar.gz",
|
||||
.hash = "1220aec83b6367c6bc64ca781828e0ad817fb38e7fca7331bd6d736b6896910f6637",
|
||||
.url = "https://github.com/mitchellh/libxev/archive/b8d1d93e5c899b27abbaa7df23b496c3e6a178c7.tar.gz",
|
||||
.hash = "1220612bc023c21d75234882ec9a8c6a1cbd9d642da3dfb899297f14bb5bd7b6cd78",
|
||||
},
|
||||
.mach_glfw = .{
|
||||
.url = "https://github.com/mitchellh/mach-glfw/archive/37c2995f31abcf7e8378fba68ddcf4a3faa02de0.tar.gz",
|
||||
@@ -54,8 +54,12 @@
|
||||
.hash = "122056fbb29863ec1678b7954fb76b1533ad8c581a34577c1b2efe419e29e05596df",
|
||||
},
|
||||
.vaxis = .{
|
||||
.url = "git+https://github.com/rockorager/libvaxis?ref=main#a8baf9ce371b89a84383130c82549bb91401d15a",
|
||||
.hash = "12207f53d7dddd3e5ca6577fcdd137dcf1fa32c9f22cbb0911ad0701cde4095a1c4c",
|
||||
.url = "git+https://github.com/rockorager/libvaxis?ref=main#1961712c1f0cf46b235dd31418dc1b52442abbd5",
|
||||
.hash = "12208cfdda4d5fdbc81b0c44b82e4d6dba2d4a86bff644a153e026fdfc80f8469133",
|
||||
},
|
||||
.zf = .{
|
||||
.url = "git+https://github.com/natecraddock/zf.git?ref=main#bb27a917c3513785c6a91f0b1c10002a5029cacc",
|
||||
.hash = "1220a74107c7f153a2f809e41c7fa7e8dbf75c91043e39fad998247804e5edac2cc8",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -30,15 +30,13 @@
|
||||
pkgs-unstable = nixpkgs-unstable.legacyPackages.${system};
|
||||
in {
|
||||
devShell.${system} = pkgs-stable.callPackage ./nix/devShell.nix {
|
||||
inherit (pkgs-unstable) tracy;
|
||||
|
||||
zig = zig.packages.${system}."0.13.0";
|
||||
wraptest = pkgs-stable.callPackage ./nix/wraptest.nix {};
|
||||
};
|
||||
|
||||
packages.${system} = let
|
||||
mkArgs = optimize: {
|
||||
inherit (pkgs-unstable) zig_0_13;
|
||||
inherit (pkgs-unstable) zig_0_13 stdenv;
|
||||
inherit optimize;
|
||||
|
||||
revision = self.shortRev or self.dirtyShortRev or "dirty";
|
||||
|
||||
@@ -30,7 +30,9 @@ typedef void* ghostty_config_t;
|
||||
typedef void* ghostty_surface_t;
|
||||
typedef void* ghostty_inspector_t;
|
||||
|
||||
// Enums are up top so we can reference them later.
|
||||
// All the types below are fully defined and must be kept in sync with
|
||||
// their Zig counterparts. Any changes to these types MUST have an associated
|
||||
// Zig change.
|
||||
typedef enum {
|
||||
GHOSTTY_PLATFORM_INVALID,
|
||||
GHOSTTY_PLATFORM_MACOS,
|
||||
@@ -48,33 +50,6 @@ typedef enum {
|
||||
GHOSTTY_CLIPBOARD_REQUEST_OSC_52_WRITE,
|
||||
} ghostty_clipboard_request_e;
|
||||
|
||||
typedef enum {
|
||||
GHOSTTY_SPLIT_RIGHT,
|
||||
GHOSTTY_SPLIT_DOWN
|
||||
} ghostty_split_direction_e;
|
||||
|
||||
typedef enum {
|
||||
GHOSTTY_SPLIT_FOCUS_PREVIOUS,
|
||||
GHOSTTY_SPLIT_FOCUS_NEXT,
|
||||
GHOSTTY_SPLIT_FOCUS_TOP,
|
||||
GHOSTTY_SPLIT_FOCUS_LEFT,
|
||||
GHOSTTY_SPLIT_FOCUS_BOTTOM,
|
||||
GHOSTTY_SPLIT_FOCUS_RIGHT,
|
||||
} ghostty_split_focus_direction_e;
|
||||
|
||||
typedef enum {
|
||||
GHOSTTY_SPLIT_RESIZE_UP,
|
||||
GHOSTTY_SPLIT_RESIZE_DOWN,
|
||||
GHOSTTY_SPLIT_RESIZE_LEFT,
|
||||
GHOSTTY_SPLIT_RESIZE_RIGHT,
|
||||
} ghostty_split_resize_direction_e;
|
||||
|
||||
typedef enum {
|
||||
GHOSTTY_INSPECTOR_TOGGLE,
|
||||
GHOSTTY_INSPECTOR_SHOW,
|
||||
GHOSTTY_INSPECTOR_HIDE,
|
||||
} ghostty_inspector_mode_e;
|
||||
|
||||
typedef enum {
|
||||
GHOSTTY_MOUSE_RELEASE,
|
||||
GHOSTTY_MOUSE_PRESS,
|
||||
@@ -97,55 +72,6 @@ typedef enum {
|
||||
GHOSTTY_MOUSE_MOMENTUM_MAY_BEGIN,
|
||||
} ghostty_input_mouse_momentum_e;
|
||||
|
||||
typedef enum {
|
||||
GHOSTTY_MOUSE_SHAPE_DEFAULT,
|
||||
GHOSTTY_MOUSE_SHAPE_CONTEXT_MENU,
|
||||
GHOSTTY_MOUSE_SHAPE_HELP,
|
||||
GHOSTTY_MOUSE_SHAPE_POINTER,
|
||||
GHOSTTY_MOUSE_SHAPE_PROGRESS,
|
||||
GHOSTTY_MOUSE_SHAPE_WAIT,
|
||||
GHOSTTY_MOUSE_SHAPE_CELL,
|
||||
GHOSTTY_MOUSE_SHAPE_CROSSHAIR,
|
||||
GHOSTTY_MOUSE_SHAPE_TEXT,
|
||||
GHOSTTY_MOUSE_SHAPE_VERTICAL_TEXT,
|
||||
GHOSTTY_MOUSE_SHAPE_ALIAS,
|
||||
GHOSTTY_MOUSE_SHAPE_COPY,
|
||||
GHOSTTY_MOUSE_SHAPE_MOVE,
|
||||
GHOSTTY_MOUSE_SHAPE_NO_DROP,
|
||||
GHOSTTY_MOUSE_SHAPE_NOT_ALLOWED,
|
||||
GHOSTTY_MOUSE_SHAPE_GRAB,
|
||||
GHOSTTY_MOUSE_SHAPE_GRABBING,
|
||||
GHOSTTY_MOUSE_SHAPE_ALL_SCROLL,
|
||||
GHOSTTY_MOUSE_SHAPE_COL_RESIZE,
|
||||
GHOSTTY_MOUSE_SHAPE_ROW_RESIZE,
|
||||
GHOSTTY_MOUSE_SHAPE_N_RESIZE,
|
||||
GHOSTTY_MOUSE_SHAPE_E_RESIZE,
|
||||
GHOSTTY_MOUSE_SHAPE_S_RESIZE,
|
||||
GHOSTTY_MOUSE_SHAPE_W_RESIZE,
|
||||
GHOSTTY_MOUSE_SHAPE_NE_RESIZE,
|
||||
GHOSTTY_MOUSE_SHAPE_NW_RESIZE,
|
||||
GHOSTTY_MOUSE_SHAPE_SE_RESIZE,
|
||||
GHOSTTY_MOUSE_SHAPE_SW_RESIZE,
|
||||
GHOSTTY_MOUSE_SHAPE_EW_RESIZE,
|
||||
GHOSTTY_MOUSE_SHAPE_NS_RESIZE,
|
||||
GHOSTTY_MOUSE_SHAPE_NESW_RESIZE,
|
||||
GHOSTTY_MOUSE_SHAPE_NWSE_RESIZE,
|
||||
GHOSTTY_MOUSE_SHAPE_ZOOM_IN,
|
||||
GHOSTTY_MOUSE_SHAPE_ZOOM_OUT,
|
||||
} ghostty_mouse_shape_e;
|
||||
|
||||
typedef enum {
|
||||
GHOSTTY_NON_NATIVE_FULLSCREEN_FALSE,
|
||||
GHOSTTY_NON_NATIVE_FULLSCREEN_TRUE,
|
||||
GHOSTTY_NON_NATIVE_FULLSCREEN_VISIBLE_MENU,
|
||||
} ghostty_non_native_fullscreen_e;
|
||||
|
||||
typedef enum {
|
||||
GHOSTTY_TAB_PREVIOUS = -1,
|
||||
GHOSTTY_TAB_NEXT = -2,
|
||||
GHOSTTY_TAB_LAST = -3,
|
||||
} ghostty_tab_e;
|
||||
|
||||
typedef enum {
|
||||
GHOSTTY_COLOR_SCHEME_LIGHT = 0,
|
||||
GHOSTTY_COLOR_SCHEME_DARK = 1,
|
||||
@@ -357,14 +283,6 @@ typedef enum {
|
||||
GHOSTTY_BUILD_MODE_RELEASE_SMALL,
|
||||
} ghostty_build_mode_e;
|
||||
|
||||
typedef enum {
|
||||
GHOSTTY_RENDERER_HEALTH_OK,
|
||||
GHOSTTY_RENDERER_HEALTH_UNHEALTHY,
|
||||
} ghostty_renderer_health_e;
|
||||
|
||||
// Fully defined types. This MUST be kept in sync with equivalent Zig
|
||||
// structs. To find the Zig struct, grep for this type name. The documentation
|
||||
// for all of these types is available in the Zig source.
|
||||
typedef struct {
|
||||
ghostty_build_mode_e build_mode;
|
||||
const char* version;
|
||||
@@ -414,13 +332,231 @@ typedef struct {
|
||||
uint32_t cell_height_px;
|
||||
} ghostty_surface_size_s;
|
||||
|
||||
// apprt.Target.Key
|
||||
typedef enum {
|
||||
GHOSTTY_TARGET_APP,
|
||||
GHOSTTY_TARGET_SURFACE,
|
||||
} ghostty_target_tag_e;
|
||||
|
||||
typedef union {
|
||||
ghostty_surface_t surface;
|
||||
} ghostty_target_u;
|
||||
|
||||
typedef struct {
|
||||
ghostty_target_tag_e tag;
|
||||
ghostty_target_u target;
|
||||
} ghostty_target_s;
|
||||
|
||||
// apprt.action.SplitDirection
|
||||
typedef enum {
|
||||
GHOSTTY_SPLIT_DIRECTION_RIGHT,
|
||||
GHOSTTY_SPLIT_DIRECTION_DOWN,
|
||||
} ghostty_action_split_direction_e;
|
||||
|
||||
// apprt.action.GotoSplit
|
||||
typedef enum {
|
||||
GHOSTTY_GOTO_SPLIT_PREVIOUS,
|
||||
GHOSTTY_GOTO_SPLIT_NEXT,
|
||||
GHOSTTY_GOTO_SPLIT_TOP,
|
||||
GHOSTTY_GOTO_SPLIT_LEFT,
|
||||
GHOSTTY_GOTO_SPLIT_BOTTOM,
|
||||
GHOSTTY_GOTO_SPLIT_RIGHT,
|
||||
} ghostty_action_goto_split_e;
|
||||
|
||||
// apprt.action.ResizeSplit.Direction
|
||||
typedef enum {
|
||||
GHOSTTY_RESIZE_SPLIT_UP,
|
||||
GHOSTTY_RESIZE_SPLIT_DOWN,
|
||||
GHOSTTY_RESIZE_SPLIT_LEFT,
|
||||
GHOSTTY_RESIZE_SPLIT_RIGHT,
|
||||
} ghostty_action_resize_split_direction_e;
|
||||
|
||||
// apprt.action.ResizeSplit
|
||||
typedef struct {
|
||||
uint16_t amount;
|
||||
ghostty_action_resize_split_direction_e direction;
|
||||
} ghostty_action_resize_split_s;
|
||||
|
||||
// apprt.action.GotoTab
|
||||
typedef enum {
|
||||
GHOSTTY_GOTO_TAB_PREVIOUS = -1,
|
||||
GHOSTTY_GOTO_TAB_NEXT = -2,
|
||||
GHOSTTY_GOTO_TAB_LAST = -3,
|
||||
} ghostty_action_goto_tab_e;
|
||||
|
||||
// apprt.action.Fullscreen
|
||||
typedef enum {
|
||||
GHOSTTY_FULLSCREEN_NATIVE,
|
||||
GHOSTTY_FULLSCREEN_NON_NATIVE,
|
||||
GHOSTTY_FULLSCREEN_NON_NATIVE_VISIBLE_MENU,
|
||||
} ghostty_action_fullscreen_e;
|
||||
|
||||
// apprt.action.SecureInput
|
||||
typedef enum {
|
||||
GHOSTTY_SECURE_INPUT_ON,
|
||||
GHOSTTY_SECURE_INPUT_OFF,
|
||||
GHOSTTY_SECURE_INPUT_TOGGLE,
|
||||
} ghostty_action_secure_input_e;
|
||||
|
||||
// apprt.action.Inspector
|
||||
typedef enum {
|
||||
GHOSTTY_INSPECTOR_TOGGLE,
|
||||
GHOSTTY_INSPECTOR_SHOW,
|
||||
GHOSTTY_INSPECTOR_HIDE,
|
||||
} ghostty_action_inspector_e;
|
||||
|
||||
// apprt.action.QuitTimer
|
||||
typedef enum {
|
||||
GHOSTTY_QUIT_TIMER_START,
|
||||
GHOSTTY_QUIT_TIMER_STOP,
|
||||
} ghostty_action_quit_timer_e;
|
||||
|
||||
// apprt.action.DesktopNotification.C
|
||||
typedef struct {
|
||||
const char* title;
|
||||
const char* body;
|
||||
} ghostty_action_desktop_notification_s;
|
||||
|
||||
// apprt.action.SetTitle.C
|
||||
typedef struct {
|
||||
const char* title;
|
||||
} ghostty_action_set_title_s;
|
||||
|
||||
// terminal.MouseShape
|
||||
typedef enum {
|
||||
GHOSTTY_MOUSE_SHAPE_DEFAULT,
|
||||
GHOSTTY_MOUSE_SHAPE_CONTEXT_MENU,
|
||||
GHOSTTY_MOUSE_SHAPE_HELP,
|
||||
GHOSTTY_MOUSE_SHAPE_POINTER,
|
||||
GHOSTTY_MOUSE_SHAPE_PROGRESS,
|
||||
GHOSTTY_MOUSE_SHAPE_WAIT,
|
||||
GHOSTTY_MOUSE_SHAPE_CELL,
|
||||
GHOSTTY_MOUSE_SHAPE_CROSSHAIR,
|
||||
GHOSTTY_MOUSE_SHAPE_TEXT,
|
||||
GHOSTTY_MOUSE_SHAPE_VERTICAL_TEXT,
|
||||
GHOSTTY_MOUSE_SHAPE_ALIAS,
|
||||
GHOSTTY_MOUSE_SHAPE_COPY,
|
||||
GHOSTTY_MOUSE_SHAPE_MOVE,
|
||||
GHOSTTY_MOUSE_SHAPE_NO_DROP,
|
||||
GHOSTTY_MOUSE_SHAPE_NOT_ALLOWED,
|
||||
GHOSTTY_MOUSE_SHAPE_GRAB,
|
||||
GHOSTTY_MOUSE_SHAPE_GRABBING,
|
||||
GHOSTTY_MOUSE_SHAPE_ALL_SCROLL,
|
||||
GHOSTTY_MOUSE_SHAPE_COL_RESIZE,
|
||||
GHOSTTY_MOUSE_SHAPE_ROW_RESIZE,
|
||||
GHOSTTY_MOUSE_SHAPE_N_RESIZE,
|
||||
GHOSTTY_MOUSE_SHAPE_E_RESIZE,
|
||||
GHOSTTY_MOUSE_SHAPE_S_RESIZE,
|
||||
GHOSTTY_MOUSE_SHAPE_W_RESIZE,
|
||||
GHOSTTY_MOUSE_SHAPE_NE_RESIZE,
|
||||
GHOSTTY_MOUSE_SHAPE_NW_RESIZE,
|
||||
GHOSTTY_MOUSE_SHAPE_SE_RESIZE,
|
||||
GHOSTTY_MOUSE_SHAPE_SW_RESIZE,
|
||||
GHOSTTY_MOUSE_SHAPE_EW_RESIZE,
|
||||
GHOSTTY_MOUSE_SHAPE_NS_RESIZE,
|
||||
GHOSTTY_MOUSE_SHAPE_NESW_RESIZE,
|
||||
GHOSTTY_MOUSE_SHAPE_NWSE_RESIZE,
|
||||
GHOSTTY_MOUSE_SHAPE_ZOOM_IN,
|
||||
GHOSTTY_MOUSE_SHAPE_ZOOM_OUT,
|
||||
} ghostty_action_mouse_shape_e;
|
||||
|
||||
// apprt.action.MouseVisibility
|
||||
typedef enum {
|
||||
GHOSTTY_MOUSE_VISIBLE,
|
||||
GHOSTTY_MOUSE_HIDDEN,
|
||||
} ghostty_action_mouse_visibility_e;
|
||||
|
||||
// apprt.action.MouseOverLink
|
||||
typedef struct {
|
||||
const char* url;
|
||||
size_t len;
|
||||
} ghostty_action_mouse_over_link_s;
|
||||
|
||||
// apprt.action.SizeLimit
|
||||
typedef struct {
|
||||
uint32_t min_width;
|
||||
uint32_t min_height;
|
||||
uint32_t max_width;
|
||||
uint32_t max_height;
|
||||
} ghostty_action_size_limit_s;
|
||||
|
||||
// apprt.action.InitialSize
|
||||
typedef struct {
|
||||
uint32_t width;
|
||||
uint32_t height;
|
||||
} ghostty_action_initial_size_s;
|
||||
|
||||
// apprt.action.CellSize
|
||||
typedef struct {
|
||||
uint32_t width;
|
||||
uint32_t height;
|
||||
} ghostty_action_cell_size_s;
|
||||
|
||||
// renderer.Health
|
||||
typedef enum {
|
||||
GHOSTTY_RENDERER_HEALTH_OK,
|
||||
GHOSTTY_RENDERER_HEALTH_UNHEALTHY,
|
||||
} ghostty_action_renderer_health_e;
|
||||
|
||||
// apprt.Action.Key
|
||||
typedef enum {
|
||||
GHOSTTY_ACTION_NEW_WINDOW,
|
||||
GHOSTTY_ACTION_NEW_TAB,
|
||||
GHOSTTY_ACTION_NEW_SPLIT,
|
||||
GHOSTTY_ACTION_CLOSE_ALL_WINDOWS,
|
||||
GHOSTTY_ACTION_TOGGLE_FULLSCREEN,
|
||||
GHOSTTY_ACTION_TOGGLE_TAB_OVERVIEW,
|
||||
GHOSTTY_ACTION_TOGGLE_WINDOW_DECORATIONS,
|
||||
GHOSTTY_ACTION_TOGGLE_QUICK_TERMINAL,
|
||||
GHOSTTY_ACTION_GOTO_TAB,
|
||||
GHOSTTY_ACTION_GOTO_SPLIT,
|
||||
GHOSTTY_ACTION_RESIZE_SPLIT,
|
||||
GHOSTTY_ACTION_EQUALIZE_SPLITS,
|
||||
GHOSTTY_ACTION_TOGGLE_SPLIT_ZOOM,
|
||||
GHOSTTY_ACTION_PRESENT_TERMINAL,
|
||||
GHOSTTY_ACTION_SIZE_LIMIT,
|
||||
GHOSTTY_ACTION_INITIAL_SIZE,
|
||||
GHOSTTY_ACTION_CELL_SIZE,
|
||||
GHOSTTY_ACTION_INSPECTOR,
|
||||
GHOSTTY_ACTION_RENDER_INSPECTOR,
|
||||
GHOSTTY_ACTION_DESKTOP_NOTIFICATION,
|
||||
GHOSTTY_ACTION_SET_TITLE,
|
||||
GHOSTTY_ACTION_MOUSE_SHAPE,
|
||||
GHOSTTY_ACTION_MOUSE_VISIBILITY,
|
||||
GHOSTTY_ACTION_MOUSE_OVER_LINK,
|
||||
GHOSTTY_ACTION_RENDERER_HEALTH,
|
||||
GHOSTTY_ACTION_OPEN_CONFIG,
|
||||
GHOSTTY_ACTION_QUIT_TIMER,
|
||||
GHOSTTY_ACTION_SECURE_INPUT,
|
||||
} ghostty_action_tag_e;
|
||||
|
||||
typedef union {
|
||||
ghostty_action_split_direction_e new_split;
|
||||
ghostty_action_fullscreen_e toggle_fullscreen;
|
||||
ghostty_action_goto_tab_e goto_tab;
|
||||
ghostty_action_goto_split_e goto_split;
|
||||
ghostty_action_resize_split_s resize_split;
|
||||
ghostty_action_size_limit_s size_limit;
|
||||
ghostty_action_initial_size_s initial_size;
|
||||
ghostty_action_cell_size_s cell_size;
|
||||
ghostty_action_inspector_e inspector;
|
||||
ghostty_action_desktop_notification_s desktop_notification;
|
||||
ghostty_action_set_title_s set_title;
|
||||
ghostty_action_mouse_shape_e mouse_shape;
|
||||
ghostty_action_mouse_visibility_e mouse_visibility;
|
||||
ghostty_action_mouse_over_link_s mouse_over_link;
|
||||
ghostty_action_renderer_health_e renderer_health;
|
||||
ghostty_action_quit_timer_e quit_timer;
|
||||
ghostty_action_secure_input_e secure_input;
|
||||
} ghostty_action_u;
|
||||
|
||||
typedef struct {
|
||||
ghostty_action_tag_e tag;
|
||||
ghostty_action_u action;
|
||||
} 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_open_config_cb)(void*);
|
||||
typedef void (*ghostty_runtime_set_title_cb)(void*, const char*);
|
||||
typedef void (*ghostty_runtime_set_mouse_shape_cb)(void*,
|
||||
ghostty_mouse_shape_e);
|
||||
typedef void (*ghostty_runtime_set_mouse_visibility_cb)(void*, bool);
|
||||
typedef void (*ghostty_runtime_read_clipboard_cb)(void*,
|
||||
ghostty_clipboard_e,
|
||||
void*);
|
||||
@@ -433,67 +569,21 @@ typedef void (*ghostty_runtime_write_clipboard_cb)(void*,
|
||||
const char*,
|
||||
ghostty_clipboard_e,
|
||||
bool);
|
||||
typedef void (*ghostty_runtime_new_split_cb)(void*,
|
||||
ghostty_split_direction_e,
|
||||
ghostty_surface_config_s);
|
||||
typedef void (*ghostty_runtime_new_tab_cb)(void*, ghostty_surface_config_s);
|
||||
typedef void (*ghostty_runtime_new_window_cb)(void*, ghostty_surface_config_s);
|
||||
typedef void (*ghostty_runtime_control_inspector_cb)(void*,
|
||||
ghostty_inspector_mode_e);
|
||||
typedef void (*ghostty_runtime_close_surface_cb)(void*, bool);
|
||||
typedef void (*ghostty_runtime_focus_split_cb)(void*,
|
||||
ghostty_split_focus_direction_e);
|
||||
typedef void (*ghostty_runtime_resize_split_cb)(
|
||||
void*,
|
||||
ghostty_split_resize_direction_e,
|
||||
uint16_t);
|
||||
typedef void (*ghostty_runtime_equalize_splits_cb)(void*);
|
||||
typedef void (*ghostty_runtime_toggle_split_zoom_cb)(void*);
|
||||
typedef void (*ghostty_runtime_goto_tab_cb)(void*, int32_t);
|
||||
typedef void (*ghostty_runtime_toggle_fullscreen_cb)(
|
||||
void*,
|
||||
ghostty_non_native_fullscreen_e);
|
||||
typedef void (*ghostty_runtime_set_initial_window_size_cb)(void*,
|
||||
uint32_t,
|
||||
uint32_t);
|
||||
typedef void (*ghostty_runtime_render_inspector_cb)(void*);
|
||||
typedef void (*ghostty_runtime_set_cell_size_cb)(void*, uint32_t, uint32_t);
|
||||
typedef void (*ghostty_runtime_show_desktop_notification_cb)(void*,
|
||||
const char*,
|
||||
const char*);
|
||||
typedef void (
|
||||
*ghostty_runtime_update_renderer_health)(void*, ghostty_renderer_health_e);
|
||||
typedef void (*ghostty_runtime_mouse_over_link_cb)(void*, const char*, size_t);
|
||||
typedef void (*ghostty_runtime_action_cb)(ghostty_app_t,
|
||||
ghostty_target_s,
|
||||
ghostty_action_s);
|
||||
|
||||
typedef struct {
|
||||
void* userdata;
|
||||
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_open_config_cb open_config_cb;
|
||||
ghostty_runtime_set_title_cb set_title_cb;
|
||||
ghostty_runtime_set_mouse_shape_cb set_mouse_shape_cb;
|
||||
ghostty_runtime_set_mouse_visibility_cb set_mouse_visibility_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;
|
||||
ghostty_runtime_new_split_cb new_split_cb;
|
||||
ghostty_runtime_new_tab_cb new_tab_cb;
|
||||
ghostty_runtime_new_window_cb new_window_cb;
|
||||
ghostty_runtime_control_inspector_cb control_inspector_cb;
|
||||
ghostty_runtime_close_surface_cb close_surface_cb;
|
||||
ghostty_runtime_focus_split_cb focus_split_cb;
|
||||
ghostty_runtime_resize_split_cb resize_split_cb;
|
||||
ghostty_runtime_equalize_splits_cb equalize_splits_cb;
|
||||
ghostty_runtime_toggle_split_zoom_cb toggle_split_zoom_cb;
|
||||
ghostty_runtime_goto_tab_cb goto_tab_cb;
|
||||
ghostty_runtime_toggle_fullscreen_cb toggle_fullscreen_cb;
|
||||
ghostty_runtime_set_initial_window_size_cb set_initial_window_size_cb;
|
||||
ghostty_runtime_render_inspector_cb render_inspector_cb;
|
||||
ghostty_runtime_set_cell_size_cb set_cell_size_cb;
|
||||
ghostty_runtime_show_desktop_notification_cb show_desktop_notification_cb;
|
||||
ghostty_runtime_update_renderer_health update_renderer_health_cb;
|
||||
ghostty_runtime_mouse_over_link_cb mouse_over_link_cb;
|
||||
} ghostty_runtime_config_s;
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
@@ -523,16 +613,20 @@ ghostty_app_t ghostty_app_new(const ghostty_runtime_config_s*,
|
||||
void ghostty_app_free(ghostty_app_t);
|
||||
bool ghostty_app_tick(ghostty_app_t);
|
||||
void* ghostty_app_userdata(ghostty_app_t);
|
||||
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);
|
||||
bool ghostty_app_needs_confirm_quit(ghostty_app_t);
|
||||
bool ghostty_app_has_global_keybinds(ghostty_app_t);
|
||||
|
||||
ghostty_surface_config_s ghostty_surface_config_new();
|
||||
|
||||
ghostty_surface_t ghostty_surface_new(ghostty_app_t, ghostty_surface_config_s*);
|
||||
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);
|
||||
bool ghostty_surface_needs_confirm_quit(ghostty_surface_t);
|
||||
void ghostty_surface_refresh(ghostty_surface_t);
|
||||
void ghostty_surface_draw(ghostty_surface_t);
|
||||
@@ -552,7 +646,10 @@ bool ghostty_surface_mouse_button(ghostty_surface_t,
|
||||
ghostty_input_mouse_state_e,
|
||||
ghostty_input_mouse_button_e,
|
||||
ghostty_input_mods_e);
|
||||
void ghostty_surface_mouse_pos(ghostty_surface_t, double, double);
|
||||
void ghostty_surface_mouse_pos(ghostty_surface_t,
|
||||
double,
|
||||
double,
|
||||
ghostty_input_mods_e);
|
||||
void ghostty_surface_mouse_scroll(ghostty_surface_t,
|
||||
double,
|
||||
double,
|
||||
@@ -560,11 +657,11 @@ void ghostty_surface_mouse_scroll(ghostty_surface_t,
|
||||
void ghostty_surface_mouse_pressure(ghostty_surface_t, uint32_t, double);
|
||||
void ghostty_surface_ime_point(ghostty_surface_t, double*, double*);
|
||||
void ghostty_surface_request_close(ghostty_surface_t);
|
||||
void ghostty_surface_split(ghostty_surface_t, ghostty_split_direction_e);
|
||||
void ghostty_surface_split(ghostty_surface_t, ghostty_action_split_direction_e);
|
||||
void ghostty_surface_split_focus(ghostty_surface_t,
|
||||
ghostty_split_focus_direction_e);
|
||||
ghostty_action_goto_split_e);
|
||||
void ghostty_surface_split_resize(ghostty_surface_t,
|
||||
ghostty_split_resize_direction_e,
|
||||
ghostty_action_resize_split_direction_e,
|
||||
uint16_t);
|
||||
void ghostty_surface_split_equalize(ghostty_surface_t);
|
||||
bool ghostty_surface_binding_action(ghostty_surface_t, const char*, uintptr_t);
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
A51BFC272B30F1B800E92F16 /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = A51BFC262B30F1B800E92F16 /* Sparkle */; };
|
||||
A51BFC2B2B30F6BE00E92F16 /* UpdateDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51BFC2A2B30F6BE00E92F16 /* UpdateDelegate.swift */; };
|
||||
A5278A9B2AA05B2600CD3039 /* Ghostty.Input.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5278A9A2AA05B2600CD3039 /* Ghostty.Input.swift */; };
|
||||
A52FFF572CA90484000C6A5B /* QuickTerminalScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = A52FFF562CA90481000C6A5B /* QuickTerminalScreen.swift */; };
|
||||
A5333E1C2B5A1CE3008AEFF7 /* CrossKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5333E1B2B5A1CE3008AEFF7 /* CrossKit.swift */; };
|
||||
A5333E1D2B5A1CE3008AEFF7 /* CrossKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5333E1B2B5A1CE3008AEFF7 /* CrossKit.swift */; };
|
||||
A5333E202B5A2111008AEFF7 /* SurfaceView_UIKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5333E152B59DE8E008AEFF7 /* SurfaceView_UIKit.swift */; };
|
||||
@@ -34,6 +35,7 @@
|
||||
A53D0C952B53B4D800305CE6 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A5B30538299BEAAB0047F10C /* Assets.xcassets */; };
|
||||
A53D0C9B2B543F3B00305CE6 /* Ghostty.App.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53D0C992B543F3B00305CE6 /* Ghostty.App.swift */; };
|
||||
A53D0C9C2B543F7B00305CE6 /* Package.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55B7BB729B6F53A0055DE60 /* Package.swift */; };
|
||||
A54D786C2CA7978E001B19B1 /* BaseTerminalController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A54D786B2CA79788001B19B1 /* BaseTerminalController.swift */; };
|
||||
A55685E029A03A9F004303CE /* AppError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55685DF29A03A9F004303CE /* AppError.swift */; };
|
||||
A55B7BB829B6F53A0055DE60 /* Package.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55B7BB729B6F53A0055DE60 /* Package.swift */; };
|
||||
A55B7BBC29B6FC330055DE60 /* SurfaceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55B7BBB29B6FC330055DE60 /* SurfaceView.swift */; };
|
||||
@@ -41,6 +43,7 @@
|
||||
A56D58862ACDDB4100508D2C /* Ghostty.Shell.swift in Sources */ = {isa = PBXBuildFile; fileRef = A56D58852ACDDB4100508D2C /* Ghostty.Shell.swift */; };
|
||||
A56D58892ACDE6CA00508D2C /* ServiceProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A56D58882ACDE6CA00508D2C /* ServiceProvider.swift */; };
|
||||
A571AB1D2A206FCF00248498 /* GhosttyKit.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = A5D495A1299BEC7E00DD1313 /* GhosttyKit.xcframework */; };
|
||||
A57D79272C9C879B001D522E /* SecureInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = A57D79262C9C8798001D522E /* SecureInput.swift */; };
|
||||
A586167C2B7703CC009BDB1D /* fish in Resources */ = {isa = PBXBuildFile; fileRef = A586167B2B7703CC009BDB1D /* fish */; };
|
||||
A59444F729A2ED5200725BBA /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59444F629A2ED5200725BBA /* SettingsView.swift */; };
|
||||
A59630972AEE163600D64628 /* HostingWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59630962AEE163600D64628 /* HostingWindow.swift */; };
|
||||
@@ -57,6 +60,16 @@
|
||||
A59FB5D12AE0DEA7009128F3 /* MetalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59FB5D02AE0DEA7009128F3 /* MetalView.swift */; };
|
||||
A5A1F8852A489D6800D1E8BC /* terminfo in Resources */ = {isa = PBXBuildFile; fileRef = A5A1F8842A489D6800D1E8BC /* terminfo */; };
|
||||
A5B30539299BEAAB0047F10C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A5B30538299BEAAB0047F10C /* Assets.xcassets */; };
|
||||
A5CBD0562C9E65B80017A1AE /* DraggableWindowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CBD0552C9E65A50017A1AE /* DraggableWindowView.swift */; };
|
||||
A5CBD0582C9F30960017A1AE /* Cursor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CBD0572C9F30860017A1AE /* Cursor.swift */; };
|
||||
A5CBD0592C9F37B10017A1AE /* Backport.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CEAFFE29C2410700646FDA /* Backport.swift */; };
|
||||
A5CBD05C2CA0C5C70017A1AE /* QuickTerminal.xib in Resources */ = {isa = PBXBuildFile; fileRef = A5CBD05B2CA0C5C70017A1AE /* QuickTerminal.xib */; };
|
||||
A5CBD05E2CA0C5EC0017A1AE /* QuickTerminalController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CBD05D2CA0C5E70017A1AE /* QuickTerminalController.swift */; };
|
||||
A5CBD0602CA0C90A0017A1AE /* QuickTerminalWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CBD05F2CA0C9080017A1AE /* QuickTerminalWindow.swift */; };
|
||||
A5CBD0642CA122E70017A1AE /* QuickTerminalPosition.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CBD0632CA122E70017A1AE /* QuickTerminalPosition.swift */; };
|
||||
A5CBD06B2CA322430017A1AE /* GlobalEventTap.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CBD06A2CA322320017A1AE /* GlobalEventTap.swift */; };
|
||||
A5CC36132C9CD72D004D6760 /* SecureInputOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CC36122C9CD729004D6760 /* SecureInputOverlay.swift */; };
|
||||
A5CC36152C9CDA06004D6760 /* View+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CC36142C9CDA03004D6760 /* View+Extension.swift */; };
|
||||
A5CDF1912AAF9A5800513312 /* ConfigurationErrors.xib in Resources */ = {isa = PBXBuildFile; fileRef = A5CDF1902AAF9A5800513312 /* ConfigurationErrors.xib */; };
|
||||
A5CDF1932AAF9E0800513312 /* ConfigurationErrorsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CDF1922AAF9E0800513312 /* ConfigurationErrorsController.swift */; };
|
||||
A5CDF1952AAFA19600513312 /* ConfigurationErrorsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CDF1942AAFA19600513312 /* ConfigurationErrorsView.swift */; };
|
||||
@@ -91,6 +104,7 @@
|
||||
A51BFC282B30F26D00E92F16 /* GhosttyDebug.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = GhosttyDebug.entitlements; sourceTree = "<group>"; };
|
||||
A51BFC2A2B30F6BE00E92F16 /* UpdateDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateDelegate.swift; sourceTree = "<group>"; };
|
||||
A5278A9A2AA05B2600CD3039 /* Ghostty.Input.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.Input.swift; sourceTree = "<group>"; };
|
||||
A52FFF562CA90481000C6A5B /* QuickTerminalScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickTerminalScreen.swift; sourceTree = "<group>"; };
|
||||
A5333E152B59DE8E008AEFF7 /* SurfaceView_UIKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SurfaceView_UIKit.swift; sourceTree = "<group>"; };
|
||||
A5333E1B2B5A1CE3008AEFF7 /* CrossKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrossKit.swift; sourceTree = "<group>"; };
|
||||
A5333E212B5A2128008AEFF7 /* SurfaceView_AppKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SurfaceView_AppKit.swift; sourceTree = "<group>"; };
|
||||
@@ -98,6 +112,7 @@
|
||||
A535B9D9299C569B0017E2E4 /* ErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorView.swift; sourceTree = "<group>"; };
|
||||
A53D0C932B53B43700305CE6 /* iOSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSApp.swift; sourceTree = "<group>"; };
|
||||
A53D0C992B543F3B00305CE6 /* Ghostty.App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.App.swift; sourceTree = "<group>"; };
|
||||
A54D786B2CA79788001B19B1 /* BaseTerminalController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseTerminalController.swift; sourceTree = "<group>"; };
|
||||
A55685DF29A03A9F004303CE /* AppError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppError.swift; sourceTree = "<group>"; };
|
||||
A55B7BB729B6F53A0055DE60 /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = "<group>"; };
|
||||
A55B7BBB29B6FC330055DE60 /* SurfaceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SurfaceView.swift; sourceTree = "<group>"; };
|
||||
@@ -105,6 +120,7 @@
|
||||
A56D58852ACDDB4100508D2C /* Ghostty.Shell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.Shell.swift; sourceTree = "<group>"; };
|
||||
A56D58882ACDE6CA00508D2C /* ServiceProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceProvider.swift; sourceTree = "<group>"; };
|
||||
A571AB1C2A206FC600248498 /* Ghostty-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = "Ghostty-Info.plist"; sourceTree = "<group>"; };
|
||||
A57D79262C9C8798001D522E /* SecureInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureInput.swift; sourceTree = "<group>"; };
|
||||
A586167B2B7703CC009BDB1D /* fish */ = {isa = PBXFileReference; lastKnownFileType = folder; name = fish; path = "../zig-out/share/fish"; sourceTree = "<group>"; };
|
||||
A59444F629A2ED5200725BBA /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
|
||||
A59630962AEE163600D64628 /* HostingWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HostingWindow.swift; sourceTree = "<group>"; };
|
||||
@@ -122,6 +138,15 @@
|
||||
A5B30531299BEAAA0047F10C /* Ghostty.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Ghostty.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
A5B30538299BEAAB0047F10C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
A5B3053D299BEAAB0047F10C /* Ghostty.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Ghostty.entitlements; sourceTree = "<group>"; };
|
||||
A5CBD0552C9E65A50017A1AE /* DraggableWindowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraggableWindowView.swift; sourceTree = "<group>"; };
|
||||
A5CBD0572C9F30860017A1AE /* Cursor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Cursor.swift; sourceTree = "<group>"; };
|
||||
A5CBD05B2CA0C5C70017A1AE /* QuickTerminal.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = QuickTerminal.xib; sourceTree = "<group>"; };
|
||||
A5CBD05D2CA0C5E70017A1AE /* QuickTerminalController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickTerminalController.swift; sourceTree = "<group>"; };
|
||||
A5CBD05F2CA0C9080017A1AE /* QuickTerminalWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickTerminalWindow.swift; sourceTree = "<group>"; };
|
||||
A5CBD0632CA122E70017A1AE /* QuickTerminalPosition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickTerminalPosition.swift; sourceTree = "<group>"; };
|
||||
A5CBD06A2CA322320017A1AE /* GlobalEventTap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobalEventTap.swift; sourceTree = "<group>"; };
|
||||
A5CC36122C9CD729004D6760 /* SecureInputOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureInputOverlay.swift; sourceTree = "<group>"; };
|
||||
A5CC36142C9CDA03004D6760 /* View+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+Extension.swift"; sourceTree = "<group>"; };
|
||||
A5CDF1902AAF9A5800513312 /* ConfigurationErrors.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ConfigurationErrors.xib; sourceTree = "<group>"; };
|
||||
A5CDF1922AAF9E0800513312 /* ConfigurationErrorsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationErrorsController.swift; sourceTree = "<group>"; };
|
||||
A5CDF1942AAFA19600513312 /* ConfigurationErrorsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationErrorsView.swift; sourceTree = "<group>"; };
|
||||
@@ -188,9 +213,12 @@
|
||||
A53426362A7DC53000EBB7A2 /* Features */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A5CBD0672CA2704E0017A1AE /* Global Keybinds */,
|
||||
A56D58872ACDE6BE00508D2C /* Services */,
|
||||
A59630982AEE1C4400D64628 /* Terminal */,
|
||||
A5CBD05A2CA0C5910017A1AE /* QuickTerminal */,
|
||||
A5E112912AF73E4D00C6E0C2 /* ClipboardConfirmation */,
|
||||
A57D79252C9C8782001D522E /* Secure Input */,
|
||||
A534263E2A7DCC5800EBB7A2 /* Settings */,
|
||||
A51BFC1C2B2FB5AB00E92F16 /* About */,
|
||||
A51BFC292B30F69F00E92F16 /* Update */,
|
||||
@@ -203,14 +231,17 @@
|
||||
children = (
|
||||
A5CEAFFE29C2410700646FDA /* Backport.swift */,
|
||||
A5333E1B2B5A1CE3008AEFF7 /* CrossKit.swift */,
|
||||
A5CBD0572C9F30860017A1AE /* Cursor.swift */,
|
||||
A5D0AF3C2B37804400D21823 /* CodableBridge.swift */,
|
||||
8503D7C62A549C66006CFF3D /* FullScreenHandler.swift */,
|
||||
A59630962AEE163600D64628 /* HostingWindow.swift */,
|
||||
A59FB5D02AE0DEA7009128F3 /* MetalView.swift */,
|
||||
A5CBD0552C9E65A50017A1AE /* DraggableWindowView.swift */,
|
||||
C159E81C2B66A06B00FDFE9C /* OSColor+Extension.swift */,
|
||||
C1F26EA62B738B9900404083 /* NSView+Extension.swift */,
|
||||
AEE8B3442B9AA39600260C5E /* NSPasteboard+Extension.swift */,
|
||||
A5985CD62C320C4500C57AD3 /* String+Extension.swift */,
|
||||
A5CC36142C9CDA03004D6760 /* View+Extension.swift */,
|
||||
C1F26EE72B76CBFC00404083 /* VibrantLayer.h */,
|
||||
C1F26EE82B76CBFC00404083 /* VibrantLayer.m */,
|
||||
A5CEAFDA29B8005900646FDA /* SplitView */,
|
||||
@@ -295,6 +326,15 @@
|
||||
path = Services;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
A57D79252C9C8782001D522E /* Secure Input */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A57D79262C9C8798001D522E /* SecureInput.swift */,
|
||||
A5CC36122C9CD729004D6760 /* SecureInputOverlay.swift */,
|
||||
);
|
||||
path = "Secure Input";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
A59630982AEE1C4400D64628 /* Terminal */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -306,6 +346,7 @@
|
||||
A51B78462AF4B58B00F3EDB9 /* TerminalWindow.swift */,
|
||||
AEF9CE232B6AD07A0017E195 /* TerminalToolbar.swift */,
|
||||
A535B9D9299C569B0017E2E4 /* ErrorView.swift */,
|
||||
A54D786B2CA79788001B19B1 /* BaseTerminalController.swift */,
|
||||
);
|
||||
path = Terminal;
|
||||
sourceTree = "<group>";
|
||||
@@ -346,6 +387,26 @@
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
A5CBD05A2CA0C5910017A1AE /* QuickTerminal */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A5CBD05B2CA0C5C70017A1AE /* QuickTerminal.xib */,
|
||||
A5CBD05D2CA0C5E70017A1AE /* QuickTerminalController.swift */,
|
||||
A5CBD0632CA122E70017A1AE /* QuickTerminalPosition.swift */,
|
||||
A52FFF562CA90481000C6A5B /* QuickTerminalScreen.swift */,
|
||||
A5CBD05F2CA0C9080017A1AE /* QuickTerminalWindow.swift */,
|
||||
);
|
||||
path = QuickTerminal;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
A5CBD0672CA2704E0017A1AE /* Global Keybinds */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A5CBD06A2CA322320017A1AE /* GlobalEventTap.swift */,
|
||||
);
|
||||
path = "Global Keybinds";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
A5CEAFDA29B8005900646FDA /* SplitView */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -422,7 +483,7 @@
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = 1;
|
||||
LastSwiftUpdateCheck = 1520;
|
||||
LastUpgradeCheck = 1420;
|
||||
LastUpgradeCheck = 1600;
|
||||
TargetAttributes = {
|
||||
A5B30530299BEAAA0047F10C = {
|
||||
CreatedOnToolsVersion = 14.2;
|
||||
@@ -471,6 +532,7 @@
|
||||
A5985CE62C33060F00C57AD3 /* man in Resources */,
|
||||
A5A1F8852A489D6800D1E8BC /* terminfo in Resources */,
|
||||
552964E62B34A9B400030505 /* vim in Resources */,
|
||||
A5CBD05C2CA0C5C70017A1AE /* QuickTerminal.xib in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -491,20 +553,28 @@
|
||||
files = (
|
||||
A59630A42AF059BB00D64628 /* Ghostty.SplitNode.swift in Sources */,
|
||||
A514C8D62B54A16400493A16 /* Ghostty.Config.swift in Sources */,
|
||||
A54D786C2CA7978E001B19B1 /* BaseTerminalController.swift in Sources */,
|
||||
A59FB5CF2AE0DB50009128F3 /* InspectorView.swift in Sources */,
|
||||
A5D0AF3D2B37804400D21823 /* CodableBridge.swift in Sources */,
|
||||
A5D0AF3B2B36A1DE00D21823 /* TerminalRestorable.swift in Sources */,
|
||||
C1F26EA72B738B9900404083 /* NSView+Extension.swift in Sources */,
|
||||
A5CBD0642CA122E70017A1AE /* QuickTerminalPosition.swift in Sources */,
|
||||
A596309C2AEE1C9E00D64628 /* TerminalController.swift in Sources */,
|
||||
A5CC36152C9CDA06004D6760 /* View+Extension.swift in Sources */,
|
||||
A56D58892ACDE6CA00508D2C /* ServiceProvider.swift in Sources */,
|
||||
A5CBD0602CA0C90A0017A1AE /* QuickTerminalWindow.swift in Sources */,
|
||||
A5CBD05E2CA0C5EC0017A1AE /* QuickTerminalController.swift in Sources */,
|
||||
A51BFC222B2FB6B400E92F16 /* AboutView.swift in Sources */,
|
||||
A5278A9B2AA05B2600CD3039 /* Ghostty.Input.swift in Sources */,
|
||||
A5CBD0562C9E65B80017A1AE /* DraggableWindowView.swift in Sources */,
|
||||
C1F26EE92B76CBFC00404083 /* VibrantLayer.m in Sources */,
|
||||
A59630972AEE163600D64628 /* HostingWindow.swift in Sources */,
|
||||
A59630A02AEF6AEB00D64628 /* TerminalManager.swift in Sources */,
|
||||
A51BFC2B2B30F6BE00E92F16 /* UpdateDelegate.swift in Sources */,
|
||||
A5CBD06B2CA322430017A1AE /* GlobalEventTap.swift in Sources */,
|
||||
AEE8B3452B9AA39600260C5E /* NSPasteboard+Extension.swift in Sources */,
|
||||
A53426352A7DA53D00EBB7A2 /* AppDelegate.swift in Sources */,
|
||||
A5CBD0582C9F30960017A1AE /* Cursor.swift in Sources */,
|
||||
A5333E222B5A2128008AEFF7 /* SurfaceView_AppKit.swift in Sources */,
|
||||
A5CDF1952AAFA19600513312 /* ConfigurationErrorsView.swift in Sources */,
|
||||
A55B7BBC29B6FC330055DE60 /* SurfaceView.swift in Sources */,
|
||||
@@ -516,10 +586,13 @@
|
||||
A5FEB3002ABB69450068369E /* main.swift in Sources */,
|
||||
A55B7BB829B6F53A0055DE60 /* Package.swift in Sources */,
|
||||
A51B78472AF4B58B00F3EDB9 /* TerminalWindow.swift in Sources */,
|
||||
A57D79272C9C879B001D522E /* SecureInput.swift in Sources */,
|
||||
A5CEAFDC29B8009000646FDA /* SplitView.swift in Sources */,
|
||||
A5CDF1932AAF9E0800513312 /* ConfigurationErrorsController.swift in Sources */,
|
||||
A59FB5D12AE0DEA7009128F3 /* MetalView.swift in Sources */,
|
||||
A55685E029A03A9F004303CE /* AppError.swift in Sources */,
|
||||
A52FFF572CA90484000C6A5B /* QuickTerminalScreen.swift in Sources */,
|
||||
A5CC36132C9CD72D004D6760 /* SecureInputOverlay.swift in Sources */,
|
||||
A535B9DA299C569B0017E2E4 /* ErrorView.swift in Sources */,
|
||||
A51BFC202B2FB64F00E92F16 /* AboutController.swift in Sources */,
|
||||
A5CEAFFF29C2410700646FDA /* Backport.swift in Sources */,
|
||||
@@ -538,6 +611,7 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
A5CBD0592C9F37B10017A1AE /* Backport.swift in Sources */,
|
||||
A53D0C942B53B43700305CE6 /* iOSApp.swift in Sources */,
|
||||
A514C8D72B54A16400493A16 /* Ghostty.Config.swift in Sources */,
|
||||
A5333E232B5A219A008AEFF7 /* SurfaceView.swift in Sources */,
|
||||
@@ -587,6 +661,7 @@
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
@@ -619,6 +694,7 @@
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -678,6 +754,7 @@
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
@@ -738,6 +815,7 @@
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
@@ -770,6 +848,7 @@
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
@@ -809,6 +888,7 @@
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1430"
|
||||
LastUpgradeVersion = "1600"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
||||
@@ -22,6 +22,7 @@ class AppDelegate: NSObject,
|
||||
@IBOutlet private var menuCheckForUpdates: NSMenuItem?
|
||||
@IBOutlet private var menuOpenConfig: NSMenuItem?
|
||||
@IBOutlet private var menuReloadConfig: NSMenuItem?
|
||||
@IBOutlet private var menuSecureInput: NSMenuItem?
|
||||
@IBOutlet private var menuQuit: NSMenuItem?
|
||||
|
||||
@IBOutlet private var menuNewWindow: NSMenuItem?
|
||||
@@ -48,6 +49,7 @@ class AppDelegate: NSObject,
|
||||
@IBOutlet private var menuIncreaseFontSize: NSMenuItem?
|
||||
@IBOutlet private var menuDecreaseFontSize: NSMenuItem?
|
||||
@IBOutlet private var menuResetFontSize: NSMenuItem?
|
||||
@IBOutlet private var menuQuickTerminal: NSMenuItem?
|
||||
@IBOutlet private var menuTerminalInspector: NSMenuItem?
|
||||
|
||||
@IBOutlet private var menuEqualizeSplits: NSMenuItem?
|
||||
@@ -62,16 +64,28 @@ class AppDelegate: NSObject,
|
||||
/// This is only true before application has become active.
|
||||
private var applicationHasBecomeActive: Bool = false
|
||||
|
||||
/// This is set in applicationDidFinishLaunching with the system uptime so we can determine the
|
||||
/// seconds since the process was launched.
|
||||
private var applicationLaunchTime: TimeInterval = 0
|
||||
|
||||
/// The ghostty global state. Only one per process.
|
||||
let ghostty: Ghostty.App = Ghostty.App()
|
||||
|
||||
/// Manages our terminal windows.
|
||||
let terminalManager: TerminalManager
|
||||
|
||||
/// Our quick terminal. This starts out uninitialized and only initializes if used.
|
||||
private var quickController: QuickTerminalController? = nil
|
||||
|
||||
/// Manages updates
|
||||
let updaterController: SPUStandardUpdaterController
|
||||
let updaterDelegate: UpdaterDelegate = UpdaterDelegate()
|
||||
|
||||
/// The elapsed time since the process was started
|
||||
var timeSinceLaunch: TimeInterval {
|
||||
return ProcessInfo.processInfo.systemUptime - applicationLaunchTime
|
||||
}
|
||||
|
||||
override init() {
|
||||
terminalManager = TerminalManager(ghostty)
|
||||
updaterController = SPUStandardUpdaterController(
|
||||
@@ -105,6 +119,14 @@ class AppDelegate: NSObject,
|
||||
"ApplePressAndHoldEnabled": false,
|
||||
])
|
||||
|
||||
// Store our start time
|
||||
applicationLaunchTime = ProcessInfo.processInfo.systemUptime
|
||||
|
||||
// Check if secure input was enabled when we last quit.
|
||||
if (UserDefaults.standard.bool(forKey: "SecureInput") != SecureInput.shared.enabled) {
|
||||
toggleSecureInput(self)
|
||||
}
|
||||
|
||||
// Hook up updater menu
|
||||
menuCheckForUpdates?.target = updaterController
|
||||
menuCheckForUpdates?.action = #selector(SPUStandardUpdaterController.checkForUpdates(_:))
|
||||
@@ -292,8 +314,11 @@ class AppDelegate: NSObject,
|
||||
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: "inspector:toggle", menuItem: self.menuTerminalInspector)
|
||||
|
||||
syncMenuShortcut(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.
|
||||
@@ -410,6 +435,25 @@ class AppDelegate: NSObject,
|
||||
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.
|
||||
@@ -445,6 +489,24 @@ class AppDelegate: NSObject,
|
||||
dockMenu.addItem(newTab)
|
||||
}
|
||||
|
||||
//MARK: - Global State
|
||||
|
||||
func setSecureInput(_ mode: Ghostty.SetSecureInput) {
|
||||
let input = SecureInput.shared
|
||||
switch (mode) {
|
||||
case .on:
|
||||
input.global = true
|
||||
|
||||
case .off:
|
||||
input.global = false
|
||||
|
||||
case .toggle:
|
||||
input.global.toggle()
|
||||
}
|
||||
self.menuSecureInput?.state = if (input.global) { .on } else { .off }
|
||||
UserDefaults.standard.set(input.global, forKey: "SecureInput")
|
||||
}
|
||||
|
||||
//MARK: - IB Actions
|
||||
|
||||
@IBAction func openConfig(_ sender: Any?) {
|
||||
@@ -484,4 +546,22 @@ class AppDelegate: NSObject,
|
||||
guard let url = URL(string: "https://github.com/ghostty-org/ghostty") else { return }
|
||||
NSWorkspace.shared.open(url)
|
||||
}
|
||||
|
||||
@IBAction func toggleSecureInput(_ sender: Any) {
|
||||
setSecureInput(.toggle)
|
||||
}
|
||||
|
||||
@IBAction func toggleQuickTerminal(_ sender: Any) {
|
||||
if quickController == nil {
|
||||
quickController = QuickTerminalController(
|
||||
ghostty,
|
||||
position: ghostty.config.quickTerminalPosition
|
||||
)
|
||||
}
|
||||
|
||||
guard let quickController = self.quickController else { return }
|
||||
quickController.toggle()
|
||||
|
||||
self.menuQuickTerminal?.state = if (quickController.visible) { .on } else { .off }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="22505" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="23094" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||
<dependencies>
|
||||
<deployment identifier="macosx"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="22505"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="23094"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<customObject id="-2" userLabel="File's Owner" customClass="NSApplication">
|
||||
@@ -32,17 +32,19 @@
|
||||
<outlet property="menuOpenConfig" destination="BOF-NM-1cW" id="Nze-Go-glw"/>
|
||||
<outlet property="menuPaste" destination="i27-pK-umN" id="ICc-X2-gV3"/>
|
||||
<outlet property="menuPreviousSplit" destination="Lic-px-1wg" id="Rto-CG-yRe"/>
|
||||
<outlet property="menuQuickTerminal" destination="kvF-d2-JsP" id="a0u-tf-IEc"/>
|
||||
<outlet property="menuQuit" destination="4sb-4s-VLi" id="qYN-S1-6UW"/>
|
||||
<outlet property="menuReloadConfig" destination="KKH-XX-5py" id="Wvp-7J-wqX"/>
|
||||
<outlet property="menuResetFontSize" destination="Jah-MY-aLX" id="ger-qM-wrm"/>
|
||||
<outlet property="menuSecureInput" destination="oC6-w4-qI7" id="PCc-pe-Mda"/>
|
||||
<outlet property="menuSelectAll" destination="q2h-lq-e4r" id="s98-r1-Jcv"/>
|
||||
<outlet property="menuSelectSplitAbove" destination="0yU-hC-8xF" id="aPc-lS-own"/>
|
||||
<outlet property="menuSelectSplitBelow" destination="QDz-d9-CBr" id="FsH-Dq-jij"/>
|
||||
<outlet property="menuSelectSplitLeft" destination="cTK-oy-KuV" id="Jpr-5q-dqz"/>
|
||||
<outlet property="menuSelectSplitRight" destination="upj-mc-L7X" id="nLY-o1-lky"/>
|
||||
<outlet property="menuServices" destination="aQe-vS-j8Q" id="uWQ-Wo-T1L"/>
|
||||
<outlet property="menuSplitRight" destination="VUR-Ld-nLx" id="RxO-Zw-ovb"/>
|
||||
<outlet property="menuSplitDown" destination="UDZ-4y-6xL" id="fgZ-Wb-8OR"/>
|
||||
<outlet property="menuSplitRight" destination="VUR-Ld-nLx" id="RxO-Zw-ovb"/>
|
||||
<outlet property="menuTerminalInspector" destination="QwP-M5-fvh" id="wJi-Dh-S9f"/>
|
||||
<outlet property="menuToggleFullScreen" destination="8kY-Pi-KaY" id="yQg-6V-OO6"/>
|
||||
<outlet property="menuZoomSplit" destination="oPd-mn-IEH" id="wTu-jK-egI"/>
|
||||
@@ -76,6 +78,12 @@
|
||||
<action selector="reloadConfig:" target="bbz-4X-AYv" id="h5x-tu-Izk"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Secure Keyboard Entry" id="oC6-w4-qI7">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleSecureInput:" target="bbz-4X-AYv" id="vWx-z8-5Sy"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="4je-JR-u6R"/>
|
||||
<menuItem title="Services" id="rJe-5J-bwL">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
@@ -209,6 +217,13 @@
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="L3L-I8-sqk"/>
|
||||
<menuItem title="Quick Terminal" id="kvF-d2-JsP">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleQuickTerminal:" target="bbz-4X-AYv" id="gm3-mk-l8N"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="bC9-n9-RbJ"/>
|
||||
<menuItem title="Terminal Inspector" id="QwP-M5-fvh">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
|
||||
@@ -34,6 +34,9 @@ struct ClipboardConfirmationView: View {
|
||||
/// Optional delegate to get results. If this is nil, then this view will never close on its own.
|
||||
weak var delegate: ClipboardConfirmationViewDelegate? = nil
|
||||
|
||||
/// Used to track if we should rehide on disappear
|
||||
@State private var cursorHiddenCount: UInt = 0
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
HStack {
|
||||
@@ -65,6 +68,25 @@ struct ClipboardConfirmationView: View {
|
||||
}
|
||||
.padding(.bottom)
|
||||
}
|
||||
.onAppear {
|
||||
// I can't find a better way to handle this. There is no API to detect
|
||||
// if the cursor is hidden and OTHER THINGS do unhide the cursor. So we
|
||||
// try to unhide it completely here and hope for the best. Issue #1516.
|
||||
cursorHiddenCount = Cursor.unhideCompletely()
|
||||
|
||||
// If we didn't unhide anything, we just send an unhide to be safe.
|
||||
// I don't think the count can go negative on NSCursor so this handles
|
||||
// scenarios cursor is hidden outside of our own NSCursor usage.
|
||||
if (cursorHiddenCount == 0) {
|
||||
_ = Cursor.unhide()
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
// Rehide if we unhid
|
||||
for _ in 0..<cursorHiddenCount {
|
||||
Cursor.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func onCancel() {
|
||||
|
||||
151
macos/Sources/Features/Global Keybinds/GlobalEventTap.swift
Normal file
151
macos/Sources/Features/Global Keybinds/GlobalEventTap.swift
Normal file
@@ -0,0 +1,151 @@
|
||||
import Cocoa
|
||||
import CoreGraphics
|
||||
import Carbon
|
||||
import OSLog
|
||||
import GhosttyKit
|
||||
|
||||
// Manages the event tap to monitor global events, currently only used for
|
||||
// global keybindings.
|
||||
class GlobalEventTap {
|
||||
static let shared = GlobalEventTap()
|
||||
|
||||
fileprivate static let logger = Logger(
|
||||
subsystem: Bundle.main.bundleIdentifier!,
|
||||
category: String(describing: GlobalEventTap.self)
|
||||
)
|
||||
|
||||
// The event tap used for global event listening. This is non-nil if it is
|
||||
// created.
|
||||
private var eventTap: CFMachPort? = nil
|
||||
|
||||
// This is the timer used to retry enabling the global event tap if we
|
||||
// don't have permissions.
|
||||
private var enableTimer: Timer? = nil
|
||||
|
||||
// Private init so it can't be constructed outside of our singleton
|
||||
private init() {}
|
||||
|
||||
deinit {
|
||||
disable()
|
||||
}
|
||||
|
||||
// Enable the global event tap. This is safe to call if it is already enabled.
|
||||
// If enabling fails due to permissions, this will start a timer to retry since
|
||||
// accessibility permissions take affect immediately.
|
||||
func enable() {
|
||||
if (eventTap != nil) {
|
||||
// Already enabled
|
||||
return
|
||||
}
|
||||
|
||||
// If we are already trying to enable, then stop the timer and restart it.
|
||||
if let enableTimer {
|
||||
enableTimer.invalidate()
|
||||
}
|
||||
|
||||
// Try to enable the event tap immediately. If this succeeds then we're done!
|
||||
if (tryEnable()) {
|
||||
return
|
||||
}
|
||||
|
||||
// Failed, probably due to permissions. The permissions dialog should've
|
||||
// popped up. We retry on a timer since once the permissions are granted
|
||||
// then they take affect immediately.
|
||||
enableTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
|
||||
_ = self.tryEnable()
|
||||
}
|
||||
}
|
||||
|
||||
// Disable the global event tap. This is safe to call if it is already disabled.
|
||||
func disable() {
|
||||
// Stop our enable timer if it is on
|
||||
if let enableTimer {
|
||||
enableTimer.invalidate()
|
||||
self.enableTimer = nil
|
||||
}
|
||||
|
||||
// Stop our event tap
|
||||
if let eventTap {
|
||||
Self.logger.debug("invalidating event tap mach port")
|
||||
CFMachPortInvalidate(eventTap)
|
||||
self.eventTap = nil
|
||||
}
|
||||
}
|
||||
|
||||
// Try to enable the global event type, returns false if it fails.
|
||||
private func tryEnable() -> Bool {
|
||||
// The events we care about
|
||||
let eventMask = [
|
||||
CGEventType.keyDown
|
||||
].reduce(CGEventMask(0), { $0 | (1 << $1.rawValue)})
|
||||
|
||||
// Try to create it
|
||||
guard let eventTap = CGEvent.tapCreate(
|
||||
tap: .cgSessionEventTap,
|
||||
place: .headInsertEventTap,
|
||||
options: .defaultTap,
|
||||
eventsOfInterest: eventMask,
|
||||
callback: cgEventFlagsChangedHandler(proxy:type:cgEvent:userInfo:),
|
||||
userInfo: nil
|
||||
) else {
|
||||
// Return false if creation failed. This is usually because we don't have
|
||||
// Accessibility permissions but can probably be other reasons I don't
|
||||
// know about.
|
||||
Self.logger.debug("creating global event tap failed, missing permissions?")
|
||||
return false
|
||||
}
|
||||
|
||||
// Store our event tap
|
||||
self.eventTap = eventTap
|
||||
|
||||
// If we have an enable timer we always want to disable it
|
||||
if let enableTimer {
|
||||
enableTimer.invalidate()
|
||||
self.enableTimer = nil
|
||||
}
|
||||
|
||||
// Attach our event tap to the main run loop. Note if you don't do this then
|
||||
// the event tap will block every
|
||||
CFRunLoopAddSource(
|
||||
CFRunLoopGetMain(),
|
||||
CFMachPortCreateRunLoopSource(nil, eventTap, 0),
|
||||
.commonModes
|
||||
)
|
||||
|
||||
Self.logger.info("global event tap enabled for global keybinds")
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func cgEventFlagsChangedHandler(
|
||||
proxy: CGEventTapProxy,
|
||||
type: CGEventType,
|
||||
cgEvent: CGEvent,
|
||||
userInfo: UnsafeMutableRawPointer?
|
||||
) -> Unmanaged<CGEvent>? {
|
||||
let result = Unmanaged.passUnretained(cgEvent)
|
||||
|
||||
// We only care about keydown events
|
||||
guard type == .keyDown else { return result }
|
||||
|
||||
// We need an app delegate to get the Ghostty app instance
|
||||
guard let appDelegate = NSApplication.shared.delegate as? AppDelegate else { return result }
|
||||
guard let ghostty = appDelegate.ghostty.app else { return result }
|
||||
|
||||
// We need an NSEvent for our logic below
|
||||
guard let event: NSEvent = .init(cgEvent: cgEvent) else { return result }
|
||||
|
||||
// Build our event input and call ghostty
|
||||
var key_ev = ghostty_input_key_s()
|
||||
key_ev.action = GHOSTTY_ACTION_PRESS
|
||||
key_ev.mods = Ghostty.ghosttyMods(event.modifierFlags)
|
||||
key_ev.keycode = UInt32(event.keyCode)
|
||||
key_ev.text = nil
|
||||
key_ev.composing = false
|
||||
if (ghostty_app_key(ghostty, key_ev)) {
|
||||
GlobalEventTap.logger.info("global key event handled event=\(event)")
|
||||
return nil
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
31
macos/Sources/Features/QuickTerminal/QuickTerminal.xib
Normal file
31
macos/Sources/Features/QuickTerminal/QuickTerminal.xib
Normal file
@@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="23094" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||
<dependencies>
|
||||
<deployment identifier="macosx"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="23094"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<customObject id="-2" userLabel="File's Owner" customClass="QuickTerminalController" customModule="Ghostty" customModuleProvider="target">
|
||||
<connections>
|
||||
<outlet property="window" destination="QvC-M9-y7g" id="JMU-zX-9Ie"/>
|
||||
</connections>
|
||||
</customObject>
|
||||
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
|
||||
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
|
||||
<window title="👻 Ghostty" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" restorable="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" animationBehavior="default" id="QvC-M9-y7g" customClass="QuickTerminalWindow" customModule="Ghostty" customModuleProvider="target">
|
||||
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
|
||||
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
|
||||
<rect key="contentRect" x="196" y="240" width="480" height="270"/>
|
||||
<rect key="screenRect" x="0.0" y="0.0" width="3008" height="1667"/>
|
||||
<view key="contentView" wantsLayer="YES" id="EiT-Mj-1SZ">
|
||||
<rect key="frame" x="0.0" y="0.0" width="480" height="270"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</view>
|
||||
<connections>
|
||||
<outlet property="delegate" destination="-2" id="u5f-FR-jJw"/>
|
||||
</connections>
|
||||
<point key="canvasLocation" x="132" y="-82"/>
|
||||
</window>
|
||||
</objects>
|
||||
</document>
|
||||
@@ -0,0 +1,211 @@
|
||||
import Foundation
|
||||
import Cocoa
|
||||
import SwiftUI
|
||||
import GhosttyKit
|
||||
|
||||
/// Controller for the "quick" terminal.
|
||||
class QuickTerminalController: BaseTerminalController {
|
||||
override var windowNibName: NSNib.Name? { "QuickTerminal" }
|
||||
|
||||
/// The position for the quick terminal.
|
||||
let position: QuickTerminalPosition
|
||||
|
||||
/// The current state of the quick terminal
|
||||
private(set) var visible: Bool = false
|
||||
|
||||
init(_ ghostty: Ghostty.App,
|
||||
position: QuickTerminalPosition = .top,
|
||||
baseConfig base: Ghostty.SurfaceConfiguration? = nil,
|
||||
surfaceTree tree: Ghostty.SplitNode? = nil
|
||||
) {
|
||||
self.position = position
|
||||
super.init(ghostty, baseConfig: base, surfaceTree: tree)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) is not supported for this view")
|
||||
}
|
||||
|
||||
// MARK: NSWindowController
|
||||
|
||||
override func windowDidLoad() {
|
||||
guard let window = self.window else { return }
|
||||
|
||||
// The controller is the window delegate so we can detect events such as
|
||||
// window close so we can animate out.
|
||||
window.delegate = self
|
||||
|
||||
// The quick window is not restorable (yet!). "Yet" because in theory we can
|
||||
// make this restorable, but it isn't currently implemented.
|
||||
window.isRestorable = false
|
||||
|
||||
// Setup our initial size based on our configured position
|
||||
position.setLoaded(window)
|
||||
|
||||
// Setup our content
|
||||
window.contentView = NSHostingView(rootView: TerminalView(
|
||||
ghostty: self.ghostty,
|
||||
viewModel: self,
|
||||
delegate: self
|
||||
))
|
||||
|
||||
// Animate the window in
|
||||
animateIn()
|
||||
}
|
||||
|
||||
// MARK: NSWindowDelegate
|
||||
|
||||
override func windowDidResignKey(_ notification: Notification) {
|
||||
super.windowDidResignKey(notification)
|
||||
|
||||
// We don't animate out if there is a modal sheet being shown currently.
|
||||
// This lets us show alerts without causing the window to disappear.
|
||||
guard window?.attachedSheet == nil else { return }
|
||||
|
||||
animateOut()
|
||||
}
|
||||
|
||||
func windowWillResize(_ sender: NSWindow, to frameSize: NSSize) -> NSSize {
|
||||
// We use the actual screen the window is on for this, since it should
|
||||
// be on the proper screen.
|
||||
guard let screen = window?.screen ?? NSScreen.main else { return frameSize }
|
||||
return position.restrictFrameSize(frameSize, on: screen)
|
||||
}
|
||||
|
||||
// MARK: Base Controller Overrides
|
||||
|
||||
override func surfaceTreeDidChange(from: Ghostty.SplitNode?, to: Ghostty.SplitNode?) {
|
||||
super.surfaceTreeDidChange(from: from, to: to)
|
||||
|
||||
// If our surface tree is nil then we animate the window out.
|
||||
if (to == nil) {
|
||||
animateOut()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Methods
|
||||
|
||||
func toggle() {
|
||||
if (visible) {
|
||||
animateOut()
|
||||
} else {
|
||||
animateIn()
|
||||
}
|
||||
}
|
||||
|
||||
func animateIn() {
|
||||
guard let window = self.window else { return }
|
||||
|
||||
// Set our visibility state
|
||||
guard !visible else { return }
|
||||
visible = true
|
||||
|
||||
// Animate the window in
|
||||
animateWindowIn(window: window, from: position)
|
||||
|
||||
// If our surface tree is nil then we initialize a new terminal. The surface
|
||||
// tree can be nil if for example we run "eixt" in the terminal and force
|
||||
// animate out.
|
||||
if (surfaceTree == nil) {
|
||||
let leaf: Ghostty.SplitNode.Leaf = .init(ghostty.app!, baseConfig: nil)
|
||||
surfaceTree = .leaf(leaf)
|
||||
focusedSurface = leaf.surface
|
||||
|
||||
// We need to grab first responder but it takes a few loop cycles
|
||||
// before the view is attached to the window so we do it async.
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(250)) {
|
||||
// We should probably retry here but I was never able to trigger this.
|
||||
// If this happens though its a crash so let's avoid it.
|
||||
guard let leafWindow = leaf.surface.window,
|
||||
leafWindow == window else { return }
|
||||
window.makeFirstResponder(leaf.surface)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func animateOut() {
|
||||
guard let window = self.window else { return }
|
||||
|
||||
// Set our visibility state
|
||||
guard visible else { return }
|
||||
visible = false
|
||||
|
||||
animateWindowOut(window: window, to: position)
|
||||
}
|
||||
|
||||
private func animateWindowIn(window: NSWindow, from position: QuickTerminalPosition) {
|
||||
guard let screen = ghostty.config.quickTerminalScreen.screen else { return }
|
||||
|
||||
// Move our window off screen to the top
|
||||
position.setInitial(in: window, on: screen)
|
||||
|
||||
// Move it to the visible position since animation requires this
|
||||
window.makeKeyAndOrderFront(nil)
|
||||
|
||||
// Run the animation that moves our window into the proper place and makes
|
||||
// it visible.
|
||||
NSAnimationContext.runAnimationGroup { context in
|
||||
context.duration = 0.2
|
||||
context.timingFunction = .init(name: .easeIn)
|
||||
position.setFinal(in: window.animator(), on: screen)
|
||||
}
|
||||
}
|
||||
|
||||
private func animateWindowOut(window: NSWindow, to position: QuickTerminalPosition) {
|
||||
// We always animate out to whatever screen the window is actually on.
|
||||
guard let screen = window.screen ?? NSScreen.main else { return }
|
||||
|
||||
// Keep track of if we were the key window. If we were the key window then we
|
||||
// want to move focus to the next window so that focus is preserved somewhere
|
||||
// in the app.
|
||||
let wasKey = window.isKeyWindow
|
||||
|
||||
NSAnimationContext.runAnimationGroup({ context in
|
||||
context.duration = 0.2
|
||||
context.timingFunction = .init(name: .easeIn)
|
||||
position.setInitial(in: window.animator(), on: screen)
|
||||
}, completionHandler: {
|
||||
guard wasKey else { return }
|
||||
self.focusNextWindow()
|
||||
})
|
||||
}
|
||||
|
||||
private func focusNextWindow() {
|
||||
// We only want to consider windows that are visible
|
||||
let windows = NSApp.windows.filter { $0.isVisible }
|
||||
|
||||
// If we have no windows there is nothing to focus.
|
||||
guard !windows.isEmpty else { return }
|
||||
|
||||
// Find the current key window (the window that is currently focused)
|
||||
if let keyWindow = NSApp.keyWindow,
|
||||
let currentIndex = windows.firstIndex(of: keyWindow) {
|
||||
// Calculate the index of the next window (cycle through the list)
|
||||
let nextIndex = (currentIndex + 1) % windows.count
|
||||
let nextWindow = windows[nextIndex]
|
||||
|
||||
// Make the next window key and bring it to the front
|
||||
nextWindow.makeKeyAndOrderFront(nil)
|
||||
} else {
|
||||
// If there's no key window, focus the first available window
|
||||
windows.first?.makeKeyAndOrderFront(nil)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: First Responder
|
||||
|
||||
@IBAction override func closeWindow(_ sender: Any) {
|
||||
// Instead of closing the window, we animate it out.
|
||||
animateOut()
|
||||
}
|
||||
|
||||
@IBAction func newTab(_ sender: Any?) {
|
||||
guard let window else { return }
|
||||
let alert = NSAlert()
|
||||
alert.messageText = "Cannot Create New Tab"
|
||||
alert.informativeText = "Tabs aren't supported in the Quick Terminal."
|
||||
alert.addButton(withTitle: "OK")
|
||||
alert.alertStyle = .warning
|
||||
alert.beginSheetModal(for: window)
|
||||
}
|
||||
}
|
||||
102
macos/Sources/Features/QuickTerminal/QuickTerminalPosition.swift
Normal file
102
macos/Sources/Features/QuickTerminal/QuickTerminalPosition.swift
Normal file
@@ -0,0 +1,102 @@
|
||||
import Cocoa
|
||||
|
||||
enum QuickTerminalPosition : String {
|
||||
case top
|
||||
case bottom
|
||||
case left
|
||||
case right
|
||||
|
||||
/// Set the loaded state for a window.
|
||||
func setLoaded(_ window: NSWindow) {
|
||||
guard let screen = window.screen ?? NSScreen.main else { return }
|
||||
switch (self) {
|
||||
case .top, .bottom:
|
||||
window.setFrame(.init(
|
||||
origin: window.frame.origin,
|
||||
size: .init(
|
||||
width: screen.frame.width,
|
||||
height: screen.frame.height / 4)
|
||||
), display: false)
|
||||
|
||||
case .left, .right:
|
||||
window.setFrame(.init(
|
||||
origin: window.frame.origin,
|
||||
size: .init(
|
||||
width: screen.frame.width / 4,
|
||||
height: screen.frame.height)
|
||||
), display: false)
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the initial state for a window for animating out of this position.
|
||||
func setInitial(in window: NSWindow, on screen: NSScreen) {
|
||||
// We always start invisible
|
||||
window.alphaValue = 0
|
||||
|
||||
// Position depends
|
||||
window.setFrame(.init(
|
||||
origin: initialOrigin(for: window, on: screen),
|
||||
size: restrictFrameSize(window.frame.size, on: screen)
|
||||
), display: false)
|
||||
}
|
||||
|
||||
/// Set the final state for a window in this position.
|
||||
func setFinal(in window: NSWindow, on screen: NSScreen) {
|
||||
// We always end visible
|
||||
window.alphaValue = 1
|
||||
|
||||
// Position depends
|
||||
window.setFrame(.init(
|
||||
origin: finalOrigin(for: window, on: screen),
|
||||
size: restrictFrameSize(window.frame.size, on: screen)
|
||||
), display: true)
|
||||
}
|
||||
|
||||
/// Restrict the frame size during resizing.
|
||||
func restrictFrameSize(_ size: NSSize, on screen: NSScreen) -> NSSize {
|
||||
var finalSize = size
|
||||
switch (self) {
|
||||
case .top, .bottom:
|
||||
finalSize.width = screen.frame.width
|
||||
|
||||
case .left, .right:
|
||||
finalSize.height = screen.frame.height
|
||||
}
|
||||
|
||||
return finalSize
|
||||
}
|
||||
|
||||
/// The initial point origin for this position.
|
||||
func initialOrigin(for window: NSWindow, on screen: NSScreen) -> CGPoint {
|
||||
switch (self) {
|
||||
case .top:
|
||||
return .init(x: screen.frame.minX, y: screen.frame.maxY)
|
||||
|
||||
case .bottom:
|
||||
return .init(x: screen.frame.minX, y: -window.frame.height)
|
||||
|
||||
case .left:
|
||||
return .init(x: -window.frame.width, y: 0)
|
||||
|
||||
case .right:
|
||||
return .init(x: screen.frame.maxX, y: 0)
|
||||
}
|
||||
}
|
||||
|
||||
/// The final point origin for this position.
|
||||
func finalOrigin(for window: NSWindow, on screen: NSScreen) -> CGPoint {
|
||||
switch (self) {
|
||||
case .top:
|
||||
return .init(x: screen.frame.minX, y: screen.visibleFrame.maxY - window.frame.height)
|
||||
|
||||
case .bottom:
|
||||
return .init(x: screen.frame.minX, y: screen.frame.minY)
|
||||
|
||||
case .left:
|
||||
return .init(x: screen.frame.minX, y: window.frame.origin.y)
|
||||
|
||||
case .right:
|
||||
return .init(x: screen.visibleFrame.maxX - window.frame.width, y: window.frame.origin.y)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import Cocoa
|
||||
|
||||
enum QuickTerminalScreen {
|
||||
case main
|
||||
case mouse
|
||||
case menuBar
|
||||
|
||||
init?(fromGhosttyConfig string: String) {
|
||||
switch (string) {
|
||||
case "main":
|
||||
self = .main
|
||||
|
||||
case "mouse":
|
||||
self = .mouse
|
||||
|
||||
case "macos-menu-bar":
|
||||
self = .menuBar
|
||||
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var screen: NSScreen? {
|
||||
switch (self) {
|
||||
case .main:
|
||||
return NSScreen.main
|
||||
|
||||
case .mouse:
|
||||
let mouseLoc = NSEvent.mouseLocation
|
||||
return NSScreen.screens.first(where: { $0.frame.contains(mouseLoc) })
|
||||
|
||||
case .menuBar:
|
||||
return NSScreen.screens.first
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import Cocoa
|
||||
|
||||
class QuickTerminalWindow: NSWindow {
|
||||
// Both of these must be true for windows without decorations to be able to
|
||||
// still become key/main and receive events.
|
||||
override var canBecomeKey: Bool { return true }
|
||||
override var canBecomeMain: Bool { return true }
|
||||
|
||||
override func awakeFromNib() {
|
||||
super.awakeFromNib()
|
||||
|
||||
// Note: almost all of this stuff can be done in the nib/xib directly
|
||||
// but I prefer to do it programmatically because the properties we
|
||||
// care about are less hidden.
|
||||
|
||||
// Remove the title completely. This will make the window square. One
|
||||
// downside is it also hides the cursor indications of resize but the
|
||||
// window remains resizable.
|
||||
self.styleMask.remove(.titled)
|
||||
|
||||
// We need to set our window level to a high value. In testing, only
|
||||
// popUpMenu and above do what we want. This gets it above the menu bar
|
||||
// and lets us render off screen.
|
||||
self.level = .popUpMenu
|
||||
|
||||
// This plus the level above was what was needed for the animation to work,
|
||||
// because it gets the window off screen properly. Plus we add some fields
|
||||
// we just want the behavior of.
|
||||
self.collectionBehavior = [
|
||||
// We want this to be part of every space because it is a singleton.
|
||||
.canJoinAllSpaces,
|
||||
|
||||
// We don't want to be part of command-tilde
|
||||
.ignoresCycle,
|
||||
|
||||
// We never support fullscreen
|
||||
.fullScreenNone]
|
||||
}
|
||||
}
|
||||
135
macos/Sources/Features/Secure Input/SecureInput.swift
Normal file
135
macos/Sources/Features/Secure Input/SecureInput.swift
Normal file
@@ -0,0 +1,135 @@
|
||||
import Carbon
|
||||
import Cocoa
|
||||
import OSLog
|
||||
|
||||
// Manages the secure keyboard input state. Secure keyboard input is an old Carbon
|
||||
// API still in use by applications such as Webkit. From the old Carbon docs:
|
||||
// "When secure event input mode is enabled, keyboard input goes only to the
|
||||
// application with keyboard focus and is not echoed to other applications that
|
||||
// might be using the event monitor target to watch keyboard input."
|
||||
//
|
||||
// Secure input is global and stateful so you need a singleton class to manage
|
||||
// it. You have to yield secure input on application deactivation (because
|
||||
// it'll affect other apps) and reacquire on reactivation, and every enable
|
||||
// needs to be balanced with a disable.
|
||||
class SecureInput : ObservableObject {
|
||||
static let shared = SecureInput()
|
||||
|
||||
private static let logger = Logger(
|
||||
subsystem: Bundle.main.bundleIdentifier!,
|
||||
category: String(describing: SecureInput.self)
|
||||
)
|
||||
|
||||
// True if you want to enable secure input globally.
|
||||
var global: Bool = false {
|
||||
didSet {
|
||||
apply()
|
||||
}
|
||||
}
|
||||
|
||||
// The scoped objects and whether they're currently in focus.
|
||||
private var scoped: [ObjectIdentifier: Bool] = [:]
|
||||
|
||||
// This is set to true when we've successfully called EnableSecureInput.
|
||||
@Published private(set) var enabled: Bool = false
|
||||
|
||||
// This is true if we want to enable secure input. We want to enable
|
||||
// secure input if its enabled globally or any of the scoped objects are
|
||||
// in focus.
|
||||
private var desired: Bool {
|
||||
global || scoped.contains(where: { $0.value })
|
||||
}
|
||||
|
||||
private init() {
|
||||
// Add notifications for application active/resign so we can disable
|
||||
// secure input. This is only useful for global enabling of secure
|
||||
// input.
|
||||
let center = NotificationCenter.default
|
||||
center.addObserver(
|
||||
self,
|
||||
selector: #selector(onDidResignActive(notification:)),
|
||||
name: NSApplication.didResignActiveNotification,
|
||||
object: nil)
|
||||
center.addObserver(
|
||||
self,
|
||||
selector: #selector(onDidBecomeActive(notification:)),
|
||||
name: NSApplication.didBecomeActiveNotification,
|
||||
object: nil)
|
||||
}
|
||||
|
||||
deinit {
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
|
||||
// Reset our state so that we can ensure we set the proper secure input
|
||||
// system state
|
||||
scoped.removeAll()
|
||||
global = false
|
||||
apply()
|
||||
}
|
||||
|
||||
// Add a scoped object that has secure input enabled. The focused value will
|
||||
// determine if the object currently has focus. This is used so that secure
|
||||
// input is only enabled while the object is focused.
|
||||
func setScoped(_ object: ObjectIdentifier, focused: Bool) {
|
||||
scoped[object] = focused
|
||||
apply()
|
||||
}
|
||||
|
||||
// Remove a scoped object completely.
|
||||
func removeScoped(_ object: ObjectIdentifier) {
|
||||
scoped[object] = nil
|
||||
apply()
|
||||
}
|
||||
|
||||
private func apply() {
|
||||
// If we aren't active then we don't do anything. The become/resign
|
||||
// active notifications will handle applying for us.
|
||||
guard NSApp.isActive else { return }
|
||||
|
||||
// We only need to apply if we're not in our desired state
|
||||
guard enabled != desired else { return }
|
||||
|
||||
let err: OSStatus
|
||||
if (enabled) {
|
||||
err = DisableSecureEventInput()
|
||||
} else {
|
||||
err = EnableSecureEventInput()
|
||||
}
|
||||
if (err == noErr) {
|
||||
enabled = desired
|
||||
Self.logger.debug("secure input state=\(self.enabled)")
|
||||
return
|
||||
}
|
||||
|
||||
Self.logger.warning("secure input apply failed err=\(err)")
|
||||
}
|
||||
|
||||
// MARK: Notifications
|
||||
|
||||
@objc private func onDidBecomeActive(notification: NSNotification) {
|
||||
// We only want to re-enable if we're not already enabled and we
|
||||
// desire to be enabled.
|
||||
guard !enabled && desired else { return }
|
||||
let err = EnableSecureEventInput()
|
||||
if (err == noErr) {
|
||||
enabled = true
|
||||
Self.logger.debug("secure input enabled on activation")
|
||||
return
|
||||
}
|
||||
|
||||
Self.logger.warning("secure input apply failed err=\(err)")
|
||||
}
|
||||
|
||||
@objc private func onDidResignActive(notification: NSNotification) {
|
||||
// We only want to disable if we're enabled.
|
||||
guard enabled else { return }
|
||||
let err = DisableSecureEventInput()
|
||||
if (err == noErr) {
|
||||
enabled = false
|
||||
Self.logger.debug("secure input disabled on deactivation")
|
||||
return
|
||||
}
|
||||
|
||||
Self.logger.warning("secure input apply failed err=\(err)")
|
||||
}
|
||||
}
|
||||
68
macos/Sources/Features/Secure Input/SecureInputOverlay.swift
Normal file
68
macos/Sources/Features/Secure Input/SecureInputOverlay.swift
Normal file
@@ -0,0 +1,68 @@
|
||||
import SwiftUI
|
||||
|
||||
struct SecureInputOverlay: View {
|
||||
// Animations
|
||||
@State private var shadowAngle: Angle = .degrees(0)
|
||||
@State private var shadowWidth: CGFloat = 6
|
||||
|
||||
// Popover explainer text
|
||||
@State private var isPopover = false
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
HStack {
|
||||
Spacer()
|
||||
|
||||
Image(systemName: "lock.shield.fill")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 25, height: 25)
|
||||
.foregroundColor(.primary)
|
||||
.padding(5)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.fill(.background)
|
||||
.innerShadow(
|
||||
using: RoundedRectangle(cornerRadius: 12),
|
||||
stroke: AngularGradient(
|
||||
gradient: Gradient(colors: [.cyan, .blue, .yellow, .blue, .cyan]),
|
||||
center: .center,
|
||||
angle: shadowAngle
|
||||
),
|
||||
width: shadowWidth
|
||||
)
|
||||
)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.stroke(Color.gray, lineWidth: 1)
|
||||
)
|
||||
.onTapGesture {
|
||||
isPopover = true
|
||||
}
|
||||
.backport.pointerStyle(.link)
|
||||
.padding(.top, 10)
|
||||
.padding(.trailing, 10)
|
||||
.popover(isPresented: $isPopover, arrowEdge: .bottom) {
|
||||
Text("""
|
||||
Secure Input is active. Secure Input is a macOS security feature that
|
||||
prevents applications from reading keyboard events. This is enabled
|
||||
automatically whenever Ghostty detects a password prompt in the terminal,
|
||||
or at all times if `Ghostty > Secure Keyboard Entry` is active.
|
||||
""")
|
||||
.padding(.all)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.onAppear {
|
||||
withAnimation(Animation.linear(duration: 2).repeatForever(autoreverses: false)) {
|
||||
shadowAngle = .degrees(360)
|
||||
}
|
||||
|
||||
withAnimation(Animation.linear(duration: 2).repeatForever(autoreverses: true)) {
|
||||
shadowWidth = 12
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
363
macos/Sources/Features/Terminal/BaseTerminalController.swift
Normal file
363
macos/Sources/Features/Terminal/BaseTerminalController.swift
Normal file
@@ -0,0 +1,363 @@
|
||||
import Cocoa
|
||||
import SwiftUI
|
||||
import GhosttyKit
|
||||
|
||||
/// A base class for windows that can contain Ghostty windows. This base class implements
|
||||
/// the bare minimum functionality that every terminal window in Ghostty should implement.
|
||||
///
|
||||
/// Usage: Specify this as the base class of your window controller for the window that contains
|
||||
/// a terminal. The window controller must also be the window delegate OR the window delegate
|
||||
/// functions on this base class must be called by your own custom delegate. For the terminal
|
||||
/// view the TerminalView SwiftUI view must be used and this class is the view model and
|
||||
/// delegate.
|
||||
///
|
||||
/// Notably, things this class does NOT implement (not exhaustive):
|
||||
///
|
||||
/// - Tabbing, because there are many ways to get tabbed behavior in macOS and we
|
||||
/// don't want to be opinionated about it.
|
||||
/// - Fullscreen
|
||||
/// - Window restoration or save state
|
||||
/// - Window visual styles (such as titlebar colors)
|
||||
///
|
||||
/// The primary idea of all the behaviors we don't implement here are that subclasses may not
|
||||
/// want these behaviors.
|
||||
class BaseTerminalController: NSWindowController,
|
||||
NSWindowDelegate,
|
||||
TerminalViewDelegate,
|
||||
TerminalViewModel,
|
||||
ClipboardConfirmationViewDelegate
|
||||
{
|
||||
/// The app instance that this terminal view will represent.
|
||||
let ghostty: Ghostty.App
|
||||
|
||||
/// The currently focused surface.
|
||||
var focusedSurface: Ghostty.SurfaceView? = nil {
|
||||
didSet { syncFocusToSurfaceTree() }
|
||||
}
|
||||
|
||||
/// The surface tree for this window.
|
||||
@Published var surfaceTree: Ghostty.SplitNode? = nil {
|
||||
didSet { surfaceTreeDidChange(from: oldValue, to: surfaceTree) }
|
||||
}
|
||||
|
||||
/// Non-nil when an alert is active so we don't overlap multiple.
|
||||
private var alert: NSAlert? = nil
|
||||
|
||||
/// The clipboard confirmation window, if shown.
|
||||
private var clipboardConfirmation: ClipboardConfirmationController? = nil
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) is not supported for this view")
|
||||
}
|
||||
|
||||
init(_ ghostty: Ghostty.App,
|
||||
baseConfig base: Ghostty.SurfaceConfiguration? = nil,
|
||||
surfaceTree tree: Ghostty.SplitNode? = nil
|
||||
) {
|
||||
self.ghostty = ghostty
|
||||
|
||||
super.init(window: nil)
|
||||
|
||||
// Initialize our initial surface.
|
||||
guard let ghostty_app = ghostty.app else { preconditionFailure("app must be loaded") }
|
||||
self.surfaceTree = tree ?? .leaf(.init(ghostty_app, baseConfig: base))
|
||||
|
||||
// Setup our notifications for behaviors
|
||||
let center = NotificationCenter.default
|
||||
center.addObserver(
|
||||
self,
|
||||
selector: #selector(onConfirmClipboardRequest),
|
||||
name: Ghostty.Notification.confirmClipboard,
|
||||
object: nil)
|
||||
}
|
||||
|
||||
/// Called when the surfaceTree variable changed.
|
||||
///
|
||||
/// Subclasses should call super first.
|
||||
func surfaceTreeDidChange(from: Ghostty.SplitNode?, to: Ghostty.SplitNode?) {
|
||||
// If our surface tree becomes nil then ensure all surfaces
|
||||
// in the old tree have closed.
|
||||
if (to == nil) {
|
||||
from?.close()
|
||||
focusedSurface = nil
|
||||
}
|
||||
}
|
||||
|
||||
/// Update all surfaces with the focus state. This ensures that libghostty has an accurate view about
|
||||
/// what surface is focused. This must be called whenever a surface OR window changes focus.
|
||||
func syncFocusToSurfaceTree() {
|
||||
guard let tree = self.surfaceTree else { return }
|
||||
|
||||
for leaf in tree {
|
||||
// Our focus state requires that this window is key and our currently
|
||||
// focused surface is the surface in this leaf.
|
||||
let focused: Bool = (window?.isKeyWindow ?? false) &&
|
||||
focusedSurface != nil &&
|
||||
leaf.surface == focusedSurface!
|
||||
leaf.surface.focusDidChange(focused)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: TerminalViewDelegate
|
||||
|
||||
// Note: this is different from surfaceDidTreeChange(from:,to:) because this is called
|
||||
// when the currently set value changed in place and the from:to: variant is called
|
||||
// when the variable was set.
|
||||
func surfaceTreeDidChange() {}
|
||||
|
||||
func focusedSurfaceDidChange(to: Ghostty.SurfaceView?) {
|
||||
focusedSurface = to
|
||||
}
|
||||
|
||||
func titleDidChange(to: String) {
|
||||
guard let window else { return }
|
||||
|
||||
// Set the main window title
|
||||
window.title = to
|
||||
}
|
||||
|
||||
func cellSizeDidChange(to: NSSize) {
|
||||
guard ghostty.config.windowStepResize else { return }
|
||||
self.window?.contentResizeIncrements = to
|
||||
}
|
||||
|
||||
func zoomStateDidChange(to: Bool) {}
|
||||
|
||||
// MARK: Clipboard Confirmation
|
||||
|
||||
@objc private func onConfirmClipboardRequest(notification: SwiftUI.Notification) {
|
||||
guard let target = notification.object as? Ghostty.SurfaceView else { return }
|
||||
guard target == self.focusedSurface else { return }
|
||||
guard let surface = target.surface else { return }
|
||||
|
||||
// We need a window
|
||||
guard let window = self.window else { return }
|
||||
|
||||
// Check whether we use non-native fullscreen
|
||||
guard let str = notification.userInfo?[Ghostty.Notification.ConfirmClipboardStrKey] as? String else { return }
|
||||
guard let state = notification.userInfo?[Ghostty.Notification.ConfirmClipboardStateKey] as? UnsafeMutableRawPointer? else { return }
|
||||
guard let request = notification.userInfo?[Ghostty.Notification.ConfirmClipboardRequestKey] as? Ghostty.ClipboardRequest else { return }
|
||||
|
||||
// If we already have a clipboard confirmation view up, we ignore this request.
|
||||
// This shouldn't be possible...
|
||||
guard self.clipboardConfirmation == nil else {
|
||||
Ghostty.App.completeClipboardRequest(surface, data: "", state: state, confirmed: true)
|
||||
return
|
||||
}
|
||||
|
||||
// Show our paste confirmation
|
||||
self.clipboardConfirmation = ClipboardConfirmationController(
|
||||
surface: surface,
|
||||
contents: str,
|
||||
request: request,
|
||||
state: state,
|
||||
delegate: self
|
||||
)
|
||||
window.beginSheet(self.clipboardConfirmation!.window!)
|
||||
}
|
||||
|
||||
func clipboardConfirmationComplete(_ action: ClipboardConfirmationView.Action, _ request: Ghostty.ClipboardRequest) {
|
||||
// End our clipboard confirmation no matter what
|
||||
guard let cc = self.clipboardConfirmation else { return }
|
||||
self.clipboardConfirmation = nil
|
||||
|
||||
// Close the sheet
|
||||
if let ccWindow = cc.window {
|
||||
window?.endSheet(ccWindow)
|
||||
}
|
||||
|
||||
switch (request) {
|
||||
case .osc_52_write:
|
||||
guard case .confirm = action else { break }
|
||||
let pb = NSPasteboard.general
|
||||
pb.declareTypes([.string], owner: nil)
|
||||
pb.setString(cc.contents, forType: .string)
|
||||
case .osc_52_read, .paste:
|
||||
let str: String
|
||||
switch (action) {
|
||||
case .cancel:
|
||||
str = ""
|
||||
|
||||
case .confirm:
|
||||
str = cc.contents
|
||||
}
|
||||
|
||||
Ghostty.App.completeClipboardRequest(cc.surface, data: str, state: cc.state, confirmed: true)
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: - NSWindowDelegate
|
||||
|
||||
// This is called when performClose is called on a window (NOT when close()
|
||||
// is called directly). performClose is called primarily when UI elements such
|
||||
// as the "red X" are pressed.
|
||||
func windowShouldClose(_ sender: NSWindow) -> Bool {
|
||||
// We must have a window. Is it even possible not to?
|
||||
guard let window = self.window else { return true }
|
||||
|
||||
// If we have no surfaces, close.
|
||||
guard let node = self.surfaceTree else { return true }
|
||||
|
||||
// If we already have an alert, continue with it
|
||||
guard alert == nil else { return false }
|
||||
|
||||
// If our surfaces don't require confirmation, close.
|
||||
if (!node.needsConfirmQuit()) { return true }
|
||||
|
||||
// We require confirmation, so show an alert as long as we aren't already.
|
||||
let alert = NSAlert()
|
||||
alert.messageText = "Close Terminal?"
|
||||
alert.informativeText = "The terminal still has a running process. If you close the " +
|
||||
"terminal the process will be killed."
|
||||
alert.addButton(withTitle: "Close the Terminal")
|
||||
alert.addButton(withTitle: "Cancel")
|
||||
alert.alertStyle = .warning
|
||||
alert.beginSheetModal(for: window, completionHandler: { response in
|
||||
self.alert = nil
|
||||
switch (response) {
|
||||
case .alertFirstButtonReturn:
|
||||
window.close()
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
self.alert = alert
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func windowWillClose(_ notification: Notification) {
|
||||
guard let window else { return }
|
||||
|
||||
// I don't know if this is required anymore. We previously had a ref cycle between
|
||||
// the view and the window so we had to nil this out to break it but I think this
|
||||
// may now be resolved. We should verify that no memory leaks and we can remove this.
|
||||
window.contentView = nil
|
||||
}
|
||||
|
||||
func windowDidBecomeKey(_ notification: Notification) {
|
||||
// Becoming/losing key means we have to notify our surface(s) that we have focus
|
||||
// so things like cursors blink, pty events are sent, etc.
|
||||
self.syncFocusToSurfaceTree()
|
||||
}
|
||||
|
||||
func windowDidResignKey(_ notification: Notification) {
|
||||
// Becoming/losing key means we have to notify our surface(s) that we have focus
|
||||
// so things like cursors blink, pty events are sent, etc.
|
||||
self.syncFocusToSurfaceTree()
|
||||
}
|
||||
|
||||
func windowDidChangeOcclusionState(_ notification: Notification) {
|
||||
guard let surfaceTree = self.surfaceTree else { return }
|
||||
let visible = self.window?.occlusionState.contains(.visible) ?? false
|
||||
for leaf in surfaceTree {
|
||||
if let surface = leaf.surface.surface {
|
||||
ghostty_surface_set_occlusion(surface, visible)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: First Responder
|
||||
|
||||
@IBAction func close(_ sender: Any) {
|
||||
guard let surface = focusedSurface?.surface else { return }
|
||||
ghostty.requestClose(surface: surface)
|
||||
}
|
||||
|
||||
@IBAction func closeWindow(_ sender: Any) {
|
||||
guard let window = window else { return }
|
||||
window.performClose(sender)
|
||||
}
|
||||
|
||||
@IBAction func splitRight(_ sender: Any) {
|
||||
guard let surface = focusedSurface?.surface else { return }
|
||||
ghostty.split(surface: surface, direction: GHOSTTY_SPLIT_DIRECTION_RIGHT)
|
||||
}
|
||||
|
||||
@IBAction func splitDown(_ sender: Any) {
|
||||
guard let surface = focusedSurface?.surface else { return }
|
||||
ghostty.split(surface: surface, direction: GHOSTTY_SPLIT_DIRECTION_DOWN)
|
||||
}
|
||||
|
||||
@IBAction func splitZoom(_ sender: Any) {
|
||||
guard let surface = focusedSurface?.surface else { return }
|
||||
ghostty.splitToggleZoom(surface: surface)
|
||||
}
|
||||
|
||||
|
||||
@IBAction func splitMoveFocusPrevious(_ sender: Any) {
|
||||
splitMoveFocus(direction: .previous)
|
||||
}
|
||||
|
||||
@IBAction func splitMoveFocusNext(_ sender: Any) {
|
||||
splitMoveFocus(direction: .next)
|
||||
}
|
||||
|
||||
@IBAction func splitMoveFocusAbove(_ sender: Any) {
|
||||
splitMoveFocus(direction: .top)
|
||||
}
|
||||
|
||||
@IBAction func splitMoveFocusBelow(_ sender: Any) {
|
||||
splitMoveFocus(direction: .bottom)
|
||||
}
|
||||
|
||||
@IBAction func splitMoveFocusLeft(_ sender: Any) {
|
||||
splitMoveFocus(direction: .left)
|
||||
}
|
||||
|
||||
@IBAction func splitMoveFocusRight(_ sender: Any) {
|
||||
splitMoveFocus(direction: .right)
|
||||
}
|
||||
|
||||
@IBAction func equalizeSplits(_ sender: Any) {
|
||||
guard let surface = focusedSurface?.surface else { return }
|
||||
ghostty.splitEqualize(surface: surface)
|
||||
}
|
||||
|
||||
@IBAction func moveSplitDividerUp(_ sender: Any) {
|
||||
guard let surface = focusedSurface?.surface else { return }
|
||||
ghostty.splitResize(surface: surface, direction: .up, amount: 10)
|
||||
}
|
||||
|
||||
@IBAction func moveSplitDividerDown(_ sender: Any) {
|
||||
guard let surface = focusedSurface?.surface else { return }
|
||||
ghostty.splitResize(surface: surface, direction: .down, amount: 10)
|
||||
}
|
||||
|
||||
@IBAction func moveSplitDividerLeft(_ sender: Any) {
|
||||
guard let surface = focusedSurface?.surface else { return }
|
||||
ghostty.splitResize(surface: surface, direction: .left, amount: 10)
|
||||
}
|
||||
|
||||
@IBAction func moveSplitDividerRight(_ sender: Any) {
|
||||
guard let surface = focusedSurface?.surface else { return }
|
||||
ghostty.splitResize(surface: surface, direction: .right, amount: 10)
|
||||
}
|
||||
|
||||
private func splitMoveFocus(direction: Ghostty.SplitFocusDirection) {
|
||||
guard let surface = focusedSurface?.surface else { return }
|
||||
ghostty.splitMoveFocus(surface: surface, direction: direction)
|
||||
}
|
||||
|
||||
@IBAction func increaseFontSize(_ sender: Any) {
|
||||
guard let surface = focusedSurface?.surface else { return }
|
||||
ghostty.changeFontSize(surface: surface, .increase(1))
|
||||
}
|
||||
|
||||
@IBAction func decreaseFontSize(_ sender: Any) {
|
||||
guard let surface = focusedSurface?.surface else { return }
|
||||
ghostty.changeFontSize(surface: surface, .decrease(1))
|
||||
}
|
||||
|
||||
@IBAction func resetFontSize(_ sender: Any) {
|
||||
guard let surface = focusedSurface?.surface else { return }
|
||||
ghostty.changeFontSize(surface: surface, .reset)
|
||||
}
|
||||
|
||||
@objc func resetTerminal(_ sender: Any) {
|
||||
guard let surface = focusedSurface?.surface else { return }
|
||||
ghostty.resetTerminal(surface: surface)
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="22505" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="23094" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||
<dependencies>
|
||||
<deployment identifier="macosx"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="22505"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="23094"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
|
||||
@@ -3,45 +3,14 @@ import Cocoa
|
||||
import SwiftUI
|
||||
import GhosttyKit
|
||||
|
||||
/// The terminal controller is an NSWindowController that maps 1:1 to a terminal window.
|
||||
class TerminalController: NSWindowController, NSWindowDelegate,
|
||||
TerminalViewDelegate, TerminalViewModel,
|
||||
ClipboardConfirmationViewDelegate
|
||||
/// A classic, tabbed terminal experience.
|
||||
class TerminalController: BaseTerminalController
|
||||
{
|
||||
override var windowNibName: NSNib.Name? { "Terminal" }
|
||||
|
||||
/// The app instance that this terminal view will represent.
|
||||
let ghostty: Ghostty.App
|
||||
|
||||
/// The currently focused surface.
|
||||
var focusedSurface: Ghostty.SurfaceView? = nil {
|
||||
didSet {
|
||||
syncFocusToSurfaceTree()
|
||||
}
|
||||
}
|
||||
|
||||
/// The surface tree for this window.
|
||||
@Published var surfaceTree: Ghostty.SplitNode? = nil {
|
||||
didSet {
|
||||
// If our surface tree becomes nil then ensure all surfaces
|
||||
// in the old tree have closed and then close the window.
|
||||
if (surfaceTree == nil) {
|
||||
oldValue?.close()
|
||||
focusedSurface = nil
|
||||
lastSurfaceDidClose()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Fullscreen state management.
|
||||
let fullscreenHandler = FullScreenHandler()
|
||||
|
||||
/// True when an alert is active so we don't overlap multiple.
|
||||
private var alert: NSAlert? = nil
|
||||
|
||||
/// The clipboard confirmation window, if shown.
|
||||
private var clipboardConfirmation: ClipboardConfirmationController? = nil
|
||||
|
||||
/// This is set to true when we care about frame changes. This is a small optimization since
|
||||
/// this controller registers a listener for ALL frame change notifications and this lets us bail
|
||||
/// early if we don't care.
|
||||
@@ -59,8 +28,6 @@ class TerminalController: NSWindowController, NSWindowDelegate,
|
||||
withBaseConfig base: Ghostty.SurfaceConfiguration? = nil,
|
||||
withSurfaceTree tree: Ghostty.SplitNode? = nil
|
||||
) {
|
||||
self.ghostty = ghostty
|
||||
|
||||
// The window we manage is not restorable if we've specified a command
|
||||
// to execute. We do this because the restored window is meaningless at the
|
||||
// time of writing this: it'd just restore to a shell in the same directory
|
||||
@@ -68,11 +35,7 @@ class TerminalController: NSWindowController, NSWindowDelegate,
|
||||
// restoration.
|
||||
self.restorable = (base?.command ?? "") == ""
|
||||
|
||||
super.init(window: nil)
|
||||
|
||||
// Initialize our initial surface.
|
||||
guard let ghostty_app = ghostty.app else { preconditionFailure("app must be loaded") }
|
||||
self.surfaceTree = tree ?? .leaf(.init(ghostty_app, baseConfig: base))
|
||||
super.init(ghostty, baseConfig: base, surfaceTree: tree)
|
||||
|
||||
// Setup our notifications for behaviors
|
||||
let center = NotificationCenter.default
|
||||
@@ -86,11 +49,6 @@ class TerminalController: NSWindowController, NSWindowDelegate,
|
||||
selector: #selector(onGotoTab),
|
||||
name: Ghostty.Notification.ghosttyGotoTab,
|
||||
object: nil)
|
||||
center.addObserver(
|
||||
self,
|
||||
selector: #selector(onConfirmClipboardRequest),
|
||||
name: Ghostty.Notification.confirmClipboard,
|
||||
object: nil)
|
||||
center.addObserver(
|
||||
self,
|
||||
selector: #selector(onFrameDidChange),
|
||||
@@ -108,6 +66,17 @@ class TerminalController: NSWindowController, NSWindowDelegate,
|
||||
center.removeObserver(self)
|
||||
}
|
||||
|
||||
// MARK: Base Controller Overrides
|
||||
|
||||
override func surfaceTreeDidChange(from: Ghostty.SplitNode?, to: Ghostty.SplitNode?) {
|
||||
super.surfaceTreeDidChange(from: from, to: to)
|
||||
|
||||
// If our surface tree is now nil then we close our window.
|
||||
if (to == nil) {
|
||||
self.window?.close()
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: - Methods
|
||||
|
||||
func configDidReload() {
|
||||
@@ -230,21 +199,6 @@ class TerminalController: NSWindowController, NSWindowDelegate,
|
||||
}
|
||||
}
|
||||
|
||||
/// Update all surfaces with the focus state. This ensures that libghostty has an accurate view about
|
||||
/// what surface is focused. This must be called whenever a surface OR window changes focus.
|
||||
private func syncFocusToSurfaceTree() {
|
||||
guard let tree = self.surfaceTree else { return }
|
||||
|
||||
for leaf in tree {
|
||||
// Our focus state requires that this window is key and our currently
|
||||
// focused surface is the surface in this leaf.
|
||||
let focused: Bool = (window?.isKeyWindow ?? false) &&
|
||||
focusedSurface != nil &&
|
||||
leaf.surface == focusedSurface!
|
||||
leaf.surface.focusDidChange(focused)
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: - NSWindowController
|
||||
|
||||
override func windowWillLoad() {
|
||||
@@ -334,6 +288,34 @@ class TerminalController: NSWindowController, NSWindowDelegate,
|
||||
delegate: self
|
||||
))
|
||||
|
||||
// If our titlebar style is "hidden" we adjust the style appropriately
|
||||
if (ghostty.config.macosTitlebarStyle == "hidden") {
|
||||
window.styleMask = [
|
||||
// We need `titled` in the mask to get the normal window frame
|
||||
.titled,
|
||||
|
||||
// Full size content view so we can extend
|
||||
// content in to the hidden titlebar's area
|
||||
.fullSizeContentView,
|
||||
|
||||
.resizable,
|
||||
.closable,
|
||||
.miniaturizable,
|
||||
]
|
||||
|
||||
// Hide the title
|
||||
window.titleVisibility = .hidden
|
||||
window.titlebarAppearsTransparent = true
|
||||
|
||||
// Hide the traffic lights (window control buttons)
|
||||
window.standardWindowButton(.closeButton)?.isHidden = true
|
||||
window.standardWindowButton(.miniaturizeButton)?.isHidden = true
|
||||
window.standardWindowButton(.zoomButton)?.isHidden = true
|
||||
|
||||
// Disallow tabbing if the titlebar is hidden, since that will (should) also hide the tab bar.
|
||||
window.tabbingMode = .disallowed
|
||||
}
|
||||
|
||||
// In various situations, macOS automatically tabs new windows. Ghostty handles
|
||||
// its own tabbing so we DONT want this behavior. This detects this scenario and undoes
|
||||
// it.
|
||||
@@ -369,84 +351,21 @@ class TerminalController: NSWindowController, NSWindowDelegate,
|
||||
|
||||
//MARK: - NSWindowDelegate
|
||||
|
||||
// This is called when performClose is called on a window (NOT when close()
|
||||
// is called directly). performClose is called primarily when UI elements such
|
||||
// as the "red X" are pressed.
|
||||
func windowShouldClose(_ sender: NSWindow) -> Bool {
|
||||
// We must have a window. Is it even possible not to?
|
||||
guard let window = self.window else { return true }
|
||||
|
||||
// If we have no surfaces, close.
|
||||
guard let node = self.surfaceTree else { return true }
|
||||
|
||||
// If we already have an alert, continue with it
|
||||
guard alert == nil else { return false }
|
||||
|
||||
// If our surfaces don't require confirmation, close.
|
||||
if (!node.needsConfirmQuit()) { return true }
|
||||
|
||||
// We require confirmation, so show an alert as long as we aren't already.
|
||||
let alert = NSAlert()
|
||||
alert.messageText = "Close Terminal?"
|
||||
alert.informativeText = "The terminal still has a running process. If you close the " +
|
||||
"terminal the process will be killed."
|
||||
alert.addButton(withTitle: "Close the Terminal")
|
||||
alert.addButton(withTitle: "Cancel")
|
||||
alert.alertStyle = .warning
|
||||
alert.beginSheetModal(for: window, completionHandler: { response in
|
||||
self.alert = nil
|
||||
switch (response) {
|
||||
case .alertFirstButtonReturn:
|
||||
window.close()
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
self.alert = alert
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func windowWillClose(_ notification: Notification) {
|
||||
// I don't know if this is required anymore. We previously had a ref cycle between
|
||||
// the view and the window so we had to nil this out to break it but I think this
|
||||
// may now be resolved. We should verify that no memory leaks and we can remove this.
|
||||
self.window?.contentView = nil
|
||||
|
||||
override func windowWillClose(_ notification: Notification) {
|
||||
super.windowWillClose(notification)
|
||||
self.relabelTabs()
|
||||
}
|
||||
|
||||
func windowDidBecomeKey(_ notification: Notification) {
|
||||
override func windowDidBecomeKey(_ notification: Notification) {
|
||||
super.windowDidBecomeKey(notification)
|
||||
self.relabelTabs()
|
||||
self.fixTabBar()
|
||||
|
||||
// Becoming/losing key means we have to notify our surface(s) that we have focus
|
||||
// so things like cursors blink, pty events are sent, etc.
|
||||
self.syncFocusToSurfaceTree()
|
||||
}
|
||||
|
||||
func windowDidResignKey(_ notification: Notification) {
|
||||
// Becoming/losing key means we have to notify our surface(s) that we have focus
|
||||
// so things like cursors blink, pty events are sent, etc.
|
||||
self.syncFocusToSurfaceTree()
|
||||
}
|
||||
|
||||
func windowDidMove(_ notification: Notification) {
|
||||
self.fixTabBar()
|
||||
}
|
||||
|
||||
func windowDidChangeOcclusionState(_ notification: Notification) {
|
||||
guard let surfaceTree = self.surfaceTree else { return }
|
||||
let visible = self.window?.occlusionState.contains(.visible) ?? false
|
||||
for leaf in surfaceTree {
|
||||
if let surface = leaf.surface.surface {
|
||||
ghostty_surface_set_occlusion(surface, visible)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Called when the window will be encoded. We handle the data encoding here in the
|
||||
// window controller.
|
||||
func window(_ window: NSWindow, willEncodeRestorableState state: NSCoder) {
|
||||
@@ -454,7 +373,7 @@ class TerminalController: NSWindowController, NSWindowDelegate,
|
||||
data.encode(with: state)
|
||||
}
|
||||
|
||||
//MARK: - First Responder
|
||||
// MARK: First Responder
|
||||
|
||||
@IBAction func newWindow(_ sender: Any?) {
|
||||
guard let surface = focusedSurface?.surface else { return }
|
||||
@@ -466,12 +385,7 @@ class TerminalController: NSWindowController, NSWindowDelegate,
|
||||
ghostty.newTab(surface: surface)
|
||||
}
|
||||
|
||||
@IBAction func close(_ sender: Any) {
|
||||
guard let surface = focusedSurface?.surface else { return }
|
||||
ghostty.requestClose(surface: surface)
|
||||
}
|
||||
|
||||
@IBAction func closeWindow(_ sender: Any) {
|
||||
@IBAction override func closeWindow(_ sender: Any) {
|
||||
guard let window = window else { return }
|
||||
guard let tabGroup = window.tabGroup else {
|
||||
// No tabs, no tab group, just perform a normal close.
|
||||
@@ -521,120 +435,26 @@ class TerminalController: NSWindowController, NSWindowDelegate,
|
||||
})
|
||||
}
|
||||
|
||||
@IBAction func splitRight(_ sender: Any) {
|
||||
guard let surface = focusedSurface?.surface else { return }
|
||||
ghostty.split(surface: surface, direction: GHOSTTY_SPLIT_RIGHT)
|
||||
}
|
||||
|
||||
@IBAction func splitDown(_ sender: Any) {
|
||||
guard let surface = focusedSurface?.surface else { return }
|
||||
ghostty.split(surface: surface, direction: GHOSTTY_SPLIT_DOWN)
|
||||
}
|
||||
|
||||
@IBAction func splitZoom(_ sender: Any) {
|
||||
guard let surface = focusedSurface?.surface else { return }
|
||||
ghostty.splitToggleZoom(surface: surface)
|
||||
}
|
||||
|
||||
@IBAction func splitMoveFocusPrevious(_ sender: Any) {
|
||||
splitMoveFocus(direction: .previous)
|
||||
}
|
||||
|
||||
@IBAction func splitMoveFocusNext(_ sender: Any) {
|
||||
splitMoveFocus(direction: .next)
|
||||
}
|
||||
|
||||
@IBAction func splitMoveFocusAbove(_ sender: Any) {
|
||||
splitMoveFocus(direction: .top)
|
||||
}
|
||||
|
||||
@IBAction func splitMoveFocusBelow(_ sender: Any) {
|
||||
splitMoveFocus(direction: .bottom)
|
||||
}
|
||||
|
||||
@IBAction func splitMoveFocusLeft(_ sender: Any) {
|
||||
splitMoveFocus(direction: .left)
|
||||
}
|
||||
|
||||
@IBAction func splitMoveFocusRight(_ sender: Any) {
|
||||
splitMoveFocus(direction: .right)
|
||||
}
|
||||
|
||||
@IBAction func equalizeSplits(_ sender: Any) {
|
||||
guard let surface = focusedSurface?.surface else { return }
|
||||
ghostty.splitEqualize(surface: surface)
|
||||
}
|
||||
|
||||
@IBAction func moveSplitDividerUp(_ sender: Any) {
|
||||
guard let surface = focusedSurface?.surface else { return }
|
||||
ghostty.splitResize(surface: surface, direction: .up, amount: 10)
|
||||
}
|
||||
|
||||
@IBAction func moveSplitDividerDown(_ sender: Any) {
|
||||
guard let surface = focusedSurface?.surface else { return }
|
||||
ghostty.splitResize(surface: surface, direction: .down, amount: 10)
|
||||
}
|
||||
|
||||
@IBAction func moveSplitDividerLeft(_ sender: Any) {
|
||||
guard let surface = focusedSurface?.surface else { return }
|
||||
ghostty.splitResize(surface: surface, direction: .left, amount: 10)
|
||||
}
|
||||
|
||||
@IBAction func moveSplitDividerRight(_ sender: Any) {
|
||||
guard let surface = focusedSurface?.surface else { return }
|
||||
ghostty.splitResize(surface: surface, direction: .right, amount: 10)
|
||||
}
|
||||
|
||||
private func splitMoveFocus(direction: Ghostty.SplitFocusDirection) {
|
||||
guard let surface = focusedSurface?.surface else { return }
|
||||
ghostty.splitMoveFocus(surface: surface, direction: direction)
|
||||
}
|
||||
|
||||
@IBAction func toggleGhosttyFullScreen(_ sender: Any) {
|
||||
guard let surface = focusedSurface?.surface else { return }
|
||||
ghostty.toggleFullscreen(surface: surface)
|
||||
}
|
||||
|
||||
@IBAction func increaseFontSize(_ sender: Any) {
|
||||
guard let surface = focusedSurface?.surface else { return }
|
||||
ghostty.changeFontSize(surface: surface, .increase(1))
|
||||
}
|
||||
|
||||
@IBAction func decreaseFontSize(_ sender: Any) {
|
||||
guard let surface = focusedSurface?.surface else { return }
|
||||
ghostty.changeFontSize(surface: surface, .decrease(1))
|
||||
}
|
||||
|
||||
@IBAction func resetFontSize(_ sender: Any) {
|
||||
guard let surface = focusedSurface?.surface else { return }
|
||||
ghostty.changeFontSize(surface: surface, .reset)
|
||||
}
|
||||
|
||||
@IBAction func toggleTerminalInspector(_ sender: Any) {
|
||||
guard let surface = focusedSurface?.surface else { return }
|
||||
ghostty.toggleTerminalInspector(surface: surface)
|
||||
}
|
||||
|
||||
@objc func resetTerminal(_ sender: Any) {
|
||||
guard let surface = focusedSurface?.surface else { return }
|
||||
ghostty.resetTerminal(surface: surface)
|
||||
}
|
||||
|
||||
//MARK: - TerminalViewDelegate
|
||||
|
||||
func focusedSurfaceDidChange(to: Ghostty.SurfaceView?) {
|
||||
self.focusedSurface = to
|
||||
}
|
||||
override func titleDidChange(to: String) {
|
||||
super.titleDidChange(to: to)
|
||||
|
||||
func titleDidChange(to: String) {
|
||||
guard let window = window as? TerminalWindow else { return }
|
||||
|
||||
// Set the main window title
|
||||
window.title = to
|
||||
|
||||
// Custom toolbar-based title used when titlebar tabs are enabled.
|
||||
if let toolbar = window.toolbar as? TerminalToolbar {
|
||||
if (window.titlebarTabs) {
|
||||
if (window.titlebarTabs || ghostty.config.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.
|
||||
@@ -644,58 +464,17 @@ class TerminalController: NSWindowController, NSWindowDelegate,
|
||||
}
|
||||
}
|
||||
|
||||
func cellSizeDidChange(to: NSSize) {
|
||||
guard ghostty.config.windowStepResize else { return }
|
||||
self.window?.contentResizeIncrements = to
|
||||
}
|
||||
|
||||
func lastSurfaceDidClose() {
|
||||
self.window?.close()
|
||||
}
|
||||
|
||||
func surfaceTreeDidChange() {
|
||||
override func surfaceTreeDidChange() {
|
||||
// Whenever our surface tree changes in any way (new split, close split, etc.)
|
||||
// we want to invalidate our state.
|
||||
invalidateRestorableState()
|
||||
}
|
||||
|
||||
func zoomStateDidChange(to: Bool) {
|
||||
override func zoomStateDidChange(to: Bool) {
|
||||
guard let window = window as? TerminalWindow else { return }
|
||||
window.surfaceIsZoomed = to
|
||||
}
|
||||
|
||||
//MARK: - Clipboard Confirmation
|
||||
|
||||
func clipboardConfirmationComplete(_ action: ClipboardConfirmationView.Action, _ request: Ghostty.ClipboardRequest) {
|
||||
// End our clipboard confirmation no matter what
|
||||
guard let cc = self.clipboardConfirmation else { return }
|
||||
self.clipboardConfirmation = nil
|
||||
|
||||
// Close the sheet
|
||||
if let ccWindow = cc.window {
|
||||
window?.endSheet(ccWindow)
|
||||
}
|
||||
|
||||
switch (request) {
|
||||
case .osc_52_write:
|
||||
guard case .confirm = action else { break }
|
||||
let pb = NSPasteboard.general
|
||||
pb.declareTypes([.string], owner: nil)
|
||||
pb.setString(cc.contents, forType: .string)
|
||||
case .osc_52_read, .paste:
|
||||
let str: String
|
||||
switch (action) {
|
||||
case .cancel:
|
||||
str = ""
|
||||
|
||||
case .confirm:
|
||||
str = cc.contents
|
||||
}
|
||||
|
||||
Ghostty.App.completeClipboardRequest(cc.surface, data: str, state: cc.state, confirmed: true)
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: - Notifications
|
||||
|
||||
@objc private func onGotoTab(notification: SwiftUI.Notification) {
|
||||
@@ -704,8 +483,9 @@ class TerminalController: NSWindowController, NSWindowDelegate,
|
||||
guard let window = self.window else { return }
|
||||
|
||||
// Get the tab index from the notification
|
||||
guard let tabIndexAny = notification.userInfo?[Ghostty.Notification.GotoTabKey] else { return }
|
||||
guard let tabIndex = tabIndexAny as? Int32 else { return }
|
||||
guard let tabEnumAny = notification.userInfo?[Ghostty.Notification.GotoTabKey] else { return }
|
||||
guard let tabEnum = tabEnumAny as? ghostty_action_goto_tab_e else { return }
|
||||
let tabIndex: Int32 = tabEnum.rawValue
|
||||
|
||||
guard let windowController = window.windowController else { return }
|
||||
guard let tabGroup = windowController.window?.tabGroup else { return }
|
||||
@@ -719,19 +499,19 @@ class TerminalController: NSWindowController, NSWindowDelegate,
|
||||
guard let selectedWindow = tabGroup.selectedWindow else { return }
|
||||
guard let selectedIndex = tabbedWindows.firstIndex(where: { $0 == selectedWindow }) else { return }
|
||||
|
||||
if (tabIndex == GHOSTTY_TAB_PREVIOUS.rawValue) {
|
||||
if (tabIndex == GHOSTTY_GOTO_TAB_PREVIOUS.rawValue) {
|
||||
if (selectedIndex == 0) {
|
||||
finalIndex = tabbedWindows.count - 1
|
||||
} else {
|
||||
finalIndex = selectedIndex - 1
|
||||
}
|
||||
} else if (tabIndex == GHOSTTY_TAB_NEXT.rawValue) {
|
||||
} else if (tabIndex == GHOSTTY_GOTO_TAB_NEXT.rawValue) {
|
||||
if (selectedIndex == tabbedWindows.count - 1) {
|
||||
finalIndex = 0
|
||||
} else {
|
||||
finalIndex = selectedIndex + 1
|
||||
}
|
||||
} else if (tabIndex == GHOSTTY_TAB_LAST.rawValue) {
|
||||
} else if (tabIndex == GHOSTTY_GOTO_TAB_LAST.rawValue) {
|
||||
finalIndex = tabbedWindows.count - 1
|
||||
} else {
|
||||
return
|
||||
@@ -755,44 +535,13 @@ class TerminalController: NSWindowController, NSWindowDelegate,
|
||||
guard let window = self.window else { return }
|
||||
|
||||
// Check whether we use non-native fullscreen
|
||||
guard let useNonNativeFullscreenAny = notification.userInfo?[Ghostty.Notification.NonNativeFullscreenKey] else { return }
|
||||
guard let useNonNativeFullscreen = useNonNativeFullscreenAny as? ghostty_non_native_fullscreen_e else { return }
|
||||
self.fullscreenHandler.toggleFullscreen(window: window, nonNativeFullscreen: useNonNativeFullscreen)
|
||||
guard let fullscreenModeAny = notification.userInfo?[Ghostty.Notification.FullscreenModeKey] else { return }
|
||||
guard let fullscreenMode = fullscreenModeAny as? ghostty_action_fullscreen_e else { return }
|
||||
self.fullscreenHandler.toggleFullscreen(window: window, mode: fullscreenMode)
|
||||
|
||||
// For some reason focus always gets lost when we toggle fullscreen, so we set it back.
|
||||
if let focusedSurface {
|
||||
Ghostty.moveFocus(to: focusedSurface)
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func onConfirmClipboardRequest(notification: SwiftUI.Notification) {
|
||||
guard let target = notification.object as? Ghostty.SurfaceView else { return }
|
||||
guard target == self.focusedSurface else { return }
|
||||
guard let surface = target.surface else { return }
|
||||
|
||||
// We need a window
|
||||
guard let window = self.window else { return }
|
||||
|
||||
// Check whether we use non-native fullscreen
|
||||
guard let str = notification.userInfo?[Ghostty.Notification.ConfirmClipboardStrKey] as? String else { return }
|
||||
guard let state = notification.userInfo?[Ghostty.Notification.ConfirmClipboardStateKey] as? UnsafeMutableRawPointer? else { return }
|
||||
guard let request = notification.userInfo?[Ghostty.Notification.ConfirmClipboardRequestKey] as? Ghostty.ClipboardRequest else { return }
|
||||
|
||||
// If we already have a clipboard confirmation view up, we ignore this request.
|
||||
// This shouldn't be possible...
|
||||
guard self.clipboardConfirmation == nil else {
|
||||
Ghostty.App.completeClipboardRequest(surface, data: "", state: state, confirmed: true)
|
||||
return
|
||||
}
|
||||
|
||||
// Show our paste confirmation
|
||||
self.clipboardConfirmation = ClipboardConfirmationController(
|
||||
surface: surface,
|
||||
contents: str,
|
||||
request: request,
|
||||
state: state,
|
||||
delegate: self
|
||||
)
|
||||
window.beginSheet(self.clipboardConfirmation!.window!)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,6 +78,12 @@ class TerminalManager {
|
||||
window.toggleFullScreen(nil)
|
||||
}
|
||||
|
||||
// If our app isn't active, we make it active. All new_window actions
|
||||
// force our app to be active.
|
||||
if !NSApp.isActive {
|
||||
NSApp.activate(ignoringOtherApps: true)
|
||||
}
|
||||
|
||||
// We're dispatching this async because otherwise the lastCascadePoint doesn't
|
||||
// take effect. Our best theory is there is some next-event-loop-tick logic
|
||||
// that Cocoa is doing that we need to be after.
|
||||
@@ -142,19 +148,24 @@ class TerminalManager {
|
||||
// the macOS APIs only work on a visible window.
|
||||
controller.showWindow(self)
|
||||
|
||||
// Add the window to the tab group and show it.
|
||||
switch ghostty.config.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.
|
||||
if let last = parent.tabGroup?.windows.last {
|
||||
last.addTabbedWindow(window, ordered: .above)
|
||||
} else {
|
||||
fallthrough
|
||||
// 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") {
|
||||
// Add the window to the tab group and show it.
|
||||
switch ghostty.config.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.
|
||||
if let last = parent.tabGroup?.windows.last {
|
||||
last.addTabbedWindow(window, ordered: .above)
|
||||
} else {
|
||||
fallthrough
|
||||
}
|
||||
case "current": fallthrough
|
||||
default:
|
||||
parent.addTabbedWindow(window, ordered: .above)
|
||||
|
||||
}
|
||||
case "current": fallthrough
|
||||
default:
|
||||
parent.addTabbedWindow(window, ordered: .above)
|
||||
}
|
||||
|
||||
window.makeKeyAndOrderFront(self)
|
||||
|
||||
@@ -18,17 +18,10 @@ protocol TerminalViewDelegate: AnyObject {
|
||||
/// not called initially.
|
||||
func surfaceTreeDidChange()
|
||||
|
||||
/// This is called when a split is zoomed.
|
||||
func zoomStateDidChange(to: Bool)
|
||||
}
|
||||
|
||||
// Default all the functions so they're optional
|
||||
extension TerminalViewDelegate {
|
||||
func focusedSurfaceDidChange(to: Ghostty.SurfaceView?) {}
|
||||
func titleDidChange(to: String) {}
|
||||
func cellSizeDidChange(to: NSSize) {}
|
||||
func zoomStateDidChange(to: Bool) {}
|
||||
}
|
||||
|
||||
/// The view model is a required implementation for TerminalView callers. This contains
|
||||
/// the main state between the TerminalView caller and SwiftUI. This abstraction is what
|
||||
/// allows AppKit to own most of the data in SwiftUI.
|
||||
@@ -83,7 +76,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)
|
||||
@@ -108,6 +101,8 @@ struct TerminalView<ViewModel: TerminalViewModel>: View {
|
||||
self.delegate?.zoomStateDidChange(to: newValue ?? false)
|
||||
}
|
||||
}
|
||||
// Ignore safe area to extend up in to the titlebar region if we have the "hidden" titlebar style
|
||||
.ignoresSafeArea(.container, edges: ghostty.config.macosTitlebarStyle == "hidden" ? .top : [])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,34 +67,12 @@ extension Ghostty {
|
||||
userdata: Unmanaged.passUnretained(self).toOpaque(),
|
||||
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) },
|
||||
open_config_cb: { userdata in App.openConfig(userdata) },
|
||||
set_title_cb: { userdata, title in App.setTitle(userdata, title: title) },
|
||||
set_mouse_shape_cb: { userdata, shape in App.setMouseShape(userdata, shape: shape) },
|
||||
set_mouse_visibility_cb: { userdata, visible in App.setMouseVisibility(userdata, visible: visible) },
|
||||
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) },
|
||||
new_split_cb: { userdata, direction, surfaceConfig in App.newSplit(userdata, direction: direction, config: surfaceConfig) },
|
||||
new_tab_cb: { userdata, surfaceConfig in App.newTab(userdata, config: surfaceConfig) },
|
||||
new_window_cb: { userdata, surfaceConfig in App.newWindow(userdata, config: surfaceConfig) },
|
||||
control_inspector_cb: { userdata, mode in App.controlInspector(userdata, mode: mode) },
|
||||
close_surface_cb: { userdata, processAlive in App.closeSurface(userdata, processAlive: processAlive) },
|
||||
focus_split_cb: { userdata, direction in App.focusSplit(userdata, direction: direction) },
|
||||
resize_split_cb: { userdata, direction, amount in
|
||||
App.resizeSplit(userdata, direction: direction, amount: amount) },
|
||||
equalize_splits_cb: { userdata in
|
||||
App.equalizeSplits(userdata) },
|
||||
toggle_split_zoom_cb: { userdata in App.toggleSplitZoom(userdata) },
|
||||
goto_tab_cb: { userdata, n in App.gotoTab(userdata, n: n) },
|
||||
toggle_fullscreen_cb: { userdata, nonNativeFullscreen in App.toggleFullscreen(userdata, nonNativeFullscreen: nonNativeFullscreen) },
|
||||
set_initial_window_size_cb: { userdata, width, height in App.setInitialWindowSize(userdata, width: width, height: height) },
|
||||
render_inspector_cb: { userdata in App.renderInspector(userdata) },
|
||||
set_cell_size_cb: { userdata, width, height in App.setCellSize(userdata, width: width, height: height) },
|
||||
show_desktop_notification_cb: { userdata, title, body in
|
||||
App.showUserNotification(userdata, title: title, body: body) },
|
||||
update_renderer_health_cb: { userdata, health in App.updateRendererHealth(userdata, health: health) },
|
||||
mouse_over_link_cb: { userdata, ptr, len in App.mouseOverLink(userdata, uri: ptr, len: len) }
|
||||
close_surface_cb: { userdata, processAlive in App.closeSurface(userdata, processAlive: processAlive) }
|
||||
)
|
||||
|
||||
// Create the ghostty app.
|
||||
@@ -183,7 +161,7 @@ extension Ghostty {
|
||||
}
|
||||
}
|
||||
|
||||
func split(surface: ghostty_surface_t, direction: ghostty_split_direction_e) {
|
||||
func split(surface: ghostty_surface_t, direction: ghostty_action_split_direction_e) {
|
||||
ghostty_surface_split(surface, direction)
|
||||
}
|
||||
|
||||
@@ -252,11 +230,8 @@ extension Ghostty {
|
||||
// MARK: Ghostty Callbacks (iOS)
|
||||
|
||||
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 openConfig(_ userdata: UnsafeMutableRawPointer?) {}
|
||||
static func setTitle(_ userdata: UnsafeMutableRawPointer?, title: UnsafePointer<CChar>?) {}
|
||||
static func setMouseShape(_ userdata: UnsafeMutableRawPointer?, shape: ghostty_mouse_shape_e) {}
|
||||
static func setMouseVisibility(_ userdata: UnsafeMutableRawPointer?, visible: Bool) {}
|
||||
static func readClipboard(
|
||||
_ userdata: UnsafeMutableRawPointer?,
|
||||
location: ghostty_clipboard_e,
|
||||
@@ -277,28 +252,7 @@ extension Ghostty {
|
||||
confirm: Bool
|
||||
) {}
|
||||
|
||||
static func newSplit(
|
||||
_ userdata: UnsafeMutableRawPointer?,
|
||||
direction: ghostty_split_direction_e,
|
||||
config: ghostty_surface_config_s
|
||||
) {}
|
||||
|
||||
static func newTab(_ userdata: UnsafeMutableRawPointer?, config: ghostty_surface_config_s) {}
|
||||
static func newWindow(_ userdata: UnsafeMutableRawPointer?, config: ghostty_surface_config_s) {}
|
||||
static func controlInspector(_ userdata: UnsafeMutableRawPointer?, mode: ghostty_inspector_mode_e) {}
|
||||
static func closeSurface(_ userdata: UnsafeMutableRawPointer?, processAlive: Bool) {}
|
||||
static func focusSplit(_ userdata: UnsafeMutableRawPointer?, direction: ghostty_split_focus_direction_e) {}
|
||||
static func resizeSplit(_ userdata: UnsafeMutableRawPointer?, direction: ghostty_split_resize_direction_e, amount: UInt16) {}
|
||||
static func equalizeSplits(_ userdata: UnsafeMutableRawPointer?) {}
|
||||
static func toggleSplitZoom(_ userdata: UnsafeMutableRawPointer?) {}
|
||||
static func gotoTab(_ userdata: UnsafeMutableRawPointer?, n: Int32) {}
|
||||
static func toggleFullscreen(_ userdata: UnsafeMutableRawPointer?, nonNativeFullscreen: ghostty_non_native_fullscreen_e) {}
|
||||
static func setInitialWindowSize(_ userdata: UnsafeMutableRawPointer?, width: UInt32, height: UInt32) {}
|
||||
static func renderInspector(_ userdata: UnsafeMutableRawPointer?) {}
|
||||
static func setCellSize(_ userdata: UnsafeMutableRawPointer?, width: UInt32, height: UInt32) {}
|
||||
static func showUserNotification(_ userdata: UnsafeMutableRawPointer?, title: UnsafePointer<CChar>?, body: UnsafePointer<CChar>?) {}
|
||||
static func updateRendererHealth(_ userdata: UnsafeMutableRawPointer?, health: ghostty_renderer_health_e) {}
|
||||
static func mouseOverLink(_ userdata: UnsafeMutableRawPointer?, uri: UnsafePointer<CChar>?, len: Int) {}
|
||||
#endif
|
||||
|
||||
#if os(macOS)
|
||||
@@ -314,14 +268,6 @@ extension Ghostty {
|
||||
|
||||
// MARK: Ghostty Callbacks (macOS)
|
||||
|
||||
static func newSplit(_ userdata: UnsafeMutableRawPointer?, direction: ghostty_split_direction_e, config: ghostty_surface_config_s) {
|
||||
let surface = self.surfaceUserdata(from: userdata)
|
||||
NotificationCenter.default.post(name: Notification.ghosttyNewSplit, object: surface, userInfo: [
|
||||
"direction": direction,
|
||||
Notification.NewSurfaceConfigKey: SurfaceConfiguration(from: config),
|
||||
])
|
||||
}
|
||||
|
||||
static func closeSurface(_ userdata: UnsafeMutableRawPointer?, processAlive: Bool) {
|
||||
let surface = self.surfaceUserdata(from: userdata)
|
||||
NotificationCenter.default.post(name: Notification.ghosttyCloseSurface, object: surface, userInfo: [
|
||||
@@ -329,56 +275,6 @@ extension Ghostty {
|
||||
])
|
||||
}
|
||||
|
||||
static func focusSplit(_ userdata: UnsafeMutableRawPointer?, direction: ghostty_split_focus_direction_e) {
|
||||
let surface = self.surfaceUserdata(from: userdata)
|
||||
guard let splitDirection = SplitFocusDirection.from(direction: direction) else { return }
|
||||
NotificationCenter.default.post(
|
||||
name: Notification.ghosttyFocusSplit,
|
||||
object: surface,
|
||||
userInfo: [
|
||||
Notification.SplitDirectionKey: splitDirection,
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
static func resizeSplit(_ userdata: UnsafeMutableRawPointer?, direction: ghostty_split_resize_direction_e, amount: UInt16) {
|
||||
let surface = self.surfaceUserdata(from: userdata)
|
||||
guard let resizeDirection = SplitResizeDirection.from(direction: direction) else { return }
|
||||
NotificationCenter.default.post(
|
||||
name: Notification.didResizeSplit,
|
||||
object: surface,
|
||||
userInfo: [
|
||||
Notification.ResizeSplitDirectionKey: resizeDirection,
|
||||
Notification.ResizeSplitAmountKey: amount,
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
static func equalizeSplits(_ userdata: UnsafeMutableRawPointer?) {
|
||||
let surface = self.surfaceUserdata(from: userdata)
|
||||
NotificationCenter.default.post(name: Notification.didEqualizeSplits, object: surface)
|
||||
}
|
||||
|
||||
static func toggleSplitZoom(_ userdata: UnsafeMutableRawPointer?) {
|
||||
let surface = self.surfaceUserdata(from: userdata)
|
||||
|
||||
NotificationCenter.default.post(
|
||||
name: Notification.didToggleSplitZoom,
|
||||
object: surface
|
||||
)
|
||||
}
|
||||
|
||||
static func gotoTab(_ userdata: UnsafeMutableRawPointer?, n: Int32) {
|
||||
let surface = self.surfaceUserdata(from: userdata)
|
||||
NotificationCenter.default.post(
|
||||
name: Notification.ghosttyGotoTab,
|
||||
object: surface,
|
||||
userInfo: [
|
||||
Notification.GotoTabKey: n,
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
static func readClipboard(_ userdata: UnsafeMutableRawPointer?, location: ghostty_clipboard_e, state: UnsafeMutableRawPointer?) {
|
||||
// If we don't even have a surface, something went terrible wrong so we have
|
||||
// to leak "state".
|
||||
@@ -450,10 +346,6 @@ extension Ghostty {
|
||||
)
|
||||
}
|
||||
|
||||
static func openConfig(_ userdata: UnsafeMutableRawPointer?) {
|
||||
ghostty_config_open();
|
||||
}
|
||||
|
||||
static func reloadConfig(_ userdata: UnsafeMutableRawPointer?) -> ghostty_config_t? {
|
||||
let newConfig = Config()
|
||||
guard newConfig.loaded else {
|
||||
@@ -484,84 +376,661 @@ extension Ghostty {
|
||||
DispatchQueue.main.async { state.appTick() }
|
||||
}
|
||||
|
||||
static func renderInspector(_ userdata: UnsafeMutableRawPointer?) {
|
||||
let surface = self.surfaceUserdata(from: userdata)
|
||||
NotificationCenter.default.post(
|
||||
name: Notification.inspectorNeedsDisplay,
|
||||
object: surface
|
||||
)
|
||||
/// Determine if a given notification should be presented to the user when Ghostty is running in the foreground.
|
||||
func shouldPresentNotification(notification: UNNotification) -> Bool {
|
||||
let userInfo = notification.request.content.userInfo
|
||||
guard let uuidString = userInfo["surface"] as? String,
|
||||
let uuid = UUID(uuidString: uuidString),
|
||||
let surface = delegate?.findSurface(forUUID: uuid),
|
||||
let window = surface.window else { return false }
|
||||
return !window.isKeyWindow || !surface.focused
|
||||
}
|
||||
|
||||
static func setTitle(_ userdata: UnsafeMutableRawPointer?, title: UnsafePointer<CChar>?) {
|
||||
let surfaceView = self.surfaceUserdata(from: userdata)
|
||||
guard let titleStr = String(cString: title!, encoding: .utf8) else { return }
|
||||
DispatchQueue.main.async {
|
||||
surfaceView.title = titleStr
|
||||
}
|
||||
/// Returns the GhosttyState from the given userdata value.
|
||||
static private func appState(fromView view: SurfaceView) -> App? {
|
||||
guard let surface = view.surface else { return nil }
|
||||
guard let app = ghostty_surface_app(surface) else { return nil }
|
||||
guard let app_ud = ghostty_app_userdata(app) else { return nil }
|
||||
return Unmanaged<App>.fromOpaque(app_ud).takeUnretainedValue()
|
||||
}
|
||||
|
||||
static func setMouseShape(_ userdata: UnsafeMutableRawPointer?, shape: ghostty_mouse_shape_e) {
|
||||
let surfaceView = self.surfaceUserdata(from: userdata)
|
||||
surfaceView.setCursorShape(shape)
|
||||
/// Returns the surface view from the userdata.
|
||||
static private func surfaceUserdata(from userdata: UnsafeMutableRawPointer?) -> SurfaceView {
|
||||
return Unmanaged<SurfaceView>.fromOpaque(userdata!).takeUnretainedValue()
|
||||
}
|
||||
|
||||
static func setMouseVisibility(_ userdata: UnsafeMutableRawPointer?, visible: Bool) {
|
||||
let surfaceView = self.surfaceUserdata(from: userdata)
|
||||
surfaceView.setCursorVisibility(visible)
|
||||
static private func surfaceView(from surface: ghostty_surface_t) -> SurfaceView? {
|
||||
guard let surface_ud = ghostty_surface_userdata(surface) else { return nil }
|
||||
return Unmanaged<SurfaceView>.fromOpaque(surface_ud).takeUnretainedValue()
|
||||
}
|
||||
|
||||
static func toggleFullscreen(_ userdata: UnsafeMutableRawPointer?, nonNativeFullscreen: ghostty_non_native_fullscreen_e) {
|
||||
let surface = self.surfaceUserdata(from: userdata)
|
||||
NotificationCenter.default.post(
|
||||
name: Notification.ghosttyToggleFullscreen,
|
||||
object: surface,
|
||||
userInfo: [
|
||||
Notification.NonNativeFullscreenKey: nonNativeFullscreen,
|
||||
]
|
||||
)
|
||||
}
|
||||
// MARK: Actions (macOS)
|
||||
|
||||
static func setInitialWindowSize(_ userdata: UnsafeMutableRawPointer?, width: UInt32, height: UInt32) {
|
||||
// We need a window to set the frame
|
||||
let surfaceView = self.surfaceUserdata(from: userdata)
|
||||
surfaceView.initialSize = NSMakeSize(Double(width), Double(height))
|
||||
}
|
||||
static func action(_ app: ghostty_app_t, target: ghostty_target_s, action: ghostty_action_s) {
|
||||
// Make sure it a target we understand so all our action handlers can assert
|
||||
switch (target.tag) {
|
||||
case GHOSTTY_TARGET_APP, GHOSTTY_TARGET_SURFACE:
|
||||
break
|
||||
|
||||
static func setCellSize(_ userdata: UnsafeMutableRawPointer?, width: UInt32, height: UInt32) {
|
||||
let surfaceView = self.surfaceUserdata(from: userdata)
|
||||
let backingSize = NSSize(width: Double(width), height: Double(height))
|
||||
surfaceView.cellSize = surfaceView.convertFromBacking(backingSize)
|
||||
}
|
||||
|
||||
static func mouseOverLink(_ userdata: UnsafeMutableRawPointer?, uri: UnsafePointer<CChar>?, len: Int) {
|
||||
let surfaceView = self.surfaceUserdata(from: userdata)
|
||||
guard len > 0 else {
|
||||
surfaceView.hoverUrl = nil
|
||||
default:
|
||||
Ghostty.logger.warning("unknown action target=\(target.tag.rawValue)")
|
||||
return
|
||||
}
|
||||
|
||||
let buffer = Data(bytes: uri!, count: len)
|
||||
surfaceView.hoverUrl = String(data: buffer, encoding: .utf8)
|
||||
// Action dispatch
|
||||
switch (action.tag) {
|
||||
case GHOSTTY_ACTION_NEW_WINDOW:
|
||||
newWindow(app, target: target)
|
||||
|
||||
case GHOSTTY_ACTION_NEW_TAB:
|
||||
newTab(app, target: target)
|
||||
|
||||
case GHOSTTY_ACTION_NEW_SPLIT:
|
||||
newSplit(app, target: target, direction: action.action.new_split)
|
||||
|
||||
case GHOSTTY_ACTION_TOGGLE_FULLSCREEN:
|
||||
toggleFullscreen(app, target: target, mode: action.action.toggle_fullscreen)
|
||||
|
||||
case GHOSTTY_ACTION_GOTO_TAB:
|
||||
gotoTab(app, target: target, tab: action.action.goto_tab)
|
||||
|
||||
case GHOSTTY_ACTION_GOTO_SPLIT:
|
||||
gotoSplit(app, target: target, direction: action.action.goto_split)
|
||||
|
||||
case GHOSTTY_ACTION_RESIZE_SPLIT:
|
||||
resizeSplit(app, target: target, resize: action.action.resize_split)
|
||||
|
||||
case GHOSTTY_ACTION_EQUALIZE_SPLITS:
|
||||
equalizeSplits(app, target: target)
|
||||
|
||||
case GHOSTTY_ACTION_TOGGLE_SPLIT_ZOOM:
|
||||
toggleSplitZoom(app, target: target)
|
||||
|
||||
case GHOSTTY_ACTION_INSPECTOR:
|
||||
controlInspector(app, target: target, mode: action.action.inspector)
|
||||
|
||||
case GHOSTTY_ACTION_RENDER_INSPECTOR:
|
||||
renderInspector(app, target: target)
|
||||
|
||||
case GHOSTTY_ACTION_DESKTOP_NOTIFICATION:
|
||||
showDesktopNotification(app, target: target, n: action.action.desktop_notification)
|
||||
|
||||
case GHOSTTY_ACTION_SET_TITLE:
|
||||
setTitle(app, target: target, v: action.action.set_title)
|
||||
|
||||
case GHOSTTY_ACTION_OPEN_CONFIG:
|
||||
ghostty_config_open()
|
||||
|
||||
case GHOSTTY_ACTION_SECURE_INPUT:
|
||||
toggleSecureInput(app, target: target, mode: action.action.secure_input)
|
||||
|
||||
case GHOSTTY_ACTION_MOUSE_SHAPE:
|
||||
setMouseShape(app, target: target, shape: action.action.mouse_shape)
|
||||
|
||||
case GHOSTTY_ACTION_MOUSE_VISIBILITY:
|
||||
setMouseVisibility(app, target: target, v: action.action.mouse_visibility)
|
||||
|
||||
case GHOSTTY_ACTION_MOUSE_OVER_LINK:
|
||||
setMouseOverLink(app, target: target, v: action.action.mouse_over_link)
|
||||
|
||||
case GHOSTTY_ACTION_INITIAL_SIZE:
|
||||
setInitialSize(app, target: target, v: action.action.initial_size)
|
||||
|
||||
case GHOSTTY_ACTION_CELL_SIZE:
|
||||
setCellSize(app, target: target, v: action.action.cell_size)
|
||||
|
||||
case GHOSTTY_ACTION_RENDERER_HEALTH:
|
||||
rendererHealth(app, target: target, v: action.action.renderer_health)
|
||||
|
||||
case GHOSTTY_ACTION_TOGGLE_QUICK_TERMINAL:
|
||||
toggleQuickTerminal(app, target: target)
|
||||
|
||||
case GHOSTTY_ACTION_CLOSE_ALL_WINDOWS:
|
||||
fallthrough
|
||||
case GHOSTTY_ACTION_TOGGLE_TAB_OVERVIEW:
|
||||
fallthrough
|
||||
case GHOSTTY_ACTION_TOGGLE_WINDOW_DECORATIONS:
|
||||
fallthrough
|
||||
case GHOSTTY_ACTION_PRESENT_TERMINAL:
|
||||
fallthrough
|
||||
case GHOSTTY_ACTION_SIZE_LIMIT:
|
||||
fallthrough
|
||||
case GHOSTTY_ACTION_QUIT_TIMER:
|
||||
Ghostty.logger.info("known but unimplemented action action=\(action.tag.rawValue)")
|
||||
|
||||
default:
|
||||
Ghostty.logger.warning("unknown action action=\(action.tag.rawValue)")
|
||||
}
|
||||
}
|
||||
|
||||
static func showUserNotification(_ userdata: UnsafeMutableRawPointer?, title: UnsafePointer<CChar>?, body: UnsafePointer<CChar>?) {
|
||||
let surfaceView = self.surfaceUserdata(from: userdata)
|
||||
guard let title = String(cString: title!, encoding: .utf8) else { return }
|
||||
guard let body = String(cString: body!, encoding: .utf8) else { return }
|
||||
private static func newWindow(_ app: ghostty_app_t, target: ghostty_target_s) {
|
||||
switch (target.tag) {
|
||||
case GHOSTTY_TARGET_APP:
|
||||
NotificationCenter.default.post(
|
||||
name: Notification.ghosttyNewWindow,
|
||||
object: nil,
|
||||
userInfo: [:]
|
||||
)
|
||||
|
||||
let center = UNUserNotificationCenter.current()
|
||||
center.requestAuthorization(options: [.alert, .sound]) { _, error in
|
||||
if let error = error {
|
||||
AppDelegate.logger.error("Error while requesting notification authorization: \(error)")
|
||||
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: Notification.ghosttyNewWindow,
|
||||
object: surfaceView,
|
||||
userInfo: [
|
||||
Notification.NewSurfaceConfigKey: SurfaceConfiguration(from: ghostty_surface_inherited_config(surface)),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
default:
|
||||
assertionFailure()
|
||||
}
|
||||
}
|
||||
|
||||
private static func newTab(_ app: ghostty_app_t, target: ghostty_target_s) {
|
||||
switch (target.tag) {
|
||||
case GHOSTTY_TARGET_APP:
|
||||
NotificationCenter.default.post(
|
||||
name: Notification.ghosttyNewTab,
|
||||
object: nil,
|
||||
userInfo: [:]
|
||||
)
|
||||
|
||||
case GHOSTTY_TARGET_SURFACE:
|
||||
guard let surface = target.target.surface else { return }
|
||||
guard let surfaceView = self.surfaceView(from: surface) else { return }
|
||||
guard let appState = self.appState(fromView: surfaceView) else { return }
|
||||
guard appState.config.windowDecorations else {
|
||||
let alert = NSAlert()
|
||||
alert.messageText = "Tabs are disabled"
|
||||
alert.informativeText = "Enable window decorations to use tabs"
|
||||
alert.addButton(withTitle: "OK")
|
||||
alert.alertStyle = .warning
|
||||
_ = alert.runModal()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
center.getNotificationSettings() { settings in
|
||||
guard settings.authorizationStatus == .authorized else { return }
|
||||
surfaceView.showUserNotification(title: title, body: body)
|
||||
NotificationCenter.default.post(
|
||||
name: Notification.ghosttyNewTab,
|
||||
object: surfaceView,
|
||||
userInfo: [
|
||||
Notification.NewSurfaceConfigKey: SurfaceConfiguration(from: ghostty_surface_inherited_config(surface)),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
default:
|
||||
assertionFailure()
|
||||
}
|
||||
}
|
||||
|
||||
private static func newSplit(
|
||||
_ app: ghostty_app_t,
|
||||
target: ghostty_target_s,
|
||||
direction: ghostty_action_split_direction_e) {
|
||||
switch (target.tag) {
|
||||
case GHOSTTY_TARGET_APP:
|
||||
// New split does nothing with an app target
|
||||
Ghostty.logger.warning("new split 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: Notification.ghosttyNewSplit,
|
||||
object: surfaceView,
|
||||
userInfo: [
|
||||
"direction": direction,
|
||||
Notification.NewSurfaceConfigKey: SurfaceConfiguration(from: ghostty_surface_inherited_config(surface)),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
default:
|
||||
assertionFailure()
|
||||
}
|
||||
}
|
||||
|
||||
private static func toggleFullscreen(
|
||||
_ app: ghostty_app_t,
|
||||
target: ghostty_target_s,
|
||||
mode: ghostty_action_fullscreen_e) {
|
||||
switch (target.tag) {
|
||||
case GHOSTTY_TARGET_APP:
|
||||
Ghostty.logger.warning("toggle fullscreen 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: Notification.ghosttyToggleFullscreen,
|
||||
object: surfaceView,
|
||||
userInfo: [
|
||||
Notification.FullscreenModeKey: mode,
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
default:
|
||||
assertionFailure()
|
||||
}
|
||||
}
|
||||
|
||||
private static func gotoTab(
|
||||
_ app: ghostty_app_t,
|
||||
target: ghostty_target_s,
|
||||
tab: ghostty_action_goto_tab_e) {
|
||||
switch (target.tag) {
|
||||
case GHOSTTY_TARGET_APP:
|
||||
Ghostty.logger.warning("goto tab 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: Notification.ghosttyGotoTab,
|
||||
object: surfaceView,
|
||||
userInfo: [
|
||||
Notification.GotoTabKey: tab,
|
||||
]
|
||||
)
|
||||
|
||||
default:
|
||||
assertionFailure()
|
||||
}
|
||||
}
|
||||
|
||||
private static func gotoSplit(
|
||||
_ app: ghostty_app_t,
|
||||
target: ghostty_target_s,
|
||||
direction: ghostty_action_goto_split_e) {
|
||||
switch (target.tag) {
|
||||
case GHOSTTY_TARGET_APP:
|
||||
Ghostty.logger.warning("goto split 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: Notification.ghosttyFocusSplit,
|
||||
object: surfaceView,
|
||||
userInfo: [
|
||||
Notification.SplitDirectionKey: SplitFocusDirection.from(direction: direction) as Any,
|
||||
]
|
||||
)
|
||||
|
||||
default:
|
||||
assertionFailure()
|
||||
}
|
||||
}
|
||||
|
||||
private static func resizeSplit(
|
||||
_ app: ghostty_app_t,
|
||||
target: ghostty_target_s,
|
||||
resize: ghostty_action_resize_split_s) {
|
||||
switch (target.tag) {
|
||||
case GHOSTTY_TARGET_APP:
|
||||
Ghostty.logger.warning("resize split 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 resizeDirection = SplitResizeDirection.from(direction: resize.direction) else { return }
|
||||
NotificationCenter.default.post(
|
||||
name: Notification.didResizeSplit,
|
||||
object: surfaceView,
|
||||
userInfo: [
|
||||
Notification.ResizeSplitDirectionKey: resizeDirection,
|
||||
Notification.ResizeSplitAmountKey: resize.amount,
|
||||
]
|
||||
)
|
||||
|
||||
default:
|
||||
assertionFailure()
|
||||
}
|
||||
}
|
||||
|
||||
private static func equalizeSplits(
|
||||
_ app: ghostty_app_t,
|
||||
target: ghostty_target_s) {
|
||||
switch (target.tag) {
|
||||
case GHOSTTY_TARGET_APP:
|
||||
Ghostty.logger.warning("equalize splits 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: Notification.didEqualizeSplits,
|
||||
object: surfaceView
|
||||
)
|
||||
|
||||
|
||||
default:
|
||||
assertionFailure()
|
||||
}
|
||||
}
|
||||
|
||||
private static func toggleSplitZoom(
|
||||
_ app: ghostty_app_t,
|
||||
target: ghostty_target_s) {
|
||||
switch (target.tag) {
|
||||
case GHOSTTY_TARGET_APP:
|
||||
Ghostty.logger.warning("toggle split zoom 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: Notification.didToggleSplitZoom,
|
||||
object: surfaceView
|
||||
)
|
||||
|
||||
|
||||
default:
|
||||
assertionFailure()
|
||||
}
|
||||
}
|
||||
|
||||
private static func controlInspector(
|
||||
_ app: ghostty_app_t,
|
||||
target: ghostty_target_s,
|
||||
mode: ghostty_action_inspector_e) {
|
||||
switch (target.tag) {
|
||||
case GHOSTTY_TARGET_APP:
|
||||
Ghostty.logger.warning("toggle split zoom 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: Notification.didControlInspector,
|
||||
object: surfaceView,
|
||||
userInfo: ["mode": mode]
|
||||
)
|
||||
|
||||
|
||||
default:
|
||||
assertionFailure()
|
||||
}
|
||||
}
|
||||
|
||||
private static func showDesktopNotification(
|
||||
_ app: ghostty_app_t,
|
||||
target: ghostty_target_s,
|
||||
n: ghostty_action_desktop_notification_s) {
|
||||
switch (target.tag) {
|
||||
case GHOSTTY_TARGET_APP:
|
||||
Ghostty.logger.warning("toggle split zoom 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 title = String(cString: n.title!, encoding: .utf8) else { return }
|
||||
guard let body = String(cString: n.body!, encoding: .utf8) else { return }
|
||||
|
||||
let center = UNUserNotificationCenter.current()
|
||||
center.requestAuthorization(options: [.alert, .sound]) { _, error in
|
||||
if let error = error {
|
||||
Ghostty.logger.error("Error while requesting notification authorization: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
center.getNotificationSettings() { settings in
|
||||
guard settings.authorizationStatus == .authorized else { return }
|
||||
surfaceView.showUserNotification(title: title, body: body)
|
||||
}
|
||||
|
||||
|
||||
default:
|
||||
assertionFailure()
|
||||
}
|
||||
}
|
||||
|
||||
private static func toggleSecureInput(
|
||||
_ app: ghostty_app_t,
|
||||
target: ghostty_target_s,
|
||||
mode mode_raw: ghostty_action_secure_input_e
|
||||
) {
|
||||
guard let mode = SetSecureInput.from(mode_raw) else { return }
|
||||
|
||||
switch (target.tag) {
|
||||
case GHOSTTY_TARGET_APP:
|
||||
guard let appDelegate = NSApplication.shared.delegate as? AppDelegate else { return }
|
||||
appDelegate.setSecureInput(mode)
|
||||
|
||||
case GHOSTTY_TARGET_SURFACE:
|
||||
guard let surface = target.target.surface else { return }
|
||||
guard let surfaceView = self.surfaceView(from: surface) else { return }
|
||||
guard let appState = self.appState(fromView: surfaceView) else { return }
|
||||
guard appState.config.autoSecureInput else { return }
|
||||
|
||||
switch (mode) {
|
||||
case .on:
|
||||
surfaceView.passwordInput = true
|
||||
|
||||
case .off:
|
||||
surfaceView.passwordInput = false
|
||||
|
||||
case .toggle:
|
||||
surfaceView.passwordInput = !surfaceView.passwordInput
|
||||
}
|
||||
|
||||
default:
|
||||
assertionFailure()
|
||||
}
|
||||
}
|
||||
|
||||
private static func toggleQuickTerminal(
|
||||
_ app: ghostty_app_t,
|
||||
target: ghostty_target_s
|
||||
) {
|
||||
guard let appDelegate = NSApplication.shared.delegate as? AppDelegate else { return }
|
||||
appDelegate.toggleQuickTerminal(self)
|
||||
}
|
||||
|
||||
private static func setTitle(
|
||||
_ app: ghostty_app_t,
|
||||
target: ghostty_target_s,
|
||||
v: ghostty_action_set_title_s) {
|
||||
switch (target.tag) {
|
||||
case GHOSTTY_TARGET_APP:
|
||||
Ghostty.logger.warning("set title 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 title = String(cString: v.title!, encoding: .utf8) else { return }
|
||||
|
||||
// We must set this in a dispatchqueue to avoid a deadlock on startup on some
|
||||
// versions of macOS. I unfortunately didn't document the exact versions so
|
||||
// I don't know when its safe to remove this.
|
||||
DispatchQueue.main.async {
|
||||
surfaceView.title = title
|
||||
}
|
||||
|
||||
|
||||
default:
|
||||
assertionFailure()
|
||||
}
|
||||
}
|
||||
|
||||
private static func setMouseShape(
|
||||
_ app: ghostty_app_t,
|
||||
target: ghostty_target_s,
|
||||
shape: ghostty_action_mouse_shape_e) {
|
||||
switch (target.tag) {
|
||||
case GHOSTTY_TARGET_APP:
|
||||
Ghostty.logger.warning("set mouse shapes 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 }
|
||||
surfaceView.setCursorShape(shape)
|
||||
|
||||
|
||||
default:
|
||||
assertionFailure()
|
||||
}
|
||||
}
|
||||
|
||||
private static func setMouseVisibility(
|
||||
_ app: ghostty_app_t,
|
||||
target: ghostty_target_s,
|
||||
v: ghostty_action_mouse_visibility_e) {
|
||||
switch (target.tag) {
|
||||
case GHOSTTY_TARGET_APP:
|
||||
Ghostty.logger.warning("set mouse shapes 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 }
|
||||
switch (v) {
|
||||
case GHOSTTY_MOUSE_VISIBLE:
|
||||
surfaceView.setCursorVisibility(true)
|
||||
|
||||
case GHOSTTY_MOUSE_HIDDEN:
|
||||
surfaceView.setCursorVisibility(false)
|
||||
|
||||
default:
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
default:
|
||||
assertionFailure()
|
||||
}
|
||||
}
|
||||
|
||||
private static func setMouseOverLink(
|
||||
_ app: ghostty_app_t,
|
||||
target: ghostty_target_s,
|
||||
v: ghostty_action_mouse_over_link_s) {
|
||||
switch (target.tag) {
|
||||
case GHOSTTY_TARGET_APP:
|
||||
Ghostty.logger.warning("mouse over link 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 v.len > 0 else {
|
||||
surfaceView.hoverUrl = nil
|
||||
return
|
||||
}
|
||||
|
||||
let buffer = Data(bytes: v.url!, count: v.len)
|
||||
surfaceView.hoverUrl = String(data: buffer, encoding: .utf8)
|
||||
|
||||
|
||||
default:
|
||||
assertionFailure()
|
||||
}
|
||||
}
|
||||
|
||||
private static func setInitialSize(
|
||||
_ app: ghostty_app_t,
|
||||
target: ghostty_target_s,
|
||||
v: ghostty_action_initial_size_s) {
|
||||
switch (target.tag) {
|
||||
case GHOSTTY_TARGET_APP:
|
||||
Ghostty.logger.warning("mouse over link 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 }
|
||||
surfaceView.initialSize = NSMakeSize(Double(v.width), Double(v.height))
|
||||
|
||||
|
||||
default:
|
||||
assertionFailure()
|
||||
}
|
||||
}
|
||||
|
||||
private static func setCellSize(
|
||||
_ app: ghostty_app_t,
|
||||
target: ghostty_target_s,
|
||||
v: ghostty_action_cell_size_s) {
|
||||
switch (target.tag) {
|
||||
case GHOSTTY_TARGET_APP:
|
||||
Ghostty.logger.warning("mouse over link 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 }
|
||||
let backingSize = NSSize(width: Double(v.width), height: Double(v.height))
|
||||
surfaceView.cellSize = surfaceView.convertFromBacking(backingSize)
|
||||
|
||||
default:
|
||||
assertionFailure()
|
||||
}
|
||||
}
|
||||
|
||||
private static func renderInspector(
|
||||
_ app: ghostty_app_t,
|
||||
target: ghostty_target_s) {
|
||||
switch (target.tag) {
|
||||
case GHOSTTY_TARGET_APP:
|
||||
Ghostty.logger.warning("mouse over link 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: Notification.inspectorNeedsDisplay,
|
||||
object: surfaceView
|
||||
)
|
||||
|
||||
default:
|
||||
assertionFailure()
|
||||
}
|
||||
}
|
||||
|
||||
private static func rendererHealth(
|
||||
_ app: ghostty_app_t,
|
||||
target: ghostty_target_s,
|
||||
v: ghostty_action_renderer_health_e) {
|
||||
switch (target.tag) {
|
||||
case GHOSTTY_TARGET_APP:
|
||||
Ghostty.logger.warning("mouse over link 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: Notification.didUpdateRendererHealth,
|
||||
object: surfaceView,
|
||||
userInfo: [
|
||||
"health": v,
|
||||
]
|
||||
)
|
||||
|
||||
default:
|
||||
assertionFailure()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: User Notifications
|
||||
|
||||
/// Handle a received user notification. This is called when a user notification is clicked or dismissed by the user
|
||||
func handleUserNotification(response: UNNotificationResponse) {
|
||||
let userInfo = response.notification.request.content.userInfo
|
||||
@@ -581,82 +1050,6 @@ extension Ghostty {
|
||||
}
|
||||
}
|
||||
|
||||
/// Determine if a given notification should be presented to the user when Ghostty is running in the foreground.
|
||||
func shouldPresentNotification(notification: UNNotification) -> Bool {
|
||||
let userInfo = notification.request.content.userInfo
|
||||
guard let uuidString = userInfo["surface"] as? String,
|
||||
let uuid = UUID(uuidString: uuidString),
|
||||
let surface = delegate?.findSurface(forUUID: uuid),
|
||||
let window = surface.window else { return false }
|
||||
return !window.isKeyWindow || !surface.focused
|
||||
}
|
||||
|
||||
static func newTab(_ userdata: UnsafeMutableRawPointer?, config: ghostty_surface_config_s) {
|
||||
let surface = self.surfaceUserdata(from: userdata)
|
||||
|
||||
guard let appState = self.appState(fromView: surface) else { return }
|
||||
guard appState.config.windowDecorations else {
|
||||
let alert = NSAlert()
|
||||
alert.messageText = "Tabs are disabled"
|
||||
alert.informativeText = "Enable window decorations to use tabs"
|
||||
alert.addButton(withTitle: "OK")
|
||||
alert.alertStyle = .warning
|
||||
_ = alert.runModal()
|
||||
return
|
||||
}
|
||||
|
||||
NotificationCenter.default.post(
|
||||
name: Notification.ghosttyNewTab,
|
||||
object: surface,
|
||||
userInfo: [
|
||||
Notification.NewSurfaceConfigKey: SurfaceConfiguration(from: config),
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
static func newWindow(_ userdata: UnsafeMutableRawPointer?, config: ghostty_surface_config_s) {
|
||||
let surface = self.surfaceUserdata(from: userdata)
|
||||
|
||||
NotificationCenter.default.post(
|
||||
name: Notification.ghosttyNewWindow,
|
||||
object: surface,
|
||||
userInfo: [
|
||||
Notification.NewSurfaceConfigKey: SurfaceConfiguration(from: config),
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
static func controlInspector(_ userdata: UnsafeMutableRawPointer?, mode: ghostty_inspector_mode_e) {
|
||||
let surface = self.surfaceUserdata(from: userdata)
|
||||
NotificationCenter.default.post(name: Notification.didControlInspector, object: surface, userInfo: [
|
||||
"mode": mode,
|
||||
])
|
||||
}
|
||||
|
||||
static func updateRendererHealth(_ userdata: UnsafeMutableRawPointer?, health: ghostty_renderer_health_e) {
|
||||
let surface = self.surfaceUserdata(from: userdata)
|
||||
NotificationCenter.default.post(
|
||||
name: Notification.didUpdateRendererHealth,
|
||||
object: surface,
|
||||
userInfo: [
|
||||
"health": health,
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns the GhosttyState from the given userdata value.
|
||||
static private func appState(fromView view: SurfaceView) -> App? {
|
||||
guard let surface = view.surface else { return nil }
|
||||
guard let app = ghostty_surface_app(surface) else { return nil }
|
||||
guard let app_ud = ghostty_app_userdata(app) else { return nil }
|
||||
return Unmanaged<App>.fromOpaque(app_ud).takeUnretainedValue()
|
||||
}
|
||||
|
||||
/// Returns the surface view from the userdata.
|
||||
static private func surfaceUserdata(from userdata: UnsafeMutableRawPointer?) -> SurfaceView {
|
||||
return Unmanaged<SurfaceView>.fromOpaque(userdata!).takeUnretainedValue()
|
||||
}
|
||||
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -332,6 +332,28 @@ extension Ghostty {
|
||||
return Color(newColor)
|
||||
}
|
||||
|
||||
#if canImport(AppKit)
|
||||
var quickTerminalPosition: QuickTerminalPosition {
|
||||
guard let config = self.config else { return .top }
|
||||
var v: UnsafePointer<Int8>? = nil
|
||||
let key = "quick-terminal-position"
|
||||
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return .top }
|
||||
guard let ptr = v else { return .top }
|
||||
let str = String(cString: ptr)
|
||||
return QuickTerminalPosition(rawValue: str) ?? .top
|
||||
}
|
||||
|
||||
var quickTerminalScreen: QuickTerminalScreen {
|
||||
guard let config = self.config else { return .main }
|
||||
var v: UnsafePointer<Int8>? = nil
|
||||
let key = "quick-terminal-screen"
|
||||
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return .main }
|
||||
guard let ptr = v else { return .main }
|
||||
let str = String(cString: ptr)
|
||||
return QuickTerminalScreen(fromGhosttyConfig: str) ?? .main
|
||||
}
|
||||
#endif
|
||||
|
||||
var resizeOverlay: ResizeOverlay {
|
||||
guard let config = self.config else { return .after_first }
|
||||
var v: UnsafePointer<Int8>? = nil
|
||||
@@ -371,6 +393,22 @@ extension Ghostty {
|
||||
let str = String(cString: ptr)
|
||||
return AutoUpdate(rawValue: str) ?? defaultValue
|
||||
}
|
||||
|
||||
var autoSecureInput: Bool {
|
||||
guard let config = self.config else { return true }
|
||||
var v = false;
|
||||
let key = "macos-auto-secure-input"
|
||||
_ = ghostty_config_get(config, &v, key, UInt(key.count))
|
||||
return v
|
||||
}
|
||||
|
||||
var secureInputIndication: Bool {
|
||||
guard let config = self.config else { return true }
|
||||
var v = false;
|
||||
let key = "macos-secure-input-indication"
|
||||
_ = ghostty_config_get(config, &v, key, UInt(key.count))
|
||||
return v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -219,13 +219,13 @@ extension Ghostty {
|
||||
|
||||
// Determine our desired direction
|
||||
guard let directionAny = notification.userInfo?["direction"] else { return }
|
||||
guard let direction = directionAny as? ghostty_split_direction_e else { return }
|
||||
guard let direction = directionAny as? ghostty_action_split_direction_e else { return }
|
||||
var splitDirection: SplitViewDirection
|
||||
switch (direction) {
|
||||
case GHOSTTY_SPLIT_RIGHT:
|
||||
case GHOSTTY_SPLIT_DIRECTION_RIGHT:
|
||||
splitDirection = .horizontal
|
||||
|
||||
case GHOSTTY_SPLIT_DOWN:
|
||||
case GHOSTTY_SPLIT_DIRECTION_DOWN:
|
||||
splitDirection = .vertical
|
||||
|
||||
default:
|
||||
|
||||
@@ -55,7 +55,7 @@ extension Ghostty {
|
||||
private func onControlInspector(_ notification: SwiftUI.Notification) {
|
||||
// Determine our mode
|
||||
guard let modeAny = notification.userInfo?["mode"] else { return }
|
||||
guard let mode = modeAny as? ghostty_inspector_mode_e else { return }
|
||||
guard let mode = modeAny as? ghostty_action_inspector_e else { return }
|
||||
|
||||
switch (mode) {
|
||||
case GHOSTTY_INSPECTOR_TOGGLE:
|
||||
|
||||
@@ -39,32 +39,54 @@ extension Ghostty {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Surface Notifications
|
||||
// MARK: Swift Types for C Types
|
||||
|
||||
extension Ghostty {
|
||||
enum SetSecureInput {
|
||||
case on
|
||||
case off
|
||||
case toggle
|
||||
|
||||
static func from(_ c: ghostty_action_secure_input_e) -> Self? {
|
||||
switch (c) {
|
||||
case GHOSTTY_SECURE_INPUT_ON:
|
||||
return .on
|
||||
|
||||
case GHOSTTY_SECURE_INPUT_OFF:
|
||||
return .off
|
||||
|
||||
case GHOSTTY_SECURE_INPUT_TOGGLE:
|
||||
return .toggle
|
||||
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An enum that is used for the directions that a split focus event can change.
|
||||
enum SplitFocusDirection {
|
||||
case previous, next, top, bottom, left, right
|
||||
|
||||
/// Initialize from a Ghostty API enum.
|
||||
static func from(direction: ghostty_split_focus_direction_e) -> Self? {
|
||||
static func from(direction: ghostty_action_goto_split_e) -> Self? {
|
||||
switch (direction) {
|
||||
case GHOSTTY_SPLIT_FOCUS_PREVIOUS:
|
||||
case GHOSTTY_GOTO_SPLIT_PREVIOUS:
|
||||
return .previous
|
||||
|
||||
case GHOSTTY_SPLIT_FOCUS_NEXT:
|
||||
case GHOSTTY_GOTO_SPLIT_NEXT:
|
||||
return .next
|
||||
|
||||
case GHOSTTY_SPLIT_FOCUS_TOP:
|
||||
case GHOSTTY_GOTO_SPLIT_TOP:
|
||||
return .top
|
||||
|
||||
case GHOSTTY_SPLIT_FOCUS_BOTTOM:
|
||||
case GHOSTTY_GOTO_SPLIT_BOTTOM:
|
||||
return .bottom
|
||||
|
||||
case GHOSTTY_SPLIT_FOCUS_LEFT:
|
||||
case GHOSTTY_GOTO_SPLIT_LEFT:
|
||||
return .left
|
||||
|
||||
case GHOSTTY_SPLIT_FOCUS_RIGHT:
|
||||
case GHOSTTY_GOTO_SPLIT_RIGHT:
|
||||
return .right
|
||||
|
||||
default:
|
||||
@@ -72,25 +94,25 @@ extension Ghostty {
|
||||
}
|
||||
}
|
||||
|
||||
func toNative() -> ghostty_split_focus_direction_e {
|
||||
func toNative() -> ghostty_action_goto_split_e {
|
||||
switch (self) {
|
||||
case .previous:
|
||||
return GHOSTTY_SPLIT_FOCUS_PREVIOUS
|
||||
return GHOSTTY_GOTO_SPLIT_PREVIOUS
|
||||
|
||||
case .next:
|
||||
return GHOSTTY_SPLIT_FOCUS_NEXT
|
||||
return GHOSTTY_GOTO_SPLIT_NEXT
|
||||
|
||||
case .top:
|
||||
return GHOSTTY_SPLIT_FOCUS_TOP
|
||||
return GHOSTTY_GOTO_SPLIT_TOP
|
||||
|
||||
case .bottom:
|
||||
return GHOSTTY_SPLIT_FOCUS_BOTTOM
|
||||
return GHOSTTY_GOTO_SPLIT_BOTTOM
|
||||
|
||||
case .left:
|
||||
return GHOSTTY_SPLIT_FOCUS_LEFT
|
||||
return GHOSTTY_GOTO_SPLIT_LEFT
|
||||
|
||||
case .right:
|
||||
return GHOSTTY_SPLIT_FOCUS_RIGHT
|
||||
return GHOSTTY_GOTO_SPLIT_RIGHT
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -99,31 +121,31 @@ extension Ghostty {
|
||||
enum SplitResizeDirection {
|
||||
case up, down, left, right
|
||||
|
||||
static func from(direction: ghostty_split_resize_direction_e) -> Self? {
|
||||
static func from(direction: ghostty_action_resize_split_direction_e) -> Self? {
|
||||
switch (direction) {
|
||||
case GHOSTTY_SPLIT_RESIZE_UP:
|
||||
case GHOSTTY_RESIZE_SPLIT_UP:
|
||||
return .up;
|
||||
case GHOSTTY_SPLIT_RESIZE_DOWN:
|
||||
case GHOSTTY_RESIZE_SPLIT_DOWN:
|
||||
return .down;
|
||||
case GHOSTTY_SPLIT_RESIZE_LEFT:
|
||||
case GHOSTTY_RESIZE_SPLIT_LEFT:
|
||||
return .left;
|
||||
case GHOSTTY_SPLIT_RESIZE_RIGHT:
|
||||
case GHOSTTY_RESIZE_SPLIT_RIGHT:
|
||||
return .right;
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func toNative() -> ghostty_split_resize_direction_e {
|
||||
func toNative() -> ghostty_action_resize_split_direction_e {
|
||||
switch (self) {
|
||||
case .up:
|
||||
return GHOSTTY_SPLIT_RESIZE_UP;
|
||||
return GHOSTTY_RESIZE_SPLIT_UP;
|
||||
case .down:
|
||||
return GHOSTTY_SPLIT_RESIZE_DOWN;
|
||||
return GHOSTTY_RESIZE_SPLIT_DOWN;
|
||||
case .left:
|
||||
return GHOSTTY_SPLIT_RESIZE_LEFT;
|
||||
return GHOSTTY_RESIZE_SPLIT_LEFT;
|
||||
case .right:
|
||||
return GHOSTTY_SPLIT_RESIZE_RIGHT;
|
||||
return GHOSTTY_RESIZE_SPLIT_RIGHT;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -174,6 +196,8 @@ extension Ghostty {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Surface Notifications
|
||||
|
||||
extension Ghostty.Notification {
|
||||
/// Used to pass a configuration along when creating a new tab/window/split.
|
||||
static let NewSurfaceConfigKey = "com.mitchellh.ghostty.newSurfaceConfig"
|
||||
@@ -201,7 +225,7 @@ extension Ghostty.Notification {
|
||||
|
||||
/// Toggle fullscreen of current window
|
||||
static let ghosttyToggleFullscreen = Notification.Name("com.mitchellh.ghostty.toggleFullscreen")
|
||||
static let NonNativeFullscreenKey = ghosttyToggleFullscreen.rawValue
|
||||
static let FullscreenModeKey = ghosttyToggleFullscreen.rawValue
|
||||
|
||||
/// Notification that a surface is becoming focused. This is only sent on macOS 12 to
|
||||
/// work around bugs. macOS 13+ should use the ".focused()" attribute.
|
||||
|
||||
@@ -52,8 +52,30 @@ extension Ghostty {
|
||||
// True if we're hovering over the left URL view, so we can show it on the right.
|
||||
@State private var isHoveringURLLeft: Bool = false
|
||||
|
||||
#if canImport(AppKit)
|
||||
// Observe SecureInput to detect when its enabled
|
||||
@ObservedObject private var secureInput = SecureInput.shared
|
||||
#endif
|
||||
|
||||
@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
|
||||
|
||||
@@ -77,6 +99,8 @@ extension Ghostty {
|
||||
.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 }
|
||||
guard let surfaceWindow = surfaceView.window else { return }
|
||||
@@ -197,6 +221,17 @@ extension Ghostty {
|
||||
}
|
||||
}
|
||||
|
||||
#if canImport(AppKit)
|
||||
// If we have secure input enabled and we're the focused surface and window
|
||||
// then we want to show the secure input overlay.
|
||||
if (ghostty.config.secureInputIndication &&
|
||||
secureInput.enabled &&
|
||||
surfaceFocus &&
|
||||
windowFocus) {
|
||||
SecureInputOverlay()
|
||||
}
|
||||
#endif
|
||||
|
||||
// If our surface is not healthy, then we render an error view over it.
|
||||
if (!surfaceView.healthy) {
|
||||
Rectangle().fill(ghostty.config.backgroundColor)
|
||||
|
||||
@@ -38,10 +38,29 @@ extension Ghostty {
|
||||
// structure because I'm lazy.
|
||||
@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
|
||||
|
||||
// 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
|
||||
|
||||
// Set whether the surface is currently on a password input or not. This is
|
||||
// detected with the set_password_input_cb on the Ghostty state.
|
||||
var passwordInput: Bool = false {
|
||||
didSet {
|
||||
// We need to update our state within the SecureInput manager.
|
||||
let input = SecureInput.shared
|
||||
let id = ObjectIdentifier(self)
|
||||
if (passwordInput) {
|
||||
input.setScoped(id, focused: focused)
|
||||
} else {
|
||||
input.removeScoped(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Returns true if quit confirmation is required for this surface to
|
||||
// exit safely.
|
||||
var needsConfirmQuit: Bool {
|
||||
@@ -59,6 +78,7 @@ extension Ghostty {
|
||||
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? {
|
||||
@@ -81,11 +101,8 @@ extension Ghostty {
|
||||
|
||||
private(set) var surface: ghostty_surface_t?
|
||||
private var markedText: NSMutableAttributedString
|
||||
private var mouseEntered: Bool = false
|
||||
private(set) var focused: Bool = true
|
||||
private var prevPressureStage: Int = 0
|
||||
private var cursor: NSCursor = .iBeam
|
||||
private var cursorVisible: CursorVisibility = .visible
|
||||
private var appearanceObserver: NSKeyValueObservation? = nil
|
||||
|
||||
// This is set to non-null during keyDown to accumulate insertText contents
|
||||
@@ -98,15 +115,6 @@ extension Ghostty {
|
||||
// so we'll use that to tell ghostty to refresh.
|
||||
override var wantsUpdateLayer: Bool { return true }
|
||||
|
||||
// State machine for mouse cursor visibility because every call to
|
||||
// NSCursor.hide/unhide must be balanced.
|
||||
enum CursorVisibility {
|
||||
case visible
|
||||
case hidden
|
||||
case pendingVisible
|
||||
case pendingHidden
|
||||
}
|
||||
|
||||
init(_ app: ghostty_app_t, baseConfig: SurfaceConfiguration? = nil, uuid: UUID? = nil) {
|
||||
self.markedText = NSMutableAttributedString()
|
||||
self.uuid = uuid ?? .init()
|
||||
@@ -178,12 +186,8 @@ extension Ghostty {
|
||||
|
||||
trackingAreas.forEach { removeTrackingArea($0) }
|
||||
|
||||
// mouseExited is not called by AppKit one last time when the view
|
||||
// closes so we do it manually to ensure our NSCursor state remains
|
||||
// accurate.
|
||||
if (mouseEntered) {
|
||||
mouseExited(with: NSEvent())
|
||||
}
|
||||
// Remove ourselves from secure input if we have to
|
||||
SecureInput.shared.removeScoped(ObjectIdentifier(self))
|
||||
|
||||
guard let surface = self.surface else { return }
|
||||
ghostty_surface_free(surface)
|
||||
@@ -209,6 +213,11 @@ extension Ghostty {
|
||||
self.focused = focused
|
||||
ghostty_surface_set_focus(surface, focused)
|
||||
|
||||
// Update our secure input state if we are a password input
|
||||
if (passwordInput) {
|
||||
SecureInput.shared.setScoped(ObjectIdentifier(self), focused: focused)
|
||||
}
|
||||
|
||||
// On macOS 13+ we can store our continuous clock...
|
||||
if #available(macOS 13, iOS 16, *) {
|
||||
if (focused) {
|
||||
@@ -218,33 +227,12 @@ extension Ghostty {
|
||||
}
|
||||
|
||||
func sizeDidChange(_ size: CGSize) {
|
||||
guard let surface = self.surface else { return }
|
||||
|
||||
// Ghostty wants to know the actual framebuffer size... It is very important
|
||||
// here that we use "size" and NOT the view frame. If we're in the middle of
|
||||
// an animation (i.e. a fullscreen animation), the frame will not yet be updated.
|
||||
// The size represents our final size we're going for.
|
||||
let scaledSize = self.convertToBacking(size)
|
||||
setSurfaceSize(width: UInt32(scaledSize.width), height: UInt32(scaledSize.height))
|
||||
|
||||
// Frame changes do not always call mouseEntered/mouseExited, so we do some
|
||||
// calculations ourself to call those events.
|
||||
if let window = self.window {
|
||||
let mouseScreen = NSEvent.mouseLocation
|
||||
let mouseWindow = window.convertPoint(fromScreen: mouseScreen)
|
||||
let mouseView = self.convert(mouseWindow, from: nil)
|
||||
let isEntered = self.isMousePoint(mouseView, in: bounds)
|
||||
if (isEntered) {
|
||||
mouseEntered(with: NSEvent())
|
||||
} else {
|
||||
mouseExited(with: NSEvent())
|
||||
}
|
||||
} else {
|
||||
// If we don't have a window, then our mouse can NOT be in our view.
|
||||
// When the window comes back, I believe this event fires again so
|
||||
// we'll get a mouseEntered.
|
||||
mouseExited(with: NSEvent())
|
||||
}
|
||||
}
|
||||
|
||||
private func setSurfaceSize(width: UInt32, height: UInt32) {
|
||||
@@ -254,105 +242,77 @@ extension Ghostty {
|
||||
ghostty_surface_set_size(surface, width, height)
|
||||
|
||||
// Update our cached size metrics
|
||||
self.surfaceSize = ghostty_surface_size(surface)
|
||||
let size = ghostty_surface_size(surface)
|
||||
DispatchQueue.main.async {
|
||||
// DispatchQueue required since this may be called by SwiftUI off
|
||||
// the main thread and Published changes need to be on the main
|
||||
// thread. This caused a crash on macOS <= 14.
|
||||
self.surfaceSize = size
|
||||
}
|
||||
}
|
||||
|
||||
func setCursorShape(_ shape: ghostty_mouse_shape_e) {
|
||||
func setCursorShape(_ shape: ghostty_action_mouse_shape_e) {
|
||||
switch (shape) {
|
||||
case GHOSTTY_MOUSE_SHAPE_DEFAULT:
|
||||
cursor = .arrow
|
||||
|
||||
case GHOSTTY_MOUSE_SHAPE_CONTEXT_MENU:
|
||||
cursor = .contextualMenu
|
||||
pointerStyle = .default
|
||||
|
||||
case GHOSTTY_MOUSE_SHAPE_TEXT:
|
||||
cursor = .iBeam
|
||||
|
||||
case GHOSTTY_MOUSE_SHAPE_CROSSHAIR:
|
||||
cursor = .crosshair
|
||||
pointerStyle = .horizontalText
|
||||
|
||||
case GHOSTTY_MOUSE_SHAPE_GRAB:
|
||||
cursor = .openHand
|
||||
pointerStyle = .grabIdle
|
||||
|
||||
case GHOSTTY_MOUSE_SHAPE_GRABBING:
|
||||
cursor = .closedHand
|
||||
pointerStyle = .grabActive
|
||||
|
||||
case GHOSTTY_MOUSE_SHAPE_POINTER:
|
||||
cursor = .pointingHand
|
||||
pointerStyle = .link
|
||||
|
||||
case GHOSTTY_MOUSE_SHAPE_W_RESIZE:
|
||||
cursor = .resizeLeft
|
||||
pointerStyle = .resizeLeft
|
||||
|
||||
case GHOSTTY_MOUSE_SHAPE_E_RESIZE:
|
||||
cursor = .resizeRight
|
||||
pointerStyle = .resizeRight
|
||||
|
||||
case GHOSTTY_MOUSE_SHAPE_N_RESIZE:
|
||||
cursor = .resizeUp
|
||||
pointerStyle = .resizeUp
|
||||
|
||||
case GHOSTTY_MOUSE_SHAPE_S_RESIZE:
|
||||
cursor = .resizeDown
|
||||
pointerStyle = .resizeDown
|
||||
|
||||
case GHOSTTY_MOUSE_SHAPE_NS_RESIZE:
|
||||
cursor = .resizeUpDown
|
||||
pointerStyle = .resizeUpDown
|
||||
|
||||
case GHOSTTY_MOUSE_SHAPE_EW_RESIZE:
|
||||
cursor = .resizeLeftRight
|
||||
pointerStyle = .resizeLeftRight
|
||||
|
||||
case GHOSTTY_MOUSE_SHAPE_VERTICAL_TEXT:
|
||||
cursor = .iBeamCursorForVerticalLayout
|
||||
pointerStyle = .default
|
||||
|
||||
// These are not yet supported. We should support them by constructing a
|
||||
// PointerStyle from an NSCursor.
|
||||
case GHOSTTY_MOUSE_SHAPE_CONTEXT_MENU:
|
||||
fallthrough
|
||||
case GHOSTTY_MOUSE_SHAPE_CROSSHAIR:
|
||||
fallthrough
|
||||
case GHOSTTY_MOUSE_SHAPE_NOT_ALLOWED:
|
||||
cursor = .operationNotAllowed
|
||||
pointerStyle = .default
|
||||
|
||||
default:
|
||||
// We ignore unknown shapes.
|
||||
return
|
||||
}
|
||||
|
||||
// Set our cursor immediately if our mouse is over our window
|
||||
if (mouseEntered) { cursorUpdate(with: NSEvent()) }
|
||||
if let window = self.window {
|
||||
window.invalidateCursorRects(for: self)
|
||||
}
|
||||
}
|
||||
|
||||
func setCursorVisibility(_ visible: Bool) {
|
||||
switch (cursorVisible) {
|
||||
case .visible:
|
||||
// If we want to be visible, do nothing. If we want to be hidden
|
||||
// enter the pending state.
|
||||
if (visible) { return }
|
||||
cursorVisible = .pendingHidden
|
||||
|
||||
case .hidden:
|
||||
// If we want to be hidden, do nothing. If we want to be visible
|
||||
// enter the pending state.
|
||||
if (!visible) { return }
|
||||
cursorVisible = .pendingVisible
|
||||
|
||||
case .pendingVisible:
|
||||
// If we want to be visible, do nothing because we're already pending.
|
||||
// If we want to be hidden, we're already hidden so reset state.
|
||||
if (visible) { return }
|
||||
cursorVisible = .hidden
|
||||
|
||||
case .pendingHidden:
|
||||
// If we want to be hidden, do nothing because we're pending that switch.
|
||||
// If we want to be visible, we're already visible so reset state.
|
||||
if (!visible) { return }
|
||||
cursorVisible = .visible
|
||||
}
|
||||
|
||||
if (mouseEntered) {
|
||||
cursorUpdate(with: NSEvent())
|
||||
}
|
||||
pointerVisible = visible
|
||||
}
|
||||
|
||||
// MARK: - Notifications
|
||||
|
||||
@objc private func onUpdateRendererHealth(notification: SwiftUI.Notification) {
|
||||
guard let healthAny = notification.userInfo?["health"] else { return }
|
||||
guard let health = healthAny as? ghostty_renderer_health_e else { return }
|
||||
guard let health = healthAny as? ghostty_action_renderer_health_e else { return }
|
||||
healthy = health == GHOSTTY_RENDERER_HEALTH_OK
|
||||
}
|
||||
|
||||
@@ -395,7 +355,6 @@ extension Ghostty {
|
||||
addTrackingArea(NSTrackingArea(
|
||||
rect: frame,
|
||||
options: [
|
||||
.mouseEnteredAndExited,
|
||||
.mouseMoved,
|
||||
|
||||
// Only send mouse events that happen in our visible (not obscured) rect
|
||||
@@ -409,11 +368,6 @@ extension Ghostty {
|
||||
userInfo: nil))
|
||||
}
|
||||
|
||||
override func resetCursorRects() {
|
||||
discardCursorRects()
|
||||
addCursorRect(frame, cursor: self.cursor)
|
||||
}
|
||||
|
||||
override func viewDidChangeBackingProperties() {
|
||||
super.viewDidChangeBackingProperties()
|
||||
|
||||
@@ -538,7 +492,8 @@ extension Ghostty {
|
||||
|
||||
// Convert window position to view position. Note (0, 0) is bottom left.
|
||||
let pos = self.convert(event.locationInWindow, from: nil)
|
||||
ghostty_surface_mouse_pos(surface, pos.x, frame.height - pos.y)
|
||||
let mods = Ghostty.ghosttyMods(event.modifierFlags)
|
||||
ghostty_surface_mouse_pos(surface, pos.x, frame.height - pos.y, mods)
|
||||
|
||||
// If focus follows mouse is enabled then move focus to this surface.
|
||||
if let window = self.window as? TerminalWindow,
|
||||
@@ -554,40 +509,6 @@ extension Ghostty {
|
||||
self.mouseMoved(with: event)
|
||||
}
|
||||
|
||||
override func mouseEntered(with event: NSEvent) {
|
||||
// For reasons unknown (Cocoaaaaaaaaa), mouseEntered is called
|
||||
// multiple times in an unbalanced way with mouseExited when a new
|
||||
// tab is created. In this scenario, we only want to process our
|
||||
// callback once since this is stateful and we expect balancing.
|
||||
if (mouseEntered) { return }
|
||||
|
||||
mouseEntered = true
|
||||
|
||||
// Update our cursor when we enter so we fully process our
|
||||
// cursorVisible state.
|
||||
cursorUpdate(with: NSEvent())
|
||||
}
|
||||
|
||||
override func mouseExited(with event: NSEvent) {
|
||||
// See mouseEntered
|
||||
if (!mouseEntered) { return }
|
||||
|
||||
mouseEntered = false
|
||||
|
||||
// If the mouse is currently hidden, we want to show it when we exit
|
||||
// this view. We go through the cursorVisible dance so that only
|
||||
// cursorUpdate manages cursor state.
|
||||
if (cursorVisible == .hidden) {
|
||||
cursorVisible = .pendingVisible
|
||||
cursorUpdate(with: NSEvent())
|
||||
assert(cursorVisible == .visible)
|
||||
|
||||
// We set the state to pending hidden again for the next time
|
||||
// we enter.
|
||||
cursorVisible = .pendingHidden
|
||||
}
|
||||
}
|
||||
|
||||
override func scrollWheel(with event: NSEvent) {
|
||||
guard let surface = self.surface else { return }
|
||||
|
||||
@@ -651,24 +572,6 @@ extension Ghostty {
|
||||
quickLook(with: event)
|
||||
}
|
||||
|
||||
override func cursorUpdate(with event: NSEvent) {
|
||||
switch (cursorVisible) {
|
||||
case .visible, .hidden:
|
||||
// Do nothing, stable state
|
||||
break
|
||||
|
||||
case .pendingHidden:
|
||||
NSCursor.hide()
|
||||
cursorVisible = .hidden
|
||||
|
||||
case .pendingVisible:
|
||||
NSCursor.unhide()
|
||||
cursorVisible = .visible
|
||||
}
|
||||
|
||||
cursor.set()
|
||||
}
|
||||
|
||||
override func keyDown(with event: NSEvent) {
|
||||
guard let surface = self.surface else {
|
||||
self.interpretKeyEvents([event])
|
||||
@@ -789,6 +692,11 @@ extension Ghostty {
|
||||
// sound and we don't like the beep sound.
|
||||
equivalent = "_"
|
||||
|
||||
case "\r":
|
||||
// Pass C-<return> through verbatim
|
||||
// (prevent the default context menu equivalent)
|
||||
equivalent = "\r"
|
||||
|
||||
default:
|
||||
// Ignore other events
|
||||
return false
|
||||
@@ -1018,12 +926,12 @@ extension Ghostty {
|
||||
|
||||
@IBAction func splitRight(_ sender: Any) {
|
||||
guard let surface = self.surface else { return }
|
||||
ghostty_surface_split(surface, GHOSTTY_SPLIT_RIGHT)
|
||||
ghostty_surface_split(surface, GHOSTTY_SPLIT_DIRECTION_RIGHT)
|
||||
}
|
||||
|
||||
@IBAction func splitDown(_ sender: Any) {
|
||||
guard let surface = self.surface else { return }
|
||||
ghostty_surface_split(surface, GHOSTTY_SPLIT_DOWN)
|
||||
ghostty_surface_split(surface, GHOSTTY_SPLIT_DIRECTION_DOWN)
|
||||
}
|
||||
|
||||
@objc func resetTerminal(_ sender: Any) {
|
||||
|
||||
@@ -23,3 +23,79 @@ extension Backport where Content: Scene {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Backport where Content: View {
|
||||
func pointerVisibility(_ v: BackportVisibility) -> some View {
|
||||
#if canImport(AppKit)
|
||||
if #available(macOS 15, *) {
|
||||
return content.pointerVisibility(v.official)
|
||||
} else {
|
||||
return content
|
||||
}
|
||||
#else
|
||||
return content
|
||||
#endif
|
||||
}
|
||||
|
||||
func pointerStyle(_ style: BackportPointerStyle?) -> some View {
|
||||
#if canImport(AppKit)
|
||||
if #available(macOS 15, *) {
|
||||
return content.pointerStyle(style?.official)
|
||||
} else {
|
||||
return content
|
||||
}
|
||||
#else
|
||||
return content
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
enum BackportVisibility {
|
||||
case automatic
|
||||
case visible
|
||||
case hidden
|
||||
|
||||
@available(macOS 15, *)
|
||||
var official: Visibility {
|
||||
switch self {
|
||||
case .automatic: return .automatic
|
||||
case .visible: return .visible
|
||||
case .hidden: return .hidden
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum BackportPointerStyle {
|
||||
case `default`
|
||||
case grabIdle
|
||||
case grabActive
|
||||
case horizontalText
|
||||
case verticalText
|
||||
case link
|
||||
case resizeLeft
|
||||
case resizeRight
|
||||
case resizeUp
|
||||
case resizeDown
|
||||
case resizeUpDown
|
||||
case resizeLeftRight
|
||||
|
||||
#if canImport(AppKit)
|
||||
@available(macOS 15, *)
|
||||
var official: PointerStyle {
|
||||
switch self {
|
||||
case .default: return .default
|
||||
case .grabIdle: return .grabIdle
|
||||
case .grabActive: return .grabActive
|
||||
case .horizontalText: return .horizontalText
|
||||
case .verticalText: return .verticalText
|
||||
case .link: return .link
|
||||
case .resizeLeft: return .frameResize(position: .trailing, directions: [.inward])
|
||||
case .resizeRight: return .frameResize(position: .leading, directions: [.inward])
|
||||
case .resizeUp: return .frameResize(position: .bottom, directions: [.inward])
|
||||
case .resizeDown: return .frameResize(position: .top, directions: [.inward])
|
||||
case .resizeUpDown: return .frameResize(position: .top)
|
||||
case .resizeLeftRight: return .frameResize(position: .trailing)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
38
macos/Sources/Helpers/Cursor.swift
Normal file
38
macos/Sources/Helpers/Cursor.swift
Normal file
@@ -0,0 +1,38 @@
|
||||
import Cocoa
|
||||
|
||||
/// This helps manage the stateful nature of NSCursor hiding and unhiding.
|
||||
class Cursor {
|
||||
private static var counter: UInt = 0
|
||||
|
||||
static var isVisible: Bool {
|
||||
counter == 0
|
||||
}
|
||||
|
||||
static func hide() {
|
||||
counter += 1
|
||||
NSCursor.hide()
|
||||
}
|
||||
|
||||
/// Unhide the cursor. Returns true if the cursor was previously hidden.
|
||||
static func unhide() -> Bool {
|
||||
// Its always safe to call unhide when the counter is zero because it
|
||||
// won't go negative.
|
||||
NSCursor.unhide()
|
||||
|
||||
if (counter > 0) {
|
||||
counter -= 1
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
static func unhideCompletely() -> UInt {
|
||||
let counter = self.counter
|
||||
for _ in 0..<counter {
|
||||
assert(unhide())
|
||||
}
|
||||
assert(self.counter == 0)
|
||||
return counter
|
||||
}
|
||||
}
|
||||
19
macos/Sources/Helpers/DraggableWindowView.swift
Normal file
19
macos/Sources/Helpers/DraggableWindowView.swift
Normal file
@@ -0,0 +1,19 @@
|
||||
import Cocoa
|
||||
import SwiftUI
|
||||
|
||||
struct DraggableWindowView: NSViewRepresentable {
|
||||
func makeNSView(context: Context) -> DraggableWindowNSView {
|
||||
return DraggableWindowNSView()
|
||||
}
|
||||
|
||||
func updateNSView(_ nsView: DraggableWindowNSView, context: Context) {
|
||||
// No need to update anything here
|
||||
}
|
||||
}
|
||||
|
||||
class DraggableWindowNSView: NSView {
|
||||
override func mouseDown(with event: NSEvent) {
|
||||
guard let window = self.window else { return }
|
||||
window.performDrag(with: event)
|
||||
}
|
||||
}
|
||||
@@ -13,8 +13,18 @@ class FullScreenHandler {
|
||||
var isInNonNativeFullscreen: Bool = false
|
||||
var isInFullscreen: Bool = false
|
||||
|
||||
func toggleFullscreen(window: NSWindow, nonNativeFullscreen: ghostty_non_native_fullscreen_e) {
|
||||
let useNonNativeFullscreen = nonNativeFullscreen != GHOSTTY_NON_NATIVE_FULLSCREEN_FALSE
|
||||
func toggleFullscreen(window: NSWindow, mode: ghostty_action_fullscreen_e) {
|
||||
let useNonNativeFullscreen = switch (mode) {
|
||||
case GHOSTTY_FULLSCREEN_NATIVE:
|
||||
false
|
||||
|
||||
case GHOSTTY_FULLSCREEN_NON_NATIVE, GHOSTTY_FULLSCREEN_NON_NATIVE_VISIBLE_MENU:
|
||||
true
|
||||
|
||||
default:
|
||||
false
|
||||
}
|
||||
|
||||
if isInFullscreen {
|
||||
if useNonNativeFullscreen || isInNonNativeFullscreen {
|
||||
leaveFullscreen(window: window)
|
||||
@@ -27,7 +37,7 @@ class FullScreenHandler {
|
||||
isInFullscreen = false
|
||||
} else {
|
||||
if useNonNativeFullscreen {
|
||||
let hideMenu = nonNativeFullscreen != GHOSTTY_NON_NATIVE_FULLSCREEN_VISIBLE_MENU
|
||||
let hideMenu = mode != GHOSTTY_FULLSCREEN_NON_NATIVE_VISIBLE_MENU
|
||||
enterFullscreen(window: window, hideMenu: hideMenu)
|
||||
isInNonNativeFullscreen = true
|
||||
} else {
|
||||
|
||||
@@ -44,15 +44,30 @@ extension SplitView {
|
||||
}
|
||||
}
|
||||
|
||||
private var pointerStyle: BackportPointerStyle {
|
||||
return switch (direction) {
|
||||
case .horizontal: .resizeLeftRight
|
||||
case .vertical: .resizeUpDown
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Color.clear
|
||||
.frame(width: invisibleWidth, height: invisibleHeight)
|
||||
.contentShape(Rectangle()) // Makes it hit testable for pointerStyle
|
||||
Rectangle()
|
||||
.fill(color)
|
||||
.frame(width: visibleWidth, height: visibleHeight)
|
||||
}
|
||||
.backport.pointerStyle(pointerStyle)
|
||||
.onHover { isHovered in
|
||||
// macOS 15+ we use the pointerStyle helper which is much less
|
||||
// error-prone versus manual NSCursor push/pop
|
||||
if #available(macOS 15, *) {
|
||||
return
|
||||
}
|
||||
|
||||
if (isHovered) {
|
||||
switch (direction) {
|
||||
case .horizontal:
|
||||
|
||||
31
macos/Sources/Helpers/View+Extension.swift
Normal file
31
macos/Sources/Helpers/View+Extension.swift
Normal file
@@ -0,0 +1,31 @@
|
||||
import SwiftUI
|
||||
|
||||
extension View {
|
||||
func innerShadow<S: Shape, ST: ShapeStyle>(
|
||||
using shape: S = Rectangle(),
|
||||
stroke: ST = Color.black,
|
||||
width: CGFloat = 6,
|
||||
blur: CGFloat = 6
|
||||
) -> some View {
|
||||
return self
|
||||
.overlay(
|
||||
shape
|
||||
.stroke(stroke, lineWidth: width)
|
||||
.blur(radius: blur)
|
||||
.mask(shape)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
func pointerStyleFromCursor(_ cursor: NSCursor) -> some View {
|
||||
if #available(macOS 15.0, *) {
|
||||
return self.pointerStyle(.image(
|
||||
Image(nsImage: cursor.image),
|
||||
hotSpot: .init(x: cursor.hotSpot.x, y: cursor.hotSpot.y)
|
||||
))
|
||||
} else {
|
||||
return self
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,16 +25,19 @@ elif [ "$1" != "--update" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TMP_CACHE_DIR="$(mktemp --directory --suffix nix-zig-cache)"
|
||||
ZIG_GLOBAL_CACHE_DIR="$(mktemp --directory --suffix nix-zig-cache)"
|
||||
export ZIG_GLOBAL_CACHE_DIR
|
||||
|
||||
# This is not 100% necessary in CI but is helpful when running locally to keep
|
||||
# a local workstation clean.
|
||||
trap 'rm -rf "${TMP_CACHE_DIR}"' EXIT
|
||||
trap 'rm -rf "${ZIG_GLOBAL_CACHE_DIR}"' EXIT
|
||||
|
||||
# Run Zig and download the cache to the temporary directory.
|
||||
zig build --fetch --global-cache-dir "${TMP_CACHE_DIR}"
|
||||
|
||||
sh ./nix/build-support/fetch-zig-cache.sh
|
||||
|
||||
# Now, calculate the hash.
|
||||
ZIG_CACHE_HASH="sha256-$(nix-hash --type sha256 --to-base64 "$(nix-hash --type sha256 "${TMP_CACHE_DIR}")")"
|
||||
ZIG_CACHE_HASH="sha256-$(nix-hash --type sha256 --to-base64 "$(nix-hash --type sha256 "${ZIG_GLOBAL_CACHE_DIR}")")"
|
||||
|
||||
if [ "${OLD_CACHE_HASH}" == "${ZIG_CACHE_HASH}" ]; then
|
||||
echo -e "\nOK: Zig cache store hash unchanged."
|
||||
|
||||
39
nix/build-support/fetch-zig-cache.sh
Normal file
39
nix/build-support/fetch-zig-cache.sh
Normal file
@@ -0,0 +1,39 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
|
||||
# Because Zig does not fetch recursive dependencies when you run `zig build
|
||||
# --fetch` (see https://github.com/ziglang/zig/issues/20976) we need to do some
|
||||
# extra work to fetch everything that we actually need to build without Internet
|
||||
# access (such as when building a Nix package).
|
||||
#
|
||||
# An example of this happening:
|
||||
#
|
||||
# error: builder for '/nix/store/cx8qcwrhjmjxik2547fw99v5j6np5san-ghostty-0.1.0.drv' failed with exit code 1;
|
||||
# la/build/tmp.xgHOheUF7V/p/12208cfdda4d5fdbc81b0c44b82e4d6dba2d4a86bff644a153e026fdfc80f8469133/build.zig.zon:7:20: error: unable to discover remote git server capabilities: TemporaryNameServerFailure
|
||||
# > .url = "git+https://github.com/zigimg/zigimg#3a667bdb3d7f0955a5a51c8468eac83210c1439e",
|
||||
# > ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
# > /build/tmp.xgHOheUF7V/p/12208cfdda4d5fdbc81b0c44b82e4d6dba2d4a86bff644a153e026fdfc80f8469133/build.zig.zon:16:20: error: unable to discover remote git server capabilities: TemporaryNameServerFailure
|
||||
# > .url = "git+https://github.com/mitchellh/libxev#f6a672a78436d8efee1aa847a43a900ad773618b",
|
||||
# > ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
# >
|
||||
# For full logs, run 'nix log /nix/store/cx8qcwrhjmjxik2547fw99v5j6np5san-ghostty-0.1.0.drv'.
|
||||
#
|
||||
# To update this script, add any failing URLs with a line like this:
|
||||
#
|
||||
# zig fetch <url>
|
||||
#
|
||||
# Periodically old URLs may need to be cleaned out.
|
||||
#
|
||||
# Hopefully when the Zig issue is fixed this script can be eliminated in favor
|
||||
# of a plain `zig build --fetch`.
|
||||
|
||||
if [ -z ${ZIG_GLOBAL_CACHE_DIR+x} ]
|
||||
then
|
||||
echo "must set ZIG_GLOBAL_CACHE_DIR!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
zig build --fetch
|
||||
zig fetch git+https://github.com/zigimg/zigimg#3a667bdb3d7f0955a5a51c8468eac83210c1439e
|
||||
zig fetch git+https://github.com/mitchellh/libxev#f6a672a78436d8efee1aa847a43a900ad773618b
|
||||
@@ -14,7 +14,6 @@
|
||||
python3,
|
||||
qemu,
|
||||
scdoc,
|
||||
tracy,
|
||||
valgrind,
|
||||
#, vulkan-loader # unused
|
||||
vttest,
|
||||
@@ -100,7 +99,6 @@ in
|
||||
# Testing
|
||||
parallel
|
||||
python3
|
||||
tracy
|
||||
vttest
|
||||
hyperfine
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
# https://github.com/ziglang/zig/issues/14281#issuecomment-1624220653 is
|
||||
# ultimately acted on and has made its way to a nixpkgs implementation, this
|
||||
# can probably be removed in favor of that.
|
||||
zig012Hook = zig_0_13.hook.overrideAttrs {
|
||||
zig_hook = zig_0_13.hook.overrideAttrs {
|
||||
zig_default_flags = "-Dcpu=baseline -Doptimize=${optimize}";
|
||||
};
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
../vendor
|
||||
../build.zig
|
||||
../build.zig.zon
|
||||
./build-support/fetch-zig-cache.sh
|
||||
]
|
||||
);
|
||||
};
|
||||
@@ -79,7 +80,7 @@
|
||||
name = "ghostty-cache";
|
||||
nativeBuildInputs = [
|
||||
git
|
||||
zig_0_13.hook
|
||||
zig_hook
|
||||
];
|
||||
|
||||
dontConfigure = true;
|
||||
@@ -90,7 +91,7 @@
|
||||
buildPhase = ''
|
||||
runHook preBuild
|
||||
|
||||
zig build --fetch
|
||||
sh ./nix/build-support/fetch-zig-cache.sh
|
||||
|
||||
runHook postBuild
|
||||
'';
|
||||
@@ -117,7 +118,7 @@ in
|
||||
ncurses
|
||||
pandoc
|
||||
pkg-config
|
||||
zig012Hook
|
||||
zig_hook
|
||||
wrapGAppsHook4
|
||||
];
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
# This file is auto-generated! check build-support/check-zig-cache-hash.sh for
|
||||
# more details.
|
||||
"sha256-YLopoyRgXV6GYiTiaKt64mH6lWjlKJbi61ck0fO4WvQ="
|
||||
"sha256-JsAEfg1jp20aGz9YXG/QEp4MS5K5J5U7zFS2Orw2K/s="
|
||||
|
||||
5
pkg/macos/carbon.zig
Normal file
5
pkg/macos/carbon.zig
Normal file
@@ -0,0 +1,5 @@
|
||||
pub const c = @import("carbon/c.zig").c;
|
||||
|
||||
test {
|
||||
@import("std").testing.refAllDecls(@This());
|
||||
}
|
||||
3
pkg/macos/carbon/c.zig
Normal file
3
pkg/macos/carbon/c.zig
Normal file
@@ -0,0 +1,3 @@
|
||||
pub const c = @cImport({
|
||||
@cInclude("Carbon/Carbon.h");
|
||||
});
|
||||
@@ -1,3 +1,4 @@
|
||||
pub const carbon = @import("carbon.zig");
|
||||
pub const foundation = @import("foundation.zig");
|
||||
pub const animation = @import("animation.zig");
|
||||
pub const dispatch = @import("dispatch.zig");
|
||||
|
||||
@@ -188,6 +188,14 @@ pub const Font = opaque {
|
||||
return c.CTFontGetUnderlineThickness(@ptrCast(self));
|
||||
}
|
||||
|
||||
pub fn getCapHeight(self: *Font) f64 {
|
||||
return c.CTFontGetCapHeight(@ptrCast(self));
|
||||
}
|
||||
|
||||
pub fn getXHeight(self: *Font) f64 {
|
||||
return c.CTFontGetXHeight(@ptrCast(self));
|
||||
}
|
||||
|
||||
pub fn getUnitsPerEm(self: *Font) u32 {
|
||||
return c.CTFontGetUnitsPerEm(@ptrCast(self));
|
||||
}
|
||||
|
||||
140
src/App.zig
140
src/App.zig
@@ -138,7 +138,13 @@ pub fn addSurface(self: *App, rt_surface: *apprt.Surface) !void {
|
||||
// Since we have non-zero surfaces, we can cancel the quit timer.
|
||||
// It is up to the apprt if there is a quit timer at all and if it
|
||||
// should be canceled.
|
||||
if (@hasDecl(apprt.App, "cancelQuitTimer")) rt_surface.app.cancelQuitTimer();
|
||||
rt_surface.app.performAction(
|
||||
.app,
|
||||
.quit_timer,
|
||||
.stop,
|
||||
) catch |err| {
|
||||
log.warn("error stopping quit timer err={}", .{err});
|
||||
};
|
||||
}
|
||||
|
||||
/// Delete the surface from the known surface list. This will NOT call the
|
||||
@@ -166,8 +172,13 @@ pub fn deleteSurface(self: *App, rt_surface: *apprt.Surface) void {
|
||||
|
||||
// If we have no surfaces, we can start the quit timer. It is up to the
|
||||
// apprt to determine if this is necessary.
|
||||
if (@hasDecl(apprt.App, "startQuitTimer") and
|
||||
self.surfaces.items.len == 0) rt_surface.app.startQuitTimer();
|
||||
if (self.surfaces.items.len == 0) rt_surface.app.performAction(
|
||||
.app,
|
||||
.quit_timer,
|
||||
.start,
|
||||
) catch |err| {
|
||||
log.warn("error starting quit timer err={}", .{err});
|
||||
};
|
||||
}
|
||||
|
||||
/// The last focused surface. This is only valid while on the main thread
|
||||
@@ -194,7 +205,7 @@ fn drainMailbox(self: *App, rt_app: *apprt.App) !void {
|
||||
log.debug("mailbox message={s}", .{@tagName(message)});
|
||||
switch (message) {
|
||||
.reload_config => try self.reloadConfig(rt_app),
|
||||
.open_config => try self.openConfig(rt_app),
|
||||
.open_config => try self.performAction(rt_app, .open_config),
|
||||
.new_window => |msg| try self.newWindow(rt_app, msg),
|
||||
.close => |surface| try self.closeSurface(surface),
|
||||
.quit => try self.setQuit(),
|
||||
@@ -205,12 +216,6 @@ fn drainMailbox(self: *App, rt_app: *apprt.App) !void {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn openConfig(self: *App, rt_app: *apprt.App) !void {
|
||||
_ = self;
|
||||
log.debug("opening configuration", .{});
|
||||
try rt_app.openConfig();
|
||||
}
|
||||
|
||||
pub fn reloadConfig(self: *App, rt_app: *apprt.App) !void {
|
||||
log.debug("reloading configuration", .{});
|
||||
if (try rt_app.reloadConfig()) |new| {
|
||||
@@ -241,19 +246,17 @@ fn redrawInspector(self: *App, rt_app: *apprt.App, surface: *apprt.Surface) !voi
|
||||
|
||||
/// Create a new window
|
||||
pub fn newWindow(self: *App, rt_app: *apprt.App, msg: Message.NewWindow) !void {
|
||||
if (!@hasDecl(apprt.App, "newWindow")) {
|
||||
log.warn("newWindow is not supported by this runtime", .{});
|
||||
return;
|
||||
}
|
||||
const target: apprt.Target = target: {
|
||||
const parent = msg.parent orelse break :target .app;
|
||||
if (self.hasSurface(parent)) break :target .{ .surface = parent };
|
||||
break :target .app;
|
||||
};
|
||||
|
||||
const parent = if (msg.parent) |parent| parent: {
|
||||
break :parent if (self.hasSurface(parent))
|
||||
parent
|
||||
else
|
||||
null;
|
||||
} else null;
|
||||
|
||||
try rt_app.newWindow(parent);
|
||||
try rt_app.performAction(
|
||||
target,
|
||||
.new_window,
|
||||
{},
|
||||
);
|
||||
}
|
||||
|
||||
/// Start quitting
|
||||
@@ -262,6 +265,99 @@ pub fn setQuit(self: *App) !void {
|
||||
self.quit = true;
|
||||
}
|
||||
|
||||
/// Handle a key event at the app-scope. If this key event is used,
|
||||
/// this will return true and the caller shouldn't continue processing
|
||||
/// the event. If the event is not used, this will return false.
|
||||
pub fn keyEvent(
|
||||
self: *App,
|
||||
rt_app: *apprt.App,
|
||||
event: input.KeyEvent,
|
||||
) bool {
|
||||
switch (event.action) {
|
||||
// We don't care about key release events.
|
||||
.release => return false,
|
||||
|
||||
// Continue processing key press events.
|
||||
.press, .repeat => {},
|
||||
}
|
||||
|
||||
// Get the keybind entry for this event. We don't support key sequences
|
||||
// so we can look directly in the top-level set.
|
||||
const entry = rt_app.config.keybind.set.getEvent(event) orelse return false;
|
||||
const leaf: input.Binding.Set.Leaf = switch (entry) {
|
||||
// Sequences aren't supported. Our configuration parser verifies
|
||||
// this for global keybinds but we may still get an entry for
|
||||
// a non-global keybind.
|
||||
.leader => return false,
|
||||
|
||||
// Leaf entries are good
|
||||
.leaf => |leaf| leaf,
|
||||
};
|
||||
|
||||
// We only care about global keybinds
|
||||
if (!leaf.flags.global) return false;
|
||||
|
||||
// Perform the action
|
||||
self.performAllAction(rt_app, leaf.action) catch |err| {
|
||||
log.warn("error performing global keybind action action={s} err={}", .{
|
||||
@tagName(leaf.action),
|
||||
err,
|
||||
});
|
||||
};
|
||||
|
||||
return 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.
|
||||
pub fn performAction(
|
||||
self: *App,
|
||||
rt_app: *apprt.App,
|
||||
action: input.Binding.Action.Scoped(.app),
|
||||
) !void {
|
||||
switch (action) {
|
||||
.unbind => unreachable,
|
||||
.ignore => {},
|
||||
.quit => try 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),
|
||||
.close_all_windows => try rt_app.performAction(.app, .close_all_windows, {}),
|
||||
.toggle_quick_terminal => try rt_app.performAction(.app, .toggle_quick_terminal, {}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Perform an app-wide binding action. If the action is surface-specific
|
||||
/// then it will be performed on all surfaces. To perform only app-scoped
|
||||
/// actions, use performAction.
|
||||
pub fn performAllAction(
|
||||
self: *App,
|
||||
rt_app: *apprt.App,
|
||||
action: input.Binding.Action,
|
||||
) !void {
|
||||
switch (action.scope()) {
|
||||
// App-scoped actions are handled by the app so that they aren't
|
||||
// repeated for each surface (since each surface forwards
|
||||
// app-scoped actions back up).
|
||||
.app => try self.performAction(
|
||||
rt_app,
|
||||
action.scoped(.app).?, // asserted through the scope match
|
||||
),
|
||||
|
||||
// Surface-scoped actions are performed on all surfaces. Errors
|
||||
// are logged but processing continues.
|
||||
.surface => for (self.surfaces.items) |surface| {
|
||||
_ = surface.core_surface.performBindingAction(action) catch |err| {
|
||||
log.warn("error performing binding action on surface ptr={X} err={}", .{
|
||||
@intFromPtr(surface),
|
||||
err,
|
||||
});
|
||||
};
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle a window message
|
||||
fn surfaceMessage(self: *App, surface: *Surface, msg: apprt.surface.Message) !void {
|
||||
// We want to ensure our window is still active. Window messages
|
||||
|
||||
494
src/Surface.zig
494
src/Surface.zig
@@ -515,14 +515,25 @@ pub fn init(
|
||||
errdefer self.io.deinit();
|
||||
|
||||
// Report initial cell size on surface creation
|
||||
try rt_surface.setCellSize(cell_size.width, cell_size.height);
|
||||
try rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.cell_size,
|
||||
.{ .width = cell_size.width, .height = cell_size.height },
|
||||
);
|
||||
|
||||
// Set a minimum size that is cols=10 h=4. This matches Mac's Terminal.app
|
||||
// but is otherwise somewhat arbitrary.
|
||||
try rt_surface.setSizeLimits(.{
|
||||
.width = cell_size.width * 10,
|
||||
.height = cell_size.height * 4,
|
||||
}, null);
|
||||
try rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.size_limit,
|
||||
.{
|
||||
.min_width = cell_size.width * 10,
|
||||
.min_height = cell_size.height * 4,
|
||||
// No max:
|
||||
.max_width = 0,
|
||||
.max_height = 0,
|
||||
},
|
||||
);
|
||||
|
||||
// Call our size callback which handles all our retina setup
|
||||
// Note: this shouldn't be necessary and when we clean up the surface
|
||||
@@ -576,13 +587,23 @@ pub fn init(
|
||||
padding.top +
|
||||
padding.bottom;
|
||||
|
||||
rt_surface.setInitialWindowSize(final_width, final_height) catch |err| {
|
||||
rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.initial_size,
|
||||
.{ .width = final_width, .height = final_height },
|
||||
) catch |err| {
|
||||
// We don't treat this as a fatal error because not setting
|
||||
// an initial size shouldn't stop our terminal from working.
|
||||
log.warn("unable to set initial window size: {s}", .{err});
|
||||
};
|
||||
}
|
||||
|
||||
if (config.title) |title| {
|
||||
try rt_surface.setTitle(title);
|
||||
try rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.set_title,
|
||||
.{ .title = title },
|
||||
);
|
||||
} else if ((comptime builtin.os.tag == .linux) and
|
||||
config.@"_xdg-terminal-exec")
|
||||
xdg: {
|
||||
@@ -599,7 +620,11 @@ pub fn init(
|
||||
break :xdg;
|
||||
};
|
||||
defer alloc.free(title);
|
||||
try rt_surface.setTitle(title);
|
||||
try rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.set_title,
|
||||
.{ .title = title },
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -743,15 +768,15 @@ pub fn handleMessage(self: *Surface, msg: Message) !void {
|
||||
// We know that our title should end in 0.
|
||||
const slice = std.mem.sliceTo(@as([*:0]const u8, @ptrCast(v)), 0);
|
||||
log.debug("changing title \"{s}\"", .{slice});
|
||||
try self.rt_surface.setTitle(slice);
|
||||
try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.set_title,
|
||||
.{ .title = slice },
|
||||
);
|
||||
},
|
||||
|
||||
.report_title => |style| {
|
||||
const title: ?[:0]const u8 = title: {
|
||||
if (!@hasDecl(apprt.runtime.Surface, "getTitle")) break :title null;
|
||||
break :title self.rt_surface.getTitle();
|
||||
};
|
||||
|
||||
const title: ?[:0]const u8 = self.rt_surface.getTitle();
|
||||
const data = switch (style) {
|
||||
.csi_21_t => try std.fmt.allocPrint(
|
||||
self.alloc,
|
||||
@@ -773,7 +798,11 @@ pub fn handleMessage(self: *Surface, msg: Message) !void {
|
||||
|
||||
.set_mouse_shape => |shape| {
|
||||
log.debug("changing mouse shape: {}", .{shape});
|
||||
try self.rt_surface.setMouseShape(shape);
|
||||
try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.mouse_shape,
|
||||
shape,
|
||||
);
|
||||
},
|
||||
|
||||
.clipboard_read => |clipboard| {
|
||||
@@ -837,6 +866,18 @@ fn passwordInput(self: *Surface, v: bool) !void {
|
||||
self.io.terminal.flags.password_input = v;
|
||||
}
|
||||
|
||||
// Notify our apprt so it can do whatever it wants.
|
||||
self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.secure_input,
|
||||
if (v) .on else .off,
|
||||
) catch |err| {
|
||||
// We ignore this error because we don't want to fail this
|
||||
// entire operation just because the apprt failed to set
|
||||
// the secure input state.
|
||||
log.warn("apprt failed to set secure input state err={}", .{err});
|
||||
};
|
||||
|
||||
try self.queueRender();
|
||||
}
|
||||
|
||||
@@ -889,8 +930,13 @@ fn modsChanged(self: *Surface, mods: input.Mods) void {
|
||||
/// Called when our renderer health state changes.
|
||||
fn updateRendererHealth(self: *Surface, health: renderer.Health) void {
|
||||
log.warn("renderer health status change status={}", .{health});
|
||||
if (!@hasDecl(apprt.runtime.Surface, "updateRendererHealth")) return;
|
||||
self.rt_surface.updateRendererHealth(health);
|
||||
self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.renderer_health,
|
||||
health,
|
||||
) catch |err| {
|
||||
log.warn("failed to notify app of renderer health change err={}", .{err});
|
||||
};
|
||||
}
|
||||
|
||||
/// Update our configuration at runtime.
|
||||
@@ -1146,10 +1192,8 @@ fn setSelection(self: *Surface, sel_: ?terminal.Selection) !void {
|
||||
|
||||
// Check if our runtime supports the selection clipboard at all.
|
||||
// We can save a lot of work if it doesn't.
|
||||
if (@hasDecl(apprt.runtime.Surface, "supportsClipboard")) {
|
||||
if (!self.rt_surface.supportsClipboard(clipboard)) {
|
||||
return;
|
||||
}
|
||||
if (!self.rt_surface.supportsClipboard(clipboard)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const buf = self.io.terminal.screen.selectionString(self.alloc, .{
|
||||
@@ -1189,7 +1233,11 @@ fn setCellSize(self: *Surface, size: renderer.CellSize) !void {
|
||||
}, .unlocked);
|
||||
|
||||
// Notify the window
|
||||
try self.rt_surface.setCellSize(size.width, size.height);
|
||||
try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.cell_size,
|
||||
.{ .width = size.width, .height = size.height },
|
||||
);
|
||||
}
|
||||
|
||||
/// Change the font size.
|
||||
@@ -1454,7 +1502,7 @@ pub fn keyCallback(
|
||||
// mod changes can affect link highlighting.
|
||||
self.mouse.link_point = null;
|
||||
const pos = self.rt_surface.getCursorPos() catch break :mouse_mods;
|
||||
self.cursorPosCallback(pos) catch {};
|
||||
self.cursorPosCallback(pos, null) catch {};
|
||||
if (rehide) self.mouse.hidden = true;
|
||||
}
|
||||
|
||||
@@ -1467,8 +1515,11 @@ pub fn keyCallback(
|
||||
.mods = self.mouse.mods,
|
||||
.over_link = self.mouse.over_link,
|
||||
.hidden = self.mouse.hidden,
|
||||
}).keyToMouseShape()) |shape|
|
||||
try self.rt_surface.setMouseShape(shape);
|
||||
}).keyToMouseShape()) |shape| try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.mouse_shape,
|
||||
shape,
|
||||
);
|
||||
|
||||
// We've processed a key event that produced some data so we want to
|
||||
// track the last pressed key.
|
||||
@@ -1556,19 +1607,8 @@ fn maybeHandleBinding(
|
||||
const entry: input.Binding.Set.Entry = entry: {
|
||||
const set = self.keyboard.bindings orelse &self.config.keybind.set;
|
||||
|
||||
var trigger: input.Binding.Trigger = .{
|
||||
.mods = event.mods.binding(),
|
||||
.key = .{ .translated = event.key },
|
||||
};
|
||||
if (set.get(trigger)) |v| break :entry v;
|
||||
|
||||
trigger.key = .{ .physical = event.physical_key };
|
||||
if (set.get(trigger)) |v| break :entry v;
|
||||
|
||||
if (event.unshifted_codepoint > 0) {
|
||||
trigger.key = .{ .unicode = event.unshifted_codepoint };
|
||||
if (set.get(trigger)) |v| break :entry v;
|
||||
}
|
||||
// Get our entry from the set for the given event.
|
||||
if (set.getEvent(event)) |v| break :entry v;
|
||||
|
||||
// No entry found. If we're not looking at the root set of the
|
||||
// bindings we need to encode everything up to this point and
|
||||
@@ -1585,7 +1625,7 @@ fn maybeHandleBinding(
|
||||
};
|
||||
|
||||
// Determine if this entry has an action or if its a leader key.
|
||||
const action: input.Binding.Action, const consumed: bool = switch (entry) {
|
||||
const leaf: input.Binding.Set.Leaf = switch (entry) {
|
||||
.leader => |set| {
|
||||
// Setup the next set we'll look at.
|
||||
self.keyboard.bindings = set;
|
||||
@@ -1600,8 +1640,20 @@ fn maybeHandleBinding(
|
||||
return .consumed;
|
||||
},
|
||||
|
||||
.action => |v| .{ v, true },
|
||||
.action_unconsumed => |v| .{ v, false },
|
||||
.leaf => |leaf| leaf,
|
||||
};
|
||||
const action = leaf.action;
|
||||
|
||||
// consumed determines if the input is consumed or if we continue
|
||||
// encoding the key (if we have a key to encode).
|
||||
const consumed = consumed: {
|
||||
// If the consumed flag is explicitly set, then we are consumed.
|
||||
if (leaf.flags.consumed) break :consumed true;
|
||||
|
||||
// If the global or all flag is set, we always consume.
|
||||
if (leaf.flags.global or leaf.flags.all) break :consumed true;
|
||||
|
||||
break :consumed false;
|
||||
};
|
||||
|
||||
// We have an action, so at this point we're handling SOMETHING so
|
||||
@@ -1613,8 +1665,22 @@ fn maybeHandleBinding(
|
||||
self.keyboard.bindings = null;
|
||||
|
||||
// Attempt to perform the action
|
||||
log.debug("key event binding consumed={} action={}", .{ consumed, action });
|
||||
const performed = try self.performBindingAction(action);
|
||||
log.debug("key event binding flags={} action={}", .{
|
||||
leaf.flags,
|
||||
action,
|
||||
});
|
||||
const performed = performed: {
|
||||
// If this is a global or all action, then we perform it on
|
||||
// the app and it applies to every surface.
|
||||
if (leaf.flags.global or leaf.flags.all) {
|
||||
try self.app.performAllAction(self.rt_app, action);
|
||||
|
||||
// "All" actions are always performed since they are global.
|
||||
break :performed true;
|
||||
}
|
||||
|
||||
break :performed try self.performBindingAction(action);
|
||||
};
|
||||
|
||||
// If we performed an action and it was a closing action,
|
||||
// our "self" pointer is not safe to use anymore so we need to
|
||||
@@ -2410,15 +2476,33 @@ pub fn mouseButtonCallback(
|
||||
if (mods.shift and
|
||||
self.mouse.left_click_count > 0 and
|
||||
!shift_capture)
|
||||
{
|
||||
extend_selection: {
|
||||
// We split this conditional out on its own because this is the
|
||||
// only one that requires a renderer mutex grab which is VERY
|
||||
// expensive because it could block all our threads.
|
||||
if (self.hasSelection()) {
|
||||
const pos = try self.rt_surface.getCursorPos();
|
||||
try self.cursorPosCallback(pos);
|
||||
return true;
|
||||
if (!self.hasSelection()) break :extend_selection;
|
||||
|
||||
// If we are within the interval that the click would register
|
||||
// an increment then we do not extend the selection.
|
||||
if (std.time.Instant.now()) |now| {
|
||||
const since = now.since(self.mouse.left_click_time);
|
||||
if (since <= self.config.mouse_interval) {
|
||||
// Click interval very short, we may be increasing
|
||||
// click counts so we don't extend the selection.
|
||||
break :extend_selection;
|
||||
}
|
||||
} else |err| {
|
||||
// This is a weird behavior, I think either behavior is actually
|
||||
// fine. This failure should be exceptionally rare anyways.
|
||||
// My thinking here is that we can't be sure if we should extend
|
||||
// the selection or not so we just don't.
|
||||
log.warn("failed to get time, not extending selection err={}", .{err});
|
||||
break :extend_selection;
|
||||
}
|
||||
|
||||
const pos = try self.rt_surface.getCursorPos();
|
||||
try self.cursorPosCallback(pos, null);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2882,9 +2966,18 @@ pub fn mousePressureCallback(
|
||||
}
|
||||
}
|
||||
|
||||
/// Cursor position callback.
|
||||
///
|
||||
/// The mods parameter is optional because some apprts do not provide
|
||||
/// modifier information on cursor position events. If mods is null then
|
||||
/// we'll use the last known mods. This is usually accurate since mod events
|
||||
/// will trigger key press events but on some platforms we don't get them.
|
||||
/// For example, on macOS, unfocused surfaces don't receive key events but
|
||||
/// do receive mouse events so we have to rely on updated mods.
|
||||
pub fn cursorPosCallback(
|
||||
self: *Surface,
|
||||
pos: apprt.CursorPos,
|
||||
mods: ?input.Mods,
|
||||
) !void {
|
||||
// Crash metadata in case we crash in here
|
||||
crash.sentry.thread_state = self.crashThreadState();
|
||||
@@ -2893,6 +2986,9 @@ pub fn cursorPosCallback(
|
||||
// Always show the mouse again if it is hidden
|
||||
if (self.mouse.hidden) self.showMouse();
|
||||
|
||||
// Update our modifiers if they changed
|
||||
if (mods) |v| self.modsChanged(v);
|
||||
|
||||
// The mouse position in the viewport
|
||||
const pos_vp = self.posToViewport(pos.x, pos.y);
|
||||
|
||||
@@ -2943,7 +3039,11 @@ pub fn cursorPosCallback(
|
||||
// We also queue a render so the renderer can undo the rendered link
|
||||
// state.
|
||||
if (over_link) {
|
||||
self.rt_surface.mouseOverLink(null);
|
||||
try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.mouse_over_link,
|
||||
.{ .url = "" },
|
||||
);
|
||||
try self.queueRender();
|
||||
}
|
||||
|
||||
@@ -3029,7 +3129,11 @@ pub fn cursorPosCallback(
|
||||
self.renderer_state.mouse.point = pos_vp;
|
||||
self.mouse.over_link = true;
|
||||
self.renderer_state.terminal.screen.dirty.hyperlink_hover = true;
|
||||
try self.rt_surface.setMouseShape(.pointer);
|
||||
try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.mouse_shape,
|
||||
.pointer,
|
||||
);
|
||||
|
||||
switch (link[0]) {
|
||||
.open => {
|
||||
@@ -3038,7 +3142,11 @@ pub fn cursorPosCallback(
|
||||
.trim = false,
|
||||
});
|
||||
defer self.alloc.free(str);
|
||||
self.rt_surface.mouseOverLink(str);
|
||||
try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.mouse_over_link,
|
||||
.{ .url = str },
|
||||
);
|
||||
},
|
||||
|
||||
._open_osc8 => link: {
|
||||
@@ -3048,14 +3156,26 @@ pub fn cursorPosCallback(
|
||||
log.warn("failed to get URI for OSC8 hyperlink", .{});
|
||||
break :link;
|
||||
};
|
||||
self.rt_surface.mouseOverLink(uri);
|
||||
try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.mouse_over_link,
|
||||
.{ .url = uri },
|
||||
);
|
||||
},
|
||||
}
|
||||
|
||||
try self.queueRender();
|
||||
} else if (over_link) {
|
||||
try self.rt_surface.setMouseShape(self.io.terminal.mouse_shape);
|
||||
self.rt_surface.mouseOverLink(null);
|
||||
try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.mouse_shape,
|
||||
self.io.terminal.mouse_shape,
|
||||
);
|
||||
try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.mouse_over_link,
|
||||
.{ .url = "" },
|
||||
);
|
||||
try self.queueRender();
|
||||
}
|
||||
}
|
||||
@@ -3364,13 +3484,25 @@ fn scrollToBottom(self: *Surface) !void {
|
||||
fn hideMouse(self: *Surface) void {
|
||||
if (self.mouse.hidden) return;
|
||||
self.mouse.hidden = true;
|
||||
self.rt_surface.setMouseVisibility(false);
|
||||
self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.mouse_visibility,
|
||||
.hidden,
|
||||
) catch |err| {
|
||||
log.warn("apprt failed to set mouse visibility err={}", .{err});
|
||||
};
|
||||
}
|
||||
|
||||
fn showMouse(self: *Surface) void {
|
||||
if (!self.mouse.hidden) return;
|
||||
self.mouse.hidden = false;
|
||||
self.rt_surface.setMouseVisibility(true);
|
||||
self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.mouse_visibility,
|
||||
.visible,
|
||||
) catch |err| {
|
||||
log.warn("apprt failed to set mouse visibility err={}", .{err});
|
||||
};
|
||||
}
|
||||
|
||||
/// Perform a binding action. A binding is a keybinding. This function
|
||||
@@ -3384,14 +3516,25 @@ fn showMouse(self: *Surface) void {
|
||||
/// will ever return false. We can expand this in the future if it becomes
|
||||
/// useful. We did previous/next tab so we could implement #498.
|
||||
pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool {
|
||||
switch (action) {
|
||||
.unbind => unreachable,
|
||||
.ignore => {},
|
||||
// Forward app-scoped actions to the app. Some app-scoped actions are
|
||||
// special-cased here because they do some special things when performed
|
||||
// from the surface.
|
||||
if (action.scoped(.app)) |app_action| {
|
||||
switch (app_action) {
|
||||
.new_window => try self.app.newWindow(
|
||||
self.rt_app,
|
||||
.{ .parent = self },
|
||||
),
|
||||
|
||||
.open_config => try self.app.openConfig(self.rt_app),
|
||||
|
||||
.reload_config => try self.app.reloadConfig(self.rt_app),
|
||||
else => try self.app.performAction(
|
||||
self.rt_app,
|
||||
action.scoped(.app).?,
|
||||
),
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (action.scoped(.surface).?) {
|
||||
.csi, .esc => |data| {
|
||||
// We need to send the CSI/ESC sequence as a single write request.
|
||||
// If you split it across two then the shell can interpret it
|
||||
@@ -3613,109 +3756,105 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
|
||||
v,
|
||||
),
|
||||
|
||||
.new_window => try self.app.newWindow(self.rt_app, .{ .parent = self }),
|
||||
.new_tab => try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.new_tab,
|
||||
{},
|
||||
),
|
||||
|
||||
.new_tab => {
|
||||
if (@hasDecl(apprt.Surface, "newTab")) {
|
||||
try self.rt_surface.newTab();
|
||||
} else log.warn("runtime doesn't implement newTab", .{});
|
||||
},
|
||||
inline .previous_tab,
|
||||
.next_tab,
|
||||
.last_tab,
|
||||
.goto_tab,
|
||||
=> |v, tag| try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.goto_tab,
|
||||
switch (tag) {
|
||||
.previous_tab => .previous,
|
||||
.next_tab => .next,
|
||||
.last_tab => .last,
|
||||
.goto_tab => @enumFromInt(v),
|
||||
else => comptime unreachable,
|
||||
},
|
||||
),
|
||||
|
||||
.previous_tab => {
|
||||
if (@hasDecl(apprt.Surface, "hasTabs")) {
|
||||
if (!self.rt_surface.hasTabs()) {
|
||||
log.debug("surface has no tabs, ignoring previous_tab binding", .{});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
.new_split => |direction| try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.new_split,
|
||||
switch (direction) {
|
||||
.right => .right,
|
||||
.down => .down,
|
||||
.auto => if (self.screen_size.width > self.screen_size.height)
|
||||
.right
|
||||
else
|
||||
.down,
|
||||
},
|
||||
),
|
||||
|
||||
if (@hasDecl(apprt.Surface, "gotoTab")) {
|
||||
self.rt_surface.gotoTab(.previous);
|
||||
} else log.warn("runtime doesn't implement gotoTab", .{});
|
||||
},
|
||||
.goto_split => |direction| try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.goto_split,
|
||||
switch (direction) {
|
||||
inline else => |tag| @field(
|
||||
apprt.action.GotoSplit,
|
||||
@tagName(tag),
|
||||
),
|
||||
},
|
||||
),
|
||||
|
||||
.next_tab => {
|
||||
if (@hasDecl(apprt.Surface, "hasTabs")) {
|
||||
if (!self.rt_surface.hasTabs()) {
|
||||
log.debug("surface has no tabs, ignoring next_tab binding", .{});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
.resize_split => |value| try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.resize_split,
|
||||
.{
|
||||
.amount = value[1],
|
||||
.direction = switch (value[0]) {
|
||||
inline else => |tag| @field(
|
||||
apprt.action.ResizeSplit.Direction,
|
||||
@tagName(tag),
|
||||
),
|
||||
},
|
||||
},
|
||||
),
|
||||
|
||||
if (@hasDecl(apprt.Surface, "gotoTab")) {
|
||||
self.rt_surface.gotoTab(.next);
|
||||
} else log.warn("runtime doesn't implement gotoTab", .{});
|
||||
},
|
||||
.equalize_splits => try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.equalize_splits,
|
||||
{},
|
||||
),
|
||||
|
||||
.last_tab => {
|
||||
if (@hasDecl(apprt.Surface, "hasTabs")) {
|
||||
if (!self.rt_surface.hasTabs()) {
|
||||
log.debug("surface has no tabs, ignoring last_tab binding", .{});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
.toggle_split_zoom => try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.toggle_split_zoom,
|
||||
{},
|
||||
),
|
||||
|
||||
if (@hasDecl(apprt.Surface, "gotoTab")) {
|
||||
self.rt_surface.gotoTab(.last);
|
||||
} else log.warn("runtime doesn't implement gotoTab", .{});
|
||||
},
|
||||
.toggle_fullscreen => try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.toggle_fullscreen,
|
||||
switch (self.config.macos_non_native_fullscreen) {
|
||||
.false => .native,
|
||||
.true => .macos_non_native,
|
||||
.@"visible-menu" => .macos_non_native_visible_menu,
|
||||
},
|
||||
),
|
||||
|
||||
.goto_tab => |n| {
|
||||
if (@hasDecl(apprt.Surface, "gotoTab")) {
|
||||
self.rt_surface.gotoTab(@enumFromInt(n));
|
||||
} else log.warn("runtime doesn't implement gotoTab", .{});
|
||||
},
|
||||
.toggle_window_decorations => try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.toggle_window_decorations,
|
||||
{},
|
||||
),
|
||||
|
||||
.new_split => |direction| {
|
||||
if (@hasDecl(apprt.Surface, "newSplit")) {
|
||||
try self.rt_surface.newSplit(switch (direction) {
|
||||
.right => .right,
|
||||
.down => .down,
|
||||
.auto => if (self.screen_size.width > self.screen_size.height)
|
||||
.right
|
||||
else
|
||||
.down,
|
||||
});
|
||||
} else log.warn("runtime doesn't implement newSplit", .{});
|
||||
},
|
||||
.toggle_tab_overview => try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.toggle_tab_overview,
|
||||
{},
|
||||
),
|
||||
|
||||
.goto_split => |direction| {
|
||||
if (@hasDecl(apprt.Surface, "gotoSplit")) {
|
||||
self.rt_surface.gotoSplit(direction);
|
||||
} else log.warn("runtime doesn't implement gotoSplit", .{});
|
||||
},
|
||||
|
||||
.resize_split => |param| {
|
||||
if (@hasDecl(apprt.Surface, "resizeSplit")) {
|
||||
const direction = param[0];
|
||||
const amount = param[1];
|
||||
self.rt_surface.resizeSplit(direction, amount);
|
||||
} else log.warn("runtime doesn't implement resizeSplit", .{});
|
||||
},
|
||||
|
||||
.equalize_splits => {
|
||||
if (@hasDecl(apprt.Surface, "equalizeSplits")) {
|
||||
self.rt_surface.equalizeSplits();
|
||||
} else log.warn("runtime doesn't implement equalizeSplits", .{});
|
||||
},
|
||||
|
||||
.toggle_split_zoom => {
|
||||
if (@hasDecl(apprt.Surface, "toggleSplitZoom")) {
|
||||
self.rt_surface.toggleSplitZoom();
|
||||
} else log.warn("runtime doesn't implement toggleSplitZoom", .{});
|
||||
},
|
||||
|
||||
.toggle_fullscreen => {
|
||||
if (@hasDecl(apprt.Surface, "toggleFullscreen")) {
|
||||
self.rt_surface.toggleFullscreen(self.config.macos_non_native_fullscreen);
|
||||
} else log.warn("runtime doesn't implement toggleFullscreen", .{});
|
||||
},
|
||||
|
||||
.toggle_window_decorations => {
|
||||
if (@hasDecl(apprt.Surface, "toggleWindowDecorations")) {
|
||||
self.rt_surface.toggleWindowDecorations();
|
||||
} else log.warn("runtime doesn't implement toggleWindowDecorations", .{});
|
||||
},
|
||||
.toggle_secure_input => try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.secure_input,
|
||||
.toggle,
|
||||
),
|
||||
|
||||
.select_all => {
|
||||
const sel = self.io.terminal.screen.selectAll();
|
||||
@@ -3725,24 +3864,21 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
|
||||
}
|
||||
},
|
||||
|
||||
.inspector => |mode| {
|
||||
if (@hasDecl(apprt.Surface, "controlInspector")) {
|
||||
self.rt_surface.controlInspector(mode);
|
||||
} else log.warn("runtime doesn't implement controlInspector", .{});
|
||||
},
|
||||
.inspector => |mode| try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.inspector,
|
||||
switch (mode) {
|
||||
inline else => |tag| @field(
|
||||
apprt.action.Inspector,
|
||||
@tagName(tag),
|
||||
),
|
||||
},
|
||||
),
|
||||
|
||||
.close_surface => self.close(),
|
||||
|
||||
.close_window => try self.app.closeSurface(self),
|
||||
|
||||
.close_all_windows => {
|
||||
if (@hasDecl(apprt.Surface, "closeAllWindows")) {
|
||||
self.rt_surface.closeAllWindows();
|
||||
} else log.warn("runtime doesn't implement closeAllWindows", .{});
|
||||
},
|
||||
|
||||
.quit => try self.app.setQuit(),
|
||||
|
||||
.crash => |location| switch (location) {
|
||||
.main => @panic("crash binding action, crashing intentionally"),
|
||||
|
||||
@@ -4124,11 +4260,6 @@ fn completeClipboardReadOSC52(
|
||||
}
|
||||
|
||||
fn showDesktopNotification(self: *Surface, title: [:0]const u8, body: [:0]const u8) !void {
|
||||
if (comptime !@hasDecl(apprt.Surface, "showDesktopNotification")) {
|
||||
log.warn("runtime doesn't support desktop notifications", .{});
|
||||
return;
|
||||
}
|
||||
|
||||
// Wyhash is used to hash the contents of the desktop notification to limit
|
||||
// how fast identical notifications can be sent sequentially.
|
||||
const hash_algorithm = std.hash.Wyhash;
|
||||
@@ -4164,7 +4295,14 @@ fn showDesktopNotification(self: *Surface, title: [:0]const u8, body: [:0]const
|
||||
|
||||
self.app.last_notification_time = now;
|
||||
self.app.last_notification_digest = new_digest;
|
||||
try self.rt_surface.showDesktopNotification(title, body);
|
||||
try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.desktop_notification,
|
||||
.{
|
||||
.title = title,
|
||||
.body = body,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn crashThreadState(self: *Surface) crash.sentry.ThreadState {
|
||||
@@ -4177,9 +4315,11 @@ fn crashThreadState(self: *Surface) crash.sentry.ThreadState {
|
||||
/// Tell the surface to present itself to the user. This may involve raising the
|
||||
/// window and switching tabs.
|
||||
fn presentSurface(self: *Surface) !void {
|
||||
if (@hasDecl(apprt.Surface, "presentSurface")) {
|
||||
self.rt_surface.presentSurface();
|
||||
} else log.warn("runtime doesn't support presentSurface", .{});
|
||||
try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.present_terminal,
|
||||
{},
|
||||
);
|
||||
}
|
||||
|
||||
pub const face_ttf = @embedFile("font/res/JetBrainsMono-Regular.ttf");
|
||||
|
||||
@@ -14,6 +14,7 @@ const build_config = @import("build_config.zig");
|
||||
|
||||
const structs = @import("apprt/structs.zig");
|
||||
|
||||
pub const action = @import("apprt/action.zig");
|
||||
pub const glfw = @import("apprt/glfw.zig");
|
||||
pub const gtk = @import("apprt/gtk.zig");
|
||||
pub const none = @import("apprt/none.zig");
|
||||
@@ -21,17 +22,17 @@ pub const browser = @import("apprt/browser.zig");
|
||||
pub const embedded = @import("apprt/embedded.zig");
|
||||
pub const surface = @import("apprt/surface.zig");
|
||||
|
||||
pub const Action = action.Action;
|
||||
pub const Target = action.Target;
|
||||
|
||||
pub const ContentScale = structs.ContentScale;
|
||||
pub const Clipboard = structs.Clipboard;
|
||||
pub const ClipboardRequest = structs.ClipboardRequest;
|
||||
pub const ClipboardRequestType = structs.ClipboardRequestType;
|
||||
pub const ColorScheme = structs.ColorScheme;
|
||||
pub const CursorPos = structs.CursorPos;
|
||||
pub const DesktopNotification = structs.DesktopNotification;
|
||||
pub const GotoTab = structs.GotoTab;
|
||||
pub const IMEPos = structs.IMEPos;
|
||||
pub const Selection = structs.Selection;
|
||||
pub const SplitDirection = structs.SplitDirection;
|
||||
pub const SurfaceSize = structs.SurfaceSize;
|
||||
|
||||
/// The implementation to use for the app runtime. This is comptime chosen
|
||||
@@ -84,4 +85,6 @@ pub const Runtime = enum {
|
||||
test {
|
||||
_ = Runtime;
|
||||
_ = runtime;
|
||||
_ = action;
|
||||
_ = structs;
|
||||
}
|
||||
|
||||
407
src/apprt/action.zig
Normal file
407
src/apprt/action.zig
Normal file
@@ -0,0 +1,407 @@
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const apprt = @import("../apprt.zig");
|
||||
const renderer = @import("../renderer.zig");
|
||||
const terminal = @import("../terminal/main.zig");
|
||||
const CoreSurface = @import("../Surface.zig");
|
||||
|
||||
/// The target for an action. This is generally the thing that had focus
|
||||
/// while the action was made but the concept of "focus" is not guaranteed
|
||||
/// since actions can also be triggered by timers, scripts, etc.
|
||||
pub const Target = union(Key) {
|
||||
app,
|
||||
surface: *CoreSurface,
|
||||
|
||||
// Sync with: ghostty_target_tag_e
|
||||
pub const Key = enum(c_int) {
|
||||
app,
|
||||
surface,
|
||||
};
|
||||
|
||||
// Sync with: ghostty_target_u
|
||||
pub const CValue = extern union {
|
||||
app: void,
|
||||
surface: *apprt.Surface,
|
||||
};
|
||||
|
||||
// Sync with: ghostty_target_s
|
||||
pub const C = extern struct {
|
||||
key: Key,
|
||||
value: CValue,
|
||||
};
|
||||
|
||||
/// Convert to ghostty_target_s.
|
||||
pub fn cval(self: Target) C {
|
||||
return .{
|
||||
.key = @as(Key, self),
|
||||
.value = switch (self) {
|
||||
.app => .{ .app = {} },
|
||||
.surface => |v| .{ .surface = v.rt_surface },
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/// The possible actions an apprt has to react to. Actions are one-way
|
||||
/// messages that are sent to the app runtime to trigger some behavior.
|
||||
///
|
||||
/// Actions are very often key binding actions but can also be triggered
|
||||
/// by lifecycle events. For example, the `quit_timer` action is not bindable.
|
||||
///
|
||||
/// Importantly, actions are generally OPTIONAL to implement by an apprt.
|
||||
/// Required functionality is called directly on the runtime structure so
|
||||
/// there is a compiler error if an action is not implemented.
|
||||
pub const Action = union(Key) {
|
||||
// A GUIDE TO ADDING NEW ACTIONS:
|
||||
//
|
||||
// 1. Add the action to the `Key` enum. The order of the enum matters
|
||||
// because it maps directly to the libghostty C enum. For ABI
|
||||
// compatibility, new actions should be added to the end of the enum.
|
||||
//
|
||||
// 2. Add the action and optional value to the Action union.
|
||||
//
|
||||
// 3. If the value type is not void, ensure the value is C ABI
|
||||
// compatible (extern). If it is not, add a `C` decl to the value
|
||||
// and a `cval` function to convert to the C ABI compatible value.
|
||||
//
|
||||
// 4. Update `include/ghostty.h`: add the new key, value, and union
|
||||
// entry. If the value type is void then only the key needs to be
|
||||
// added. Ensure the order matches exactly with the Zig code.
|
||||
|
||||
/// Open a new window. The target determines whether properties such
|
||||
/// as font size should be inherited.
|
||||
new_window,
|
||||
|
||||
/// Open a new tab. If the target is a surface it should be opened in
|
||||
/// the same window as the surface. If the target is the app then
|
||||
/// the tab should be opened in a new window.
|
||||
new_tab,
|
||||
|
||||
/// Create a new split. The value determines the location of the split
|
||||
/// relative to the target.
|
||||
new_split: SplitDirection,
|
||||
|
||||
/// Close all open windows.
|
||||
close_all_windows,
|
||||
|
||||
/// Toggle fullscreen mode.
|
||||
toggle_fullscreen: Fullscreen,
|
||||
|
||||
/// Toggle tab overview.
|
||||
toggle_tab_overview,
|
||||
|
||||
/// Toggle whether window directions are shown.
|
||||
toggle_window_decorations,
|
||||
|
||||
/// Toggle the quick terminal in or out.
|
||||
toggle_quick_terminal,
|
||||
|
||||
/// Jump to a specific tab. Must handle the scenario that the tab
|
||||
/// value is invalid.
|
||||
goto_tab: GotoTab,
|
||||
|
||||
/// Jump to a specific split.
|
||||
goto_split: GotoSplit,
|
||||
|
||||
/// Resize the split in the given direction.
|
||||
resize_split: ResizeSplit,
|
||||
|
||||
/// Equalize all the splits in the target window.
|
||||
equalize_splits,
|
||||
|
||||
/// Toggle whether a split is zoomed or not. A zoomed split is resized
|
||||
/// to take up the entire window.
|
||||
toggle_split_zoom,
|
||||
|
||||
/// Present the target terminal whether its a tab, split, or window.
|
||||
present_terminal,
|
||||
|
||||
/// Sets a size limit (in pixels) for the target terminal.
|
||||
size_limit: SizeLimit,
|
||||
|
||||
/// Specifies the initial size of the target terminal. This will be
|
||||
/// sent only during the initialization of a surface. If it is received
|
||||
/// after the surface is initialized it should be ignored.
|
||||
initial_size: InitialSize,
|
||||
|
||||
/// The cell size has changed to the given dimensions in pixels.
|
||||
cell_size: CellSize,
|
||||
|
||||
/// Control whether the inspector is shown or hidden.
|
||||
inspector: Inspector,
|
||||
|
||||
/// The inspector for the given target has changes and should be
|
||||
/// rendered at the next opportunity.
|
||||
render_inspector,
|
||||
|
||||
/// Show a desktop notification.
|
||||
desktop_notification: DesktopNotification,
|
||||
|
||||
/// Set the title of the target.
|
||||
set_title: SetTitle,
|
||||
|
||||
/// Set the mouse cursor shape.
|
||||
mouse_shape: terminal.MouseShape,
|
||||
|
||||
/// Set whether the mouse cursor is visible or not.
|
||||
mouse_visibility: MouseVisibility,
|
||||
|
||||
/// Called when the mouse is over or recently left a link.
|
||||
mouse_over_link: MouseOverLink,
|
||||
|
||||
/// The health of the renderer has changed.
|
||||
renderer_health: renderer.Health,
|
||||
|
||||
/// Open the Ghostty configuration. This is platform-specific about
|
||||
/// what it means; it can mean opening a dedicated UI or just opening
|
||||
/// a file in a text editor.
|
||||
open_config,
|
||||
|
||||
/// Called when there are no more surfaces and the app should quit
|
||||
/// after the configured delay. This can be cancelled by sending
|
||||
/// another quit_timer action with "stop". Multiple "starts" shouldn't
|
||||
/// happen and can be ignored or cause a restart it isn't that important.
|
||||
quit_timer: QuitTimer,
|
||||
|
||||
/// Set the secure input functionality on or off. "Secure input" means
|
||||
/// that the user is currently at some sort of prompt where they may be
|
||||
/// entering a password or other sensitive information. This can be used
|
||||
/// by the app runtime to change the appearance of the cursor, setup
|
||||
/// system APIs to not log the input, etc.
|
||||
secure_input: SecureInput,
|
||||
|
||||
/// Sync with: ghostty_action_tag_e
|
||||
pub const Key = enum(c_int) {
|
||||
new_window,
|
||||
new_tab,
|
||||
new_split,
|
||||
close_all_windows,
|
||||
toggle_fullscreen,
|
||||
toggle_tab_overview,
|
||||
toggle_window_decorations,
|
||||
toggle_quick_terminal,
|
||||
goto_tab,
|
||||
goto_split,
|
||||
resize_split,
|
||||
equalize_splits,
|
||||
toggle_split_zoom,
|
||||
present_terminal,
|
||||
size_limit,
|
||||
initial_size,
|
||||
cell_size,
|
||||
inspector,
|
||||
render_inspector,
|
||||
desktop_notification,
|
||||
set_title,
|
||||
mouse_shape,
|
||||
mouse_visibility,
|
||||
mouse_over_link,
|
||||
renderer_health,
|
||||
open_config,
|
||||
quit_timer,
|
||||
secure_input,
|
||||
};
|
||||
|
||||
/// Sync with: ghostty_action_u
|
||||
pub const CValue = cvalue: {
|
||||
const key_fields = @typeInfo(Key).Enum.fields;
|
||||
var union_fields: [key_fields.len]std.builtin.Type.UnionField = undefined;
|
||||
for (key_fields, 0..) |field, i| {
|
||||
const action = @unionInit(Action, field.name, undefined);
|
||||
const Type = t: {
|
||||
const Type = @TypeOf(@field(action, field.name));
|
||||
// Types can provide custom types for their CValue.
|
||||
if (Type != void and @hasDecl(Type, "C")) break :t Type.C;
|
||||
break :t Type;
|
||||
};
|
||||
|
||||
union_fields[i] = .{
|
||||
.name = field.name,
|
||||
.type = Type,
|
||||
.alignment = @alignOf(Type),
|
||||
};
|
||||
}
|
||||
|
||||
break :cvalue @Type(.{ .Union = .{
|
||||
.layout = .@"extern",
|
||||
.tag_type = Key,
|
||||
.fields = &union_fields,
|
||||
.decls = &.{},
|
||||
} });
|
||||
};
|
||||
|
||||
/// Sync with: ghostty_action_s
|
||||
pub const C = extern struct {
|
||||
key: Key,
|
||||
value: CValue,
|
||||
};
|
||||
|
||||
/// Returns the value type for the given key.
|
||||
pub fn Value(comptime key: Key) type {
|
||||
inline for (@typeInfo(Action).Union.fields) |field| {
|
||||
const field_key = @field(Key, field.name);
|
||||
if (field_key == key) return field.type;
|
||||
}
|
||||
|
||||
unreachable;
|
||||
}
|
||||
|
||||
/// Convert to ghostty_action_s.
|
||||
pub fn cval(self: Action) C {
|
||||
const value: CValue = switch (self) {
|
||||
inline else => |v, tag| @unionInit(
|
||||
CValue,
|
||||
@tagName(tag),
|
||||
if (@TypeOf(v) != void and @hasDecl(@TypeOf(v), "cval")) v.cval() else v,
|
||||
),
|
||||
};
|
||||
|
||||
return .{
|
||||
.key = @as(Key, self),
|
||||
.value = value,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// This is made extern (c_int) to make interop easier with our embedded
|
||||
// runtime. The small size cost doesn't make a difference in our union.
|
||||
pub const SplitDirection = enum(c_int) {
|
||||
right,
|
||||
down,
|
||||
};
|
||||
|
||||
// This is made extern (c_int) to make interop easier with our embedded
|
||||
// runtime. The small size cost doesn't make a difference in our union.
|
||||
pub const GotoSplit = enum(c_int) {
|
||||
previous,
|
||||
next,
|
||||
|
||||
top,
|
||||
left,
|
||||
bottom,
|
||||
right,
|
||||
};
|
||||
|
||||
/// The amount to resize the split by and the direction to resize it in.
|
||||
pub const ResizeSplit = extern struct {
|
||||
amount: u16,
|
||||
direction: Direction,
|
||||
|
||||
pub const Direction = enum(c_int) {
|
||||
up,
|
||||
down,
|
||||
left,
|
||||
right,
|
||||
};
|
||||
};
|
||||
|
||||
/// The tab to jump to. This is non-exhaustive so that integer values represent
|
||||
/// the index (zero-based) of the tab to jump to. Negative values are special
|
||||
/// values.
|
||||
pub const GotoTab = enum(c_int) {
|
||||
previous = -1,
|
||||
next = -2,
|
||||
last = -3,
|
||||
_,
|
||||
};
|
||||
|
||||
/// The fullscreen mode to toggle to if we're moving to fullscreen.
|
||||
pub const Fullscreen = enum(c_int) {
|
||||
native,
|
||||
|
||||
/// macOS has a non-native fullscreen mode that is more like a maximized
|
||||
/// window. This is much faster to enter and exit than the native mode.
|
||||
macos_non_native,
|
||||
macos_non_native_visible_menu,
|
||||
};
|
||||
|
||||
pub const SecureInput = enum(c_int) {
|
||||
on,
|
||||
off,
|
||||
toggle,
|
||||
};
|
||||
|
||||
/// The inspector mode to toggle to if we're toggling the inspector.
|
||||
pub const Inspector = enum(c_int) {
|
||||
toggle,
|
||||
show,
|
||||
hide,
|
||||
};
|
||||
|
||||
pub const QuitTimer = enum(c_int) {
|
||||
start,
|
||||
stop,
|
||||
};
|
||||
|
||||
pub const MouseVisibility = enum(c_int) {
|
||||
visible,
|
||||
hidden,
|
||||
};
|
||||
|
||||
pub const MouseOverLink = struct {
|
||||
url: []const u8,
|
||||
|
||||
// Sync with: ghostty_action_mouse_over_link_s
|
||||
pub const C = extern struct {
|
||||
url: [*]const u8,
|
||||
len: usize,
|
||||
};
|
||||
|
||||
pub fn cval(self: MouseOverLink) C {
|
||||
return .{
|
||||
.url = self.url.ptr,
|
||||
.len = self.url.len,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub const SizeLimit = extern struct {
|
||||
min_width: u32,
|
||||
min_height: u32,
|
||||
max_width: u32,
|
||||
max_height: u32,
|
||||
};
|
||||
|
||||
pub const InitialSize = extern struct {
|
||||
width: u32,
|
||||
height: u32,
|
||||
};
|
||||
|
||||
pub const CellSize = extern struct {
|
||||
width: u32,
|
||||
height: u32,
|
||||
};
|
||||
|
||||
pub const SetTitle = struct {
|
||||
title: [:0]const u8,
|
||||
|
||||
// Sync with: ghostty_action_set_title_s
|
||||
pub const C = extern struct {
|
||||
title: [*:0]const u8,
|
||||
};
|
||||
|
||||
pub fn cval(self: SetTitle) C {
|
||||
return .{
|
||||
.title = self.title.ptr,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/// The desktop notification to show.
|
||||
pub const DesktopNotification = struct {
|
||||
title: [:0]const u8,
|
||||
body: [:0]const u8,
|
||||
|
||||
// Sync with: ghostty_action_desktop_notification_s
|
||||
pub const C = extern struct {
|
||||
title: [*:0]const u8,
|
||||
body: [*:0]const u8,
|
||||
};
|
||||
|
||||
pub fn cval(self: DesktopNotification) C {
|
||||
return .{
|
||||
.title = self.title.ptr,
|
||||
.body = self.body.ptr,
|
||||
};
|
||||
}
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@@ -127,9 +127,87 @@ pub const App = struct {
|
||||
glfw.postEmptyEvent();
|
||||
}
|
||||
|
||||
/// Open the configuration in the system editor.
|
||||
pub fn openConfig(self: *App) !void {
|
||||
try configpkg.edit.open(self.app.alloc);
|
||||
/// Perform a given action.
|
||||
pub fn performAction(
|
||||
self: *App,
|
||||
target: apprt.Target,
|
||||
comptime action: apprt.Action.Key,
|
||||
value: apprt.Action.Value(action),
|
||||
) !void {
|
||||
switch (action) {
|
||||
.new_window => _ = try self.newSurface(switch (target) {
|
||||
.app => null,
|
||||
.surface => |v| v,
|
||||
}),
|
||||
|
||||
.new_tab => try self.newTab(switch (target) {
|
||||
.app => null,
|
||||
.surface => |v| v,
|
||||
}),
|
||||
|
||||
.size_limit => switch (target) {
|
||||
.app => {},
|
||||
.surface => |surface| try surface.rt_surface.setSizeLimits(.{
|
||||
.width = value.min_width,
|
||||
.height = value.min_height,
|
||||
}, if (value.max_width > 0) .{
|
||||
.width = value.max_width,
|
||||
.height = value.max_height,
|
||||
} else null),
|
||||
},
|
||||
|
||||
.initial_size => switch (target) {
|
||||
.app => {},
|
||||
.surface => |surface| try surface.rt_surface.setInitialWindowSize(
|
||||
value.width,
|
||||
value.height,
|
||||
),
|
||||
},
|
||||
|
||||
.toggle_fullscreen => self.toggleFullscreen(target),
|
||||
|
||||
.open_config => try configpkg.edit.open(self.app.alloc),
|
||||
|
||||
.set_title => switch (target) {
|
||||
.app => {},
|
||||
.surface => |surface| try surface.rt_surface.setTitle(value.title),
|
||||
},
|
||||
|
||||
.mouse_shape => switch (target) {
|
||||
.app => {},
|
||||
.surface => |surface| try surface.rt_surface.setMouseShape(value),
|
||||
},
|
||||
|
||||
.mouse_visibility => switch (target) {
|
||||
.app => {},
|
||||
.surface => |surface| surface.rt_surface.setMouseVisibility(switch (value) {
|
||||
.visible => true,
|
||||
.hidden => false,
|
||||
}),
|
||||
},
|
||||
|
||||
// Unimplemented
|
||||
.new_split,
|
||||
.goto_split,
|
||||
.resize_split,
|
||||
.equalize_splits,
|
||||
.toggle_split_zoom,
|
||||
.present_terminal,
|
||||
.close_all_windows,
|
||||
.toggle_tab_overview,
|
||||
.toggle_window_decorations,
|
||||
.toggle_quick_terminal,
|
||||
.goto_tab,
|
||||
.inspector,
|
||||
.render_inspector,
|
||||
.quit_timer,
|
||||
.secure_input,
|
||||
.desktop_notification,
|
||||
.mouse_over_link,
|
||||
.cell_size,
|
||||
.renderer_health,
|
||||
=> log.info("unimplemented action={}", .{action}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Reload the configuration. This should return the new configuration.
|
||||
@@ -150,8 +228,12 @@ pub const App = struct {
|
||||
}
|
||||
|
||||
/// Toggle the window to fullscreen mode.
|
||||
pub fn toggleFullscreen(self: *App, surface: *Surface) void {
|
||||
fn toggleFullscreen(self: *App, target: apprt.Target) void {
|
||||
_ = self;
|
||||
const surface: *Surface = switch (target) {
|
||||
.app => return,
|
||||
.surface => |v| v.rt_surface,
|
||||
};
|
||||
const win = surface.window;
|
||||
|
||||
if (surface.isFullscreen()) {
|
||||
@@ -195,18 +277,18 @@ pub const App = struct {
|
||||
win.setMonitor(monitor, 0, 0, video_mode.getWidth(), video_mode.getHeight(), 0);
|
||||
}
|
||||
|
||||
/// Create a new window for the app.
|
||||
pub fn newWindow(self: *App, parent_: ?*CoreSurface) !void {
|
||||
_ = try self.newSurface(parent_);
|
||||
}
|
||||
|
||||
/// Create a new tab in the parent surface.
|
||||
fn newTab(self: *App, parent: *CoreSurface) !void {
|
||||
fn newTab(self: *App, parent_: ?*CoreSurface) !void {
|
||||
if (!Darwin.enabled) {
|
||||
log.warn("tabbing is not supported on this platform", .{});
|
||||
return;
|
||||
}
|
||||
|
||||
const parent = parent_ orelse {
|
||||
_ = try self.newSurface(null);
|
||||
return;
|
||||
};
|
||||
|
||||
// Create the new window
|
||||
const window = try self.newSurface(parent);
|
||||
|
||||
@@ -370,7 +452,6 @@ pub const Surface = struct {
|
||||
/// Initialize the surface into the given self pointer. This gives a
|
||||
/// stable pointer to the destination that can be used for callbacks.
|
||||
pub fn init(self: *Surface, app: *App) !void {
|
||||
|
||||
// Create our window
|
||||
const win = glfw.Window.create(
|
||||
640,
|
||||
@@ -525,20 +606,11 @@ pub const Surface = struct {
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new tab in the window containing this surface.
|
||||
pub fn newTab(self: *Surface) !void {
|
||||
try self.app.newTab(&self.core_surface);
|
||||
}
|
||||
|
||||
/// Checks if the glfw window is in fullscreen.
|
||||
pub fn isFullscreen(self: *Surface) bool {
|
||||
return self.window.getMonitor() != null;
|
||||
}
|
||||
|
||||
pub fn toggleFullscreen(self: *Surface, _: Config.NonNativeFullscreen) void {
|
||||
self.app.toggleFullscreen(self);
|
||||
}
|
||||
|
||||
/// Close this surface.
|
||||
pub fn close(self: *Surface, processActive: bool) void {
|
||||
_ = processActive;
|
||||
@@ -550,7 +622,7 @@ pub const Surface = struct {
|
||||
/// Set the initial window size. This is called exactly once at
|
||||
/// surface initialization time. This may be called before "self"
|
||||
/// is fully initialized.
|
||||
pub fn setInitialWindowSize(self: *const Surface, width: u32, height: u32) !void {
|
||||
fn setInitialWindowSize(self: *const Surface, width: u32, height: u32) !void {
|
||||
const monitor = self.window.getMonitor() orelse glfw.Monitor.getPrimary() orelse {
|
||||
log.warn("window is not on a monitor, not setting initial size", .{});
|
||||
return;
|
||||
@@ -563,18 +635,11 @@ pub const Surface = struct {
|
||||
});
|
||||
}
|
||||
|
||||
/// Set the cell size. Unused by GLFW.
|
||||
pub fn setCellSize(self: *const Surface, width: u32, height: u32) !void {
|
||||
_ = self;
|
||||
_ = width;
|
||||
_ = height;
|
||||
}
|
||||
|
||||
/// Set the size limits of the window.
|
||||
/// Note: this interface is not good, we should redo it if we plan
|
||||
/// to use this more. i.e. you can't set max width but no max height,
|
||||
/// or no mins.
|
||||
pub fn setSizeLimits(self: *Surface, min: apprt.SurfaceSize, max_: ?apprt.SurfaceSize) !void {
|
||||
fn setSizeLimits(self: *Surface, min: apprt.SurfaceSize, max_: ?apprt.SurfaceSize) !void {
|
||||
self.window.setSizeLimits(.{
|
||||
.width = min.width,
|
||||
.height = min.height,
|
||||
@@ -624,7 +689,7 @@ pub const Surface = struct {
|
||||
}
|
||||
|
||||
/// Set the title of the window.
|
||||
pub fn setTitle(self: *Surface, slice: [:0]const u8) !void {
|
||||
fn setTitle(self: *Surface, slice: [:0]const u8) !void {
|
||||
if (self.title_text) |t| self.core_surface.alloc.free(t);
|
||||
self.title_text = try self.core_surface.alloc.dupeZ(u8, slice);
|
||||
self.window.setTitle(self.title_text.?.ptr);
|
||||
@@ -636,7 +701,7 @@ pub const Surface = struct {
|
||||
}
|
||||
|
||||
/// Set the shape of the cursor.
|
||||
pub fn setMouseShape(self: *Surface, shape: terminal.MouseShape) !void {
|
||||
fn setMouseShape(self: *Surface, shape: terminal.MouseShape) !void {
|
||||
if ((comptime builtin.target.isDarwin()) and
|
||||
!internal_os.macosVersionAtLeast(13, 0, 0))
|
||||
{
|
||||
@@ -672,15 +737,20 @@ pub const Surface = struct {
|
||||
self.cursor = new;
|
||||
}
|
||||
|
||||
pub fn mouseOverLink(self: *Surface, uri: ?[]const u8) void {
|
||||
// We don't do anything in GLFW.
|
||||
_ = self;
|
||||
_ = uri;
|
||||
/// Set the visibility of the mouse cursor.
|
||||
fn setMouseVisibility(self: *Surface, visible: bool) void {
|
||||
self.window.setInputModeCursor(if (visible) .normal else .hidden);
|
||||
}
|
||||
|
||||
/// Set the visibility of the mouse cursor.
|
||||
pub fn setMouseVisibility(self: *Surface, visible: bool) void {
|
||||
self.window.setInputModeCursor(if (visible) .normal else .hidden);
|
||||
pub fn supportsClipboard(
|
||||
self: *const Surface,
|
||||
clipboard_type: apprt.Clipboard,
|
||||
) bool {
|
||||
_ = self;
|
||||
return switch (clipboard_type) {
|
||||
.standard => true,
|
||||
.selection, .primary => comptime builtin.os.tag == .linux,
|
||||
};
|
||||
}
|
||||
|
||||
/// Start an async clipboard request.
|
||||
@@ -1040,7 +1110,7 @@ pub const Surface = struct {
|
||||
core_win.cursorPosCallback(.{
|
||||
.x = @floatCast(pos.xpos),
|
||||
.y = @floatCast(pos.ypos),
|
||||
}) catch |err| {
|
||||
}, null) catch |err| {
|
||||
log.err("error in cursor pos callback err={}", .{err});
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -17,6 +17,7 @@ const apprt = @import("../../apprt.zig");
|
||||
const configpkg = @import("../../config.zig");
|
||||
const input = @import("../../input.zig");
|
||||
const internal_os = @import("../../os/main.zig");
|
||||
const terminal = @import("../../terminal/main.zig");
|
||||
const Config = configpkg.Config;
|
||||
const CoreApp = @import("../../App.zig");
|
||||
const CoreSurface = @import("../../Surface.zig");
|
||||
@@ -27,6 +28,7 @@ const Surface = @import("Surface.zig");
|
||||
const Window = @import("Window.zig");
|
||||
const ConfigErrorsWindow = @import("ConfigErrorsWindow.zig");
|
||||
const ClipboardConfirmationWindow = @import("ClipboardConfirmationWindow.zig");
|
||||
const Split = @import("Split.zig");
|
||||
const c = @import("c.zig").c;
|
||||
const version = @import("version.zig");
|
||||
const inspector = @import("inspector.zig");
|
||||
@@ -86,6 +88,16 @@ quit_timer: union(enum) {
|
||||
pub fn init(core_app: *CoreApp, opts: Options) !App {
|
||||
_ = opts;
|
||||
|
||||
// Log our GTK version
|
||||
log.info("GTK version build={d}.{d}.{d} runtime={d}.{d}.{d}", .{
|
||||
c.GTK_MAJOR_VERSION,
|
||||
c.GTK_MINOR_VERSION,
|
||||
c.GTK_MICRO_VERSION,
|
||||
c.gtk_get_major_version(),
|
||||
c.gtk_get_minor_version(),
|
||||
c.gtk_get_micro_version(),
|
||||
});
|
||||
|
||||
if (version.atLeast(4, 16, 0)) {
|
||||
// From gtk 4.16, GDK_DEBUG is split into GDK_DEBUG and GDK_DISABLE
|
||||
_ = internal_os.setenv("GDK_DISABLE", "gles-api");
|
||||
@@ -340,9 +352,340 @@ pub fn terminate(self: *App) void {
|
||||
self.config.deinit();
|
||||
}
|
||||
|
||||
/// Open the configuration in the system editor.
|
||||
pub fn openConfig(self: *App) !void {
|
||||
try configpkg.edit.open(self.core_app.alloc);
|
||||
/// Perform a given action.
|
||||
pub fn performAction(
|
||||
self: *App,
|
||||
target: apprt.Target,
|
||||
comptime action: apprt.Action.Key,
|
||||
value: apprt.Action.Value(action),
|
||||
) !void {
|
||||
switch (action) {
|
||||
.new_window => _ = try self.newWindow(switch (target) {
|
||||
.app => null,
|
||||
.surface => |v| v,
|
||||
}),
|
||||
.toggle_fullscreen => self.toggleFullscreen(target, value),
|
||||
|
||||
.new_tab => try self.newTab(target),
|
||||
.goto_tab => self.gotoTab(target, value),
|
||||
.new_split => try self.newSplit(target, value),
|
||||
.resize_split => self.resizeSplit(target, value),
|
||||
.equalize_splits => self.equalizeSplits(target),
|
||||
.goto_split => self.gotoSplit(target, value),
|
||||
.open_config => try configpkg.edit.open(self.core_app.alloc),
|
||||
.inspector => self.controlInspector(target, value),
|
||||
.desktop_notification => self.showDesktopNotification(target, value),
|
||||
.set_title => try self.setTitle(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_window_decorations => self.toggleWindowDecorations(target),
|
||||
.quit_timer => self.quitTimer(value),
|
||||
|
||||
// Unimplemented
|
||||
.close_all_windows,
|
||||
.toggle_split_zoom,
|
||||
.toggle_quick_terminal,
|
||||
.size_limit,
|
||||
.cell_size,
|
||||
.secure_input,
|
||||
.render_inspector,
|
||||
.renderer_health,
|
||||
=> log.warn("unimplemented action={}", .{action}),
|
||||
}
|
||||
}
|
||||
|
||||
fn newTab(_: *App, target: apprt.Target) !void {
|
||||
switch (target) {
|
||||
.app => {},
|
||||
.surface => |v| {
|
||||
const window = v.rt_surface.container.window() orelse {
|
||||
log.info(
|
||||
"new_tab invalid for container={s}",
|
||||
.{@tagName(v.rt_surface.container)},
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
try window.newTab(v);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn gotoTab(_: *App, target: apprt.Target, tab: apprt.action.GotoTab) void {
|
||||
switch (target) {
|
||||
.app => {},
|
||||
.surface => |v| {
|
||||
const window = v.rt_surface.container.window() orelse {
|
||||
log.info(
|
||||
"gotoTab invalid for container={s}",
|
||||
.{@tagName(v.rt_surface.container)},
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
switch (tab) {
|
||||
.previous => window.gotoPreviousTab(v.rt_surface),
|
||||
.next => window.gotoNextTab(v.rt_surface),
|
||||
.last => window.gotoLastTab(),
|
||||
else => window.gotoTab(@intCast(@intFromEnum(tab))),
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn newSplit(
|
||||
self: *App,
|
||||
target: apprt.Target,
|
||||
direction: apprt.action.SplitDirection,
|
||||
) !void {
|
||||
switch (target) {
|
||||
.app => {},
|
||||
.surface => |v| {
|
||||
const alloc = self.core_app.alloc;
|
||||
_ = try Split.create(alloc, v.rt_surface, direction);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn equalizeSplits(_: *App, target: apprt.Target) void {
|
||||
switch (target) {
|
||||
.app => {},
|
||||
.surface => |v| {
|
||||
const tab = v.rt_surface.container.tab() orelse return;
|
||||
const top_split = switch (tab.elem) {
|
||||
.split => |s| s,
|
||||
else => return,
|
||||
};
|
||||
_ = top_split.equalize();
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn gotoSplit(
|
||||
_: *const App,
|
||||
target: apprt.Target,
|
||||
direction: apprt.action.GotoSplit,
|
||||
) void {
|
||||
switch (target) {
|
||||
.app => {},
|
||||
.surface => |v| {
|
||||
const s = v.rt_surface.container.split() orelse return;
|
||||
const map = s.directionMap(switch (v.rt_surface.container) {
|
||||
.split_tl => .top_left,
|
||||
.split_br => .bottom_right,
|
||||
.none, .tab_ => unreachable,
|
||||
});
|
||||
const surface_ = map.get(direction) orelse return;
|
||||
if (surface_) |surface| surface.grabFocus();
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn resizeSplit(
|
||||
_: *const App,
|
||||
target: apprt.Target,
|
||||
resize: apprt.action.ResizeSplit,
|
||||
) void {
|
||||
switch (target) {
|
||||
.app => {},
|
||||
.surface => |v| {
|
||||
const s = v.rt_surface.container.firstSplitWithOrientation(
|
||||
Split.Orientation.fromResizeDirection(resize.direction),
|
||||
) orelse return;
|
||||
s.moveDivider(resize.direction, resize.amount);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn presentTerminal(
|
||||
_: *const App,
|
||||
target: apprt.Target,
|
||||
) void {
|
||||
switch (target) {
|
||||
.app => {},
|
||||
.surface => |v| v.rt_surface.present(),
|
||||
}
|
||||
}
|
||||
|
||||
fn controlInspector(
|
||||
_: *const App,
|
||||
target: apprt.Target,
|
||||
mode: apprt.action.Inspector,
|
||||
) void {
|
||||
const surface: *Surface = switch (target) {
|
||||
.app => return,
|
||||
.surface => |v| v.rt_surface,
|
||||
};
|
||||
|
||||
surface.controlInspector(mode);
|
||||
}
|
||||
|
||||
fn toggleFullscreen(
|
||||
_: *App,
|
||||
target: apprt.Target,
|
||||
_: apprt.action.Fullscreen,
|
||||
) void {
|
||||
switch (target) {
|
||||
.app => {},
|
||||
.surface => |v| {
|
||||
const window = v.rt_surface.container.window() orelse {
|
||||
log.info(
|
||||
"toggleFullscreen invalid for container={s}",
|
||||
.{@tagName(v.rt_surface.container)},
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
window.toggleFullscreen();
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn toggleTabOverview(_: *App, target: apprt.Target) void {
|
||||
switch (target) {
|
||||
.app => {},
|
||||
.surface => |v| {
|
||||
const window = v.rt_surface.container.window() orelse {
|
||||
log.info(
|
||||
"toggleTabOverview invalid for container={s}",
|
||||
.{@tagName(v.rt_surface.container)},
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
window.toggleTabOverview();
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn toggleWindowDecorations(
|
||||
_: *App,
|
||||
target: apprt.Target,
|
||||
) void {
|
||||
switch (target) {
|
||||
.app => {},
|
||||
.surface => |v| {
|
||||
const window = v.rt_surface.container.window() orelse {
|
||||
log.info(
|
||||
"toggleFullscreen invalid for container={s}",
|
||||
.{@tagName(v.rt_surface.container)},
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
window.toggleWindowDecorations();
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn quitTimer(self: *App, mode: apprt.action.QuitTimer) void {
|
||||
switch (mode) {
|
||||
.start => self.startQuitTimer(),
|
||||
.stop => self.stopQuitTimer(),
|
||||
}
|
||||
}
|
||||
|
||||
fn setTitle(
|
||||
_: *App,
|
||||
target: apprt.Target,
|
||||
title: apprt.action.SetTitle,
|
||||
) !void {
|
||||
switch (target) {
|
||||
.app => {},
|
||||
.surface => |v| try v.rt_surface.setTitle(title.title),
|
||||
}
|
||||
}
|
||||
|
||||
fn setMouseVisibility(
|
||||
_: *App,
|
||||
target: apprt.Target,
|
||||
visibility: apprt.action.MouseVisibility,
|
||||
) void {
|
||||
switch (target) {
|
||||
.app => {},
|
||||
.surface => |v| v.rt_surface.setMouseVisibility(switch (visibility) {
|
||||
.visible => true,
|
||||
.hidden => false,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn setMouseShape(
|
||||
_: *App,
|
||||
target: apprt.Target,
|
||||
shape: terminal.MouseShape,
|
||||
) !void {
|
||||
switch (target) {
|
||||
.app => {},
|
||||
.surface => |v| try v.rt_surface.setMouseShape(shape),
|
||||
}
|
||||
}
|
||||
|
||||
fn setMouseOverLink(
|
||||
_: *App,
|
||||
target: apprt.Target,
|
||||
value: apprt.action.MouseOverLink,
|
||||
) void {
|
||||
switch (target) {
|
||||
.app => {},
|
||||
.surface => |v| v.rt_surface.mouseOverLink(if (value.url.len > 0)
|
||||
value.url
|
||||
else
|
||||
null),
|
||||
}
|
||||
}
|
||||
|
||||
fn setInitialSize(
|
||||
_: *App,
|
||||
target: apprt.Target,
|
||||
value: apprt.action.InitialSize,
|
||||
) !void {
|
||||
switch (target) {
|
||||
.app => {},
|
||||
.surface => |v| try v.rt_surface.setInitialWindowSize(
|
||||
value.width,
|
||||
value.height,
|
||||
),
|
||||
}
|
||||
}
|
||||
fn showDesktopNotification(
|
||||
self: *App,
|
||||
target: apprt.Target,
|
||||
n: apprt.action.DesktopNotification,
|
||||
) void {
|
||||
// Set a default title if we don't already have one
|
||||
const t = switch (n.title.len) {
|
||||
0 => "Ghostty",
|
||||
else => n.title,
|
||||
};
|
||||
|
||||
const notification = c.g_notification_new(t.ptr);
|
||||
defer c.g_object_unref(notification);
|
||||
c.g_notification_set_body(notification, n.body.ptr);
|
||||
|
||||
const icon = c.g_themed_icon_new("com.mitchellh.ghostty");
|
||||
defer c.g_object_unref(icon);
|
||||
c.g_notification_set_icon(notification, icon);
|
||||
|
||||
const pointer = c.g_variant_new_uint64(switch (target) {
|
||||
.app => 0,
|
||||
.surface => |v| @intFromPtr(v),
|
||||
});
|
||||
c.g_notification_set_default_action_and_target_value(
|
||||
notification,
|
||||
"app.present-surface",
|
||||
pointer,
|
||||
);
|
||||
|
||||
const g_app: *c.GApplication = @ptrCast(self.app);
|
||||
|
||||
// We set the notification ID to the body content. If the content is the
|
||||
// same, this notification may replace a previous notification
|
||||
c.g_application_send_notification(g_app, n.body.ptr, notification);
|
||||
}
|
||||
|
||||
/// Reload the configuration. This should return the new configuration.
|
||||
@@ -442,9 +785,9 @@ fn loadRuntimeCss(config: *const Config, provider: *c.GtkCssProvider) !void {
|
||||
\\ opacity: {d:.2};
|
||||
\\ background-color: rgb({d},{d},{d});
|
||||
\\}}
|
||||
\\window.ghostty-theme-inherit headerbar,
|
||||
\\window.ghostty-theme-inherit toolbarview > revealer > windowhandle,
|
||||
\\window.ghostty-theme-inherit box > tabbar {{
|
||||
\\window.window-theme-ghostty .top-bar,
|
||||
\\window.window-theme-ghostty .bottom-bar,
|
||||
\\window.window-theme-ghostty box > tabbar {{
|
||||
\\ background-color: rgb({d},{d},{d});
|
||||
\\ color: rgb({d},{d},{d});
|
||||
\\}}
|
||||
@@ -564,9 +907,9 @@ pub fn gtkQuitTimerExpired(ud: ?*anyopaque) callconv(.C) c.gboolean {
|
||||
}
|
||||
|
||||
/// This will get called when there are no more open surfaces.
|
||||
pub fn startQuitTimer(self: *App) void {
|
||||
fn startQuitTimer(self: *App) void {
|
||||
// Cancel any previous timer.
|
||||
self.cancelQuitTimer();
|
||||
self.stopQuitTimer();
|
||||
|
||||
// This is a no-op unless we are configured to quit after last window is closed.
|
||||
if (!self.config.@"quit-after-last-window-closed") return;
|
||||
@@ -581,7 +924,7 @@ pub fn startQuitTimer(self: *App) void {
|
||||
}
|
||||
|
||||
/// This will get called when a new surface gets opened.
|
||||
pub fn cancelQuitTimer(self: *App) void {
|
||||
fn stopQuitTimer(self: *App) void {
|
||||
switch (self.quit_timer) {
|
||||
.off => {},
|
||||
.expired => self.quit_timer = .{ .off = {} },
|
||||
@@ -607,7 +950,7 @@ pub fn redrawInspector(self: *App, surface: *Surface) void {
|
||||
}
|
||||
|
||||
/// Called by CoreApp to create a new window with a new surface.
|
||||
pub fn newWindow(self: *App, parent_: ?*CoreSurface) !void {
|
||||
fn newWindow(self: *App, parent_: ?*CoreSurface) !void {
|
||||
const alloc = self.core_app.alloc;
|
||||
|
||||
// Allocate a fixed pointer for our window. We try to minimize
|
||||
@@ -856,8 +1199,12 @@ fn gtkActionPresentSurface(
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert that u64 to pointer to a core surface.
|
||||
const surface: *CoreSurface = @ptrFromInt(c.g_variant_get_uint64(parameter));
|
||||
// Convert that u64 to pointer to a core surface. A value of zero
|
||||
// means that there was no target surface for the notification so
|
||||
// we dont' focus any surface.
|
||||
const ptr_int: u64 = c.g_variant_get_uint64(parameter);
|
||||
if (ptr_int == 0) return;
|
||||
const surface: *CoreSurface = @ptrFromInt(ptr_int);
|
||||
|
||||
// Send a message through the core app mailbox rather than presenting the
|
||||
// surface directly so that it can validate that the surface pointer is
|
||||
|
||||
@@ -7,7 +7,6 @@ const Allocator = std.mem.Allocator;
|
||||
const assert = std.debug.assert;
|
||||
const apprt = @import("../../apprt.zig");
|
||||
const font = @import("../../font/main.zig");
|
||||
const input = @import("../../input.zig");
|
||||
const CoreSurface = @import("../../Surface.zig");
|
||||
|
||||
const Surface = @import("Surface.zig");
|
||||
@@ -21,14 +20,14 @@ pub const Orientation = enum {
|
||||
horizontal,
|
||||
vertical,
|
||||
|
||||
pub fn fromDirection(direction: apprt.SplitDirection) Orientation {
|
||||
pub fn fromDirection(direction: apprt.action.SplitDirection) Orientation {
|
||||
return switch (direction) {
|
||||
.right => .horizontal,
|
||||
.down => .vertical,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn fromResizeDirection(direction: input.SplitResizeDirection) Orientation {
|
||||
pub fn fromResizeDirection(direction: apprt.action.ResizeSplit.Direction) Orientation {
|
||||
return switch (direction) {
|
||||
.up, .down => .vertical,
|
||||
.left, .right => .horizontal,
|
||||
@@ -58,7 +57,7 @@ bottom_right: Surface.Container.Elem,
|
||||
pub fn create(
|
||||
alloc: Allocator,
|
||||
sibling: *Surface,
|
||||
direction: apprt.SplitDirection,
|
||||
direction: apprt.action.SplitDirection,
|
||||
) !*Split {
|
||||
var split = try alloc.create(Split);
|
||||
errdefer alloc.destroy(split);
|
||||
@@ -69,7 +68,7 @@ pub fn create(
|
||||
pub fn init(
|
||||
self: *Split,
|
||||
sibling: *Surface,
|
||||
direction: apprt.SplitDirection,
|
||||
direction: apprt.action.SplitDirection,
|
||||
) !void {
|
||||
// Create the new child surface for the other direction.
|
||||
const alloc = sibling.app.core_app.alloc;
|
||||
@@ -164,7 +163,11 @@ fn removeChild(
|
||||
}
|
||||
|
||||
/// Move the divider in the given direction by the given amount.
|
||||
pub fn moveDivider(self: *Split, direction: input.SplitResizeDirection, amount: u16) void {
|
||||
pub fn moveDivider(
|
||||
self: *Split,
|
||||
direction: apprt.action.ResizeSplit.Direction,
|
||||
amount: u16,
|
||||
) void {
|
||||
const min_pos = 10;
|
||||
|
||||
const pos = c.gtk_paned_get_position(self.paned);
|
||||
@@ -263,7 +266,7 @@ fn updateChildren(self: *const Split) void {
|
||||
|
||||
/// A mapping of direction to the element (if any) in that direction.
|
||||
pub const DirectionMap = std.EnumMap(
|
||||
input.SplitFocusDirection,
|
||||
apprt.action.GotoSplit,
|
||||
?*Surface,
|
||||
);
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ const configpkg = @import("../../config.zig");
|
||||
const apprt = @import("../../apprt.zig");
|
||||
const font = @import("../../font/main.zig");
|
||||
const input = @import("../../input.zig");
|
||||
const renderer = @import("../../renderer.zig");
|
||||
const terminal = @import("../../terminal/main.zig");
|
||||
const CoreSurface = @import("../../Surface.zig");
|
||||
const internal_os = @import("../../os/main.zig");
|
||||
@@ -688,7 +689,10 @@ pub fn close(self: *Surface, processActive: bool) void {
|
||||
c.gtk_widget_show(alert);
|
||||
}
|
||||
|
||||
pub fn controlInspector(self: *Surface, mode: input.InspectorMode) void {
|
||||
pub fn controlInspector(
|
||||
self: *Surface,
|
||||
mode: apprt.action.Inspector,
|
||||
) void {
|
||||
const show = switch (mode) {
|
||||
.toggle => self.inspector == null,
|
||||
.show => true,
|
||||
@@ -715,30 +719,6 @@ pub fn controlInspector(self: *Surface, mode: input.InspectorMode) void {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn toggleFullscreen(self: *Surface, mac_non_native: configpkg.NonNativeFullscreen) void {
|
||||
const window = self.container.window() orelse {
|
||||
log.info(
|
||||
"toggleFullscreen invalid for container={s}",
|
||||
.{@tagName(self.container)},
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
window.toggleFullscreen(mac_non_native);
|
||||
}
|
||||
|
||||
pub fn toggleWindowDecorations(self: *Surface) void {
|
||||
const window = self.container.window() orelse {
|
||||
log.info(
|
||||
"toggleWindowDecorations invalid for container={s}",
|
||||
.{@tagName(self.container)},
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
window.toggleWindowDecorations();
|
||||
}
|
||||
|
||||
pub fn getTitleLabel(self: *Surface) ?*c.GtkWidget {
|
||||
switch (self.title) {
|
||||
.none => return null,
|
||||
@@ -749,69 +729,6 @@ pub fn getTitleLabel(self: *Surface) ?*c.GtkWidget {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn newSplit(self: *Surface, direction: apprt.SplitDirection) !void {
|
||||
const alloc = self.app.core_app.alloc;
|
||||
_ = try Split.create(alloc, self, direction);
|
||||
}
|
||||
|
||||
pub fn gotoSplit(self: *const Surface, direction: input.SplitFocusDirection) void {
|
||||
const s = self.container.split() orelse return;
|
||||
const map = s.directionMap(switch (self.container) {
|
||||
.split_tl => .top_left,
|
||||
.split_br => .bottom_right,
|
||||
.none, .tab_ => unreachable,
|
||||
});
|
||||
const surface_ = map.get(direction) orelse return;
|
||||
if (surface_) |surface| surface.grabFocus();
|
||||
}
|
||||
|
||||
pub fn resizeSplit(self: *const Surface, direction: input.SplitResizeDirection, amount: u16) void {
|
||||
const s = self.container.firstSplitWithOrientation(
|
||||
Split.Orientation.fromResizeDirection(direction),
|
||||
) orelse return;
|
||||
s.moveDivider(direction, amount);
|
||||
}
|
||||
|
||||
pub fn equalizeSplits(self: *const Surface) void {
|
||||
const tab = self.container.tab() orelse return;
|
||||
const top_split = switch (tab.elem) {
|
||||
.split => |s| s,
|
||||
else => return,
|
||||
};
|
||||
_ = top_split.equalize();
|
||||
}
|
||||
|
||||
pub fn newTab(self: *Surface) !void {
|
||||
const window = self.container.window() orelse {
|
||||
log.info("surface cannot create new tab when not attached to a window", .{});
|
||||
return;
|
||||
};
|
||||
|
||||
try window.newTab(&self.core_surface);
|
||||
}
|
||||
|
||||
pub fn hasTabs(self: *const Surface) bool {
|
||||
const window = self.container.window() orelse return false;
|
||||
return window.hasTabs();
|
||||
}
|
||||
|
||||
pub fn gotoTab(self: *Surface, tab: apprt.GotoTab) void {
|
||||
const window = self.container.window() orelse {
|
||||
log.info(
|
||||
"gotoTab invalid for container={s}",
|
||||
.{@tagName(self.container)},
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
switch (tab) {
|
||||
.previous => window.gotoPreviousTab(self),
|
||||
.next => window.gotoNextTab(self),
|
||||
.last => window.gotoLastTab(),
|
||||
else => window.gotoTab(@intCast(@intFromEnum(tab))),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn setShouldClose(self: *Surface) void {
|
||||
_ = self;
|
||||
}
|
||||
@@ -867,18 +784,6 @@ pub fn setInitialWindowSize(self: *const Surface, width: u32, height: u32) !void
|
||||
);
|
||||
}
|
||||
|
||||
pub fn setCellSize(self: *const Surface, width: u32, height: u32) !void {
|
||||
_ = self;
|
||||
_ = width;
|
||||
_ = height;
|
||||
}
|
||||
|
||||
pub fn setSizeLimits(self: *Surface, min: apprt.SurfaceSize, max_: ?apprt.SurfaceSize) !void {
|
||||
_ = self;
|
||||
_ = min;
|
||||
_ = max_;
|
||||
}
|
||||
|
||||
pub fn grabFocus(self: *Surface) void {
|
||||
if (self.container.tab()) |tab| tab.focus_child = self;
|
||||
|
||||
@@ -1026,6 +931,19 @@ pub fn mouseOverLink(self: *Surface, uri_: ?[]const u8) void {
|
||||
self.url_widget = URLWidget.init(self, uriZ);
|
||||
}
|
||||
|
||||
pub fn supportsClipboard(
|
||||
self: *const Surface,
|
||||
clipboard_type: apprt.Clipboard,
|
||||
) bool {
|
||||
_ = self;
|
||||
return switch (clipboard_type) {
|
||||
.standard,
|
||||
.selection,
|
||||
.primary,
|
||||
=> true,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn clipboardRequest(
|
||||
self: *Surface,
|
||||
clipboard_type: apprt.Clipboard,
|
||||
@@ -1386,7 +1304,7 @@ fn gtkMouseUp(
|
||||
}
|
||||
|
||||
fn gtkMouseMotion(
|
||||
_: *c.GtkEventControllerMotion,
|
||||
ec: *c.GtkEventControllerMotion,
|
||||
x: c.gdouble,
|
||||
y: c.gdouble,
|
||||
ud: ?*anyopaque,
|
||||
@@ -1415,7 +1333,12 @@ fn gtkMouseMotion(
|
||||
self.grabFocus();
|
||||
}
|
||||
|
||||
self.core_surface.cursorPosCallback(self.cursor_pos) catch |err| {
|
||||
// Get our modifiers
|
||||
const event = c.gtk_event_controller_get_current_event(@ptrCast(ec));
|
||||
const gtk_mods = c.gdk_event_get_modifier_state(event);
|
||||
const mods = translateMods(gtk_mods);
|
||||
|
||||
self.core_surface.cursorPosCallback(self.cursor_pos, mods) catch |err| {
|
||||
log.err("error in cursor pos callback err={}", .{err});
|
||||
return;
|
||||
};
|
||||
@@ -1975,7 +1898,7 @@ fn translateMods(state: c.GdkModifierType) input.Mods {
|
||||
return mods;
|
||||
}
|
||||
|
||||
pub fn presentSurface(self: *Surface) void {
|
||||
pub fn present(self: *Surface) void {
|
||||
if (self.container.window()) |window| {
|
||||
if (self.container.tab()) |tab| {
|
||||
if (window.notebook.getTabPosition(tab)) |position|
|
||||
@@ -1983,5 +1906,6 @@ pub fn presentSurface(self: *Surface) void {
|
||||
}
|
||||
c.gtk_window_present(window.window);
|
||||
}
|
||||
|
||||
self.grabFocus();
|
||||
}
|
||||
|
||||
@@ -35,6 +35,10 @@ window: *c.GtkWindow,
|
||||
/// GtkHeaderBar depending on if adw is enabled and linked.
|
||||
header: ?*c.GtkWidget,
|
||||
|
||||
/// The tab overview for the window. This is possibly null since there is no
|
||||
/// taboverview without a AdwApplicationWindow (libadwaita >= 1.4.0).
|
||||
tab_overview: ?*c.GtkWidget,
|
||||
|
||||
/// The notebook (tab grouping) for this window.
|
||||
/// can be either c.GtkNotebook or c.AdwTabView.
|
||||
notebook: Notebook,
|
||||
@@ -68,6 +72,7 @@ pub fn init(self: *Window, app: *App) !void {
|
||||
.app = app,
|
||||
.window = undefined,
|
||||
.header = null,
|
||||
.tab_overview = null,
|
||||
.notebook = undefined,
|
||||
.context_menu = undefined,
|
||||
.toast_overlay = undefined,
|
||||
@@ -97,7 +102,7 @@ pub fn init(self: *Window, app: *App) !void {
|
||||
|
||||
// Apply class to color headerbar if window-theme is set to `ghostty`.
|
||||
if (app.config.@"window-theme" == .ghostty) {
|
||||
c.gtk_widget_add_css_class(@ptrCast(gtk_window), "ghostty-theme-inherit");
|
||||
c.gtk_widget_add_css_class(@ptrCast(gtk_window), "window-theme-ghostty");
|
||||
}
|
||||
|
||||
// Remove the window's background if any of the widgets need to be transparent
|
||||
@@ -114,7 +119,7 @@ pub fn init(self: *Window, app: *App) !void {
|
||||
const box = c.gtk_box_new(c.GTK_ORIENTATION_VERTICAL, 0);
|
||||
|
||||
// If we are using an AdwWindow then we can support the tab overview.
|
||||
const tab_overview_: ?*c.GtkWidget = if (self.isAdwWindow()) overview: {
|
||||
self.tab_overview = if (self.isAdwWindow()) overview: {
|
||||
const tab_overview = c.adw_tab_overview_new();
|
||||
c.adw_tab_overview_set_enable_new_tab(@ptrCast(tab_overview), 1);
|
||||
_ = c.g_signal_connect_data(
|
||||
@@ -152,14 +157,15 @@ pub fn init(self: *Window, app: *App) !void {
|
||||
c.gtk_widget_set_tooltip_text(btn, "Main Menu");
|
||||
c.gtk_menu_button_set_icon_name(@ptrCast(btn), "open-menu-symbolic");
|
||||
c.gtk_menu_button_set_menu_model(@ptrCast(btn), @ptrCast(@alignCast(app.menu)));
|
||||
if (self.isAdwWindow())
|
||||
c.adw_header_bar_pack_end(@ptrCast(header), btn)
|
||||
else
|
||||
c.gtk_header_bar_pack_end(@ptrCast(header), btn);
|
||||
if (self.isAdwWindow()) {
|
||||
if (comptime !adwaita.versionAtLeast(1, 4, 0)) unreachable;
|
||||
c.adw_header_bar_pack_end(@ptrCast(header), btn);
|
||||
} else c.gtk_header_bar_pack_end(@ptrCast(header), btn);
|
||||
}
|
||||
|
||||
// If we're using an AdwWindow then we can support the tab overview.
|
||||
if (tab_overview_) |tab_overview| {
|
||||
if (self.tab_overview) |tab_overview| {
|
||||
if (comptime !adwaita.versionAtLeast(1, 4, 0)) unreachable;
|
||||
assert(self.isAdwWindow());
|
||||
|
||||
const btn = c.gtk_toggle_button_new();
|
||||
@@ -235,7 +241,8 @@ pub fn init(self: *Window, app: *App) !void {
|
||||
};
|
||||
|
||||
// If we have a tab overview then we can set it on our notebook.
|
||||
if (tab_overview_) |tab_overview| {
|
||||
if (self.tab_overview) |tab_overview| {
|
||||
if (comptime !adwaita.versionAtLeast(1, 4, 0)) unreachable;
|
||||
assert(self.notebook == .adw_tab_view);
|
||||
c.adw_tab_overview_set_view(@ptrCast(tab_overview), self.notebook.adw_tab_view);
|
||||
}
|
||||
@@ -256,7 +263,8 @@ pub fn init(self: *Window, app: *App) !void {
|
||||
// Our actions for the menu
|
||||
initActions(self);
|
||||
|
||||
if (self.hasAdwToolbar()) {
|
||||
if (self.isAdwWindow()) {
|
||||
if (comptime !adwaita.versionAtLeast(1, 4, 0)) unreachable;
|
||||
const toolbar_view: *c.AdwToolbarView = @ptrCast(c.adw_toolbar_view_new());
|
||||
|
||||
const header_widget: *c.GtkWidget = @ptrCast(@alignCast(self.header.?));
|
||||
@@ -289,7 +297,7 @@ pub fn init(self: *Window, app: *App) !void {
|
||||
|
||||
// Set our application window content. The content depends on if
|
||||
// we're using an AdwTabOverview or not.
|
||||
if (tab_overview_) |tab_overview| {
|
||||
if (self.tab_overview) |tab_overview| {
|
||||
c.adw_tab_overview_set_child(
|
||||
@ptrCast(tab_overview),
|
||||
@ptrCast(@alignCast(toolbar_view)),
|
||||
@@ -391,18 +399,9 @@ pub fn deinit(self: *Window) void {
|
||||
/// paths that are not enabled.
|
||||
inline fn isAdwWindow(self: *Window) bool {
|
||||
return (comptime adwaita.versionAtLeast(1, 4, 0)) and
|
||||
adwaita.enabled(&self.app.config) and
|
||||
self.app.config.@"gtk-titlebar" and
|
||||
adwaita.versionAtLeast(1, 4, 0);
|
||||
}
|
||||
|
||||
/// This must be `inline` so that the comptime check noops conditional
|
||||
/// paths that are not enabled.
|
||||
inline fn hasAdwToolbar(self: *Window) bool {
|
||||
return ((comptime adwaita.versionAtLeast(1, 4, 0)) and
|
||||
adwaita.enabled(&self.app.config) and
|
||||
adwaita.versionAtLeast(1, 4, 0) and
|
||||
self.app.config.@"gtk-titlebar");
|
||||
self.app.config.@"gtk-titlebar";
|
||||
}
|
||||
|
||||
/// Add a new tab to this window.
|
||||
@@ -421,11 +420,6 @@ pub fn closeTab(self: *Window, tab: *Tab) void {
|
||||
self.notebook.closeTab(tab);
|
||||
}
|
||||
|
||||
/// Returns true if this window has any tabs.
|
||||
pub fn hasTabs(self: *const Window) bool {
|
||||
return self.notebook.nPages() > 0;
|
||||
}
|
||||
|
||||
/// Go to the previous tab for a surface.
|
||||
pub fn gotoPreviousTab(self: *Window, surface: *Surface) void {
|
||||
const tab = surface.container.tab() orelse {
|
||||
@@ -463,8 +457,17 @@ pub fn gotoTab(self: *Window, n: usize) void {
|
||||
}
|
||||
}
|
||||
|
||||
/// Toggle tab overview (if present)
|
||||
pub fn toggleTabOverview(self: *Window) void {
|
||||
if (self.tab_overview) |tab_overview_widget| {
|
||||
if (comptime !adwaita.versionAtLeast(1, 4, 0)) unreachable;
|
||||
const tab_overview: *c.AdwTabOverview = @ptrCast(@alignCast(tab_overview_widget));
|
||||
c.adw_tab_overview_set_open(tab_overview, 1 - c.adw_tab_overview_get_open(tab_overview));
|
||||
}
|
||||
}
|
||||
|
||||
/// Toggle fullscreen for this window.
|
||||
pub fn toggleFullscreen(self: *Window, _: configpkg.NonNativeFullscreen) void {
|
||||
pub fn toggleFullscreen(self: *Window) void {
|
||||
const is_fullscreen = c.gtk_window_is_fullscreen(self.window);
|
||||
if (is_fullscreen == 0) {
|
||||
c.gtk_window_fullscreen(self.window);
|
||||
|
||||
@@ -66,9 +66,11 @@ pub const Notebook = union(enum) {
|
||||
|
||||
const tab_view: *c.AdwTabView = c.adw_tab_view_new().?;
|
||||
|
||||
// Adwaita enables all of the shortcuts by default.
|
||||
// We want to manage keybindings ourselves.
|
||||
c.adw_tab_view_remove_shortcuts(tab_view, c.ADW_TAB_VIEW_SHORTCUT_ALL_SHORTCUTS);
|
||||
if (comptime adwaita.versionAtLeast(1, 2, 0) and adwaita.versionAtLeast(1, 2, 0)) {
|
||||
// Adwaita enables all of the shortcuts by default.
|
||||
// We want to manage keybindings ourselves.
|
||||
c.adw_tab_view_remove_shortcuts(tab_view, c.ADW_TAB_VIEW_SHORTCUT_ALL_SHORTCUTS);
|
||||
}
|
||||
|
||||
_ = c.g_signal_connect_data(tab_view, "page-attached", c.G_CALLBACK(&adwPageAttached), window, null, c.G_CONNECT_DEFAULT);
|
||||
_ = c.g_signal_connect_data(tab_view, "create-window", c.G_CALLBACK(&adwTabViewCreateWindow), window, null, c.G_CONNECT_DEFAULT);
|
||||
@@ -261,6 +263,10 @@ pub const Notebook = union(enum) {
|
||||
_ = c.g_signal_connect_data(label_close, "clicked", c.G_CALLBACK(&Tab.gtkTabCloseClick), tab, null, c.G_CONNECT_DEFAULT);
|
||||
_ = c.g_signal_connect_data(gesture_tab_click, "pressed", c.G_CALLBACK(&Tab.gtkTabClick), tab, null, c.G_CONNECT_DEFAULT);
|
||||
|
||||
// Tab settings
|
||||
c.gtk_notebook_set_tab_reorderable(notebook, box_widget, 1);
|
||||
c.gtk_notebook_set_tab_detachable(notebook, box_widget, 1);
|
||||
|
||||
if (self.nPages() > 1) {
|
||||
c.gtk_notebook_set_show_tabs(notebook, 1);
|
||||
}
|
||||
|
||||
40
src/apprt/gtk/version.zig
Normal file
40
src/apprt/gtk/version.zig
Normal file
@@ -0,0 +1,40 @@
|
||||
const c = @import("c.zig").c;
|
||||
|
||||
/// Verifies that the GTK version is at least the given version.
|
||||
///
|
||||
/// This can be run in both a comptime and runtime context. If it
|
||||
/// is run in a comptime context, it will only check the version
|
||||
/// in the headers. If it is run in a runtime context, it will
|
||||
/// check the actual version of the library we are linked against.
|
||||
///
|
||||
/// This is inlined so that the comptime checks will disable the
|
||||
/// runtime checks if the comptime checks fail.
|
||||
pub inline fn atLeast(
|
||||
comptime major: u16,
|
||||
comptime minor: u16,
|
||||
comptime micro: u16,
|
||||
) bool {
|
||||
// If our header has lower versions than the given version,
|
||||
// we can return false immediately. This prevents us from
|
||||
// compiling against unknown symbols and makes runtime checks
|
||||
// very slightly faster.
|
||||
if (comptime c.GTK_MAJOR_VERSION < major or
|
||||
c.GTK_MINOR_VERSION < minor or
|
||||
c.GTK_MICRO_VERSION < micro) return false;
|
||||
|
||||
// If we're in comptime then we can't check the runtime version.
|
||||
if (@inComptime()) return true;
|
||||
|
||||
// We use the functions instead of the constants such as
|
||||
// c.GTK_MINOR_VERSION because the function gets the actual
|
||||
// runtime version.
|
||||
if (c.gtk_get_major_version() >= major) {
|
||||
if (c.gtk_get_major_version() > major) return true;
|
||||
if (c.gtk_get_minor_version() >= minor) {
|
||||
if (c.gtk_get_minor_version() > minor) return true;
|
||||
return c.gtk_get_micro_version() >= micro;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -52,33 +52,6 @@ pub const ClipboardRequest = union(ClipboardRequestType) {
|
||||
osc_52_write: Clipboard,
|
||||
};
|
||||
|
||||
/// A desktop notification.
|
||||
pub const DesktopNotification = struct {
|
||||
/// The title of the notification. May be an empty string to not show a
|
||||
/// title.
|
||||
title: []const u8,
|
||||
|
||||
/// The body of a notification. This will always be shown.
|
||||
body: []const u8,
|
||||
};
|
||||
|
||||
/// The tab to jump to. This is non-exhaustive so that integer values represent
|
||||
/// the index (zero-based) of the tab to jump to. Negative values are special
|
||||
/// values.
|
||||
pub const GotoTab = enum(c_int) {
|
||||
previous = -1,
|
||||
next = -2,
|
||||
last = -3,
|
||||
_,
|
||||
};
|
||||
|
||||
// This is made extern (c_int) to make interop easier with our embedded
|
||||
// runtime. The small size cost doesn't make a difference in our union.
|
||||
pub const SplitDirection = enum(c_int) {
|
||||
right,
|
||||
down,
|
||||
};
|
||||
|
||||
/// The color scheme in use (light vs dark).
|
||||
pub const ColorScheme = enum(u2) {
|
||||
light = 0,
|
||||
|
||||
@@ -12,7 +12,7 @@ pub const fish_completions = comptimeGenerateFishCompletions();
|
||||
|
||||
fn comptimeGenerateFishCompletions() []const u8 {
|
||||
comptime {
|
||||
@setEvalBranchQuota(16000);
|
||||
@setEvalBranchQuota(17000);
|
||||
var counter = std.io.countingWriter(std.io.null_writer);
|
||||
try writeFishCompletions(&counter.writer());
|
||||
|
||||
|
||||
@@ -116,7 +116,7 @@ fn prettyPrint(alloc: Allocator, keybinds: Config.Keybinds) !u8 {
|
||||
while (iter.next()) |bind| {
|
||||
const action = switch (bind.value_ptr.*) {
|
||||
.leader => continue, // TODO: support this
|
||||
.action, .action_unconsumed => |action| action,
|
||||
.leaf => |leaf| leaf.action,
|
||||
};
|
||||
const key = switch (bind.key_ptr.key) {
|
||||
.translated => |k| try std.fmt.bufPrint(&buf, "{s}", .{@tagName(k)}),
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
45
src/cli/lorem_ipsum.txt
Normal file
45
src/cli/lorem_ipsum.txt
Normal file
@@ -0,0 +1,45 @@
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras hendrerit aliquet
|
||||
turpis non dictum. Mauris pulvinar nisl sit amet dui cursus tempus. Pellentesque
|
||||
ut dui justo. Etiam quis magna sagittis nisi pretium consequat vitae ut nisl.
|
||||
Sed at metus id odio pulvinar sodales. Vestibulum sollicitudin, sem id tristique
|
||||
vestibulum, neque ante dictum tortor, in convallis mi enim ac lorem. Suspendisse
|
||||
orci ex, ullamcorper sed leo vitae, mattis egestas nisl. Morbi id est vel
|
||||
ipsum mollis convallis vel at mauris. Duis vehicula facilisis placerat. Aliquam
|
||||
venenatis auctor ipsum vel elementum. Proin ac tincidunt lacus. Sed facilisis
|
||||
tellus ullamcorper bibendum lobortis. Pellentesque porta, lacus quis efficitur
|
||||
pulvinar, sem mi varius ante, sed finibus diam ante et risus.
|
||||
|
||||
Morbi ut sollicitudin justo. Nulla mattis mi ac mauris tincidunt tempor. Morbi
|
||||
vel gravida erat. Ut eu risus quis nisi facilisis aliquet varius id orci.
|
||||
Pellentesque tortor diam, porttitor nec urna nec, convallis consectetur dui.
|
||||
Vestibulum et hendrerit ipsum. Morbi pharetra dictum turpis in elementum. Ut
|
||||
nec volutpat nunc, at venenatis leo. Morbi eget nulla luctus, tincidunt dui vel,
|
||||
cursus urna. Maecenas ac pellentesque nisi. Quisque ut lorem porta, eleifend
|
||||
metus id, pellentesque tellus.
|
||||
|
||||
Vivamus gravida convallis felis, at hendrerit dolor. Vestibulum tincidunt id
|
||||
augue quis hendrerit. Praesent venenatis elit quis posuere gravida. Praesent
|
||||
at massa a purus maximus tempus. Proin dui leo, feugiat et erat ac, tincidunt
|
||||
aliquam risus. Aenean rutrum hendrerit turpis, sit amet consectetur justo porta
|
||||
non. Sed auctor justo elit, sed mollis odio ullamcorper nec. Pellentesque ac
|
||||
hendrerit tortor. Praesent quis viverra dui, sit amet imperdiet magna.
|
||||
|
||||
Mauris iaculis maximus felis, aliquet vehicula neque sagittis nec. Duis
|
||||
convallis purus enim, vel scelerisque purus dignissim eu. Donec congue sapien
|
||||
a neque rhoncus, sit amet accumsan libero tincidunt. Proin vitae placerat urna.
|
||||
Donec dolor sapien, fringilla sed semper sit amet, sollicitudin sit amet orci.
|
||||
Mauris maximus convallis vehicula. Aliquam urna ipsum, fermentum ac iaculis vel,
|
||||
blandit eget lorem. Sed enim ante, sodales a diam in, convallis interdum quam.
|
||||
Duis non urna risus. Proin ac neque at risus ullamcorper mattis eu vel nunc.
|
||||
Proin et ipsum euismod, ullamcorper justo et, imperdiet est. Curabitur quis
|
||||
arcu faucibus, bibendum nisl nec, hendrerit sapien. Curabitur vitae ante risus.
|
||||
Praesent eget sagittis tortor.
|
||||
|
||||
Mauris aliquam nec nibh eu congue. Nullam congue auctor vestibulum. Donec
|
||||
posuere sapien nec massa efficitur tincidunt. Vestibulum ante ipsum primis in
|
||||
faucibus orci luctus et ultrices posuere cubilia curae; Proin molestie, nisl
|
||||
in tincidunt condimentum, ante metus fermentum felis, ac molestie lacus dui vel
|
||||
dolor. Donec ornare laoreet posuere. Etiam id tincidunt ante. Maecenas semper
|
||||
diam ac tortor facilisis egestas. Nam eu bibendum nisl. Integer tempor nisl nec
|
||||
ex consectetur, quis lobortis enim finibus. Sed ac erat posuere, fermentum metus
|
||||
sed, suscipit nisl.
|
||||
@@ -223,7 +223,7 @@ const c = @cImport({
|
||||
@"font-codepoint-map": RepeatableCodepointMap = .{},
|
||||
|
||||
/// Draw fonts with a thicker stroke, if supported. This is only supported
|
||||
/// currently on MacOS.
|
||||
/// currently on macOS.
|
||||
@"font-thicken": bool = false,
|
||||
|
||||
/// All of the configurations behavior adjust various metrics determined by the
|
||||
@@ -429,6 +429,8 @@ palette: Palette = .{},
|
||||
/// Hide the mouse immediately when typing. The mouse becomes visible again when
|
||||
/// the mouse is used. The mouse is only hidden if the mouse cursor is over the
|
||||
/// active terminal surface.
|
||||
///
|
||||
/// macOS: This feature requires macOS 15.0 (Sequoia) or later.
|
||||
@"mouse-hide-while-typing": bool = false,
|
||||
|
||||
/// Determines whether running programs can detect the shift key pressed with a
|
||||
@@ -649,7 +651,8 @@ class: ?[:0]const u8 = null,
|
||||
@"working-directory": ?[]const u8 = null,
|
||||
|
||||
/// Key bindings. The format is `trigger=action`. Duplicate triggers will
|
||||
/// overwrite previously set values.
|
||||
/// overwrite previously set values. The list of actions is available in
|
||||
/// the documentation or using the `ghostty +list-actions` command.
|
||||
///
|
||||
/// Trigger: `+`-separated list of keys and modifiers. Example: `ctrl+a`,
|
||||
/// `ctrl+shift+b`, `up`. Some notes:
|
||||
@@ -701,6 +704,9 @@ class: ?[:0]const u8 = null,
|
||||
/// `ctrl+a>t`, and then bind `ctrl+a` directly, both `ctrl+a>n` and
|
||||
/// `ctrl+a>t` will become unbound.
|
||||
///
|
||||
/// * Trigger sequences are not allowed for `global:` or `all:`-prefixed
|
||||
/// triggers. This is a limitation we could remove in the future.
|
||||
///
|
||||
/// Action is the action to take when the trigger is satisfied. It takes the
|
||||
/// format `action` or `action:param`. The latter form is only valid if the
|
||||
/// action requires a parameter.
|
||||
@@ -720,6 +726,9 @@ class: ?[:0]const u8 = null,
|
||||
/// * `text:text` - Send a string. Uses Zig string literal syntax.
|
||||
/// i.e. `text:\x15` sends Ctrl-U.
|
||||
///
|
||||
/// * All other actions can be found in the documentation or by using the
|
||||
/// `ghostty +list-actions` command.
|
||||
///
|
||||
/// Some notes for the action:
|
||||
///
|
||||
/// * The parameter is taken as-is after the `:`. Double quotes or
|
||||
@@ -734,11 +743,48 @@ class: ?[:0]const u8 = null,
|
||||
/// removes ALL keybindings up to this point, including the default
|
||||
/// keybindings.
|
||||
///
|
||||
/// A keybind by default causes the input to be consumed. This means that the
|
||||
/// associated encoding (if any) will not be sent to the running program
|
||||
/// in the terminal. If you wish to send the encoded value to the program,
|
||||
/// specify the "unconsumed:" prefix before the entire keybind. For example:
|
||||
/// "unconsumed:ctrl+a=reload_config"
|
||||
/// The keybind trigger can be prefixed with some special values to change
|
||||
/// the behavior of the keybind. These are:
|
||||
///
|
||||
/// * `all:` - Make the keybind apply to all terminal surfaces. By default,
|
||||
/// keybinds only apply to the focused terminal surface. If this is true,
|
||||
/// then the keybind will be sent to all terminal surfaces. This only
|
||||
/// applies to actions that are surface-specific. For actions that
|
||||
/// are already global (i.e. `quit`), this prefix has no effect.
|
||||
///
|
||||
/// * `global:` - Make the keybind global. By default, keybinds only work
|
||||
/// within Ghostty and under the right conditions (application focused,
|
||||
/// sometimes terminal focused, etc.). If you want a keybind to work
|
||||
/// globally across your system (i.e. even when Ghostty is not focused),
|
||||
/// specify this prefix. This prefix implies `all:`. Note: this does not
|
||||
/// work in all environments; see the additional notes below for more
|
||||
/// information.
|
||||
///
|
||||
/// * `unconsumed:` - Do not consume the input. By default, a keybind
|
||||
/// will consume the input, meaning that the associated encoding (if
|
||||
/// any) will not be sent to the running program in the terminal. If
|
||||
/// you wish to send the encoded value to the program, specify the
|
||||
/// `unconsumed:` prefix before the entire keybind. For example:
|
||||
/// `unconsumed:ctrl+a=reload_config`. `global:` and `all:`-prefixed
|
||||
/// keybinds will always consume the input regardless of this setting.
|
||||
/// Since they are not associated with a specific terminal surface,
|
||||
/// they're never encoded.
|
||||
///
|
||||
/// Keybind trigger are not unique per prefix combination. For example,
|
||||
/// `ctrl+a` and `global:ctrl+a` are not two separate keybinds. The keybind
|
||||
/// set later will overwrite the keybind set earlier. In this case, the
|
||||
/// `global:` keybind will be used.
|
||||
///
|
||||
/// Multiple prefixes can be specified. For example,
|
||||
/// `global:unconsumed:ctrl+a=reload_config` will make the keybind global
|
||||
/// and not consume the input to reload the config.
|
||||
///
|
||||
/// A note on `global:`: this feature is only supported on macOS. On macOS,
|
||||
/// this feature requires accessibility permissions to be granted to Ghostty.
|
||||
/// When a `global:` keybind is specified and Ghostty is launched or reloaded,
|
||||
/// Ghostty will attempt to request these permissions. If the permissions are
|
||||
/// not granted, the keybind will not work. On macOS, you can find these
|
||||
/// permissions in System Preferences -> Privacy & Security -> Accessibility.
|
||||
keybind: Keybinds = .{},
|
||||
|
||||
/// Horizontal window padding. This applies padding between the terminal cells
|
||||
@@ -845,13 +891,16 @@ keybind: Keybinds = .{},
|
||||
///
|
||||
/// * `true`
|
||||
/// * `false` - windows won't have native decorations, i.e. titlebar and
|
||||
/// borders. On MacOS this also disables tabs and tab overview.
|
||||
/// borders. On macOS this also disables tabs and tab overview.
|
||||
///
|
||||
/// The "toggle_window_decoration" keybind action can be used to create
|
||||
/// a keybinding to toggle this setting at runtime.
|
||||
///
|
||||
/// Changing this configuration in your configuration and reloading will
|
||||
/// only affect new windows. Existing windows will not be affected.
|
||||
///
|
||||
/// macOS: To hide the titlebar without removing the native window borders
|
||||
/// or rounded corners, use `macos-titlebar-style = hidden` instead.
|
||||
@"window-decoration": bool = true,
|
||||
|
||||
/// The font that will be used for the application's window and tab titles.
|
||||
@@ -1171,6 +1220,40 @@ keybind: Keybinds = .{},
|
||||
/// window is ever created. Only implemented on Linux.
|
||||
@"initial-window": bool = true,
|
||||
|
||||
/// The position of the "quick" terminal window. To learn more about the
|
||||
/// quick terminal, see the documentation for the `toggle_quick_terminal`
|
||||
/// binding action.
|
||||
///
|
||||
/// Valid values are:
|
||||
///
|
||||
/// * `top` - Terminal appears at the top of the screen.
|
||||
/// * `bottom` - Terminal appears at the bottom of the screen.
|
||||
/// * `left` - Terminal appears at the left of the screen.
|
||||
/// * `right` - Terminal appears at the right of the screen.
|
||||
///
|
||||
/// Changing this configuration requires restarting Ghostty completely.
|
||||
@"quick-terminal-position": QuickTerminalPosition = .top,
|
||||
|
||||
/// The screen where the quick terminal should show up.
|
||||
///
|
||||
/// Valid values are:
|
||||
///
|
||||
/// * `main` - The screen that the operating system recommends as the main
|
||||
/// screen. On macOS, this is the screen that is currently receiving
|
||||
/// keyboard input. This screen is defined by the operating system and
|
||||
/// not chosen by Ghostty.
|
||||
///
|
||||
/// * `mouse` - The screen that the mouse is currently hovered over.
|
||||
///
|
||||
/// * `macos-menu-bar` - The screen that contains the macOS menu bar as
|
||||
/// set in the display settings on macOS. This is a bit confusing because
|
||||
/// every screen on macOS has a menu bar, but this is the screen that
|
||||
/// contains the primary menu bar.
|
||||
///
|
||||
/// The default value is `main` because this is the recommended screen
|
||||
/// by the operating system.
|
||||
@"quick-terminal-screen": QuickTerminalScreen = .main,
|
||||
|
||||
/// Whether to enable shell integration auto-injection or not. Shell integration
|
||||
/// greatly enhances the terminal experience by enabling a number of features:
|
||||
///
|
||||
@@ -1304,7 +1387,7 @@ keybind: Keybinds = .{},
|
||||
@"macos-non-native-fullscreen": NonNativeFullscreen = .false,
|
||||
|
||||
/// The style of the macOS titlebar. Available values are: "native",
|
||||
/// "transparent", and "tabs".
|
||||
/// "transparent", "tabs", and "hidden".
|
||||
///
|
||||
/// The "native" style uses the native macOS titlebar with zero customization.
|
||||
/// The titlebar will match your window theme (see `window-theme`).
|
||||
@@ -1321,6 +1404,13 @@ keybind: Keybinds = .{},
|
||||
/// macOS 14 does not have this issue and any other macOS version has not
|
||||
/// been tested.
|
||||
///
|
||||
/// The "hidden" style hides the titlebar. Unlike `window-decoration = false`,
|
||||
/// however, it does not remove the frame from the window or cause it to have
|
||||
/// squared corners. Changing to or from this option at run-time may affect
|
||||
/// existing windows in buggy ways. The top titlebar area of the window will
|
||||
/// continue to drag the window around and you will not be able to use
|
||||
/// the mouse for terminal events in this space.
|
||||
///
|
||||
/// The default value is "transparent". This is an opinionated choice
|
||||
/// but its one I think is the most aesthetically pleasing and works in
|
||||
/// most cases.
|
||||
@@ -1348,6 +1438,34 @@ keybind: Keybinds = .{},
|
||||
/// find false more visually appealing.
|
||||
@"macos-window-shadow": bool = true,
|
||||
|
||||
/// If true, Ghostty on macOS will automatically enable the "Secure Input"
|
||||
/// feature when it detects that a password prompt is being displayed.
|
||||
///
|
||||
/// "Secure Input" is a macOS security feature that prevents applications from
|
||||
/// reading keyboard events. This can always be enabled manually using the
|
||||
/// `Ghostty > Secure Keyboard Entry` menu item.
|
||||
///
|
||||
/// Note that automatic password prompt detection is based on heuristics
|
||||
/// and may not always work as expected. Specifically, it does not work
|
||||
/// over SSH connections, but there may be other cases where it also
|
||||
/// doesn't work.
|
||||
///
|
||||
/// A reason to disable this feature is if you find that it is interfering
|
||||
/// with legitimate accessibility software (or software that uses the
|
||||
/// accessibility APIs), since secure input prevents any application from
|
||||
/// reading keyboard events.
|
||||
@"macos-auto-secure-input": bool = true,
|
||||
|
||||
/// If true, Ghostty will show a graphical indication when secure input is
|
||||
/// enabled. This indication is generally recommended to know when secure input
|
||||
/// is enabled.
|
||||
///
|
||||
/// Normally, secure input is only active when a password prompt is displayed
|
||||
/// or it is manually (and typically temporarily) enabled. However, if you
|
||||
/// always have secure input enabled, the indication can be distracting and
|
||||
/// you may want to disable it.
|
||||
@"macos-secure-input-indication": bool = true,
|
||||
|
||||
/// Put every surface (tab, split, window) into a dedicated Linux cgroup.
|
||||
///
|
||||
/// This makes it so that resource management can be done on a per-surface
|
||||
@@ -3664,11 +3782,16 @@ pub const Keybinds = struct {
|
||||
)) return false,
|
||||
|
||||
// Actions are compared by field directly
|
||||
inline .action, .action_unconsumed => |_, tag| if (!equalField(
|
||||
inputpkg.Binding.Action,
|
||||
@field(self_entry.value_ptr.*, @tagName(tag)),
|
||||
@field(other_entry.value_ptr.*, @tagName(tag)),
|
||||
)) return false,
|
||||
.leaf => {
|
||||
const self_leaf = self_entry.value_ptr.*.leaf;
|
||||
const other_leaf = other_entry.value_ptr.*.leaf;
|
||||
|
||||
if (!equalField(
|
||||
inputpkg.Binding.Set.Leaf,
|
||||
self_leaf,
|
||||
other_leaf,
|
||||
)) return false;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4241,6 +4364,7 @@ pub const MacTitlebarStyle = enum {
|
||||
native,
|
||||
transparent,
|
||||
tabs,
|
||||
hidden,
|
||||
};
|
||||
|
||||
/// See gtk-single-instance
|
||||
@@ -4311,6 +4435,21 @@ pub const ResizeOverlayPosition = enum {
|
||||
@"bottom-right",
|
||||
};
|
||||
|
||||
/// See quick-terminal-position
|
||||
pub const QuickTerminalPosition = enum {
|
||||
top,
|
||||
bottom,
|
||||
left,
|
||||
right,
|
||||
};
|
||||
|
||||
/// See quick-terminal-screen
|
||||
pub const QuickTerminalScreen = enum {
|
||||
main,
|
||||
mouse,
|
||||
@"macos-menu-bar",
|
||||
};
|
||||
|
||||
/// See grapheme-width-method
|
||||
pub const GraphemeWidthMethod = enum {
|
||||
legacy,
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
const dir = @import("dir.zig");
|
||||
const sentry_envelope = @import("sentry_envelope.zig");
|
||||
|
||||
pub const minidump = @import("minidump.zig");
|
||||
pub const sentry = @import("sentry.zig");
|
||||
pub const Envelope = sentry_envelope.Envelope;
|
||||
pub const defaultDir = dir.defaultDir;
|
||||
|
||||
7
src/crash/minidump.zig
Normal file
7
src/crash/minidump.zig
Normal file
@@ -0,0 +1,7 @@
|
||||
pub const reader = @import("minidump/reader.zig");
|
||||
pub const stream = @import("minidump/stream.zig");
|
||||
pub const Reader = reader.Reader;
|
||||
|
||||
test {
|
||||
@import("std").testing.refAllDecls(@This());
|
||||
}
|
||||
59
src/crash/minidump/external.zig
Normal file
59
src/crash/minidump/external.zig
Normal file
@@ -0,0 +1,59 @@
|
||||
//! This file contains the external structs and constants for the minidump
|
||||
//! format. Most are from the Microsoft documentation on the minidump format:
|
||||
//! https://learn.microsoft.com/en-us/windows/win32/api/minidumpapiset/
|
||||
//!
|
||||
//! Wherever possible, we also compare our definitions to other projects
|
||||
//! such as rust-minidump, libmdmp, breakpad, etc. to ensure we're doing
|
||||
//! the right thing.
|
||||
|
||||
/// "MDMP" in little-endian.
|
||||
pub const signature = 0x504D444D;
|
||||
|
||||
/// The version of the minidump format.
|
||||
pub const version = 0xA793;
|
||||
|
||||
/// https://learn.microsoft.com/en-us/windows/win32/api/minidumpapiset/ns-minidumpapiset-minidump_header
|
||||
pub const Header = extern struct {
|
||||
signature: u32,
|
||||
version: packed struct(u32) { low: u16, high: u16 },
|
||||
stream_count: u32,
|
||||
stream_directory_rva: u32,
|
||||
checksum: u32,
|
||||
time_date_stamp: u32,
|
||||
flags: u64,
|
||||
};
|
||||
|
||||
/// https://learn.microsoft.com/en-us/windows/win32/api/minidumpapiset/ns-minidumpapiset-minidump_directory
|
||||
pub const Directory = extern struct {
|
||||
stream_type: u32,
|
||||
location: LocationDescriptor,
|
||||
};
|
||||
|
||||
/// https://learn.microsoft.com/en-us/windows/win32/api/minidumpapiset/ns-minidumpapiset-minidump_location_descriptor
|
||||
pub const LocationDescriptor = extern struct {
|
||||
data_size: u32,
|
||||
rva: u32,
|
||||
};
|
||||
|
||||
/// https://learn.microsoft.com/en-us/windows/win32/api/minidumpapiset/ns-minidumpapiset-minidump_memory_descriptor
|
||||
pub const MemoryDescriptor = extern struct {
|
||||
start_of_memory_range: u64,
|
||||
memory: LocationDescriptor,
|
||||
};
|
||||
|
||||
/// https://learn.microsoft.com/en-us/windows/win32/api/minidumpapiset/ns-minidumpapiset-minidump_thread_list
|
||||
pub const ThreadList = extern struct {
|
||||
number_of_threads: u32,
|
||||
threads: [1]Thread,
|
||||
};
|
||||
|
||||
/// https://learn.microsoft.com/en-us/windows/win32/api/minidumpapiset/ns-minidumpapiset-minidump_thread
|
||||
pub const Thread = extern struct {
|
||||
thread_id: u32,
|
||||
suspend_count: u32,
|
||||
priority_class: u32,
|
||||
priority: u32,
|
||||
teb: u64,
|
||||
stack: MemoryDescriptor,
|
||||
thread_context: LocationDescriptor,
|
||||
};
|
||||
242
src/crash/minidump/reader.zig
Normal file
242
src/crash/minidump/reader.zig
Normal file
@@ -0,0 +1,242 @@
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const external = @import("external.zig");
|
||||
const stream = @import("stream.zig");
|
||||
const EncodedStream = stream.EncodedStream;
|
||||
|
||||
const log = std.log.scoped(.minidump_reader);
|
||||
|
||||
/// Possible minidump-specific errors that can occur when reading a minidump.
|
||||
/// This isn't the full error set since IO errors can also occur depending
|
||||
/// on the Source type.
|
||||
pub const ReadError = error{
|
||||
InvalidHeader,
|
||||
InvalidVersion,
|
||||
StreamSizeMismatch,
|
||||
};
|
||||
|
||||
/// Reader creates a new minidump reader for the given source type. The
|
||||
/// source must have both a "reader()" and "seekableStream()" function.
|
||||
///
|
||||
/// Given the format of a minidump file, we must keep the source open and
|
||||
/// continually access it because the format of the minidump is full of
|
||||
/// pointers and offsets that we must follow depending on the stream types.
|
||||
/// Also, since we're not aware of all stream types (in fact its impossible
|
||||
/// to be aware since custom stream types are allowed), its possible any stream
|
||||
/// type can define their own pointers and offsets. So, the source must always
|
||||
/// be available so callers can decode the streams as needed.
|
||||
pub fn Reader(comptime S: type) type {
|
||||
return struct {
|
||||
const Self = @This();
|
||||
|
||||
/// The source data.
|
||||
source: Source,
|
||||
|
||||
/// The endianness of the minidump file. This is detected by reading
|
||||
/// the byte order of the header.
|
||||
endian: std.builtin.Endian,
|
||||
|
||||
/// The number of streams within the minidump file. This is read from
|
||||
/// the header and stored here so we can quickly access them. Note
|
||||
/// the stream types require reading the source; this is an optimization
|
||||
/// to avoid any allocations on the reader and the caller can choose
|
||||
/// to store them if they want.
|
||||
stream_count: u32,
|
||||
stream_directory_rva: u32,
|
||||
|
||||
const SourceCallable = switch (@typeInfo(Source)) {
|
||||
.Pointer => |v| v.child,
|
||||
.Struct => Source,
|
||||
else => @compileError("Source type must be a pointer or struct"),
|
||||
};
|
||||
|
||||
const SourceReader = @typeInfo(@TypeOf(SourceCallable.reader)).Fn.return_type.?;
|
||||
const SourceSeeker = @typeInfo(@TypeOf(SourceCallable.seekableStream)).Fn.return_type.?;
|
||||
|
||||
/// A limited reader for reading data from the source.
|
||||
pub const LimitedReader = std.io.LimitedReader(SourceReader);
|
||||
|
||||
/// The source type for the reader.
|
||||
pub const Source = S;
|
||||
|
||||
/// The stream types for reading
|
||||
pub const ThreadList = stream.thread_list.ThreadListReader(Self);
|
||||
|
||||
/// The reader type for stream reading. This has some other methods so
|
||||
/// you must still call reader() on the result to get the actual
|
||||
/// reader to read the data.
|
||||
pub const StreamReader = struct {
|
||||
source: Source,
|
||||
endian: std.builtin.Endian,
|
||||
directory: external.Directory,
|
||||
|
||||
/// Should not be accessed directly. This is setup whenever
|
||||
/// reader() is called.
|
||||
limit_reader: LimitedReader = undefined,
|
||||
|
||||
pub const Reader = LimitedReader.Reader;
|
||||
|
||||
/// Returns a Reader implementation that reads the bytes of the
|
||||
/// stream.
|
||||
///
|
||||
/// The reader is dependent on the state of Source so any
|
||||
/// state-changing operations on Source will invalidate the
|
||||
/// reader. For example, making another reader, reading another
|
||||
/// stream directory, closing the source, etc.
|
||||
pub fn reader(self: *StreamReader) LimitedReader.Reader {
|
||||
try self.source.seekableStream().seekTo(self.directory.location.rva);
|
||||
self.limit_reader = .{
|
||||
.inner_reader = self.source.reader(),
|
||||
.bytes_left = self.directory.location.data_size,
|
||||
};
|
||||
return self.limit_reader.reader();
|
||||
}
|
||||
|
||||
/// Seeks the source to the location of the directory.
|
||||
pub fn seekToPayload(self: *StreamReader) !void {
|
||||
try self.source.seekableStream().seekTo(self.directory.location.rva);
|
||||
}
|
||||
};
|
||||
|
||||
/// Iterator type to read over the streams in the minidump file.
|
||||
pub const StreamIterator = struct {
|
||||
reader: *const Self,
|
||||
i: u32 = 0,
|
||||
|
||||
pub fn next(self: *StreamIterator) !?StreamReader {
|
||||
if (self.i >= self.reader.stream_count) return null;
|
||||
const dir = try self.reader.directory(self.i);
|
||||
self.i += 1;
|
||||
return try self.reader.streamReader(dir);
|
||||
}
|
||||
};
|
||||
|
||||
/// Initialize a reader. The source must remain available for the entire
|
||||
/// lifetime of the reader. The reader does not take ownership of the
|
||||
/// source so if it has resources that need to be cleaned up, the caller
|
||||
/// must do so once the reader is no longer needed.
|
||||
pub fn init(source: Source) !Self {
|
||||
const header, const endian = try readHeader(Source, source);
|
||||
return .{
|
||||
.source = source,
|
||||
.endian = endian,
|
||||
.stream_count = header.stream_count,
|
||||
.stream_directory_rva = header.stream_directory_rva,
|
||||
};
|
||||
}
|
||||
|
||||
/// Return an iterator to read over the streams in the minidump file.
|
||||
/// This is very similar to using a simple for loop to stream_count
|
||||
/// and calling directory() on each index, but is more idiomatic
|
||||
/// Zig.
|
||||
pub fn streamIterator(self: *const Self) StreamIterator {
|
||||
return .{ .reader = self };
|
||||
}
|
||||
|
||||
/// Return a StreamReader for the given directory type. This streams
|
||||
/// from the underlying source so the returned reader is only valid
|
||||
/// as long as the source is unmodified (i.e. the source is not
|
||||
/// closed, the source seek position is not moved, etc.).
|
||||
pub fn streamReader(
|
||||
self: *const Self,
|
||||
dir: external.Directory,
|
||||
) SourceSeeker.SeekError!StreamReader {
|
||||
return .{
|
||||
.source = self.source,
|
||||
.endian = self.endian,
|
||||
.directory = dir,
|
||||
};
|
||||
}
|
||||
|
||||
/// Get the directory entry with the given index.
|
||||
///
|
||||
/// Asserts the index is valid (idx < stream_count).
|
||||
pub fn directory(self: *const Self, idx: usize) !external.Directory {
|
||||
assert(idx < self.stream_count);
|
||||
|
||||
// Seek to the directory.
|
||||
const offset: u32 = @intCast(@sizeOf(external.Directory) * idx);
|
||||
const rva: u32 = self.stream_directory_rva + offset;
|
||||
try self.source.seekableStream().seekTo(rva);
|
||||
|
||||
// Read the directory.
|
||||
return try self.source.reader().readStructEndian(
|
||||
external.Directory,
|
||||
self.endian,
|
||||
);
|
||||
}
|
||||
|
||||
/// Return a reader for the given location descriptor. This is only
|
||||
/// valid until the reader source is modified in some way.
|
||||
pub fn locationReader(
|
||||
self: *const Self,
|
||||
loc: external.LocationDescriptor,
|
||||
) !LimitedReader {
|
||||
try self.source.seekableStream().seekTo(loc.rva);
|
||||
return .{
|
||||
.inner_reader = self.source.reader(),
|
||||
.bytes_left = loc.data_size,
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Reads the header for the minidump file and returns endianness of
|
||||
/// the file.
|
||||
fn readHeader(comptime T: type, source: T) !struct {
|
||||
external.Header,
|
||||
std.builtin.Endian,
|
||||
} {
|
||||
// Start by trying LE.
|
||||
var endian: std.builtin.Endian = .little;
|
||||
var header = try source.reader().readStructEndian(external.Header, endian);
|
||||
|
||||
// If the signature doesn't match, we assume its BE.
|
||||
if (header.signature != external.signature) {
|
||||
// Seek back to the start of the file so we can reread.
|
||||
try source.seekableStream().seekTo(0);
|
||||
|
||||
// Try BE, if the signature doesn't match, return an error.
|
||||
endian = .big;
|
||||
header = try source.reader().readStructEndian(external.Header, endian);
|
||||
if (header.signature != external.signature) return ReadError.InvalidHeader;
|
||||
}
|
||||
|
||||
// "The low-order word is MINIDUMP_VERSION. The high-order word is an
|
||||
// internal value that is implementation specific."
|
||||
if (header.version.low != external.version) return ReadError.InvalidVersion;
|
||||
|
||||
return .{ header, endian };
|
||||
}
|
||||
|
||||
// Uncomment to dump some debug information for a minidump file.
|
||||
test "minidump debug" {
|
||||
var fbs = std.io.fixedBufferStream(@embedFile("../testdata/macos.dmp"));
|
||||
const r = try Reader(*@TypeOf(fbs)).init(&fbs);
|
||||
var it = r.streamIterator();
|
||||
while (try it.next()) |s| {
|
||||
log.warn("directory i={} dir={}", .{ it.i - 1, s.directory });
|
||||
}
|
||||
}
|
||||
|
||||
test "minidump read" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var fbs = std.io.fixedBufferStream(@embedFile("../testdata/macos.dmp"));
|
||||
const r = try Reader(*@TypeOf(fbs)).init(&fbs);
|
||||
try testing.expectEqual(std.builtin.Endian.little, r.endian);
|
||||
try testing.expectEqual(7, r.stream_count);
|
||||
{
|
||||
const dir = try r.directory(0);
|
||||
try testing.expectEqual(3, dir.stream_type);
|
||||
try testing.expectEqual(584, dir.location.data_size);
|
||||
|
||||
var bytes = std.ArrayList(u8).init(alloc);
|
||||
defer bytes.deinit();
|
||||
var sr = try r.streamReader(dir);
|
||||
try sr.reader().readAllArrayList(&bytes, std.math.maxInt(usize));
|
||||
try testing.expectEqual(584, bytes.items.len);
|
||||
}
|
||||
}
|
||||
30
src/crash/minidump/stream.zig
Normal file
30
src/crash/minidump/stream.zig
Normal file
@@ -0,0 +1,30 @@
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const log = std.log.scoped(.minidump_stream);
|
||||
|
||||
/// The known stream types.
|
||||
pub const thread_list = @import("stream_threadlist.zig");
|
||||
|
||||
/// A stream within the minidump file. A stream can be either in an encoded
|
||||
/// form or decoded form. The encoded form are raw bytes and aren't validated
|
||||
/// until they're decoded. The decoded form is a structured form of the stream.
|
||||
///
|
||||
/// The decoded form is more ergonomic to work with but the encoded form is
|
||||
/// more efficient to read/write.
|
||||
pub const Stream = union(enum) {
|
||||
encoded: EncodedStream,
|
||||
};
|
||||
|
||||
/// An encoded stream value. It is "encoded" in the sense that it is raw bytes
|
||||
/// with a type associated. The raw bytes are not validated to be correct for
|
||||
/// the type.
|
||||
pub const EncodedStream = struct {
|
||||
type: u32,
|
||||
data: []const u8,
|
||||
};
|
||||
|
||||
test {
|
||||
@import("std").testing.refAllDecls(@This());
|
||||
}
|
||||
117
src/crash/minidump/stream_threadlist.zig
Normal file
117
src/crash/minidump/stream_threadlist.zig
Normal file
@@ -0,0 +1,117 @@
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const external = @import("external.zig");
|
||||
const readerpkg = @import("reader.zig");
|
||||
const Reader = readerpkg.Reader;
|
||||
const ReadError = readerpkg.ReadError;
|
||||
|
||||
const log = std.log.scoped(.minidump_stream);
|
||||
|
||||
/// This is the list of threads from the process.
|
||||
///
|
||||
/// This is the Reader implementation. You usually do not use this directly.
|
||||
/// Instead, use Reader(T).ThreadList which will get you the same thing.
|
||||
///
|
||||
/// ThreadList is stream type 0x3.
|
||||
/// StreamReader is the Reader(T).StreamReader type.
|
||||
pub fn ThreadListReader(comptime R: type) type {
|
||||
return struct {
|
||||
const Self = @This();
|
||||
|
||||
/// The number of threads in the list.
|
||||
count: u32,
|
||||
|
||||
/// The rva to the first thread in the list.
|
||||
rva: u32,
|
||||
|
||||
/// Source data and endianness so we can read.
|
||||
source: R.Source,
|
||||
endian: std.builtin.Endian,
|
||||
|
||||
pub fn init(r: *R.StreamReader) !Self {
|
||||
assert(r.directory.stream_type == 0x3);
|
||||
try r.seekToPayload();
|
||||
const reader = r.source.reader();
|
||||
|
||||
// Our count is always a u32 in the header.
|
||||
const count = try reader.readInt(u32, r.endian);
|
||||
|
||||
// Determine if we have padding in our header. It is possible
|
||||
// for there to be padding if the list header was written by
|
||||
// a 32-bit process but is being read on a 64-bit process.
|
||||
const padding = padding: {
|
||||
const maybe_size = @sizeOf(u32) + (@sizeOf(external.Thread) * count);
|
||||
switch (std.math.order(maybe_size, r.directory.location.data_size)) {
|
||||
// It should never be larger than what the directory says.
|
||||
.gt => return ReadError.StreamSizeMismatch,
|
||||
|
||||
// If the sizes match exactly we're good.
|
||||
.eq => break :padding 0,
|
||||
|
||||
.lt => {
|
||||
const padding = r.directory.location.data_size - maybe_size;
|
||||
if (padding != 4) return ReadError.StreamSizeMismatch;
|
||||
break :padding padding;
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
// Rva is the location of the first thread in the list.
|
||||
const rva = r.directory.location.rva + @as(u32, @sizeOf(u32)) + padding;
|
||||
|
||||
return .{
|
||||
.count = count,
|
||||
.rva = rva,
|
||||
.source = r.source,
|
||||
.endian = r.endian,
|
||||
};
|
||||
}
|
||||
|
||||
/// Get the thread entry for the given index.
|
||||
///
|
||||
/// Index is asserted to be less than count.
|
||||
pub fn thread(self: *const Self, i: usize) !external.Thread {
|
||||
assert(i < self.count);
|
||||
|
||||
// Seek to the thread
|
||||
const offset: u32 = @intCast(@sizeOf(external.Thread) * i);
|
||||
const rva: u32 = self.rva + offset;
|
||||
try self.source.seekableStream().seekTo(rva);
|
||||
|
||||
// Read the thread
|
||||
return try self.source.reader().readStructEndian(
|
||||
external.Thread,
|
||||
self.endian,
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
test "minidump: threadlist" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var fbs = std.io.fixedBufferStream(@embedFile("../testdata/macos.dmp"));
|
||||
const R = Reader(*@TypeOf(fbs));
|
||||
const r = try R.init(&fbs);
|
||||
|
||||
// Get our thread list stream
|
||||
const dir = try r.directory(0);
|
||||
try testing.expectEqual(3, dir.stream_type);
|
||||
var sr = try r.streamReader(dir);
|
||||
|
||||
// Get our rich structure
|
||||
const v = try R.ThreadList.init(&sr);
|
||||
log.warn("threadlist count={} rva={}", .{ v.count, v.rva });
|
||||
|
||||
try testing.expectEqual(12, v.count);
|
||||
for (0..v.count) |i| {
|
||||
const t = try v.thread(i);
|
||||
log.warn("thread i={} thread={}", .{ i, t });
|
||||
|
||||
// Read our stack memory
|
||||
var stack_reader = try r.locationReader(t.stack.memory);
|
||||
const bytes = try stack_reader.reader().readAllAlloc(alloc, t.stack.memory.data_size);
|
||||
defer alloc.free(bytes);
|
||||
}
|
||||
}
|
||||
BIN
src/crash/testdata/macos.dmp
vendored
Normal file
BIN
src/crash/testdata/macos.dmp
vendored
Normal file
Binary file not shown.
@@ -594,30 +594,37 @@ pub const Face = struct {
|
||||
// All of these metrics are based on our layout above.
|
||||
const cell_height = @ceil(layout_metrics.height);
|
||||
const cell_baseline = @ceil(layout_metrics.height - layout_metrics.ascent);
|
||||
const underline_thickness = @ceil(@as(f32, @floatCast(ct_font.getUnderlineThickness())));
|
||||
const strikethrough_position = strikethrough_position: {
|
||||
// This is the height above baseline consumed by text. We must take
|
||||
// into account that our cell height splits the leading between two
|
||||
// rows so we subtract leading space (blank space).
|
||||
const above_baseline = layout_metrics.ascent - (layout_metrics.leading / 2);
|
||||
|
||||
// We want to position the strikethrough at 65% of the height.
|
||||
// This generally gives a nice visual appearance. The number 65%
|
||||
// is somewhat arbitrary but is a common value across terminals.
|
||||
const pos = above_baseline * 0.65;
|
||||
const underline_thickness = @ceil(@as(f32, @floatCast(ct_font.getUnderlineThickness())));
|
||||
const strikethrough_thickness = underline_thickness;
|
||||
|
||||
const strikethrough_position = strikethrough_position: {
|
||||
// This is the height of lower case letters in our font.
|
||||
const ex_height = ct_font.getXHeight();
|
||||
|
||||
// We want to position the strikethrough so that it's
|
||||
// vertically centered on any lower case text. This is
|
||||
// a fairly standard choice for strikethrough positioning.
|
||||
//
|
||||
// Because our `strikethrough_position` is relative to the
|
||||
// top of the cell we start with the ascent metric, which
|
||||
// is the distance from the top down to the baseline, then
|
||||
// we subtract half of the ex height to go back up to the
|
||||
// correct height that should evenly split lowercase text.
|
||||
const pos = layout_metrics.ascent -
|
||||
ex_height * 0.5 -
|
||||
strikethrough_thickness * 0.5;
|
||||
|
||||
break :strikethrough_position @ceil(pos);
|
||||
};
|
||||
const strikethrough_thickness = underline_thickness;
|
||||
|
||||
// Underline position reported is usually something like "-1" to
|
||||
// represent the amount under the baseline. We add this to our real
|
||||
// baseline to get the actual value from the bottom (+y is up).
|
||||
// The final underline position is +y from the TOP (confusing)
|
||||
// so we have to subtract from the cell height.
|
||||
const underline_position = cell_height -
|
||||
(cell_baseline + @ceil(@as(f32, @floatCast(ct_font.getUnderlinePosition())))) +
|
||||
1;
|
||||
const underline_position = @ceil(layout_metrics.ascent -
|
||||
@as(f32, @floatCast(ct_font.getUnderlinePosition())));
|
||||
|
||||
// Note: is this useful?
|
||||
// const units_per_em = ct_font.getUnitsPerEm();
|
||||
|
||||
@@ -607,6 +607,20 @@ pub const Face = struct {
|
||||
break :cell_width f26dot6ToFloat(size_metrics.max_advance);
|
||||
};
|
||||
|
||||
// Ex height is calculated by measuring the height of the `x` glyph.
|
||||
// If that fails then we just pretend it's 65% of the ascent height.
|
||||
const ex_height: f32 = ex_height: {
|
||||
if (face.getCharIndex('x')) |glyph_index| {
|
||||
if (face.loadGlyph(glyph_index, .{ .render = true })) {
|
||||
break :ex_height f26dot6ToFloat(face.handle.*.glyph.*.metrics.height);
|
||||
} else |_| {
|
||||
// Ignore the error since we just fall back to 65% of the ascent below
|
||||
}
|
||||
}
|
||||
|
||||
break :ex_height f26dot6ToFloat(size_metrics.ascender) * 0.65;
|
||||
};
|
||||
|
||||
// Cell height is calculated as the maximum of multiple things in order
|
||||
// to handle edge cases in fonts: (1) the height as reported in metadata
|
||||
// by the font designer (2) the maximum glyph height as measured in the
|
||||
@@ -646,50 +660,55 @@ pub const Face = struct {
|
||||
// is reversed.
|
||||
const cell_baseline = -1 * f26dot6ToFloat(size_metrics.descender);
|
||||
|
||||
const underline_thickness = @max(@as(f32, 1), fontUnitsToPxY(
|
||||
face,
|
||||
face.handle.*.underline_thickness,
|
||||
));
|
||||
|
||||
// The underline position. This is a value from the top where the
|
||||
// underline should go.
|
||||
const underline_position: f32 = underline_pos: {
|
||||
// The ascender is already scaled for scalable fonts, but the
|
||||
// underline position is not.
|
||||
const ascender_px = @as(i32, @intCast(size_metrics.ascender)) >> 6;
|
||||
const declared_px = freetype.mulFix(
|
||||
// From the FreeType docs:
|
||||
// > `underline_position`
|
||||
// > The position, in font units, of the underline line for
|
||||
// > this face. It is the center of the underlining stem.
|
||||
|
||||
const declared_px = @as(f32, @floatFromInt(freetype.mulFix(
|
||||
face.handle.*.underline_position,
|
||||
@intCast(face.handle.*.size.*.metrics.y_scale),
|
||||
) >> 6;
|
||||
))) / 64;
|
||||
|
||||
// We use the declared underline position if its available
|
||||
const declared = ascender_px - declared_px;
|
||||
// We use the declared underline position if its available.
|
||||
const declared = @ceil(cell_height - cell_baseline - declared_px - underline_thickness * 0.5);
|
||||
if (declared > 0)
|
||||
break :underline_pos @floatFromInt(declared);
|
||||
break :underline_pos declared;
|
||||
|
||||
// If we have no declared underline position, we go slightly under the
|
||||
// cell height (mainly: non-scalable fonts, i.e. emoji)
|
||||
break :underline_pos cell_height - 1;
|
||||
};
|
||||
const underline_thickness = @max(@as(f32, 1), fontUnitsToPxY(
|
||||
face,
|
||||
face.handle.*.underline_thickness,
|
||||
));
|
||||
|
||||
// The strikethrough position. We use the position provided by the
|
||||
// font if it exists otherwise we calculate a best guess.
|
||||
const strikethrough: struct {
|
||||
pos: f32,
|
||||
thickness: f32,
|
||||
} = if (face.getSfntTable(.os2)) |os2| .{
|
||||
.pos = pos: {
|
||||
// Ascender is scaled, strikeout pos is not
|
||||
const ascender_px = @as(i32, @intCast(size_metrics.ascender)) >> 6;
|
||||
const declared_px = freetype.mulFix(
|
||||
os2.yStrikeoutPosition,
|
||||
@as(i32, @intCast(face.handle.*.size.*.metrics.y_scale)),
|
||||
) >> 6;
|
||||
} = if (face.getSfntTable(.os2)) |os2| st: {
|
||||
const thickness = @max(@as(f32, 1), fontUnitsToPxY(face, os2.yStrikeoutSize));
|
||||
|
||||
break :pos @floatFromInt(ascender_px - declared_px);
|
||||
},
|
||||
.thickness = @max(@as(f32, 1), fontUnitsToPxY(face, os2.yStrikeoutSize)),
|
||||
const pos = @as(f32, @floatFromInt(freetype.mulFix(
|
||||
os2.yStrikeoutPosition,
|
||||
@as(i32, @intCast(face.handle.*.size.*.metrics.y_scale)),
|
||||
))) / 64;
|
||||
|
||||
break :st .{
|
||||
.pos = @ceil(cell_height - cell_baseline - pos),
|
||||
.thickness = thickness,
|
||||
};
|
||||
} else .{
|
||||
.pos = cell_baseline * 0.6,
|
||||
// Exactly 50% of the ex height so that our strikethrough is
|
||||
// centered through lowercase text. This is a common choice.
|
||||
.pos = @ceil(cell_height - cell_baseline - ex_height * 0.5 - underline_thickness * 0.5),
|
||||
.thickness = underline_thickness,
|
||||
};
|
||||
|
||||
@@ -832,7 +851,7 @@ test "metrics" {
|
||||
.cell_width = 16,
|
||||
.cell_height = 35,
|
||||
.cell_baseline = 7,
|
||||
.underline_position = 36,
|
||||
.underline_position = 35,
|
||||
.underline_thickness = 2,
|
||||
.strikethrough_position = 20,
|
||||
.strikethrough_thickness = 2,
|
||||
|
||||
@@ -27,189 +27,174 @@ pub fn renderGlyph(
|
||||
line_pos: u32,
|
||||
line_thickness: u32,
|
||||
) !font.Glyph {
|
||||
// Create the canvas we'll use to draw. We draw the underline in
|
||||
// a full cell size and position it according to "pos".
|
||||
var canvas = try font.sprite.Canvas.init(alloc, width, height);
|
||||
// Draw the appropriate sprite
|
||||
var canvas: font.sprite.Canvas, const offset_y: i32 = switch (sprite) {
|
||||
.underline => try drawSingle(alloc, width, line_thickness),
|
||||
.underline_double => try drawDouble(alloc, width, line_thickness),
|
||||
.underline_dotted => try drawDotted(alloc, width, line_thickness),
|
||||
.underline_dashed => try drawDashed(alloc, width, line_thickness),
|
||||
.underline_curly => try drawCurly(alloc, width, line_thickness),
|
||||
.strikethrough => try drawSingle(alloc, width, line_thickness),
|
||||
else => unreachable,
|
||||
};
|
||||
defer canvas.deinit(alloc);
|
||||
|
||||
// Perform the actual drawing
|
||||
(Draw{
|
||||
.width = width,
|
||||
.height = height,
|
||||
.pos = line_pos,
|
||||
.thickness = line_thickness,
|
||||
}).draw(&canvas, sprite);
|
||||
|
||||
// Write the drawing to the atlas
|
||||
const region = try canvas.writeAtlas(alloc, atlas);
|
||||
|
||||
// Our coordinates start at the BOTTOM for our renderers so we have to
|
||||
// specify an offset of the full height because we rendered a full size
|
||||
// cell.
|
||||
const offset_y = @as(i32, @intCast(height));
|
||||
|
||||
return font.Glyph{
|
||||
.width = width,
|
||||
.height = height,
|
||||
.height = @intCast(region.height),
|
||||
.offset_x = 0,
|
||||
.offset_y = offset_y,
|
||||
// Glyph.offset_y is the distance between the top of the glyph and the
|
||||
// bottom of the cell. We want the top of the glyph to be at line_pos
|
||||
// from the TOP of the cell, and then offset by the offset_y from the
|
||||
// draw function.
|
||||
.offset_y = @as(i32, @intCast(height - line_pos)) - offset_y,
|
||||
.atlas_x = region.x,
|
||||
.atlas_y = region.y,
|
||||
.advance_x = @floatFromInt(width),
|
||||
};
|
||||
}
|
||||
|
||||
/// Stores drawing state.
|
||||
const Draw = struct {
|
||||
width: u32,
|
||||
height: u32,
|
||||
pos: u32,
|
||||
thickness: u32,
|
||||
/// A tuple with the canvas that the desired sprite was drawn on and
|
||||
/// a recommended offset (+Y = down) to shift its Y position by, to
|
||||
/// correct for underline styles with additional thickness.
|
||||
const CanvasAndOffset = struct { font.sprite.Canvas, i32 };
|
||||
|
||||
/// Draw a specific underline sprite to the canvas.
|
||||
fn draw(self: Draw, canvas: *font.sprite.Canvas, sprite: Sprite) void {
|
||||
switch (sprite) {
|
||||
.underline => self.drawSingle(canvas),
|
||||
.underline_double => self.drawDouble(canvas),
|
||||
.underline_dotted => self.drawDotted(canvas),
|
||||
.underline_dashed => self.drawDashed(canvas),
|
||||
.underline_curly => self.drawCurly(canvas),
|
||||
.strikethrough => self.drawSingle(canvas),
|
||||
else => unreachable,
|
||||
}
|
||||
}
|
||||
/// Draw a single underline.
|
||||
fn drawSingle(alloc: Allocator, width: u32, thickness: u32) !CanvasAndOffset {
|
||||
const height: u32 = thickness;
|
||||
var canvas = try font.sprite.Canvas.init(alloc, width, height);
|
||||
|
||||
/// Draw a single underline.
|
||||
fn drawSingle(self: Draw, canvas: *font.sprite.Canvas) void {
|
||||
// Ensure we never overflow out of bounds on the canvas
|
||||
const y_max = self.height -| 1;
|
||||
const bottom = @min(self.pos + self.thickness, y_max);
|
||||
const y = bottom -| self.thickness;
|
||||
const max_height = self.height - y;
|
||||
canvas.rect(.{
|
||||
.x = 0,
|
||||
.y = 0,
|
||||
.width = width,
|
||||
.height = thickness,
|
||||
}, .on);
|
||||
|
||||
const offset_y: i32 = 0;
|
||||
|
||||
return .{ canvas, offset_y };
|
||||
}
|
||||
|
||||
/// Draw a double underline.
|
||||
fn drawDouble(alloc: Allocator, width: u32, thickness: u32) !CanvasAndOffset {
|
||||
const height: u32 = thickness * 3;
|
||||
var canvas = try font.sprite.Canvas.init(alloc, width, height);
|
||||
|
||||
canvas.rect(.{
|
||||
.x = 0,
|
||||
.y = 0,
|
||||
.width = width,
|
||||
.height = thickness,
|
||||
}, .on);
|
||||
|
||||
canvas.rect(.{
|
||||
.x = 0,
|
||||
.y = @intCast(thickness * 2),
|
||||
.width = width,
|
||||
.height = thickness,
|
||||
}, .on);
|
||||
|
||||
const offset_y: i32 = -@as(i32, @intCast(thickness));
|
||||
|
||||
return .{ canvas, offset_y };
|
||||
}
|
||||
|
||||
/// Draw a dotted underline.
|
||||
fn drawDotted(alloc: Allocator, width: u32, thickness: u32) !CanvasAndOffset {
|
||||
const height: u32 = thickness;
|
||||
var canvas = try font.sprite.Canvas.init(alloc, width, height);
|
||||
|
||||
const dot_width = @max(thickness, 3);
|
||||
const dot_count = @max((width / dot_width) / 2, 1);
|
||||
const gap_width = try std.math.divCeil(u32, width -| (dot_count * dot_width), dot_count);
|
||||
var i: u32 = 0;
|
||||
while (i < dot_count) : (i += 1) {
|
||||
// Ensure we never go out of bounds for the rect
|
||||
const x = @min(i * (dot_width + gap_width), width - 1);
|
||||
const rect_width = @min(width - x, dot_width);
|
||||
canvas.rect(.{
|
||||
.x = 0,
|
||||
.y = @intCast(y),
|
||||
.width = self.width,
|
||||
.height = @min(self.thickness, max_height),
|
||||
.x = @intCast(x),
|
||||
.y = 0,
|
||||
.width = rect_width,
|
||||
.height = thickness,
|
||||
}, .on);
|
||||
}
|
||||
|
||||
/// Draw a double underline.
|
||||
fn drawDouble(self: Draw, canvas: *font.sprite.Canvas) void {
|
||||
// The maximum y value has to have space for the bottom underline.
|
||||
// If we underflow (saturated) to 0, then we don't draw. This should
|
||||
// never happen but we don't want to draw something undefined.
|
||||
const y_max = self.height -| 1 -| self.thickness;
|
||||
if (y_max == 0) return;
|
||||
const offset_y: i32 = 0;
|
||||
|
||||
const space = self.thickness * 2;
|
||||
const bottom = @min(self.pos + space, y_max);
|
||||
const top = bottom - space;
|
||||
return .{ canvas, offset_y };
|
||||
}
|
||||
|
||||
/// Draw a dashed underline.
|
||||
fn drawDashed(alloc: Allocator, width: u32, thickness: u32) !CanvasAndOffset {
|
||||
const height: u32 = thickness;
|
||||
var canvas = try font.sprite.Canvas.init(alloc, width, height);
|
||||
|
||||
const dash_width = width / 3 + 1;
|
||||
const dash_count = (width / dash_width) + 1;
|
||||
var i: u32 = 0;
|
||||
while (i < dash_count) : (i += 2) {
|
||||
// Ensure we never go out of bounds for the rect
|
||||
const x = @min(i * dash_width, width - 1);
|
||||
const rect_width = @min(width - x, dash_width);
|
||||
canvas.rect(.{
|
||||
.x = 0,
|
||||
.y = @intCast(top),
|
||||
.width = self.width,
|
||||
.height = self.thickness,
|
||||
}, .on);
|
||||
|
||||
canvas.rect(.{
|
||||
.x = 0,
|
||||
.y = @intCast(bottom),
|
||||
.width = self.width,
|
||||
.height = self.thickness,
|
||||
.x = @intCast(x),
|
||||
.y = 0,
|
||||
.width = rect_width,
|
||||
.height = thickness,
|
||||
}, .on);
|
||||
}
|
||||
|
||||
/// Draw a dotted underline.
|
||||
fn drawDotted(self: Draw, canvas: *font.sprite.Canvas) void {
|
||||
const y_max = self.height -| 1 -| self.thickness;
|
||||
if (y_max == 0) return;
|
||||
const y = @min(self.pos, y_max);
|
||||
const dot_width = @max(self.thickness, 3);
|
||||
const dot_count = self.width / dot_width;
|
||||
var i: u32 = 0;
|
||||
while (i < dot_count) : (i += 2) {
|
||||
// Ensure we never go out of bounds for the rect
|
||||
const x = @min(i * dot_width, self.width - 1);
|
||||
const width = @min(self.width - 1 - x, dot_width);
|
||||
canvas.rect(.{
|
||||
.x = @intCast(i * dot_width),
|
||||
.y = @intCast(y),
|
||||
.width = width,
|
||||
.height = self.thickness,
|
||||
}, .on);
|
||||
const offset_y: i32 = 0;
|
||||
|
||||
return .{ canvas, offset_y };
|
||||
}
|
||||
|
||||
/// Draw a curly underline. Thanks to Wez Furlong for providing
|
||||
/// the basic math structure for this since I was lazy with the
|
||||
/// geometry.
|
||||
fn drawCurly(alloc: Allocator, width: u32, thickness: u32) !CanvasAndOffset {
|
||||
const height: u32 = thickness * 4;
|
||||
var canvas = try font.sprite.Canvas.init(alloc, width, height);
|
||||
|
||||
// Calculate the wave period for a single character
|
||||
// `2 * pi...` = 1 peak per character
|
||||
// `4 * pi...` = 2 peaks per character
|
||||
const wave_period = 2 * std.math.pi / @as(f64, @floatFromInt(width - 1));
|
||||
|
||||
// The full amplitude of the wave can be from the bottom to the
|
||||
// underline position. We also calculate our mid y point of the wave
|
||||
const half_amplitude: f64 = @as(f64, @floatFromInt(thickness));
|
||||
const y_mid: f64 = half_amplitude + 1;
|
||||
|
||||
// follow Xiaolin Wu's antialias algorithm to draw the curve
|
||||
var x: u32 = 0;
|
||||
while (x < width) : (x += 1) {
|
||||
const cosx: f64 = @cos(@as(f64, @floatFromInt(x)) * wave_period);
|
||||
const y: f64 = y_mid + half_amplitude * cosx;
|
||||
const y_upper: u32 = @intFromFloat(@floor(y));
|
||||
const y_lower: u32 = y_upper + thickness + (thickness >> 1);
|
||||
const alpha: u8 = @intFromFloat(255 * @abs(y - @floor(y)));
|
||||
|
||||
// upper and lower bounds
|
||||
canvas.pixel(x, @min(y_upper, height), @enumFromInt(255 - alpha));
|
||||
canvas.pixel(x, @min(y_lower, height), @enumFromInt(alpha));
|
||||
|
||||
// fill between upper and lower bound
|
||||
var y_fill: u32 = y_upper + 1;
|
||||
while (y_fill < y_lower) : (y_fill += 1) {
|
||||
canvas.pixel(x, @min(y_fill, height), .on);
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw a dashed underline.
|
||||
fn drawDashed(self: Draw, canvas: *font.sprite.Canvas) void {
|
||||
const y_max = self.height -| 1 -| self.thickness;
|
||||
if (y_max == 0) return;
|
||||
const y = @min(self.pos, y_max);
|
||||
const dash_width = self.width / 3 + 1;
|
||||
const dash_count = (self.width / dash_width) + 1;
|
||||
var i: u32 = 0;
|
||||
while (i < dash_count) : (i += 2) {
|
||||
// Ensure we never go out of bounds for the rect
|
||||
const x = @min(i * dash_width, self.width - 1);
|
||||
const width = @min(self.width - 1 - x, dash_width);
|
||||
canvas.rect(.{
|
||||
.x = @intCast(x),
|
||||
.y = @intCast(y),
|
||||
.width = width,
|
||||
.height = self.thickness,
|
||||
}, .on);
|
||||
}
|
||||
}
|
||||
const offset_y: i32 = -@as(i32, @intCast(thickness * 2));
|
||||
|
||||
/// Draw a curly underline. Thanks to Wez Furlong for providing
|
||||
/// the basic math structure for this since I was lazy with the
|
||||
/// geometry.
|
||||
fn drawCurly(self: Draw, canvas: *font.sprite.Canvas) void {
|
||||
// This is the lowest that the curl can go.
|
||||
const y_max = self.height - 1;
|
||||
|
||||
// Calculate the wave period for a single character
|
||||
// `2 * pi...` = 1 peak per character
|
||||
// `4 * pi...` = 2 peaks per character
|
||||
const wave_period = 2 * std.math.pi / @as(f64, @floatFromInt(self.width - 1));
|
||||
|
||||
// Some fonts put the underline too close to the bottom of the
|
||||
// cell height and this doesn't allow us to make a high enough
|
||||
// wave. This constant is arbitrary, change it for aesthetics.
|
||||
const pos: u32 = pos: {
|
||||
const MIN_AMPLITUDE: u32 = @max(self.height / 9, 2);
|
||||
break :pos y_max - (MIN_AMPLITUDE * 2);
|
||||
};
|
||||
|
||||
// The full amplitude of the wave can be from the bottom to the
|
||||
// underline position. We also calculate our mid y point of the wave
|
||||
const double_amplitude: f64 = @floatFromInt(y_max - pos);
|
||||
const half_amplitude: f64 = @max(1, double_amplitude / 4);
|
||||
const y_mid: u32 = pos + @as(u32, @intFromFloat(2 * half_amplitude));
|
||||
|
||||
// follow Xiaolin Wu's antialias algorithm to draw the curve
|
||||
var x: u32 = 0;
|
||||
while (x < self.width) : (x += 1) {
|
||||
const y: f64 = @as(f64, @floatFromInt(y_mid)) + (half_amplitude * @cos(@as(f64, @floatFromInt(x)) * wave_period));
|
||||
const y_upper: u32 = @intFromFloat(@floor(y));
|
||||
const y_lower: u32 = y_upper + self.thickness;
|
||||
const alpha: u8 = @intFromFloat(255 * @abs(y - @floor(y)));
|
||||
|
||||
// upper and lower bounds
|
||||
canvas.pixel(x, @min(y_upper, y_max), @enumFromInt(255 - alpha));
|
||||
canvas.pixel(x, @min(y_lower, y_max), @enumFromInt(alpha));
|
||||
|
||||
// fill between upper and lower bound
|
||||
var y_fill: u32 = y_upper + 1;
|
||||
while (y_fill < y_lower) : (y_fill += 1) {
|
||||
canvas.pixel(x, @min(y_fill, y_max), .on);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
return .{ canvas, offset_y };
|
||||
}
|
||||
|
||||
test "single" {
|
||||
const testing = std.testing;
|
||||
|
||||
@@ -6,6 +6,7 @@ const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const assert = std.debug.assert;
|
||||
const key = @import("key.zig");
|
||||
const KeyEvent = key.KeyEvent;
|
||||
|
||||
/// The trigger that needs to be performed to execute the action.
|
||||
trigger: Trigger,
|
||||
@@ -13,21 +14,36 @@ trigger: Trigger,
|
||||
/// The action to take if this binding matches
|
||||
action: Action,
|
||||
|
||||
/// True if this binding should consume the input when the
|
||||
/// action is triggered.
|
||||
consumed: bool = true,
|
||||
/// Boolean flags that can be set per binding.
|
||||
flags: Flags = .{},
|
||||
|
||||
pub const Error = error{
|
||||
InvalidFormat,
|
||||
InvalidAction,
|
||||
};
|
||||
|
||||
/// Flags the full binding-scoped flags that can be set per binding.
|
||||
pub const Flags = packed struct {
|
||||
/// True if this binding should consume the input when the
|
||||
/// action is triggered.
|
||||
consumed: bool = true,
|
||||
|
||||
/// True if this binding should be forwarded to all active surfaces
|
||||
/// in the application.
|
||||
all: bool = false,
|
||||
|
||||
/// True if this binding is global. Global bindings should work system-wide
|
||||
/// and not just while Ghostty is focused. This may not work on all platforms.
|
||||
/// See the keybind config documentation for more information.
|
||||
global: bool = false,
|
||||
};
|
||||
|
||||
/// Full binding parser. The binding parser is implemented as an iterator
|
||||
/// which yields elements to support multi-key sequences without allocation.
|
||||
pub const Parser = struct {
|
||||
unconsumed: bool = false,
|
||||
trigger_it: SequenceIterator,
|
||||
action: Action,
|
||||
flags: Flags = .{},
|
||||
|
||||
pub const Elem = union(enum) {
|
||||
/// A leader trigger in a sequence.
|
||||
@@ -38,11 +54,7 @@ pub const Parser = struct {
|
||||
};
|
||||
|
||||
pub fn init(raw_input: []const u8) Error!Parser {
|
||||
// If our entire input is prefixed with "unconsumed:" then we are
|
||||
// not consuming this keybind when the action is triggered.
|
||||
const unconsumed_prefix = "unconsumed:";
|
||||
const unconsumed = std.mem.startsWith(u8, raw_input, unconsumed_prefix);
|
||||
const start_idx = if (unconsumed) unconsumed_prefix.len else 0;
|
||||
const flags, const start_idx = try parseFlags(raw_input);
|
||||
const input = raw_input[start_idx..];
|
||||
|
||||
// Find the first = which splits are mapping into the trigger
|
||||
@@ -52,24 +64,63 @@ pub const Parser = struct {
|
||||
// Sequence iterator goes up to the equal, action is after. We can
|
||||
// parse the action now.
|
||||
return .{
|
||||
.unconsumed = unconsumed,
|
||||
.trigger_it = .{ .input = input[0..eql_idx] },
|
||||
.action = try Action.parse(input[eql_idx + 1 ..]),
|
||||
.flags = flags,
|
||||
};
|
||||
}
|
||||
|
||||
fn parseFlags(raw_input: []const u8) Error!struct { Flags, usize } {
|
||||
var flags: Flags = .{};
|
||||
|
||||
var start_idx: usize = 0;
|
||||
var input: []const u8 = raw_input;
|
||||
while (true) {
|
||||
// Find the next prefix
|
||||
const idx = std.mem.indexOf(u8, input, ":") orelse break;
|
||||
const prefix = input[0..idx];
|
||||
|
||||
// If the prefix is one of our flags then set it.
|
||||
if (std.mem.eql(u8, prefix, "all")) {
|
||||
if (flags.all) return Error.InvalidFormat;
|
||||
flags.all = true;
|
||||
} else if (std.mem.eql(u8, prefix, "global")) {
|
||||
if (flags.global) return Error.InvalidFormat;
|
||||
flags.global = true;
|
||||
} else if (std.mem.eql(u8, prefix, "unconsumed")) {
|
||||
if (!flags.consumed) return Error.InvalidFormat;
|
||||
flags.consumed = false;
|
||||
} else {
|
||||
// If we don't recognize the prefix then we're done.
|
||||
// There are trigger-specific prefixes like "physical:" so
|
||||
// this lets us fall into that.
|
||||
break;
|
||||
}
|
||||
|
||||
// Move past the prefix
|
||||
start_idx += idx + 1;
|
||||
input = input[idx + 1 ..];
|
||||
}
|
||||
|
||||
return .{ flags, start_idx };
|
||||
}
|
||||
|
||||
pub fn next(self: *Parser) Error!?Elem {
|
||||
// Get our trigger. If we're out of triggers then we're done.
|
||||
const trigger = (try self.trigger_it.next()) orelse return null;
|
||||
|
||||
// If this is our last trigger then it is our final binding.
|
||||
if (!self.trigger_it.done()) return .{ .leader = trigger };
|
||||
if (!self.trigger_it.done()) {
|
||||
// Global/all bindings can't be sequences
|
||||
if (self.flags.global or self.flags.all) return error.InvalidFormat;
|
||||
return .{ .leader = trigger };
|
||||
}
|
||||
|
||||
// Out of triggers, yield the final action.
|
||||
return .{ .binding = .{
|
||||
.trigger = trigger,
|
||||
.action = self.action,
|
||||
.consumed = !self.unconsumed,
|
||||
.flags = self.flags,
|
||||
} };
|
||||
}
|
||||
|
||||
@@ -228,7 +279,8 @@ pub const Action = union(enum) {
|
||||
/// available values.
|
||||
write_selection_file: WriteScreenAction,
|
||||
|
||||
/// Open a new window.
|
||||
/// Open a new window. If the application isn't currently focused,
|
||||
/// this will bring it to the front.
|
||||
new_window: void,
|
||||
|
||||
/// Open a new tab.
|
||||
@@ -246,6 +298,10 @@ pub const Action = union(enum) {
|
||||
/// Go to the tab with the specific number, 1-indexed.
|
||||
goto_tab: usize,
|
||||
|
||||
/// Toggle the tab overview.
|
||||
/// This only works with libadwaita enabled currently.
|
||||
toggle_tab_overview: void,
|
||||
|
||||
/// Create a new split in the given direction. The new split will appear in
|
||||
/// the direction given.
|
||||
new_split: SplitDirection,
|
||||
@@ -297,6 +353,37 @@ pub const Action = union(enum) {
|
||||
/// Toggle window decorations on and off. This only works on Linux.
|
||||
toggle_window_decorations: void,
|
||||
|
||||
/// Toggle secure input mode on or off. This is used to prevent apps
|
||||
/// that monitor input from seeing what you type. This is useful for
|
||||
/// entering passwords or other sensitive information.
|
||||
///
|
||||
/// This applies to the entire application, not just the focused
|
||||
/// terminal. You must toggle it off to disable it, or quit Ghostty.
|
||||
///
|
||||
/// This only works on macOS, since this is a system API on macOS.
|
||||
toggle_secure_input: void,
|
||||
|
||||
/// Toggle the "quick" terminal. The quick terminal is a terminal that
|
||||
/// appears on demand from a keybinding, often sliding in from a screen
|
||||
/// edge such as the top. This is useful for quick access to a terminal
|
||||
/// without having to open a new window or tab.
|
||||
///
|
||||
/// When the quick terminal loses focus, it disappears. The terminal state
|
||||
/// is preserved between appearances, so you can always press the keybinding
|
||||
/// to bring it back up.
|
||||
///
|
||||
/// The quick terminal has some limitations:
|
||||
///
|
||||
/// - It is a singleton; only one instance can exist at a time.
|
||||
/// - It does not support tabs.
|
||||
/// - It does not support fullscreen.
|
||||
/// - It will not be restored when the application is restarted
|
||||
/// (for systems that support window restoration).
|
||||
///
|
||||
/// See the various configurations for the quick terminal in the
|
||||
/// configuration file to customize its behavior.
|
||||
toggle_quick_terminal: void,
|
||||
|
||||
/// Quit ghostty.
|
||||
quit: void,
|
||||
|
||||
@@ -348,8 +435,7 @@ pub const Action = union(enum) {
|
||||
// Note: we don't support top or left yet
|
||||
};
|
||||
|
||||
// Extern because it is used in the embedded runtime ABI.
|
||||
pub const SplitFocusDirection = enum(c_int) {
|
||||
pub const SplitFocusDirection = enum {
|
||||
previous,
|
||||
next,
|
||||
|
||||
@@ -359,8 +445,7 @@ pub const Action = union(enum) {
|
||||
right,
|
||||
};
|
||||
|
||||
// Extern because it is used in the embedded runtime ABI.
|
||||
pub const SplitResizeDirection = enum(c_int) {
|
||||
pub const SplitResizeDirection = enum {
|
||||
up,
|
||||
down,
|
||||
left,
|
||||
@@ -378,7 +463,7 @@ pub const Action = union(enum) {
|
||||
};
|
||||
|
||||
// Extern because it is used in the embedded runtime ABI.
|
||||
pub const InspectorMode = enum(c_int) {
|
||||
pub const InspectorMode = enum {
|
||||
toggle,
|
||||
show,
|
||||
hide,
|
||||
@@ -479,6 +564,144 @@ pub const Action = union(enum) {
|
||||
return Error.InvalidAction;
|
||||
}
|
||||
|
||||
/// The scope of an action. The scope is the context in which an action
|
||||
/// must be executed.
|
||||
pub const Scope = enum {
|
||||
app,
|
||||
surface,
|
||||
};
|
||||
|
||||
/// Returns the scope of an action.
|
||||
pub fn scope(self: Action) Scope {
|
||||
return switch (self) {
|
||||
// Doesn't really matter, so we'll see app.
|
||||
.ignore,
|
||||
.unbind,
|
||||
=> .app,
|
||||
|
||||
// Obviously app actions.
|
||||
.open_config,
|
||||
.reload_config,
|
||||
.close_all_windows,
|
||||
.quit,
|
||||
.toggle_quick_terminal,
|
||||
=> .app,
|
||||
|
||||
// These are app but can be special-cased in a surface context.
|
||||
.new_window,
|
||||
=> .app,
|
||||
|
||||
// Obviously surface actions.
|
||||
.csi,
|
||||
.esc,
|
||||
.text,
|
||||
.cursor_key,
|
||||
.reset,
|
||||
.copy_to_clipboard,
|
||||
.paste_from_clipboard,
|
||||
.paste_from_selection,
|
||||
.increase_font_size,
|
||||
.decrease_font_size,
|
||||
.reset_font_size,
|
||||
.clear_screen,
|
||||
.select_all,
|
||||
.scroll_to_top,
|
||||
.scroll_to_bottom,
|
||||
.scroll_page_up,
|
||||
.scroll_page_down,
|
||||
.scroll_page_fractional,
|
||||
.scroll_page_lines,
|
||||
.adjust_selection,
|
||||
.jump_to_prompt,
|
||||
.write_scrollback_file,
|
||||
.write_screen_file,
|
||||
.write_selection_file,
|
||||
.close_surface,
|
||||
.close_window,
|
||||
.toggle_fullscreen,
|
||||
.toggle_window_decorations,
|
||||
.toggle_secure_input,
|
||||
.crash,
|
||||
=> .surface,
|
||||
|
||||
// These are less obvious surface actions. They're surface
|
||||
// actions because they are relevant to the surface they
|
||||
// come from. For example `new_window` needs to be sourced to
|
||||
// a surface so inheritance can be done correctly.
|
||||
.new_tab,
|
||||
.previous_tab,
|
||||
.next_tab,
|
||||
.last_tab,
|
||||
.goto_tab,
|
||||
.toggle_tab_overview,
|
||||
.new_split,
|
||||
.goto_split,
|
||||
.toggle_split_zoom,
|
||||
.resize_split,
|
||||
.equalize_splits,
|
||||
.inspector,
|
||||
=> .surface,
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns a union type that only contains actions that are scoped to
|
||||
/// the given scope.
|
||||
pub fn Scoped(comptime s: Scope) type {
|
||||
const all_fields = @typeInfo(Action).Union.fields;
|
||||
|
||||
// Find all fields that are app-scoped
|
||||
var i: usize = 0;
|
||||
var union_fields: [all_fields.len]std.builtin.Type.UnionField = undefined;
|
||||
var enum_fields: [all_fields.len]std.builtin.Type.EnumField = undefined;
|
||||
for (all_fields) |field| {
|
||||
const action = @unionInit(Action, field.name, undefined);
|
||||
if (action.scope() == s) {
|
||||
union_fields[i] = field;
|
||||
enum_fields[i] = .{ .name = field.name, .value = i };
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Build our union
|
||||
return @Type(.{ .Union = .{
|
||||
.layout = .auto,
|
||||
.tag_type = @Type(.{ .Enum = .{
|
||||
.tag_type = std.math.IntFittingRange(0, i),
|
||||
.fields = enum_fields[0..i],
|
||||
.decls = &.{},
|
||||
.is_exhaustive = true,
|
||||
} }),
|
||||
.fields = union_fields[0..i],
|
||||
.decls = &.{},
|
||||
} });
|
||||
}
|
||||
|
||||
/// Returns the scoped version of this action. If the action is not
|
||||
/// scoped to the given scope then this returns null.
|
||||
///
|
||||
/// The benefit of this function is that it allows us to use Zig's
|
||||
/// exhaustive switch safety to ensure we always properly handle certain
|
||||
/// scoped actions.
|
||||
pub fn scoped(self: Action, comptime s: Scope) ?Scoped(s) {
|
||||
switch (self) {
|
||||
inline else => |v, tag| {
|
||||
// Use comptime to prune out non-app actions
|
||||
if (comptime @unionInit(
|
||||
Action,
|
||||
@tagName(tag),
|
||||
undefined,
|
||||
).scope() != s) return null;
|
||||
|
||||
// Initialize our app action
|
||||
return @unionInit(
|
||||
Scoped(s),
|
||||
@tagName(tag),
|
||||
v,
|
||||
);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Implements the formatter for the fmt package. This encodes the
|
||||
/// action back into the format used by parse.
|
||||
pub fn format(
|
||||
@@ -534,10 +757,15 @@ pub const Action = union(enum) {
|
||||
/// action.
|
||||
pub fn hash(self: Action) u64 {
|
||||
var hasher = std.hash.Wyhash.init(0);
|
||||
self.hashIncremental(&hasher);
|
||||
return hasher.final();
|
||||
}
|
||||
|
||||
/// Hash the action into the given hasher.
|
||||
fn hashIncremental(self: Action, hasher: anytype) void {
|
||||
// Always has the active tag.
|
||||
const Tag = @typeInfo(Action).Union.tag_type.?;
|
||||
std.hash.autoHash(&hasher, @as(Tag, self));
|
||||
std.hash.autoHash(hasher, @as(Tag, self));
|
||||
|
||||
// Hash the value of the field.
|
||||
switch (self) {
|
||||
@@ -552,25 +780,23 @@ pub const Action = union(enum) {
|
||||
// signed zeros but these are not cases we expect for
|
||||
// our bindings.
|
||||
f32 => std.hash.autoHash(
|
||||
&hasher,
|
||||
hasher,
|
||||
@as(u32, @bitCast(field)),
|
||||
),
|
||||
f64 => std.hash.autoHash(
|
||||
&hasher,
|
||||
hasher,
|
||||
@as(u64, @bitCast(field)),
|
||||
),
|
||||
|
||||
// Everything else automatically handle.
|
||||
else => std.hash.autoHashStrat(
|
||||
&hasher,
|
||||
hasher,
|
||||
field,
|
||||
.DeepRecursive,
|
||||
),
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
return hasher.final();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -727,11 +953,16 @@ pub const Trigger = struct {
|
||||
/// Returns a hash code that can be used to uniquely identify this trigger.
|
||||
pub fn hash(self: Trigger) u64 {
|
||||
var hasher = std.hash.Wyhash.init(0);
|
||||
std.hash.autoHash(&hasher, self.key);
|
||||
std.hash.autoHash(&hasher, self.mods.binding());
|
||||
self.hashIncremental(&hasher);
|
||||
return hasher.final();
|
||||
}
|
||||
|
||||
/// Hash the trigger into the given hasher.
|
||||
fn hashIncremental(self: Trigger, hasher: anytype) void {
|
||||
std.hash.autoHash(hasher, self.key);
|
||||
std.hash.autoHash(hasher, self.mods.binding());
|
||||
}
|
||||
|
||||
/// Convert the trigger to a C API compatible trigger.
|
||||
pub fn cval(self: Trigger) C {
|
||||
return .{
|
||||
@@ -808,10 +1039,8 @@ pub const Set = struct {
|
||||
leader: *Set,
|
||||
|
||||
/// This trigger completes a sequence and the value is the action
|
||||
/// to take. The "_unconsumed" variant is used for triggers that
|
||||
/// should not consume the input.
|
||||
action: Action,
|
||||
action_unconsumed: Action,
|
||||
/// to take along with the flags that may define binding behavior.
|
||||
leaf: Leaf,
|
||||
|
||||
/// Implements the formatter for the fmt package. This encodes the
|
||||
/// action back into the format used by parse.
|
||||
@@ -836,14 +1065,28 @@ pub const Set = struct {
|
||||
}
|
||||
},
|
||||
|
||||
.action, .action_unconsumed => |action| {
|
||||
.leaf => |leaf| {
|
||||
// action implements the format
|
||||
try writer.print("={s}", .{action});
|
||||
try writer.print("={s}", .{leaf.action});
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/// Leaf node of a set is an action to trigger. This is a "leaf" compared
|
||||
/// to the inner nodes which are "leaders" for sequences.
|
||||
pub const Leaf = struct {
|
||||
action: Action,
|
||||
flags: Flags,
|
||||
|
||||
pub fn hash(self: Leaf) u64 {
|
||||
var hasher = std.hash.Wyhash.init(0);
|
||||
self.action.hash(&hasher);
|
||||
std.hash.autoHash(&hasher, self.flags);
|
||||
return hasher.final();
|
||||
}
|
||||
};
|
||||
|
||||
pub fn deinit(self: *Set, alloc: Allocator) void {
|
||||
// Clear any leaders if we have them
|
||||
var it = self.bindings.iterator();
|
||||
@@ -852,7 +1095,7 @@ pub const Set = struct {
|
||||
s.deinit(alloc);
|
||||
alloc.destroy(s);
|
||||
},
|
||||
.action, .action_unconsumed => {},
|
||||
.leaf => {},
|
||||
};
|
||||
|
||||
self.bindings.deinit(alloc);
|
||||
@@ -924,7 +1167,7 @@ pub const Set = struct {
|
||||
error.OutOfMemory => return error.OutOfMemory,
|
||||
},
|
||||
|
||||
.action, .action_unconsumed => {
|
||||
.leaf => {
|
||||
// Remove the existing action. Fallthrough as if
|
||||
// we don't have a leader.
|
||||
set.remove(alloc, t);
|
||||
@@ -948,11 +1191,11 @@ pub const Set = struct {
|
||||
set.remove(alloc, t);
|
||||
if (old) |entry| switch (entry) {
|
||||
.leader => unreachable, // Handled above
|
||||
inline .action, .action_unconsumed => |action, tag| set.put_(
|
||||
.leaf => |leaf| set.putFlags(
|
||||
alloc,
|
||||
t,
|
||||
action,
|
||||
tag == .action,
|
||||
leaf.action,
|
||||
leaf.flags,
|
||||
) catch {},
|
||||
};
|
||||
},
|
||||
@@ -967,11 +1210,12 @@ pub const Set = struct {
|
||||
return error.SequenceUnbind;
|
||||
},
|
||||
|
||||
else => if (b.consumed) {
|
||||
try set.put(alloc, b.trigger, b.action);
|
||||
} else {
|
||||
try set.putUnconsumed(alloc, b.trigger, b.action);
|
||||
},
|
||||
else => try set.putFlags(
|
||||
alloc,
|
||||
b.trigger,
|
||||
b.action,
|
||||
b.flags,
|
||||
),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -984,29 +1228,16 @@ pub const Set = struct {
|
||||
t: Trigger,
|
||||
action: Action,
|
||||
) Allocator.Error!void {
|
||||
try self.put_(alloc, t, action, true);
|
||||
try self.putFlags(alloc, t, action, .{});
|
||||
}
|
||||
|
||||
/// Same as put but marks the trigger as unconsumed. An unconsumed
|
||||
/// trigger will evaluate the action and continue to encode for the
|
||||
/// terminal.
|
||||
///
|
||||
/// This is a separate function because this case is rare.
|
||||
pub fn putUnconsumed(
|
||||
/// Add a binding to the set with explicit flags.
|
||||
pub fn putFlags(
|
||||
self: *Set,
|
||||
alloc: Allocator,
|
||||
t: Trigger,
|
||||
action: Action,
|
||||
) Allocator.Error!void {
|
||||
try self.put_(alloc, t, action, false);
|
||||
}
|
||||
|
||||
fn put_(
|
||||
self: *Set,
|
||||
alloc: Allocator,
|
||||
t: Trigger,
|
||||
action: Action,
|
||||
consumed: bool,
|
||||
flags: Flags,
|
||||
) Allocator.Error!void {
|
||||
// unbind should never go into the set, it should be handled prior
|
||||
assert(action != .unbind);
|
||||
@@ -1022,7 +1253,7 @@ pub const Set = struct {
|
||||
|
||||
// If we have an existing binding for this trigger, we have to
|
||||
// update the reverse mapping to remove the old action.
|
||||
.action, .action_unconsumed => {
|
||||
.leaf => {
|
||||
const t_hash = t.hash();
|
||||
var it = self.reverse.iterator();
|
||||
while (it.next()) |reverse_entry| it: {
|
||||
@@ -1034,11 +1265,10 @@ pub const Set = struct {
|
||||
},
|
||||
};
|
||||
|
||||
gop.value_ptr.* = if (consumed) .{
|
||||
gop.value_ptr.* = .{ .leaf = .{
|
||||
.action = action,
|
||||
} else .{
|
||||
.action_unconsumed = action,
|
||||
};
|
||||
.flags = flags,
|
||||
} };
|
||||
errdefer _ = self.bindings.remove(t);
|
||||
try self.reverse.put(alloc, action, t);
|
||||
errdefer _ = self.reverse.remove(action);
|
||||
@@ -1055,6 +1285,31 @@ pub const Set = struct {
|
||||
return self.reverse.get(a);
|
||||
}
|
||||
|
||||
/// Get an entry for the given key event. This will attempt to find
|
||||
/// a binding using multiple parts of the event in the following order:
|
||||
///
|
||||
/// 1. Translated key (event.key)
|
||||
/// 2. Physical key (event.physical_key)
|
||||
/// 3. Unshifted Unicode codepoint (event.unshifted_codepoint)
|
||||
///
|
||||
pub fn getEvent(self: *const Set, event: KeyEvent) ?Entry {
|
||||
var trigger: Trigger = .{
|
||||
.mods = event.mods.binding(),
|
||||
.key = .{ .translated = event.key },
|
||||
};
|
||||
if (self.get(trigger)) |v| return v;
|
||||
|
||||
trigger.key = .{ .physical = event.physical_key };
|
||||
if (self.get(trigger)) |v| return v;
|
||||
|
||||
if (event.unshifted_codepoint > 0) {
|
||||
trigger.key = .{ .unicode = event.unshifted_codepoint };
|
||||
if (self.get(trigger)) |v| return v;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Remove a binding for a given trigger.
|
||||
pub fn remove(self: *Set, alloc: Allocator, t: Trigger) void {
|
||||
const entry = self.bindings.get(t) orelse return;
|
||||
@@ -1073,15 +1328,16 @@ pub const Set = struct {
|
||||
// Note: we'd LIKE to replace this with the most recent binding but
|
||||
// our hash map obviously has no concept of ordering so we have to
|
||||
// choose whatever. Maybe a switch to an array hash map here.
|
||||
.action, .action_unconsumed => |action| {
|
||||
const action_hash = action.hash();
|
||||
.leaf => |leaf| {
|
||||
const action_hash = leaf.action.hash();
|
||||
|
||||
var it = self.bindings.iterator();
|
||||
while (it.next()) |it_entry| {
|
||||
switch (it_entry.value_ptr.*) {
|
||||
.leader => {},
|
||||
.action, .action_unconsumed => |action_search| {
|
||||
if (action_search.hash() == action_hash) {
|
||||
self.reverse.putAssumeCapacity(action, it_entry.key_ptr.*);
|
||||
.leaf => |leaf_search| {
|
||||
if (leaf_search.action.hash() == action_hash) {
|
||||
self.reverse.putAssumeCapacity(leaf.action, it_entry.key_ptr.*);
|
||||
break;
|
||||
}
|
||||
},
|
||||
@@ -1089,7 +1345,7 @@ pub const Set = struct {
|
||||
} else {
|
||||
// No over trigger points to this action so we remove
|
||||
// the reverse mapping completely.
|
||||
_ = self.reverse.remove(action);
|
||||
_ = self.reverse.remove(leaf.action);
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -1106,7 +1362,7 @@ pub const Set = struct {
|
||||
var it = result.bindings.iterator();
|
||||
while (it.next()) |entry| switch (entry.value_ptr.*) {
|
||||
// No data to clone
|
||||
.action, .action_unconsumed => {},
|
||||
.leaf => {},
|
||||
|
||||
// Must be deep cloned.
|
||||
.leader => |*s| {
|
||||
@@ -1208,7 +1464,7 @@ test "parse: triggers" {
|
||||
.key = .{ .translated = .a },
|
||||
},
|
||||
.action = .{ .ignore = {} },
|
||||
.consumed = false,
|
||||
.flags = .{ .consumed = false },
|
||||
}, try parseSingle("unconsumed:shift+a=ignore"));
|
||||
|
||||
// unconsumed physical keys
|
||||
@@ -1218,7 +1474,7 @@ test "parse: triggers" {
|
||||
.key = .{ .physical = .a },
|
||||
},
|
||||
.action = .{ .ignore = {} },
|
||||
.consumed = false,
|
||||
.flags = .{ .consumed = false },
|
||||
}, try parseSingle("unconsumed:physical:a+shift=ignore"));
|
||||
|
||||
// invalid key
|
||||
@@ -1231,6 +1487,92 @@ test "parse: triggers" {
|
||||
try testing.expectError(Error.InvalidFormat, parseSingle("a+b=ignore"));
|
||||
}
|
||||
|
||||
test "parse: global triggers" {
|
||||
const testing = std.testing;
|
||||
|
||||
// global keys
|
||||
try testing.expectEqual(Binding{
|
||||
.trigger = .{
|
||||
.mods = .{ .shift = true },
|
||||
.key = .{ .translated = .a },
|
||||
},
|
||||
.action = .{ .ignore = {} },
|
||||
.flags = .{ .global = true },
|
||||
}, try parseSingle("global:shift+a=ignore"));
|
||||
|
||||
// global physical keys
|
||||
try testing.expectEqual(Binding{
|
||||
.trigger = .{
|
||||
.mods = .{ .shift = true },
|
||||
.key = .{ .physical = .a },
|
||||
},
|
||||
.action = .{ .ignore = {} },
|
||||
.flags = .{ .global = true },
|
||||
}, try parseSingle("global:physical:a+shift=ignore"));
|
||||
|
||||
// global unconsumed keys
|
||||
try testing.expectEqual(Binding{
|
||||
.trigger = .{
|
||||
.mods = .{ .shift = true },
|
||||
.key = .{ .translated = .a },
|
||||
},
|
||||
.action = .{ .ignore = {} },
|
||||
.flags = .{
|
||||
.global = true,
|
||||
.consumed = false,
|
||||
},
|
||||
}, try parseSingle("unconsumed:global:a+shift=ignore"));
|
||||
|
||||
// global sequences not allowed
|
||||
{
|
||||
var p = try Parser.init("global:a>b=ignore");
|
||||
try testing.expectError(Error.InvalidFormat, p.next());
|
||||
}
|
||||
}
|
||||
|
||||
test "parse: all triggers" {
|
||||
const testing = std.testing;
|
||||
|
||||
// all keys
|
||||
try testing.expectEqual(Binding{
|
||||
.trigger = .{
|
||||
.mods = .{ .shift = true },
|
||||
.key = .{ .translated = .a },
|
||||
},
|
||||
.action = .{ .ignore = {} },
|
||||
.flags = .{ .all = true },
|
||||
}, try parseSingle("all:shift+a=ignore"));
|
||||
|
||||
// all physical keys
|
||||
try testing.expectEqual(Binding{
|
||||
.trigger = .{
|
||||
.mods = .{ .shift = true },
|
||||
.key = .{ .physical = .a },
|
||||
},
|
||||
.action = .{ .ignore = {} },
|
||||
.flags = .{ .all = true },
|
||||
}, try parseSingle("all:physical:a+shift=ignore"));
|
||||
|
||||
// all unconsumed keys
|
||||
try testing.expectEqual(Binding{
|
||||
.trigger = .{
|
||||
.mods = .{ .shift = true },
|
||||
.key = .{ .translated = .a },
|
||||
},
|
||||
.action = .{ .ignore = {} },
|
||||
.flags = .{
|
||||
.all = true,
|
||||
.consumed = false,
|
||||
},
|
||||
}, try parseSingle("unconsumed:all:a+shift=ignore"));
|
||||
|
||||
// all sequences not allowed
|
||||
{
|
||||
var p = try Parser.init("all:a>b=ignore");
|
||||
try testing.expectError(Error.InvalidFormat, p.next());
|
||||
}
|
||||
}
|
||||
|
||||
test "parse: modifier aliases" {
|
||||
const testing = std.testing;
|
||||
|
||||
@@ -1456,8 +1798,9 @@ test "set: parseAndPut typical binding" {
|
||||
|
||||
// Creates forward mapping
|
||||
{
|
||||
const action = s.get(.{ .key = .{ .translated = .a } }).?.action;
|
||||
try testing.expect(action == .new_window);
|
||||
const action = s.get(.{ .key = .{ .translated = .a } }).?.leaf;
|
||||
try testing.expect(action.action == .new_window);
|
||||
try testing.expectEqual(Flags{}, action.flags);
|
||||
}
|
||||
|
||||
// Creates reverse mapping
|
||||
@@ -1479,8 +1822,9 @@ test "set: parseAndPut unconsumed binding" {
|
||||
// Creates forward mapping
|
||||
{
|
||||
const trigger: Trigger = .{ .key = .{ .translated = .a } };
|
||||
const action = s.get(trigger).?.action_unconsumed;
|
||||
try testing.expect(action == .new_window);
|
||||
const action = s.get(trigger).?.leaf;
|
||||
try testing.expect(action.action == .new_window);
|
||||
try testing.expectEqual(Flags{ .consumed = false }, action.flags);
|
||||
}
|
||||
|
||||
// Creates reverse mapping
|
||||
@@ -1526,8 +1870,9 @@ test "set: parseAndPut sequence" {
|
||||
{
|
||||
const t: Trigger = .{ .key = .{ .translated = .b } };
|
||||
const e = current.get(t).?;
|
||||
try testing.expect(e == .action);
|
||||
try testing.expect(e.action == .new_window);
|
||||
try testing.expect(e == .leaf);
|
||||
try testing.expect(e.leaf.action == .new_window);
|
||||
try testing.expectEqual(Flags{}, e.leaf.flags);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1550,14 +1895,16 @@ test "set: parseAndPut sequence with two actions" {
|
||||
{
|
||||
const t: Trigger = .{ .key = .{ .translated = .b } };
|
||||
const e = current.get(t).?;
|
||||
try testing.expect(e == .action);
|
||||
try testing.expect(e.action == .new_window);
|
||||
try testing.expect(e == .leaf);
|
||||
try testing.expect(e.leaf.action == .new_window);
|
||||
try testing.expectEqual(Flags{}, e.leaf.flags);
|
||||
}
|
||||
{
|
||||
const t: Trigger = .{ .key = .{ .translated = .c } };
|
||||
const e = current.get(t).?;
|
||||
try testing.expect(e == .action);
|
||||
try testing.expect(e.action == .new_tab);
|
||||
try testing.expect(e == .leaf);
|
||||
try testing.expect(e.leaf.action == .new_tab);
|
||||
try testing.expectEqual(Flags{}, e.leaf.flags);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1580,8 +1927,9 @@ test "set: parseAndPut overwrite sequence" {
|
||||
{
|
||||
const t: Trigger = .{ .key = .{ .translated = .b } };
|
||||
const e = current.get(t).?;
|
||||
try testing.expect(e == .action);
|
||||
try testing.expect(e.action == .new_window);
|
||||
try testing.expect(e == .leaf);
|
||||
try testing.expect(e.leaf.action == .new_window);
|
||||
try testing.expectEqual(Flags{}, e.leaf.flags);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1604,8 +1952,9 @@ test "set: parseAndPut overwrite leader" {
|
||||
{
|
||||
const t: Trigger = .{ .key = .{ .translated = .b } };
|
||||
const e = current.get(t).?;
|
||||
try testing.expect(e == .action);
|
||||
try testing.expect(e.action == .new_window);
|
||||
try testing.expect(e == .leaf);
|
||||
try testing.expect(e.leaf.action == .new_window);
|
||||
try testing.expectEqual(Flags{}, e.leaf.flags);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1734,11 +2083,19 @@ test "set: consumed state" {
|
||||
defer s.deinit(alloc);
|
||||
|
||||
try s.put(alloc, .{ .key = .{ .translated = .a } }, .{ .new_window = {} });
|
||||
try testing.expect(s.get(.{ .key = .{ .translated = .a } }).? == .action);
|
||||
try testing.expect(s.get(.{ .key = .{ .translated = .a } }).? == .leaf);
|
||||
try testing.expect(s.get(.{ .key = .{ .translated = .a } }).?.leaf.flags.consumed);
|
||||
|
||||
try s.putUnconsumed(alloc, .{ .key = .{ .translated = .a } }, .{ .new_window = {} });
|
||||
try testing.expect(s.get(.{ .key = .{ .translated = .a } }).? == .action_unconsumed);
|
||||
try s.putFlags(
|
||||
alloc,
|
||||
.{ .key = .{ .translated = .a } },
|
||||
.{ .new_window = {} },
|
||||
.{ .consumed = false },
|
||||
);
|
||||
try testing.expect(s.get(.{ .key = .{ .translated = .a } }).? == .leaf);
|
||||
try testing.expect(!s.get(.{ .key = .{ .translated = .a } }).?.leaf.flags.consumed);
|
||||
|
||||
try s.put(alloc, .{ .key = .{ .translated = .a } }, .{ .new_window = {} });
|
||||
try testing.expect(s.get(.{ .key = .{ .translated = .a } }).? == .action);
|
||||
try testing.expect(s.get(.{ .key = .{ .translated = .a } }).? == .leaf);
|
||||
try testing.expect(s.get(.{ .key = .{ .translated = .a } }).?.leaf.flags.consumed);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
const std = @import("std");
|
||||
const build_config = @import("build_config.zig");
|
||||
|
||||
/// See build_config.ExeEntrypoint for why we do this.
|
||||
@@ -16,6 +17,12 @@ const entrypoint = switch (build_config.exe_entrypoint) {
|
||||
/// The main entrypoint for the program.
|
||||
pub const main = entrypoint.main;
|
||||
|
||||
/// Standard options such as logger overrides.
|
||||
pub const std_options: std.Options = if (@hasDecl(entrypoint, "std_options"))
|
||||
entrypoint.std_options
|
||||
else
|
||||
.{};
|
||||
|
||||
test {
|
||||
_ = entrypoint;
|
||||
}
|
||||
|
||||
@@ -2523,6 +2523,45 @@ fn updateCell(
|
||||
}
|
||||
}
|
||||
|
||||
// If the cell has an underline, draw it before the character glyph,
|
||||
// so that it layers underneath instead of overtop, since that can
|
||||
// make text difficult to read.
|
||||
if (underline != .none) {
|
||||
const sprite: font.Sprite = switch (underline) {
|
||||
.none => unreachable,
|
||||
.single => .underline,
|
||||
.double => .underline_double,
|
||||
.dotted => .underline_dotted,
|
||||
.dashed => .underline_dashed,
|
||||
.curly => .underline_curly,
|
||||
};
|
||||
|
||||
const render = try self.font_grid.renderGlyph(
|
||||
self.alloc,
|
||||
font.sprite_index,
|
||||
@intFromEnum(sprite),
|
||||
.{
|
||||
.cell_width = if (cell.wide == .wide) 2 else 1,
|
||||
.grid_metrics = self.grid_metrics,
|
||||
},
|
||||
);
|
||||
|
||||
const color = style.underlineColor(palette) orelse colors.fg;
|
||||
|
||||
try self.cells.add(self.alloc, .underline, .{
|
||||
.mode = .fg,
|
||||
.grid_pos = .{ @intCast(coord.x), @intCast(coord.y) },
|
||||
.constraint_width = cell.gridWidth(),
|
||||
.color = .{ color.r, color.g, color.b, alpha },
|
||||
.glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y },
|
||||
.glyph_size = .{ render.glyph.width, render.glyph.height },
|
||||
.bearings = .{
|
||||
@intCast(render.glyph.offset_x),
|
||||
@intCast(render.glyph.offset_y),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// If the shaper cell has a glyph, draw it.
|
||||
if (shaper_cell.glyph_index) |glyph_index| glyph: {
|
||||
// Render
|
||||
@@ -2566,42 +2605,6 @@ fn updateCell(
|
||||
});
|
||||
}
|
||||
|
||||
if (underline != .none) {
|
||||
const sprite: font.Sprite = switch (underline) {
|
||||
.none => unreachable,
|
||||
.single => .underline,
|
||||
.double => .underline_double,
|
||||
.dotted => .underline_dotted,
|
||||
.dashed => .underline_dashed,
|
||||
.curly => .underline_curly,
|
||||
};
|
||||
|
||||
const render = try self.font_grid.renderGlyph(
|
||||
self.alloc,
|
||||
font.sprite_index,
|
||||
@intFromEnum(sprite),
|
||||
.{
|
||||
.cell_width = if (cell.wide == .wide) 2 else 1,
|
||||
.grid_metrics = self.grid_metrics,
|
||||
},
|
||||
);
|
||||
|
||||
const color = style.underlineColor(palette) orelse colors.fg;
|
||||
|
||||
try self.cells.add(self.alloc, .underline, .{
|
||||
.mode = .fg,
|
||||
.grid_pos = .{ @intCast(coord.x), @intCast(coord.y) },
|
||||
.constraint_width = cell.gridWidth(),
|
||||
.color = .{ color.r, color.g, color.b, alpha },
|
||||
.glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y },
|
||||
.glyph_size = .{ render.glyph.width, render.glyph.height },
|
||||
.bearings = .{
|
||||
@intCast(render.glyph.offset_x),
|
||||
@intCast(render.glyph.offset_y),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (style.flags.strikethrough) {
|
||||
const render = try self.font_grid.renderGlyph(
|
||||
self.alloc,
|
||||
|
||||
@@ -1761,52 +1761,9 @@ fn updateCell(
|
||||
@intFromFloat(@max(0, @min(255, @round(self.config.background_opacity * 255)))),
|
||||
};
|
||||
|
||||
// If the cell has a character, draw it
|
||||
if (cell.hasText()) fg: {
|
||||
// Render
|
||||
const render = try self.font_grid.renderGlyph(
|
||||
self.alloc,
|
||||
shaper_run.font_index,
|
||||
shaper_cell.glyph_index orelse break :fg,
|
||||
.{
|
||||
.grid_metrics = self.grid_metrics,
|
||||
.thicken = self.config.font_thicken,
|
||||
},
|
||||
);
|
||||
|
||||
// If we're rendering a color font, we use the color atlas
|
||||
const mode: CellProgram.CellMode = switch (try fgMode(
|
||||
render.presentation,
|
||||
cell_pin,
|
||||
)) {
|
||||
.normal => .fg,
|
||||
.color => .fg_color,
|
||||
.constrained => .fg_constrained,
|
||||
.powerline => .fg_powerline,
|
||||
};
|
||||
|
||||
try self.cells.append(self.alloc, .{
|
||||
.mode = mode,
|
||||
.grid_col = @intCast(x),
|
||||
.grid_row = @intCast(y),
|
||||
.grid_width = cell.gridWidth(),
|
||||
.glyph_x = render.glyph.atlas_x,
|
||||
.glyph_y = render.glyph.atlas_y,
|
||||
.glyph_width = render.glyph.width,
|
||||
.glyph_height = render.glyph.height,
|
||||
.glyph_offset_x = render.glyph.offset_x + shaper_cell.x_offset,
|
||||
.glyph_offset_y = render.glyph.offset_y + shaper_cell.y_offset,
|
||||
.r = colors.fg.r,
|
||||
.g = colors.fg.g,
|
||||
.b = colors.fg.b,
|
||||
.a = alpha,
|
||||
.bg_r = bg[0],
|
||||
.bg_g = bg[1],
|
||||
.bg_b = bg[2],
|
||||
.bg_a = bg[3],
|
||||
});
|
||||
}
|
||||
|
||||
// If the cell has an underline, draw it before the character glyph,
|
||||
// so that it layers underneath instead of overtop, since that can
|
||||
// make text difficult to read.
|
||||
if (underline != .none) {
|
||||
const sprite: font.Sprite = switch (underline) {
|
||||
.none => unreachable,
|
||||
@@ -1851,6 +1808,58 @@ fn updateCell(
|
||||
});
|
||||
}
|
||||
|
||||
// If the shaper cell has a glyph, draw it.
|
||||
if (shaper_cell.glyph_index) |glyph_index| glyph: {
|
||||
// Render
|
||||
const render = try self.font_grid.renderGlyph(
|
||||
self.alloc,
|
||||
shaper_run.font_index,
|
||||
glyph_index,
|
||||
.{
|
||||
.grid_metrics = self.grid_metrics,
|
||||
.thicken = self.config.font_thicken,
|
||||
},
|
||||
);
|
||||
|
||||
// If the glyph is 0 width or height, it will be invisible
|
||||
// when drawn, so don't bother adding it to the buffer.
|
||||
if (render.glyph.width == 0 or render.glyph.height == 0) {
|
||||
break :glyph;
|
||||
}
|
||||
|
||||
// If we're rendering a color font, we use the color atlas
|
||||
const mode: CellProgram.CellMode = switch (try fgMode(
|
||||
render.presentation,
|
||||
cell_pin,
|
||||
)) {
|
||||
.normal => .fg,
|
||||
.color => .fg_color,
|
||||
.constrained => .fg_constrained,
|
||||
.powerline => .fg_powerline,
|
||||
};
|
||||
|
||||
try self.cells.append(self.alloc, .{
|
||||
.mode = mode,
|
||||
.grid_col = @intCast(x),
|
||||
.grid_row = @intCast(y),
|
||||
.grid_width = cell.gridWidth(),
|
||||
.glyph_x = render.glyph.atlas_x,
|
||||
.glyph_y = render.glyph.atlas_y,
|
||||
.glyph_width = render.glyph.width,
|
||||
.glyph_height = render.glyph.height,
|
||||
.glyph_offset_x = render.glyph.offset_x + shaper_cell.x_offset,
|
||||
.glyph_offset_y = render.glyph.offset_y + shaper_cell.y_offset,
|
||||
.r = colors.fg.r,
|
||||
.g = colors.fg.g,
|
||||
.b = colors.fg.b,
|
||||
.a = alpha,
|
||||
.bg_r = bg[0],
|
||||
.bg_g = bg[1],
|
||||
.bg_b = bg[2],
|
||||
.bg_a = bg[3],
|
||||
});
|
||||
}
|
||||
|
||||
if (style.flags.strikethrough) {
|
||||
const render = try self.font_grid.renderGlyph(
|
||||
self.alloc,
|
||||
|
||||
@@ -224,30 +224,30 @@ vertex CellTextVertexOut cell_text_vertex(
|
||||
out.color = float4(in.color) / 255.0f;
|
||||
|
||||
// === Grid Cell ===
|
||||
//
|
||||
// offset.x = bearings.x
|
||||
// .|.
|
||||
// | |
|
||||
// +-------+_.
|
||||
// ._| | |
|
||||
// | | .###. | |
|
||||
// | | #...# | +- bearings.y
|
||||
// glyph_size.y -+ | ##### | |
|
||||
// | | #.... | |
|
||||
// ^ |_| .#### |_| _.
|
||||
// | | | +- offset.y = cell_size.y - bearings.y
|
||||
// . cell_pos -> +-------+ -'
|
||||
// +Y. |_._|
|
||||
// . |
|
||||
// | glyph_size.x
|
||||
// 0,0--...->
|
||||
// +X
|
||||
// 0,0--...->
|
||||
// |
|
||||
// . offset.x = bearings.x
|
||||
// +Y. .|.
|
||||
// . | |
|
||||
// | cell_pos -> +-------+ _.
|
||||
// v ._| |_. _|- offset.y = cell_size.y - bearings.y
|
||||
// | | .###. | |
|
||||
// | | #...# | |
|
||||
// glyph_size.y -+ | ##### | |
|
||||
// | | #.... | +- bearings.y
|
||||
// |_| .#### | |
|
||||
// | |_|
|
||||
// +-------+
|
||||
// |_._|
|
||||
// |
|
||||
// glyph_size.x
|
||||
//
|
||||
// In order to get the bottom left of the glyph, we compute an offset based
|
||||
// on the bearings. The Y bearing is the distance from the top of the cell
|
||||
// to the bottom of the glyph, so we subtract it from the cell height to get
|
||||
// the y offset. The X bearing is the distance from the left of the cell to
|
||||
// the left of the glyph, so it works as the x offset directly.
|
||||
// In order to get the top left of the glyph, we compute an offset based on
|
||||
// the bearings. The Y bearing is the distance from the bottom of the cell
|
||||
// to the top of the glyph, so we subtract it from the cell height to get
|
||||
// the y offset. The X bearing is the distance from the left of the cell
|
||||
// to the left of the glyph, so it works as the x offset directly.
|
||||
|
||||
float2 size = float2(in.glyph_size);
|
||||
float2 offset = float2(in.bearings);
|
||||
|
||||
@@ -1694,7 +1694,14 @@ pub fn grow(self: *PageList) !?*List.Node {
|
||||
// If allocation would exceed our max size, we prune the first page.
|
||||
// We don't need to reallocate because we can simply reuse that first
|
||||
// page.
|
||||
if (self.page_size + PagePool.item_size > self.maxSize()) prune: {
|
||||
//
|
||||
// We only take this path if we have more than one page since pruning
|
||||
// reuses the popped page. It is possible to have a single page and
|
||||
// exceed the max size if that page was adjusted to be larger after
|
||||
// initial allocation.
|
||||
if (self.pages.len > 1 and
|
||||
self.page_size + PagePool.item_size > self.maxSize())
|
||||
prune: {
|
||||
// If we need to add more memory to ensure our active area is
|
||||
// satisfied then we do not prune.
|
||||
if (self.growRequiredForActive()) break :prune;
|
||||
@@ -3772,6 +3779,51 @@ test "PageList grow allows exceeding max size for active area" {
|
||||
try testing.expectEqual(start_pages + 1, s.totalPages());
|
||||
}
|
||||
|
||||
test "PageList grow prune required with a single page" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s = try init(alloc, 80, 24, 0);
|
||||
defer s.deinit();
|
||||
|
||||
// This block is all test setup. There is nothing required about this
|
||||
// behavior during a refactor. This is setting up a scenario that is
|
||||
// possible to trigger a bug (#2280).
|
||||
{
|
||||
// Adjust our capacity until our page is larger than the standard size.
|
||||
// This is important because it triggers a scenario where our calculated
|
||||
// minSize() which is supposed to accommodate 2 pages is no longer true.
|
||||
var cap = std_capacity;
|
||||
while (true) {
|
||||
cap.grapheme_bytes *= 2;
|
||||
const layout = Page.layout(cap);
|
||||
if (layout.total_size > std_size) break;
|
||||
}
|
||||
|
||||
// Adjust to that capacity. After we should still have one page.
|
||||
_ = try s.adjustCapacity(
|
||||
s.pages.first.?,
|
||||
.{ .grapheme_bytes = cap.grapheme_bytes },
|
||||
);
|
||||
try testing.expect(s.pages.first != null);
|
||||
try testing.expect(s.pages.first == s.pages.last);
|
||||
}
|
||||
|
||||
// Figure out the remaining number of rows. This is the amount that
|
||||
// can be added to the current page before we need to allocate a new
|
||||
// page.
|
||||
const rem = rem: {
|
||||
const page = s.pages.first.?;
|
||||
break :rem page.data.capacity.rows - page.data.size.rows;
|
||||
};
|
||||
for (0..rem) |_| try testing.expect(try s.grow() == null);
|
||||
|
||||
// The next one we add will trigger a new page.
|
||||
const new = try s.grow();
|
||||
try testing.expect(new != null);
|
||||
try testing.expect(new != s.pages.first);
|
||||
}
|
||||
|
||||
test "PageList scroll top" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
@@ -122,6 +122,26 @@ test "garbage Kitty command" {
|
||||
try testing.expect(h.end() == null);
|
||||
}
|
||||
|
||||
test "Kitty command with overflow u32" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var h: Handler = .{};
|
||||
h.start();
|
||||
for ("Ga=p,i=10000000000") |c| h.feed(alloc, c);
|
||||
try testing.expect(h.end() == null);
|
||||
}
|
||||
|
||||
test "Kitty command with overflow i32" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var h: Handler = .{};
|
||||
h.start();
|
||||
for ("Ga=p,i=1,z=-9999999999") |c| h.feed(alloc, c);
|
||||
try testing.expect(h.end() == null);
|
||||
}
|
||||
|
||||
test "valid Kitty command" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
@@ -23,10 +23,10 @@ pub const Parser = struct {
|
||||
/// This is the list of KV pairs that we're building up.
|
||||
kv: KV = .{},
|
||||
|
||||
/// This is used as a buffer to store the key/value of a KV pair.
|
||||
/// The value of a KV pair is at most a 32-bit integer which at most
|
||||
/// is 10 characters (4294967295).
|
||||
kv_temp: [10]u8 = undefined,
|
||||
/// This is used as a buffer to store the key/value of a KV pair. The value
|
||||
/// of a KV pair is at most a 32-bit integer which at most is 10 characters
|
||||
/// (4294967295), plus one character for the sign bit on signed ints.
|
||||
kv_temp: [11]u8 = undefined,
|
||||
kv_temp_len: u4 = 0,
|
||||
kv_current: u8 = 0, // Current kv key
|
||||
|
||||
@@ -237,16 +237,14 @@ pub const Parser = struct {
|
||||
}
|
||||
}
|
||||
|
||||
// Only "z" is currently signed. This is a bit of a kloodge; if more
|
||||
// fields become signed we can rethink this but for now we parse
|
||||
// "z" as i32 then bitcast it to u32 then bitcast it back later.
|
||||
if (self.kv_current == 'z') {
|
||||
const v = try std.fmt.parseInt(i32, self.kv_temp[0..self.kv_temp_len], 10);
|
||||
try self.kv.put(alloc, self.kv_current, @bitCast(v));
|
||||
} else {
|
||||
const v = try std.fmt.parseInt(u32, self.kv_temp[0..self.kv_temp_len], 10);
|
||||
try self.kv.put(alloc, self.kv_current, v);
|
||||
}
|
||||
// Handle integer fields, parsing signed fields accordingly. We still
|
||||
// store the fields as u32 as they can be bitcast back later during
|
||||
// building of the higher-level command tree.
|
||||
const v: u32 = switch (self.kv_current) {
|
||||
'z', 'H', 'V' => @bitCast(try std.fmt.parseInt(i32, self.kv_temp[0..self.kv_temp_len], 10)),
|
||||
else => try std.fmt.parseInt(u32, self.kv_temp[0..self.kv_temp_len], 10),
|
||||
};
|
||||
try self.kv.put(alloc, self.kv_current, v);
|
||||
|
||||
// Clear our temp buffer
|
||||
self.kv_temp_len = 0;
|
||||
@@ -505,8 +503,8 @@ pub const Display = struct {
|
||||
virtual_placement: bool = false, // U
|
||||
parent_id: u32 = 0, // P
|
||||
parent_placement_id: u32 = 0, // Q
|
||||
horizontal_offset: u32 = 0, // H
|
||||
vertical_offset: u32 = 0, // V
|
||||
horizontal_offset: i32 = 0, // H
|
||||
vertical_offset: i32 = 0, // V
|
||||
z: i32 = 0, // z
|
||||
|
||||
pub const CursorMovement = enum {
|
||||
@@ -591,11 +589,13 @@ pub const Display = struct {
|
||||
}
|
||||
|
||||
if (kv.get('H')) |v| {
|
||||
result.horizontal_offset = v;
|
||||
// We can bitcast here because of how we parse it earlier.
|
||||
result.horizontal_offset = @bitCast(v);
|
||||
}
|
||||
|
||||
if (kv.get('V')) |v| {
|
||||
result.vertical_offset = v;
|
||||
// We can bitcast here because of how we parse it earlier.
|
||||
result.vertical_offset = @bitCast(v);
|
||||
}
|
||||
|
||||
return result;
|
||||
@@ -1069,6 +1069,95 @@ test "ignore very long values" {
|
||||
try testing.expectEqual(@as(u32, 0), v.height);
|
||||
}
|
||||
|
||||
test "ensure very large negative values don't get skipped" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
var p = Parser.init(alloc);
|
||||
defer p.deinit();
|
||||
|
||||
const input = "a=p,i=1,z=-2000000000";
|
||||
for (input) |c| try p.feed(c);
|
||||
const command = try p.complete();
|
||||
defer command.deinit(alloc);
|
||||
|
||||
try testing.expect(command.control == .display);
|
||||
const v = command.control.display;
|
||||
try testing.expectEqual(1, v.image_id);
|
||||
try testing.expectEqual(-2000000000, v.z);
|
||||
}
|
||||
|
||||
test "ensure proper overflow error for u32" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
var p = Parser.init(alloc);
|
||||
defer p.deinit();
|
||||
|
||||
const input = "a=p,i=10000000000";
|
||||
for (input) |c| try p.feed(c);
|
||||
try testing.expectError(error.Overflow, p.complete());
|
||||
}
|
||||
|
||||
test "ensure proper overflow error for i32" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
var p = Parser.init(alloc);
|
||||
defer p.deinit();
|
||||
|
||||
const input = "a=p,i=1,z=-9999999999";
|
||||
for (input) |c| try p.feed(c);
|
||||
try testing.expectError(error.Overflow, p.complete());
|
||||
}
|
||||
|
||||
test "all i32 values" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
{
|
||||
// 'z' (usually z-axis values)
|
||||
var p = Parser.init(alloc);
|
||||
defer p.deinit();
|
||||
const input = "a=p,i=1,z=-1";
|
||||
for (input) |c| try p.feed(c);
|
||||
const command = try p.complete();
|
||||
defer command.deinit(alloc);
|
||||
|
||||
try testing.expect(command.control == .display);
|
||||
const v = command.control.display;
|
||||
try testing.expectEqual(1, v.image_id);
|
||||
try testing.expectEqual(-1, v.z);
|
||||
}
|
||||
|
||||
{
|
||||
// 'H' (relative placement, horizontal offset)
|
||||
var p = Parser.init(alloc);
|
||||
defer p.deinit();
|
||||
const input = "a=p,i=1,H=-1";
|
||||
for (input) |c| try p.feed(c);
|
||||
const command = try p.complete();
|
||||
defer command.deinit(alloc);
|
||||
|
||||
try testing.expect(command.control == .display);
|
||||
const v = command.control.display;
|
||||
try testing.expectEqual(1, v.image_id);
|
||||
try testing.expectEqual(-1, v.horizontal_offset);
|
||||
}
|
||||
|
||||
{
|
||||
// 'V' (relative placement, vertical offset)
|
||||
var p = Parser.init(alloc);
|
||||
defer p.deinit();
|
||||
const input = "a=p,i=1,V=-1";
|
||||
for (input) |c| try p.feed(c);
|
||||
const command = try p.complete();
|
||||
defer command.deinit(alloc);
|
||||
|
||||
try testing.expect(command.control == .display);
|
||||
const v = command.control.display;
|
||||
try testing.expectEqual(1, v.image_id);
|
||||
try testing.expectEqual(-1, v.vertical_offset);
|
||||
}
|
||||
}
|
||||
|
||||
test "response: encode nothing without ID or image number" {
|
||||
const testing = std.testing;
|
||||
var buf: [1024]u8 = undefined;
|
||||
|
||||
@@ -495,3 +495,37 @@ test "kittygfx default format is rgba" {
|
||||
const img = storage.imageById(1).?;
|
||||
try testing.expectEqual(command.Transmission.Format.rgba, img.format);
|
||||
}
|
||||
|
||||
test "kittygfx test valid u32 (expect invalid image ID)" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var t = try Terminal.init(alloc, .{ .rows = 5, .cols = 5 });
|
||||
defer t.deinit(alloc);
|
||||
|
||||
const cmd = try command.Parser.parseString(
|
||||
alloc,
|
||||
"a=p,i=4294967295",
|
||||
);
|
||||
defer cmd.deinit(alloc);
|
||||
const resp = execute(alloc, &t, &cmd).?;
|
||||
try testing.expect(!resp.ok());
|
||||
try testing.expectEqual(resp.message, "ENOENT: image not found");
|
||||
}
|
||||
|
||||
test "kittygfx test valid i32 (expect invalid image ID)" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var t = try Terminal.init(alloc, .{ .rows = 5, .cols = 5 });
|
||||
defer t.deinit(alloc);
|
||||
|
||||
const cmd = try command.Parser.parseString(
|
||||
alloc,
|
||||
"a=p,i=1,z=-2147483648",
|
||||
);
|
||||
defer cmd.deinit(alloc);
|
||||
const resp = execute(alloc, &t, &cmd).?;
|
||||
try testing.expect(!resp.ok());
|
||||
try testing.expectEqual(resp.message, "ENOENT: image not found");
|
||||
}
|
||||
|
||||
@@ -165,6 +165,8 @@ pub const Parser = struct {
|
||||
|
||||
9 => return Attribute{ .strikethrough = {} },
|
||||
|
||||
21 => return Attribute{ .underline = .double },
|
||||
|
||||
22 => return Attribute{ .reset_bold = {} },
|
||||
|
||||
23 => return Attribute{ .reset_italic = {} },
|
||||
|
||||
@@ -154,7 +154,7 @@ pub fn threadEnter(
|
||||
processExit,
|
||||
);
|
||||
|
||||
// Start our termios timer. We only support this on Windows.
|
||||
// Start our termios timer. We don't support this on Windows.
|
||||
// Fundamentally, we could support this on Windows so we're just
|
||||
// waiting for someone to implement it.
|
||||
if (comptime builtin.os.tag != .windows) {
|
||||
@@ -1257,9 +1257,19 @@ const Subprocess = struct {
|
||||
// descendents are well and truly dead. We will not rest
|
||||
// until the entire family tree is obliterated.
|
||||
while (true) {
|
||||
if (c.killpg(pgid, c.SIGHUP) < 0) {
|
||||
log.warn("error killing process group pgid={}", .{pgid});
|
||||
return error.KillFailed;
|
||||
switch (posix.errno(c.killpg(pgid, c.SIGHUP))) {
|
||||
.SUCCESS => log.debug("process group killed pgid={}", .{pgid}),
|
||||
else => |err| killpg: {
|
||||
if ((comptime builtin.target.isDarwin()) and
|
||||
err == .PERM)
|
||||
{
|
||||
log.debug("killpg failed with EPERM, expected on Darwin and ignoring", .{});
|
||||
break :killpg;
|
||||
}
|
||||
|
||||
log.warn("error killing process group pgid={} err={}", .{ pgid, err });
|
||||
return error.KillFailed;
|
||||
},
|
||||
}
|
||||
|
||||
// See Command.zig wait for why we specify WNOHANG.
|
||||
@@ -1267,6 +1277,7 @@ const Subprocess = struct {
|
||||
// are still alive without blocking so that we can
|
||||
// kill them again.
|
||||
const res = posix.waitpid(pid, std.c.W.NOHANG);
|
||||
log.debug("waitpid result={}", .{res.pid});
|
||||
if (res.pid != 0) break;
|
||||
std.time.sleep(10 * std.time.ns_per_ms);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
[files]
|
||||
extend-exclude = [
|
||||
# vendored code
|
||||
"vendor/*",
|
||||
"pkg/*",
|
||||
"src/stb/*",
|
||||
"*.xib",
|
||||
# "grey" color names are valid
|
||||
"src/terminal/res/rgb.txt",
|
||||
# Do not self-check
|
||||
@@ -17,7 +17,9 @@ extend-exclude = [
|
||||
"*.icns",
|
||||
# Other
|
||||
"*.pdf",
|
||||
"*.data"
|
||||
"*.data",
|
||||
"*.xib",
|
||||
"src/cli/lorem_ipsum.txt"
|
||||
]
|
||||
|
||||
[default]
|
||||
|
||||
Reference in New Issue
Block a user