shell-integration: switch to $GHOSTTY_SHELL_FEATURES (#6871)
This change consolidates all three opt-out shell integration environment variables into a single opt-in $GHOSTTY_SHELL_FEATURES variable. Its value is a comma-delimited list of the enabled shell feature names (e.g. "cursor,title"). $GHOSTTY_SHELL_FEATURES is set at runtime and automatically added to the shell environment. Its value is based on the shell-integration-features configuration option. $GHOSTTY_SHELL_FEATURES is only set when at least one shell feature is enabled. It won't be set when 'shell-integration-features = false'. $GHOSTTY_SHELL_FEATURES lists only the enabled shell feature names. We could have alternatively gone in the opposite direction and listed the disabled features, letting the scripts assume each feature is on by default like we did before, but I think this explicit approach is a little safer and easier to reason about / debug. It also doesn't support the "no-" negation prefix used by the config system (e.g. "cursor,no-title"). This simplifies the implementation requirements of our (multiple) shell integration scripts, and because $GHOSTTY_SHELL_FEATURES is derived from shell-integration-features, the user-facing configuration interface retains that expressiveness. $GHOSTTY_SHELL_FEATURES is intended to primarily be an internal concern: an interface between the runtime and our shell integration scripts. It could be used by people with particular use cases who want to manually source those scripts, but that isn't the intended audience. ... and because the previous $GHOSTTY_SHELL_INTEGRATION_NO_* variables were also meant to be an internal concern, this change does not include backwards compatibility support for those names. One last advantage of a using a single $GHOSTTY_SHELL_FEATURES variable is that it can be easily forwarded to e.g. ssh sessions or other shell environments. See: #5070pull/6998/head
commit
60da3cf6a0
|
|
@ -70,7 +70,7 @@ if [ -n "$GHOSTTY_BASH_INJECT" ]; then
|
|||
fi
|
||||
|
||||
# Sudo
|
||||
if [[ "$GHOSTTY_SHELL_INTEGRATION_NO_SUDO" != "1" && -n "$TERMINFO" ]]; then
|
||||
if [[ "$GHOSTTY_SHELL_FEATURES" == *"sudo"* && -n "$TERMINFO" ]]; then
|
||||
# Wrap `sudo` command to ensure Ghostty terminfo is preserved.
|
||||
#
|
||||
# This approach supports wrapping a `sudo` alias, but the alias definition
|
||||
|
|
@ -124,13 +124,13 @@ function __ghostty_precmd() {
|
|||
fi
|
||||
|
||||
# Cursor
|
||||
if test "$GHOSTTY_SHELL_INTEGRATION_NO_CURSOR" != "1"; then
|
||||
if [[ "$GHOSTTY_SHELL_FEATURES" == *"cursor"* ]]; then
|
||||
PS1=$PS1'\[\e[5 q\]'
|
||||
PS0=$PS0'\[\e[0 q\]'
|
||||
fi
|
||||
|
||||
# Title (working directory)
|
||||
if [[ "$GHOSTTY_SHELL_INTEGRATION_NO_TITLE" != 1 ]]; then
|
||||
if [[ "$GHOSTTY_SHELL_FEATURES" == *"title"* ]]; then
|
||||
PS1=$PS1'\[\e]2;\w\a\]'
|
||||
fi
|
||||
fi
|
||||
|
|
@ -161,7 +161,7 @@ function __ghostty_preexec() {
|
|||
PS2="$_GHOSTTY_SAVE_PS2"
|
||||
|
||||
# Title (current command)
|
||||
if [[ -n $cmd && "$GHOSTTY_SHELL_INTEGRATION_NO_TITLE" != 1 ]]; then
|
||||
if [[ -n $cmd && "$GHOSTTY_SHELL_FEATURES" == *"title"* ]]; then
|
||||
builtin printf "\e]2;%s\a" "${cmd//[[:cntrl:]]}"
|
||||
fi
|
||||
|
||||
|
|
|
|||
|
|
@ -36,6 +36,8 @@
|
|||
}
|
||||
|
||||
{
|
||||
use str
|
||||
|
||||
# helper used by `mark-*` functions
|
||||
fn set-prompt-state {|new| set-env __ghostty_prompt_state $new }
|
||||
|
||||
|
|
@ -104,20 +106,18 @@
|
|||
set edit:after-readline = (conj $edit:after-readline $mark-output-start~)
|
||||
set edit:after-command = (conj $edit:after-command $mark-output-end~)
|
||||
|
||||
var no-title = (eq 1 $E:GHOSTTY_SHELL_INTEGRATION_NO_TITLE)
|
||||
var no-cursor = (eq 1 $E:GHOSTTY_SHELL_INTEGRATION_NO_CURSOR)
|
||||
var no-sudo = (eq 1 $E:GHOSTTY_SHELL_INTEGRATION_NO_SUDO)
|
||||
var features = [(str:split ',' $E:GHOSTTY_SHELL_FEATURES)]
|
||||
|
||||
if (not $no-title) {
|
||||
if (has-value $features title) {
|
||||
set after-chdir = (conj $after-chdir {|_| report-pwd })
|
||||
}
|
||||
if (not $no-cursor) {
|
||||
if (has-value $features cursor) {
|
||||
fn beam { printf "\e[5 q" }
|
||||
fn block { printf "\e[0 q" }
|
||||
set edit:before-readline = (conj $edit:before-readline $beam~)
|
||||
set edit:after-readline = (conj $edit:after-readline {|_| block })
|
||||
}
|
||||
if (and (not $no-sudo) (not-eq "" $E:TERMINFO) (has-external sudo)) {
|
||||
if (and (has-value $features sudo) (not-eq "" $E:TERMINFO) (has-external sudo)) {
|
||||
edit:add-var sudo~ $sudo-with-terminfo~
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,10 +49,9 @@ status --is-interactive || ghostty_exit
|
|||
function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration"
|
||||
functions -e __ghostty_setup
|
||||
|
||||
# Check if we are setting cursors
|
||||
set --local no_cursor "$GHOSTTY_SHELL_INTEGRATION_NO_CURSOR"
|
||||
set --local features (string split , $GHOSTTY_SHELL_FEATURES)
|
||||
|
||||
if test -z $no_cursor
|
||||
if contains cursor $features
|
||||
# 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"
|
||||
|
|
@ -62,13 +61,9 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration"
|
|||
end
|
||||
end
|
||||
|
||||
# Check if we are setting sudo
|
||||
set --local no_sudo "$GHOSTTY_SHELL_INTEGRATION_NO_SUDO"
|
||||
|
||||
# When using sudo shell integration feature, ensure $TERMINFO is set
|
||||
# and `sudo` is not already a function or alias
|
||||
if test -z $no_sudo
|
||||
and test -n "$TERMINFO"; and test "file" = (type -t sudo 2> /dev/null; or echo "x")
|
||||
if contains sudo $features and test -n "$TERMINFO"; and test "file" = (type -t sudo 2> /dev/null; or echo "x")
|
||||
# Wrap `sudo` command to ensure Ghostty terminfo is preserved
|
||||
function sudo -d "Wrap sudo to preserve terminfo"
|
||||
set --function sudo_has_sudoedit_flags "no"
|
||||
|
|
@ -125,7 +120,7 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration"
|
|||
set --global fish_handle_reflow 1
|
||||
|
||||
# Initial calls for first prompt
|
||||
if test -z $no_cursor
|
||||
if contains cursor $features
|
||||
__ghostty_set_cursor_beam
|
||||
end
|
||||
__ghostty_mark_prompt_start
|
||||
|
|
|
|||
|
|
@ -194,7 +194,7 @@ _ghostty_deferred_init() {
|
|||
_ghostty_report_pwd"
|
||||
_ghostty_report_pwd
|
||||
|
||||
if [[ "$GHOSTTY_SHELL_INTEGRATION_NO_TITLE" != 1 ]]; then
|
||||
if [[ "$GHOSTTY_SHELL_FEATURES" == *"title"* ]]; then
|
||||
# Enable terminal title changes.
|
||||
functions[_ghostty_precmd]+="
|
||||
builtin print -rnu $_ghostty_fd \$'\\e]2;'\"\${(%):-%(4~|…/%3~|%~)}\"\$'\\a'"
|
||||
|
|
@ -202,7 +202,7 @@ _ghostty_deferred_init() {
|
|||
builtin print -rnu $_ghostty_fd \$'\\e]2;'\"\${(V)1}\"\$'\\a'"
|
||||
fi
|
||||
|
||||
if [[ "$GHOSTTY_SHELL_INTEGRATION_NO_CURSOR" != 1 ]]; then
|
||||
if [[ "$GHOSTTY_SHELL_FEATURES" == *"cursor"* ]]; then
|
||||
# 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
|
||||
|
|
@ -221,7 +221,7 @@ _ghostty_deferred_init() {
|
|||
fi
|
||||
|
||||
# Sudo
|
||||
if [[ "$GHOSTTY_SHELL_INTEGRATION_NO_SUDO" != "1" ]] && [[ -n "$TERMINFO" ]]; then
|
||||
if [[ "$GHOSTTY_SHELL_FEATURES" == *"sudo"* ]] && [[ -n "$TERMINFO" ]]; then
|
||||
# Wrap `sudo` command to ensure Ghostty terminfo is preserved
|
||||
sudo() {
|
||||
builtin local sudo_has_sudoedit_flags="no"
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ pub const ShellIntegration = struct {
|
|||
command: []const u8,
|
||||
};
|
||||
|
||||
/// Setup the command execution environment for automatic
|
||||
/// Set up the command execution environment for automatic
|
||||
/// integrated shell integration and return a ShellIntegration
|
||||
/// struct describing the integration. If integration fails
|
||||
/// (shell type couldn't be detected, etc.), this will return null.
|
||||
|
|
@ -144,15 +144,29 @@ test "force shell" {
|
|||
}
|
||||
}
|
||||
|
||||
/// Setup shell integration feature environment variables without
|
||||
/// performing full shell integration setup.
|
||||
/// Set up the shell integration features environment variable.
|
||||
pub fn setupFeatures(
|
||||
env: *EnvMap,
|
||||
features: config.ShellIntegrationFeatures,
|
||||
) !void {
|
||||
if (!features.cursor) try env.put("GHOSTTY_SHELL_INTEGRATION_NO_CURSOR", "1");
|
||||
if (!features.sudo) try env.put("GHOSTTY_SHELL_INTEGRATION_NO_SUDO", "1");
|
||||
if (!features.title) try env.put("GHOSTTY_SHELL_INTEGRATION_NO_TITLE", "1");
|
||||
const fields = @typeInfo(@TypeOf(features)).@"struct".fields;
|
||||
const capacity: usize = capacity: {
|
||||
comptime var n: usize = fields.len - 1; // commas
|
||||
inline for (fields) |field| n += field.name.len;
|
||||
break :capacity n;
|
||||
};
|
||||
var buffer = try std.BoundedArray(u8, capacity).init(0);
|
||||
|
||||
inline for (fields) |field| {
|
||||
if (@field(features, field.name)) {
|
||||
if (buffer.len > 0) try buffer.append(',');
|
||||
try buffer.appendSlice(field.name);
|
||||
}
|
||||
}
|
||||
|
||||
if (buffer.len > 0) {
|
||||
try env.put("GHOSTTY_SHELL_FEATURES", buffer.slice());
|
||||
}
|
||||
}
|
||||
|
||||
test "setup features" {
|
||||
|
|
@ -162,15 +176,13 @@ test "setup features" {
|
|||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
|
||||
// Test: all features enabled (no environment variables should be set)
|
||||
// Test: all features enabled
|
||||
{
|
||||
var env = EnvMap.init(alloc);
|
||||
defer env.deinit();
|
||||
|
||||
try setupFeatures(&env, .{ .cursor = true, .sudo = true, .title = true });
|
||||
try testing.expect(env.get("GHOSTTY_SHELL_INTEGRATION_NO_CURSOR") == null);
|
||||
try testing.expect(env.get("GHOSTTY_SHELL_INTEGRATION_NO_SUDO") == null);
|
||||
try testing.expect(env.get("GHOSTTY_SHELL_INTEGRATION_NO_TITLE") == null);
|
||||
try testing.expectEqualStrings("cursor,sudo,title", env.get("GHOSTTY_SHELL_FEATURES").?);
|
||||
}
|
||||
|
||||
// Test: all features disabled
|
||||
|
|
@ -179,9 +191,7 @@ test "setup features" {
|
|||
defer env.deinit();
|
||||
|
||||
try setupFeatures(&env, .{ .cursor = false, .sudo = false, .title = false });
|
||||
try testing.expectEqualStrings("1", env.get("GHOSTTY_SHELL_INTEGRATION_NO_CURSOR").?);
|
||||
try testing.expectEqualStrings("1", env.get("GHOSTTY_SHELL_INTEGRATION_NO_SUDO").?);
|
||||
try testing.expectEqualStrings("1", env.get("GHOSTTY_SHELL_INTEGRATION_NO_TITLE").?);
|
||||
try testing.expect(env.get("GHOSTTY_SHELL_FEATURES") == null);
|
||||
}
|
||||
|
||||
// Test: mixed features
|
||||
|
|
@ -190,9 +200,7 @@ test "setup features" {
|
|||
defer env.deinit();
|
||||
|
||||
try setupFeatures(&env, .{ .cursor = false, .sudo = true, .title = false });
|
||||
try testing.expectEqualStrings("1", env.get("GHOSTTY_SHELL_INTEGRATION_NO_CURSOR").?);
|
||||
try testing.expect(env.get("GHOSTTY_SHELL_INTEGRATION_NO_SUDO") == null);
|
||||
try testing.expectEqualStrings("1", env.get("GHOSTTY_SHELL_INTEGRATION_NO_TITLE").?);
|
||||
try testing.expectEqualStrings("sudo", env.get("GHOSTTY_SHELL_FEATURES").?);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue