mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-15 16:26:08 +03:00
Merge pull request #191 from mitchellh/shell-integration
Automatic Shell Integration Script Injection
This commit is contained in:
@ -121,15 +121,18 @@ Ghostty supports some features that require shell integration. I am aiming
|
|||||||
to support many of the features that
|
to support many of the features that
|
||||||
[Kitty supports for shell integration](https://sw.kovidgoyal.net/kitty/shell-integration/).
|
[Kitty supports for shell integration](https://sw.kovidgoyal.net/kitty/shell-integration/).
|
||||||
|
|
||||||
To enable this functionality, I recommend sourcing Kitty's shell integration
|
Ghostty will automatically inject the shell integration code for `zsh` and
|
||||||
files directly for your shell configuration when running Ghostty. For
|
`fish`. Other shells are not supported. You can also manually load them
|
||||||
example, for fish, [source this file](https://github.com/kovidgoyal/kitty/blob/master/shell-integration/fish/vendor_conf.d/kitty-shell-integration.fish).
|
in many cases (see `src/shell-integration`). **If you want to disable this feature,**
|
||||||
|
set `shell-integration = none` in your configuration file.
|
||||||
|
|
||||||
The currently support shell integration features in Ghostty:
|
The currently support shell integration features in Ghostty:
|
||||||
|
|
||||||
* We do not confirm close for windows where the cursor is at a prompt.
|
* We do not confirm close for windows where the cursor is at a prompt.
|
||||||
* New terminals start in the working directory of the previously focused terminal.
|
* New terminals start in the working directory of the previously focused terminal.
|
||||||
* The cursor at the prompt is turned into a bar.
|
* The cursor at the prompt is turned into a bar.
|
||||||
|
* The `scroll_to_prompt` keybinding can be used to scroll the terminal window
|
||||||
|
forward and back through prompts.
|
||||||
|
|
||||||
## Roadmap and Status
|
## Roadmap and Status
|
||||||
|
|
||||||
|
22
build.zig
22
build.zig
@ -269,6 +269,28 @@ pub fn build(b: *std.Build) !void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Shell-integration
|
||||||
|
{
|
||||||
|
const install = b.addInstallDirectory(.{
|
||||||
|
.source_dir = .{ .path = "src/shell-integration" },
|
||||||
|
.install_dir = .{ .custom = "share" },
|
||||||
|
.install_subdir = "shell-integration",
|
||||||
|
.exclude_extensions = &.{".md"},
|
||||||
|
});
|
||||||
|
b.getInstallStep().dependOn(&install.step);
|
||||||
|
|
||||||
|
if (target.isDarwin()) {
|
||||||
|
const mac_install = b.addInstallDirectory(options: {
|
||||||
|
var copy = install.options;
|
||||||
|
copy.install_dir = .{
|
||||||
|
.custom = "Ghostty.app/Contents/Resources",
|
||||||
|
};
|
||||||
|
break :options copy;
|
||||||
|
});
|
||||||
|
b.getInstallStep().dependOn(&mac_install.step);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Terminfo
|
// Terminfo
|
||||||
{
|
{
|
||||||
// Encode our terminfo
|
// Encode our terminfo
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
A535B9DA299C569B0017E2E4 /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A535B9D9299C569B0017E2E4 /* ErrorView.swift */; };
|
A535B9DA299C569B0017E2E4 /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A535B9D9299C569B0017E2E4 /* ErrorView.swift */; };
|
||||||
|
A545D1A22A5772CE006E0AE4 /* shell-integration in Resources */ = {isa = PBXBuildFile; fileRef = A545D1A12A5772CE006E0AE4 /* shell-integration */; };
|
||||||
A55685E029A03A9F004303CE /* AppError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55685DF29A03A9F004303CE /* AppError.swift */; };
|
A55685E029A03A9F004303CE /* AppError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55685DF29A03A9F004303CE /* AppError.swift */; };
|
||||||
A55B7BB629B6F47F0055DE60 /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55B7BB529B6F47F0055DE60 /* AppState.swift */; };
|
A55B7BB629B6F47F0055DE60 /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55B7BB529B6F47F0055DE60 /* AppState.swift */; };
|
||||||
A55B7BB829B6F53A0055DE60 /* Package.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55B7BB729B6F53A0055DE60 /* Package.swift */; };
|
A55B7BB829B6F53A0055DE60 /* Package.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55B7BB729B6F53A0055DE60 /* Package.swift */; };
|
||||||
@ -27,6 +28,7 @@
|
|||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
A535B9D9299C569B0017E2E4 /* ErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorView.swift; sourceTree = "<group>"; };
|
A535B9D9299C569B0017E2E4 /* ErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorView.swift; sourceTree = "<group>"; };
|
||||||
|
A545D1A12A5772CE006E0AE4 /* shell-integration */ = {isa = PBXFileReference; lastKnownFileType = folder; name = "shell-integration"; path = "../zig-out/share/shell-integration"; sourceTree = "<group>"; };
|
||||||
A55685DF29A03A9F004303CE /* AppError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppError.swift; sourceTree = "<group>"; };
|
A55685DF29A03A9F004303CE /* AppError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppError.swift; sourceTree = "<group>"; };
|
||||||
A55B7BB529B6F47F0055DE60 /* AppState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppState.swift; sourceTree = "<group>"; };
|
A55B7BB529B6F47F0055DE60 /* AppState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppState.swift; sourceTree = "<group>"; };
|
||||||
A55B7BB729B6F53A0055DE60 /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = "<group>"; };
|
A55B7BB729B6F53A0055DE60 /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = "<group>"; };
|
||||||
@ -90,6 +92,7 @@
|
|||||||
A5A1F8862A489D7400D1E8BC /* Resources */ = {
|
A5A1F8862A489D7400D1E8BC /* Resources */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
A545D1A12A5772CE006E0AE4 /* shell-integration */,
|
||||||
A5A1F8842A489D6800D1E8BC /* terminfo */,
|
A5A1F8842A489D6800D1E8BC /* terminfo */,
|
||||||
);
|
);
|
||||||
name = Resources;
|
name = Resources;
|
||||||
@ -198,6 +201,7 @@
|
|||||||
isa = PBXResourcesBuildPhase;
|
isa = PBXResourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
A545D1A22A5772CE006E0AE4 /* shell-integration in Resources */,
|
||||||
A5A1F8852A489D6800D1E8BC /* terminfo in Resources */,
|
A5A1F8852A489D6800D1E8BC /* terminfo in Resources */,
|
||||||
A5B30539299BEAAB0047F10C /* Assets.xcassets in Resources */,
|
A5B30539299BEAAB0047F10C /* Assets.xcassets in Resources */,
|
||||||
);
|
);
|
||||||
|
@ -91,11 +91,11 @@ fn parseIntoField(
|
|||||||
.Optional => |opt| opt.child,
|
.Optional => |opt| opt.child,
|
||||||
else => field.type,
|
else => field.type,
|
||||||
};
|
};
|
||||||
const fieldInfo = @typeInfo(Field);
|
|
||||||
|
|
||||||
// If we are a struct and have parseCLI, we call that and use
|
// If we are a struct and have parseCLI, we call that and use
|
||||||
// that to set the value.
|
// that to set the value.
|
||||||
if (fieldInfo == .Struct and @hasDecl(Field, "parseCLI")) {
|
switch (@typeInfo(Field)) {
|
||||||
|
.Struct => if (@hasDecl(Field, "parseCLI")) {
|
||||||
const fnInfo = @typeInfo(@TypeOf(Field.parseCLI)).Fn;
|
const fnInfo = @typeInfo(@TypeOf(Field.parseCLI)).Fn;
|
||||||
switch (fnInfo.params.len) {
|
switch (fnInfo.params.len) {
|
||||||
// 1 arg = (input) => output
|
// 1 arg = (input) => output
|
||||||
@ -111,6 +111,17 @@ fn parseIntoField(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
},
|
||||||
|
|
||||||
|
.Enum => {
|
||||||
|
@field(dst, field.name) = std.meta.stringToEnum(
|
||||||
|
Field,
|
||||||
|
value orelse return error.ValueRequired,
|
||||||
|
) orelse return error.InvalidValue;
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
|
||||||
|
else => {},
|
||||||
}
|
}
|
||||||
|
|
||||||
// No parseCLI, magic the value based on the type
|
// No parseCLI, magic the value based on the type
|
||||||
@ -317,6 +328,21 @@ test "parseIntoField: floats" {
|
|||||||
try testing.expectEqual(@as(f64, 1.0), data.f64);
|
try testing.expectEqual(@as(f64, 1.0), data.f64);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "parseIntoField: enums" {
|
||||||
|
const testing = std.testing;
|
||||||
|
var arena = ArenaAllocator.init(testing.allocator);
|
||||||
|
defer arena.deinit();
|
||||||
|
const alloc = arena.allocator();
|
||||||
|
|
||||||
|
const Enum = enum { one, two, three };
|
||||||
|
var data: struct {
|
||||||
|
v: Enum,
|
||||||
|
} = undefined;
|
||||||
|
|
||||||
|
try parseIntoField(@TypeOf(data), alloc, &data, "v", "two");
|
||||||
|
try testing.expectEqual(Enum.two, data.v);
|
||||||
|
}
|
||||||
|
|
||||||
test "parseIntoField: optional field" {
|
test "parseIntoField: optional field" {
|
||||||
const testing = std.testing;
|
const testing = std.testing;
|
||||||
var arena = ArenaAllocator.init(testing.allocator);
|
var arena = ArenaAllocator.init(testing.allocator);
|
||||||
|
@ -197,10 +197,32 @@ pub const Config = struct {
|
|||||||
/// Additional configuration files to read.
|
/// Additional configuration files to read.
|
||||||
@"config-file": RepeatableString = .{},
|
@"config-file": RepeatableString = .{},
|
||||||
|
|
||||||
// Confirms that a surface should be closed before closing it. This defaults
|
/// Confirms that a surface should be closed before closing it. This defaults
|
||||||
// to true. If set to false, surfaces will close without any confirmation.
|
/// to true. If set to false, surfaces will close without any confirmation.
|
||||||
@"confirm-close-surface": bool = true,
|
@"confirm-close-surface": bool = true,
|
||||||
|
|
||||||
|
/// Whether to enable shell integration auto-injection or not. Shell
|
||||||
|
/// integration greatly enhances the terminal experience by enabling
|
||||||
|
/// a number of features:
|
||||||
|
///
|
||||||
|
/// * Working directory reporting so new tabs, splits inherit the
|
||||||
|
/// previous terminal's working directory.
|
||||||
|
/// * Prompt marking that enables the "scroll_to_prompt" keybinding.
|
||||||
|
/// * If you're sitting at a prompt, closing a terminal will not ask
|
||||||
|
/// for confirmation.
|
||||||
|
/// * Resizing the window with a complex prompt usually paints much
|
||||||
|
/// better.
|
||||||
|
///
|
||||||
|
/// Allowable values are:
|
||||||
|
///
|
||||||
|
/// * "none" - Do not do any automatic injection. You can still manually
|
||||||
|
/// configure your shell to enable the integration.
|
||||||
|
/// * "detect" - Detect the shell based on the filename.
|
||||||
|
/// * "fish", "zsh" - Use this specific shell injection scheme.
|
||||||
|
///
|
||||||
|
/// The default value is "detect".
|
||||||
|
@"shell-integration": ShellIntegration = .detect,
|
||||||
|
|
||||||
/// This is set by the CLI parser for deinit.
|
/// This is set by the CLI parser for deinit.
|
||||||
_arena: ?ArenaAllocator = null,
|
_arena: ?ArenaAllocator = null,
|
||||||
|
|
||||||
@ -796,6 +818,7 @@ pub const Config = struct {
|
|||||||
inline .Bool,
|
inline .Bool,
|
||||||
.Int,
|
.Int,
|
||||||
.Float,
|
.Float,
|
||||||
|
.Enum,
|
||||||
=> return src,
|
=> return src,
|
||||||
|
|
||||||
.Optional => |info| return try cloneValue(
|
.Optional => |info| return try cloneValue(
|
||||||
@ -1209,6 +1232,13 @@ pub const Keybinds = struct {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub const ShellIntegration = enum {
|
||||||
|
none,
|
||||||
|
detect,
|
||||||
|
fish,
|
||||||
|
zsh,
|
||||||
|
};
|
||||||
|
|
||||||
// Wasm API.
|
// Wasm API.
|
||||||
pub const Wasm = if (!builtin.target.isWasm()) struct {} else struct {
|
pub const Wasm = if (!builtin.target.isWasm()) struct {} else struct {
|
||||||
const wasm = @import("os/wasm.zig");
|
const wasm = @import("os/wasm.zig");
|
||||||
|
20
src/shell-integration/README.md
Normal file
20
src/shell-integration/README.md
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
# Shell Integration Code
|
||||||
|
|
||||||
|
This is the shell-specific shell-integration code that is
|
||||||
|
used for the shell-integration feature set that Ghostty
|
||||||
|
supports.
|
||||||
|
|
||||||
|
This README is meant as developer documentation and not as
|
||||||
|
user documentation. For user documentation, see the main
|
||||||
|
README.
|
||||||
|
|
||||||
|
## Implementation Details
|
||||||
|
|
||||||
|
### Fish
|
||||||
|
|
||||||
|
For [Fish](https://fishshell.com/), Ghostty prepends to the
|
||||||
|
`XDG_DATA_DIRS` directory. Fish automatically loads configuration
|
||||||
|
files in `<XDG_DATA_DIR>/fish/vendor_conf.d/*.fish` on startup,
|
||||||
|
allowing us to automatically integrate with the shell. For details
|
||||||
|
on the Fish startup process, see the
|
||||||
|
[Fish documentation](https://fishshell.com/docs/current/language.html).
|
101
src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish
Executable file
101
src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish
Executable file
@ -0,0 +1,101 @@
|
|||||||
|
#!/bin/fish
|
||||||
|
#
|
||||||
|
# This shell script aims to be written in a way where it can't really fail
|
||||||
|
# or all failure scenarios are handled, so that we never leave the shell in
|
||||||
|
# a weird state. If you find a way to break this, please report a bug!
|
||||||
|
|
||||||
|
function ghostty_restore_xdg_data_dir -d "restore the original XDG_DATA_DIR value"
|
||||||
|
# If we don't have our own data dir then we don't need to do anything.
|
||||||
|
if not set -q GHOSTTY_FISH_XDG_DIR
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
# If the data dir isn't set at all then we don't need to do anything.
|
||||||
|
if not set -q XDG_DATA_DIRS
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
# We need to do this so that XDG_DATA_DIRS turns into an array.
|
||||||
|
set --function --path xdg_data_dirs "$XDG_DATA_DIRS"
|
||||||
|
|
||||||
|
# If our data dir is in the list then remove it.
|
||||||
|
if set --function index (contains --index "$GHOSTTY_FISH_XDG_DIR" $xdg_data_dirs)
|
||||||
|
set --erase --function xdg_data_dirs[$index]
|
||||||
|
end
|
||||||
|
|
||||||
|
# Re-export our data dir
|
||||||
|
if set -q xdg_data_dirs[1]
|
||||||
|
set --global --export --unpath XDG_DATA_DIRS "$xdg_data_dirs"
|
||||||
|
else
|
||||||
|
set --erase --global XDG_DATA_DIRS
|
||||||
|
end
|
||||||
|
|
||||||
|
set --erase GHOSTTY_FISH_XDG_DIR
|
||||||
|
end
|
||||||
|
|
||||||
|
function ghostty_exit -d "exit the shell integration setup"
|
||||||
|
functions -e ghostty_restore_xdg_data_dir
|
||||||
|
functions -e ghostty_exit
|
||||||
|
exit 0
|
||||||
|
end
|
||||||
|
|
||||||
|
# We always try to restore the XDG data dir
|
||||||
|
ghostty_restore_xdg_data_dir
|
||||||
|
|
||||||
|
# If we aren't interactive or we've already run, don't run.
|
||||||
|
status --is-interactive || ghostty_exit
|
||||||
|
|
||||||
|
# We do the full setup on the first prompt render. We do this so that other
|
||||||
|
# shell integrations that setup the prompt and modify things are able to run
|
||||||
|
# first. We want to run _last_.
|
||||||
|
function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration"
|
||||||
|
functions -e __ghostty_setup
|
||||||
|
|
||||||
|
# Change the cursor to a beam on prompt.
|
||||||
|
function __ghostty_set_cursor_beam --on-event fish_prompt -d "Set cursor shape"
|
||||||
|
echo -en "\e[5 q"
|
||||||
|
end
|
||||||
|
function __ghostty_reset_cursor --on-event fish_preexec -d "Reset cursor shape"
|
||||||
|
echo -en "\e[0 q"
|
||||||
|
end
|
||||||
|
|
||||||
|
# Setup prompt marking
|
||||||
|
function __ghostty_mark_prompt_start --on-event fish_prompt --on-event fish_cancel --on-event fish_posterror
|
||||||
|
# If we never got the output end event, then we need to send it now.
|
||||||
|
if test "$__ghostty_prompt_state" != prompt-start
|
||||||
|
echo -en "\e]133;D\a"
|
||||||
|
end
|
||||||
|
|
||||||
|
set --global __ghostty_prompt_state prompt-start
|
||||||
|
echo -en "\e]133;A\a"
|
||||||
|
end
|
||||||
|
|
||||||
|
function __ghostty_mark_output_start --on-event fish_preexec
|
||||||
|
set --global __ghostty_prompt_state pre-exec
|
||||||
|
echo -en "\e]133;C\a"
|
||||||
|
end
|
||||||
|
|
||||||
|
function __ghostty_mark_output_end --on-event fish_postexec
|
||||||
|
set --global __ghostty_prompt_state post-exec
|
||||||
|
echo -en "\e]133;D;$status\a"
|
||||||
|
end
|
||||||
|
|
||||||
|
# Report pwd. This is actually built-in to fish but only for terminals
|
||||||
|
# that match an allowlist and that isn't us.
|
||||||
|
function __update_cwd_osc --on-variable PWD -d 'Notify capable terminals when $PWD changes'
|
||||||
|
if status --is-command-substitution || set -q INSIDE_EMACS
|
||||||
|
return
|
||||||
|
end
|
||||||
|
printf \e\]7\;file://%s%s\a $hostname (string escape --style=url $PWD)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Enable fish to handle reflow because Ghostty clears the prompt on resize.
|
||||||
|
set --global fish_handle_reflow 1
|
||||||
|
|
||||||
|
# Initial calls for first prompt
|
||||||
|
__ghostty_set_cursor_beam
|
||||||
|
__ghostty_mark_prompt_start
|
||||||
|
__update_cwd_osc
|
||||||
|
end
|
||||||
|
|
||||||
|
ghostty_exit
|
60
src/shell-integration/zsh/.zshenv
Normal file
60
src/shell-integration/zsh/.zshenv
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
# Based on (started as) a copy of Kitty's zsh integration. Kitty is
|
||||||
|
# distributed under GPLv3, so this file is also distributed under GPLv3.
|
||||||
|
# The license header is reproduced below:
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
# This file can get sourced with aliases enabled. To avoid alias expansion
|
||||||
|
# we quote everything that can be quoted. Some aliases will still break us
|
||||||
|
# though.
|
||||||
|
|
||||||
|
# Restore the original ZDOTDIR value.
|
||||||
|
if [[ -n "${GHOSTTY_ZSH_ZDOTDIR+X}" ]]; then
|
||||||
|
'builtin' 'export' ZDOTDIR="$GHOSTTY_ZSH_ZDOTDIR"
|
||||||
|
'builtin' 'unset' 'GHOSTTY_ZSH_ZDOTDIR'
|
||||||
|
else
|
||||||
|
'builtin' 'unset' 'ZDOTDIR'
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Use try-always to have the right error code.
|
||||||
|
{
|
||||||
|
# Zsh treats empty $ZDOTDIR as if it was "/". We do the same.
|
||||||
|
#
|
||||||
|
# Source the user's zshenv before sourcing ghostty.zsh because the former
|
||||||
|
# might set fpath and other things without which ghostty.zsh won't work.
|
||||||
|
#
|
||||||
|
# Use typeset in case we are in a function with warn_create_global in
|
||||||
|
# effect. Unlikely but better safe than sorry.
|
||||||
|
'builtin' 'typeset' _ghostty_file=${ZDOTDIR-~}"/.zshenv"
|
||||||
|
# Zsh ignores unreadable rc files. We do the same.
|
||||||
|
# Zsh ignores rc files that are directories, and so does source.
|
||||||
|
[[ ! -r "$_ghostty_file" ]] || 'builtin' 'source' '--' "$_ghostty_file"
|
||||||
|
} always {
|
||||||
|
if [[ -o 'interactive' ]]; then
|
||||||
|
'builtin' 'autoload' '--' 'is-at-least'
|
||||||
|
'is-at-least' "5.1" || {
|
||||||
|
builtin echo "ZSH ${ZSH_VERSION} is too old for ghostty shell integration" > /dev/stderr
|
||||||
|
return
|
||||||
|
}
|
||||||
|
# ${(%):-%x} is the path to the current file.
|
||||||
|
# On top of it we add :A:h to get the directory.
|
||||||
|
'builtin' 'typeset' _ghostty_file="${${(%):-%x}:A:h}"/ghostty-integration
|
||||||
|
if [[ -r "$_ghostty_file" ]]; then
|
||||||
|
'builtin' 'autoload' '-Uz' '--' "$_ghostty_file"
|
||||||
|
"${_ghostty_file:t}"
|
||||||
|
'builtin' 'unfunction' '--' "${_ghostty_file:t}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
'builtin' 'unset' '_ghostty_file'
|
||||||
|
}
|
280
src/shell-integration/zsh/ghostty-integration
Executable file
280
src/shell-integration/zsh/ghostty-integration
Executable file
@ -0,0 +1,280 @@
|
|||||||
|
#!/bin/zsh
|
||||||
|
#
|
||||||
|
# Based on (started as) a copy of Kitty's zsh integration. Kitty is
|
||||||
|
# distributed under GPLv3, so this file is also distributed under GPLv3.
|
||||||
|
# The license header is reproduced below:
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
#
|
||||||
|
# Enables integration between zsh and ghostty.
|
||||||
|
#
|
||||||
|
# This is an autoloadable function. It's invoked automatically in shells
|
||||||
|
# directly spawned by Ghostty but not in any other shells. For example, running
|
||||||
|
# `exec zsh`, `sudo -E zsh`, `tmux`, or plain `zsh` will create a shell where
|
||||||
|
# ghostty-integration won't automatically run. Zsh users who want integration with
|
||||||
|
# Ghostty in all shells should add the following lines to their .zshrc:
|
||||||
|
#
|
||||||
|
# if [[ -n $GHOSTTY_RESOURCES_DIR ]]; then
|
||||||
|
# autoload -Uz -- "$GHOSTTY_RESOURCSE_DIR"/shell-integration/zsh/ghostty-integration
|
||||||
|
# ghostty-integration
|
||||||
|
# unfunction ghostty-integration
|
||||||
|
# fi
|
||||||
|
#
|
||||||
|
# Implementation note: We can assume that alias expansion is disabled in this
|
||||||
|
# file, so no need to quote defensively. We still have to defensively prefix all
|
||||||
|
# builtins with `builtin` to avoid accidentally invoking user-defined functions.
|
||||||
|
# We avoid `function` reserved word as an additional defensive measure.
|
||||||
|
|
||||||
|
builtin emulate -L zsh -o no_warn_create_global -o no_aliases
|
||||||
|
|
||||||
|
[[ -o interactive ]] || builtin return 0 # non-interactive shell
|
||||||
|
(( ! $+_ghostty_state )) || builtin return 0 # already initialized
|
||||||
|
|
||||||
|
# 0: no OSC 133 [AC] marks have been written yet.
|
||||||
|
# 1: the last written OSC 133 C has not been closed with D yet.
|
||||||
|
# 2: none of the above.
|
||||||
|
builtin typeset -gi _ghostty_state
|
||||||
|
|
||||||
|
# Attempt to create a writable file descriptor to the TTY so that we can print
|
||||||
|
# to the TTY later even when STDOUT is redirected. This code is fairly subtle.
|
||||||
|
#
|
||||||
|
# - It's tempting to do `[[ -t 1 ]] && exec {_ghostty_state}>&1` but we cannot do this
|
||||||
|
# because it'll create a file descriptor >= 10 without O_CLOEXEC. This file
|
||||||
|
# descriptor will leak to child processes.
|
||||||
|
# - If we do `exec {3}>&1`, the file descriptor won't leak to the child processes
|
||||||
|
# but it'll still leak if the current process is replaced with another. In
|
||||||
|
# addition, it'll break user code that relies on fd 3 being available.
|
||||||
|
# - Zsh doesn't expose dup3, which would have allowed us to copy STDOUT with
|
||||||
|
# O_CLOEXEC. The only way to create a file descriptor with O_CLOEXEC is via
|
||||||
|
# sysopen.
|
||||||
|
# - `zmodload zsh/system` and `sysopen -o cloexec -wu _ghostty_fd -- /dev/tty` can
|
||||||
|
# fail with an error message to STDERR (the latter can happen even if /dev/tty
|
||||||
|
# is writable), hence the redirection of STDERR. We do it for the whole block
|
||||||
|
# for performance reasons (redirections are slow).
|
||||||
|
# - We must open the file descriptor right here rather than in _ghostty_deferred_init
|
||||||
|
# because there are broken zsh plugins out there that run `exec {fd}< <(cmd)`
|
||||||
|
# and then close the file descriptor more than once while suppressing errors.
|
||||||
|
# This could end up closing our file descriptor if we opened it in
|
||||||
|
# _ghostty_deferred_init.
|
||||||
|
typeset -gi _ghostty_fd
|
||||||
|
{
|
||||||
|
builtin zmodload zsh/system && (( $+builtins[sysopen] )) && {
|
||||||
|
{ [[ -w $TTY ]] && builtin sysopen -o cloexec -wu _ghostty_fd -- $TTY } ||
|
||||||
|
{ [[ -w /dev/tty ]] && builtin sysopen -o cloexec -wu _ghostty_fd -- /dev/tty }
|
||||||
|
}
|
||||||
|
} 2>/dev/null || (( _ghostty_fd = 1 ))
|
||||||
|
|
||||||
|
# Defer initialization so that other zsh init files can be configure
|
||||||
|
# the integration.
|
||||||
|
builtin typeset -ag precmd_functions
|
||||||
|
precmd_functions+=(_ghostty_deferred_init)
|
||||||
|
|
||||||
|
_ghostty_deferred_init() {
|
||||||
|
builtin emulate -L zsh -o no_warn_create_global -o no_aliases
|
||||||
|
|
||||||
|
# The directory where ghostty-integration is located: /../shell-integration/zsh.
|
||||||
|
builtin local self_dir="${functions_source[_ghostty_deferred_init]:A:h}"
|
||||||
|
|
||||||
|
# Enable semantic markup with OSC 133.
|
||||||
|
_ghostty_precmd() {
|
||||||
|
builtin local -i cmd_status=$?
|
||||||
|
builtin emulate -L zsh -o no_warn_create_global -o no_aliases
|
||||||
|
|
||||||
|
# Don't write OSC 133 D when our precmd handler is invoked from zle.
|
||||||
|
# Some plugins do that to update prompt on cd.
|
||||||
|
if ! builtin zle; then
|
||||||
|
# This code works incorrectly in the presence of a precmd or chpwd
|
||||||
|
# hook that prints. For example, sindresorhus/pure prints an empty
|
||||||
|
# line on precmd and marlonrichert/zsh-snap prints $PWD on chpwd.
|
||||||
|
# We'll end up writing our OSC 133 D mark too late.
|
||||||
|
#
|
||||||
|
# Another failure mode is when the output of a command doesn't end
|
||||||
|
# with LF and prompst_sp is set (it is by default). In this case
|
||||||
|
# we'll incorrectly state that '%' from prompt_sp is a part of the
|
||||||
|
# command's output.
|
||||||
|
if (( _ghostty_state == 1 )); then
|
||||||
|
# The last written OSC 133 C has not been closed with D yet.
|
||||||
|
# Close it and supply status.
|
||||||
|
builtin print -nu $_ghostty_fd '\e]133;D;'$cmd_status'\a'
|
||||||
|
(( _ghostty_state = 2 ))
|
||||||
|
elif (( _ghostty_state == 2 )); then
|
||||||
|
# There might be an unclosed OSC 133 C. Close that.
|
||||||
|
builtin print -nu $_ghostty_fd '\e]133;D\a'
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
builtin local mark1=$'%{\e]133;A\a%}'
|
||||||
|
if [[ -o prompt_percent ]]; then
|
||||||
|
builtin typeset -g precmd_functions
|
||||||
|
if [[ ${precmd_functions[-1]} == _ghostty_precmd ]]; then
|
||||||
|
# This is the best case for us: we can add our marks to PS1 and
|
||||||
|
# PS2. This way our marks will be printed whenever zsh
|
||||||
|
# redisplays prompt: on reset-prompt, on SIGWINCH, and on
|
||||||
|
# SIGCHLD if notify is set. Themes that update prompt
|
||||||
|
# asynchronously from a `zle -F` handler might still remove our
|
||||||
|
# marks. Oh well.
|
||||||
|
builtin local mark2=$'%{\e]133;A;k=s\a%}'
|
||||||
|
# Add marks conditionally to avoid a situation where we have
|
||||||
|
# several marks in place. These conditions can have false
|
||||||
|
# positives and false negatives though.
|
||||||
|
#
|
||||||
|
# - False positive (with prompt_percent): PS1="%(?.$mark1.)"
|
||||||
|
# - False negative (with prompt_subst): PS1='$mark1'
|
||||||
|
[[ $PS1 == *$mark1* ]] || PS1=${mark1}${PS1}
|
||||||
|
# PS2 mark is needed when clearing the prompt on resize
|
||||||
|
[[ $PS2 == *$mark2* ]] || PS2=${mark2}${PS2}
|
||||||
|
(( _ghostty_state = 2 ))
|
||||||
|
else
|
||||||
|
# If our precmd hook is not the last, we cannot rely on prompt
|
||||||
|
# changes to stick, so we don't even try. At least we can move
|
||||||
|
# our hook to the end to have better luck next time. If there is
|
||||||
|
# another piece of code that wants to take this privileged
|
||||||
|
# position, this won't work well. We'll break them as much as
|
||||||
|
# they are breaking us.
|
||||||
|
precmd_functions=(${precmd_functions:#_ghostty_precmd} _ghostty_precmd)
|
||||||
|
# Plugins that invoke precmd hooks from zle do that before zle
|
||||||
|
# is trashed. This means that the cursor is in the middle of
|
||||||
|
# BUFFER and we cannot print our mark there. Prompt might
|
||||||
|
# already have a mark, so the following reset-prompt will write
|
||||||
|
# it. If it doesn't, there is nothing we can do.
|
||||||
|
if ! builtin zle; then
|
||||||
|
builtin print -rnu $_ghostty_fd -- $mark1[3,-3]
|
||||||
|
(( _ghostty_state = 2 ))
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
elif ! builtin zle; then
|
||||||
|
# Without prompt_percent we cannot patch prompt. Just print the
|
||||||
|
# mark, except when we are invoked from zle. In the latter case we
|
||||||
|
# cannot do anything.
|
||||||
|
builtin print -rnu $_ghostty_fd -- $mark1[3,-3]
|
||||||
|
(( _ghostty_state = 2 ))
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
_ghostty_preexec() {
|
||||||
|
builtin emulate -L zsh -o no_warn_create_global -o no_aliases
|
||||||
|
|
||||||
|
# This can potentially break user prompt. Oh well. The robustness of
|
||||||
|
# this code can be improved in the case prompt_subst is set because
|
||||||
|
# it'll allow us distinguish (not perfectly but close enough) between
|
||||||
|
# our own prompt, user prompt, and our own prompt with user additions on
|
||||||
|
# top. We cannot force prompt_subst on the user though, so we would
|
||||||
|
# still need this code for the no_prompt_subst case.
|
||||||
|
PS1=${PS1//$'%{\e]133;A\a%}'}
|
||||||
|
PS2=${PS2//$'%{\e]133;A;k=s\a%}'}
|
||||||
|
|
||||||
|
# This will work incorrectly in the presence of a preexec hook that
|
||||||
|
# prints. For example, if MichaelAquilina/zsh-you-should-use installs
|
||||||
|
# its preexec hook before us, we'll incorrectly mark its output as
|
||||||
|
# belonging to the command (as if the user typed it into zle) rather
|
||||||
|
# than command output.
|
||||||
|
builtin print -nu $_ghostty_fd '\e]133;C\a'
|
||||||
|
(( _ghostty_state = 1 ))
|
||||||
|
}
|
||||||
|
|
||||||
|
# Enable reporting current working dir to terminal. Ghostty supports
|
||||||
|
# the kitty-shell-cwd format.
|
||||||
|
_ghostty_report_pwd() { builtin print -nu $_ghostty_fd '\e]7;kitty-shell-cwd://'"$HOST""$PWD"'\a'; }
|
||||||
|
chpwd_functions=(${chpwd_functions[@]} "_ghostty_report_pwd")
|
||||||
|
# An executed program could change cwd and report the changed cwd, so also report cwd at each new prompt
|
||||||
|
# as in this case chpwd_functions is insufficient. chpwd_functions is still needed for things like: cd x && something
|
||||||
|
functions[_ghostty_precmd]+="
|
||||||
|
_ghostty_report_pwd"
|
||||||
|
_ghostty_report_pwd
|
||||||
|
|
||||||
|
# Enable cursor shape changes depending on the current keymap.
|
||||||
|
# This implementation leaks blinking block cursor into external commands
|
||||||
|
# executed from zle. For example, users of fzf-based widgets may find
|
||||||
|
# themselves with a blinking block cursor within fzf.
|
||||||
|
_ghostty_zle_line_init _ghostty_zle_line_finish _ghostty_zle_keymap_select() {
|
||||||
|
case ${KEYMAP-} in
|
||||||
|
# Blinking block cursor.
|
||||||
|
vicmd|visual) builtin print -nu "$_ghostty_fd" '\e[1 q';;
|
||||||
|
# Blinking bar cursor.
|
||||||
|
*) builtin print -nu "$_ghostty_fd" '\e[5 q';;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
# Restore the blinking default shape before executing an external command
|
||||||
|
functions[_ghostty_preexec]+="
|
||||||
|
builtin print -rnu $_ghostty_fd \$'\\e[0 q'"
|
||||||
|
|
||||||
|
# Some zsh users manually run `source ~/.zshrc` in order to apply rc file
|
||||||
|
# changes to the current shell. This is a terrible practice that breaks many
|
||||||
|
# things, including our shell integration. For example, Oh My Zsh and Prezto
|
||||||
|
# (both very popular among zsh users) will remove zle-line-init and
|
||||||
|
# zle-line-finish hooks if .zshrc is manually sourced. Prezto will also remove
|
||||||
|
# zle-keymap-select.
|
||||||
|
#
|
||||||
|
# Another common (and much more robust) way to apply rc file changes to the
|
||||||
|
# current shell is `exec zsh`. This will remove our integration from the shell
|
||||||
|
# unless it's explicitly invoked from .zshrc. This is not an issue with
|
||||||
|
# `exec zsh` but rather with our implementation of automatic shell integration.
|
||||||
|
|
||||||
|
# In the ideal world we would use add-zle-hook-widget to hook zle-line-init
|
||||||
|
# and similar widget. This breaks user configs though, so we have do this
|
||||||
|
# horrible thing instead.
|
||||||
|
builtin local hook func widget orig_widget flag
|
||||||
|
for hook in line-init line-finish keymap-select; do
|
||||||
|
func=_ghostty_zle_${hook/-/_}
|
||||||
|
(( $+functions[$func] )) || builtin continue
|
||||||
|
widget=zle-$hook
|
||||||
|
if [[ $widgets[$widget] == user:azhw:* &&
|
||||||
|
$+functions[add-zle-hook-widget] -eq 1 ]]; then
|
||||||
|
# If the widget is already hooked by add-zle-hook-widget at the top
|
||||||
|
# level, add our hook at the end. We MUST do it this way. We cannot
|
||||||
|
# just wrap the widget ourselves in this case because it would
|
||||||
|
# trigger bugs in add-zle-hook-widget.
|
||||||
|
add-zle-hook-widget $hook $func
|
||||||
|
else
|
||||||
|
if (( $+widgets[$widget] )); then
|
||||||
|
# There is a widget but it's not from add-zle-hook-widget. We
|
||||||
|
# can rename the original widget, install our own and invoke
|
||||||
|
# the original when we are called.
|
||||||
|
#
|
||||||
|
# Note: The leading dot is to work around bugs in
|
||||||
|
# zsh-syntax-highlighting.
|
||||||
|
orig_widget=._ghostty_orig_$widget
|
||||||
|
builtin zle -A $widget $orig_widget
|
||||||
|
if [[ $widgets[$widget] == user:* ]]; then
|
||||||
|
# No -w here to preserve $WIDGET within the original widget.
|
||||||
|
flag=
|
||||||
|
else
|
||||||
|
flag=w
|
||||||
|
fi
|
||||||
|
functions[$func]+="
|
||||||
|
builtin zle $orig_widget -N$flag -- \"\$@\""
|
||||||
|
fi
|
||||||
|
builtin zle -N $widget $func
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if (( $+functions[_ghostty_preexec] )); then
|
||||||
|
builtin typeset -ag preexec_functions
|
||||||
|
preexec_functions+=(_ghostty_preexec)
|
||||||
|
fi
|
||||||
|
|
||||||
|
builtin typeset -ag precmd_functions
|
||||||
|
if (( $+functions[_ghostty_precmd] )); then
|
||||||
|
precmd_functions=(${precmd_functions:/_ghostty_deferred_init/_ghostty_precmd})
|
||||||
|
_ghostty_precmd
|
||||||
|
else
|
||||||
|
precmd_functions=(${precmd_functions:#_ghostty_deferred_init})
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Unfunction _ghostty_deferred_init to save memory. Don't unfunction
|
||||||
|
# ghostty-integration though because decent public functions aren't supposed to
|
||||||
|
# to unfunction themselves when invoked. Unfunctioning is done by calling code.
|
||||||
|
builtin unfunction _ghostty_deferred_init
|
||||||
|
}
|
@ -21,6 +21,7 @@ const apprt = @import("../apprt.zig");
|
|||||||
const fastmem = @import("../fastmem.zig");
|
const fastmem = @import("../fastmem.zig");
|
||||||
const internal_os = @import("../os/main.zig");
|
const internal_os = @import("../os/main.zig");
|
||||||
const configpkg = @import("../config.zig");
|
const configpkg = @import("../config.zig");
|
||||||
|
const shell_integration = @import("shell_integration.zig");
|
||||||
|
|
||||||
const log = std.log.scoped(.io_exec);
|
const log = std.log.scoped(.io_exec);
|
||||||
|
|
||||||
@ -526,13 +527,20 @@ const Subprocess = struct {
|
|||||||
};
|
};
|
||||||
errdefer env.deinit();
|
errdefer env.deinit();
|
||||||
|
|
||||||
|
// Get our bundled resources directory, if it exists. We use this
|
||||||
|
// for terminfo, shell-integration, etc.
|
||||||
|
const resources_dir = try resourcesDir(alloc);
|
||||||
|
if (resources_dir) |dir| {
|
||||||
|
try env.put("GHOSTTY_RESOURCES_DIR", dir);
|
||||||
|
}
|
||||||
|
|
||||||
// Set our TERM var. This is a bit complicated because we want to use
|
// Set our TERM var. This is a bit complicated because we want to use
|
||||||
// the ghostty TERM value but we want to only do that if we have
|
// the ghostty TERM value but we want to only do that if we have
|
||||||
// ghostty in the TERMINFO database.
|
// ghostty in the TERMINFO database.
|
||||||
//
|
//
|
||||||
// For now, we just look up a bundled dir but in the future we should
|
// For now, we just look up a bundled dir but in the future we should
|
||||||
// also load the terminfo database and look for it.
|
// also load the terminfo database and look for it.
|
||||||
if (try terminfoDir(alloc)) |dir| {
|
if (try terminfoDir(alloc, resources_dir)) |dir| {
|
||||||
try env.put("TERM", "xterm-ghostty");
|
try env.put("TERM", "xterm-ghostty");
|
||||||
try env.put("COLORTERM", "truecolor");
|
try env.put("COLORTERM", "truecolor");
|
||||||
try env.put("TERMINFO", dir);
|
try env.put("TERMINFO", dir);
|
||||||
@ -590,11 +598,40 @@ const Subprocess = struct {
|
|||||||
else
|
else
|
||||||
null;
|
null;
|
||||||
|
|
||||||
|
// The execution path
|
||||||
|
const final_path = if (internal_os.isFlatpak()) args[0] else path;
|
||||||
|
|
||||||
|
// Setup our shell integration, if we can.
|
||||||
|
const shell_integrated: ?shell_integration.Shell = shell: {
|
||||||
|
const force: ?shell_integration.Shell = switch (opts.full_config.@"shell-integration") {
|
||||||
|
.none => break :shell null,
|
||||||
|
.detect => null,
|
||||||
|
.fish => .fish,
|
||||||
|
.zsh => .zsh,
|
||||||
|
};
|
||||||
|
|
||||||
|
const dir = resources_dir orelse break :shell null;
|
||||||
|
break :shell try shell_integration.setup(
|
||||||
|
dir,
|
||||||
|
final_path,
|
||||||
|
&env,
|
||||||
|
force,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
if (shell_integrated) |shell| {
|
||||||
|
log.info(
|
||||||
|
"shell integration automatically injected shell={}",
|
||||||
|
.{shell},
|
||||||
|
);
|
||||||
|
} else if (opts.full_config.@"shell-integration" != .none) {
|
||||||
|
log.warn("shell could not be detected, no automatic shell integration will be injected", .{});
|
||||||
|
}
|
||||||
|
|
||||||
return .{
|
return .{
|
||||||
.arena = arena,
|
.arena = arena,
|
||||||
.env = env,
|
.env = env,
|
||||||
.cwd = cwd,
|
.cwd = cwd,
|
||||||
.path = if (internal_os.isFlatpak()) args[0] else path,
|
.path = final_path,
|
||||||
.args = args,
|
.args = args,
|
||||||
.grid_size = opts.grid_size,
|
.grid_size = opts.grid_size,
|
||||||
.screen_size = opts.screen_size,
|
.screen_size = opts.screen_size,
|
||||||
@ -802,7 +839,17 @@ const Subprocess = struct {
|
|||||||
/// Gets the directory to the terminfo database, if it can be detected.
|
/// Gets the directory to the terminfo database, if it can be detected.
|
||||||
/// The memory returned can't be easily freed so the alloc should be
|
/// The memory returned can't be easily freed so the alloc should be
|
||||||
/// an arena or something similar.
|
/// an arena or something similar.
|
||||||
fn terminfoDir(alloc: Allocator) !?[]const u8 {
|
fn terminfoDir(alloc: Allocator, base: ?[]const u8) !?[]const u8 {
|
||||||
|
const dir = base orelse return null;
|
||||||
|
return try tryDir(alloc, dir, "terminfo");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets the directory to the bundled resources directory, if it
|
||||||
|
/// exists (not all platforms or packages have it).
|
||||||
|
///
|
||||||
|
/// The memory returned can't be easily freed so the alloc should be
|
||||||
|
/// an arena or something similar.
|
||||||
|
fn resourcesDir(alloc: Allocator) !?[]const u8 {
|
||||||
// We only support Mac lookups right now because the terminfo
|
// We only support Mac lookups right now because the terminfo
|
||||||
// DB can be embedded directly in the App bundle.
|
// DB can be embedded directly in the App bundle.
|
||||||
if (comptime !builtin.target.isDarwin()) return null;
|
if (comptime !builtin.target.isDarwin()) return null;
|
||||||
@ -815,13 +862,23 @@ const Subprocess = struct {
|
|||||||
// bundle as we expect it.
|
// bundle as we expect it.
|
||||||
while (std.fs.path.dirname(exe)) |dir| {
|
while (std.fs.path.dirname(exe)) |dir| {
|
||||||
exe = dir;
|
exe = dir;
|
||||||
|
if (try tryDir(alloc, dir, "Contents/Resources")) |v| {
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Little helper to check if the "base/sub" directory exists and
|
||||||
|
/// if so, duplicate the path and return it.
|
||||||
|
fn tryDir(
|
||||||
|
alloc: Allocator,
|
||||||
|
base: []const u8,
|
||||||
|
sub: []const u8,
|
||||||
|
) !?[]const u8 {
|
||||||
var buf: [std.fs.MAX_PATH_BYTES]u8 = undefined;
|
var buf: [std.fs.MAX_PATH_BYTES]u8 = undefined;
|
||||||
const path = try std.fmt.bufPrint(
|
const path = try std.fmt.bufPrint(&buf, "{s}/{s}", .{ base, sub });
|
||||||
&buf,
|
|
||||||
"{s}/Contents/Resources/terminfo",
|
|
||||||
.{dir},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (std.fs.accessAbsolute(path, .{})) {
|
if (std.fs.accessAbsolute(path, .{})) {
|
||||||
return try alloc.dupe(u8, path);
|
return try alloc.dupe(u8, path);
|
||||||
@ -829,7 +886,6 @@ const Subprocess = struct {
|
|||||||
// Folder doesn't exist. If a different error happens its okay
|
// Folder doesn't exist. If a different error happens its okay
|
||||||
// we just ignore it and move on.
|
// we just ignore it and move on.
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
104
src/termio/shell_integration.zig
Normal file
104
src/termio/shell_integration.zig
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const EnvMap = std.process.EnvMap;
|
||||||
|
|
||||||
|
const log = std.log.scoped(.shell_integration);
|
||||||
|
|
||||||
|
/// Shell types we support
|
||||||
|
pub const Shell = enum {
|
||||||
|
fish,
|
||||||
|
zsh,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Setup the command execution environment for automatic
|
||||||
|
/// integrated shell integration. This returns true if shell
|
||||||
|
/// integration was successful. False could mean many things:
|
||||||
|
/// the shell type wasn't detected, etc.
|
||||||
|
pub fn setup(
|
||||||
|
resource_dir: []const u8,
|
||||||
|
command_path: []const u8,
|
||||||
|
env: *EnvMap,
|
||||||
|
force_shell: ?Shell,
|
||||||
|
) !?Shell {
|
||||||
|
const exe = if (force_shell) |shell| switch (shell) {
|
||||||
|
.fish => "/fish",
|
||||||
|
.zsh => "/zsh",
|
||||||
|
} else std.fs.path.basename(command_path);
|
||||||
|
|
||||||
|
if (std.mem.eql(u8, "fish", exe)) {
|
||||||
|
try setupFish(resource_dir, env);
|
||||||
|
return .fish;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std.mem.eql(u8, "zsh", exe)) {
|
||||||
|
try setupZsh(resource_dir, env);
|
||||||
|
return .zsh;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Setup the fish automatic shell integration. This works by
|
||||||
|
/// modify XDG_DATA_DIRS to include the resource directory.
|
||||||
|
/// Fish will automatically load configuration in XDG_DATA_DIRS
|
||||||
|
/// "fish/vendor_conf.d/*.fish".
|
||||||
|
fn setupFish(
|
||||||
|
resource_dir: []const u8,
|
||||||
|
env: *EnvMap,
|
||||||
|
) !void {
|
||||||
|
var path_buf: [std.fs.MAX_PATH_BYTES]u8 = undefined;
|
||||||
|
|
||||||
|
// Get our path to the shell integration directory.
|
||||||
|
const integ_dir = try std.fmt.bufPrint(
|
||||||
|
&path_buf,
|
||||||
|
"{s}/shell-integration",
|
||||||
|
.{resource_dir},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Set an env var so we can remove this from XDG_DATA_DIRS later.
|
||||||
|
// This happens in the shell integration config itself. We do this
|
||||||
|
// so that our modifications don't interfere with other commands.
|
||||||
|
try env.put("GHOSTTY_FISH_XDG_DIR", integ_dir);
|
||||||
|
|
||||||
|
if (env.get("XDG_DATA_DIRS")) |old| {
|
||||||
|
// We have an old value, We need to prepend our value to it.
|
||||||
|
|
||||||
|
// We use a 4K buffer to hold our XDG_DATA_DIR value. The stack
|
||||||
|
// on macOS is at least 512K and Linux is 8MB or more. So this
|
||||||
|
// should fit. If the user has a XDG_DATA_DIR value that is longer
|
||||||
|
// than this then it will fail... and we will cross that bridge
|
||||||
|
// when we actually get there. This avoids us needing an allocator.
|
||||||
|
var buf: [4096]u8 = undefined;
|
||||||
|
const prepended = try std.fmt.bufPrint(
|
||||||
|
&buf,
|
||||||
|
"{s}{c}{s}",
|
||||||
|
.{ integ_dir, std.fs.path.delimiter, old },
|
||||||
|
);
|
||||||
|
|
||||||
|
try env.put("XDG_DATA_DIRS", prepended);
|
||||||
|
} else {
|
||||||
|
// No XDG_DATA_DIRS set, we just set it our desired value.
|
||||||
|
try env.put("XDG_DATA_DIRS", integ_dir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Setup the zsh automatic shell integration. This works by setting
|
||||||
|
/// ZDOTDIR to our resources dir so that zsh will load our config. This
|
||||||
|
/// config then loads the true user config.
|
||||||
|
fn setupZsh(
|
||||||
|
resource_dir: []const u8,
|
||||||
|
env: *EnvMap,
|
||||||
|
) !void {
|
||||||
|
// Preserve the old zdotdir value so we can recover it.
|
||||||
|
if (env.get("ZDOTDIR")) |old| {
|
||||||
|
try env.put("GHOSTTY_ZSH_ZDOTDIR", old);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set our new ZDOTDIR
|
||||||
|
var path_buf: [std.fs.MAX_PATH_BYTES]u8 = undefined;
|
||||||
|
const integ_dir = try std.fmt.bufPrint(
|
||||||
|
&path_buf,
|
||||||
|
"{s}/shell-integration/zsh",
|
||||||
|
.{resource_dir},
|
||||||
|
);
|
||||||
|
try env.put("ZDOTDIR", integ_dir);
|
||||||
|
}
|
Reference in New Issue
Block a user