shell-integration: respect cursor-style-blink

The `cursor` shell feature always used a blinking bar (beam), often to
the surprise of users who set `cursor-style-blink = false`.

This change extends our GHOSTTY_SHELL_FEATURES format to include either
`cursor:blink` (default) or `cursor:steady` based on cursor-style-blink
when the `cursor` feature is enabled, and all shell integrations have
been updated to use that additional information to choose the DECSCUSR
cursor value (5=blinking bar, 6=steady bar).

I also considered passing a DECSCUSR value in GHOSTTY_SHELL_FEATURES
(e.g. `cursor:5`). This mostly worked well, but zsh also needs the blink
state on its own for its block cursor. We also don't support any other
shell feature cursor configurability (e.g. the shape), so this was an
over generalization.

This does change the behavior for users who like the blinking bar in the
shell but have `cursor-blink-style = false` for other reasons. We could
provide additional `cursor` shell feature configurability, but I think
that's best left to a separate change.
This commit is contained in:
Jon Parise
2026-02-08 21:52:49 -05:00
parent f831258c0f
commit 3cfb9d64d1
8 changed files with 57 additions and 21 deletions

View File

@@ -632,6 +632,7 @@ pub fn init(
.env_override = config.env,
.shell_integration = config.@"shell-integration",
.shell_integration_features = config.@"shell-integration-features",
.cursor_blink = config.@"cursor-style-blink",
.working_directory = config.@"working-directory",
.resources_dir = global_state.resources_dir.host(),
.term = config.term,

View File

@@ -2710,7 +2710,7 @@ keybind: Keybinds = .{},
///
/// Available features:
///
/// * `cursor` - Set the cursor to a blinking bar at the prompt.
/// * `cursor` - Set the cursor to a bar at the prompt.
///
/// * `sudo` - Set sudo wrapper to preserve terminfo.
///

View File

@@ -213,7 +213,10 @@ function __ghostty_precmd() {
# Cursor
if [[ "$GHOSTTY_SHELL_FEATURES" == *"cursor"* ]]; then
[[ "$PS1" != *'\[\e[5 q\]'* ]] && PS1=$PS1'\[\e[5 q\]' # input
builtin local cursor=5 # blinking bar
[[ "$GHOSTTY_SHELL_FEATURES" == *"cursor:steady"* ]] && cursor=6 # steady bar
[[ "$PS1" != *"\[\e[${cursor} q\]"* ]] && PS1=$PS1"\[\e[${cursor} q\]"
[[ "$PS0" != *'\[\e[0 q\]'* ]] && PS0=$PS0'\[\e[0 q\]' # reset
fi

View File

@@ -154,11 +154,16 @@
set edit:after-readline = (conj $edit:after-readline $mark-output-start~)
set edit:after-command = (conj $edit:after-command $mark-output-end~)
if (has-value $features cursor) {
fn beam { printf "\e[5 q" }
fn block { printf "\e[0 q" }
if (str:contains $E:GHOSTTY_SHELL_FEATURES "cursor") {
var cursor = "5" # blinking bar
if (has-value $features cursor:steady) {
set cursor = "6" # steady bar
}
fn beam { printf "\e["$cursor" q" }
fn reset { printf "\e[0 q" }
set edit:before-readline = (conj $edit:before-readline $beam~)
set edit:after-readline = (conj $edit:after-readline {|_| block })
set edit:after-readline = (conj $edit:after-readline {|_| reset })
}
if (and (has-value $features path) (has-env GHOSTTY_BIN_DIR)) {
if (not (has-value $paths $E:GHOSTTY_BIN_DIR)) {

View File

@@ -72,11 +72,14 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration"
set -g __ghostty_prompt_start_mark "\e]133;A;click_events=1\a"
end
if contains cursor $features
if string match -q 'cursor*' -- $features
set -l cursor 5 # blinking bar
contains cursor:steady $features && set cursor 6 # steady bar
# Change the cursor to a beam on prompt.
function __ghostty_set_cursor_beam --on-event fish_prompt -d "Set cursor shape"
function __ghostty_set_cursor_beam --on-event fish_prompt -V cursor -d "Set cursor shape"
if not functions -q fish_vi_cursor_handle
echo -en "\e[5 q"
echo -en "\e[$cursor q"
end
end
function __ghostty_reset_cursor --on-event fish_preexec -d "Reset cursor shape"
@@ -233,7 +236,7 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration"
set --global fish_handle_reflow 1
# Initial calls for first prompt
if contains cursor $features
if string match -q 'cursor*' -- $features
__ghostty_set_cursor_beam
end
__ghostty_mark_prompt_start

View File

@@ -227,14 +227,14 @@ _ghostty_deferred_init() {
# 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
builtin local steady=0
[[ "$GHOSTTY_SHELL_FEATURES" == *"cursor:steady"* ]] && steady=1
case ${KEYMAP-} in
vicmd|visual) builtin print -nu "$_ghostty_fd" "\e[$(( 1 + steady )) q" ;; # block
*) builtin print -nu "$_ghostty_fd" "\e[$(( 5 + steady )) q" ;; # bar
esac
}
# Restore the blinking default shape before executing an external command
# Restore the default shape before executing an external command
functions[_ghostty_preexec]+="
builtin print -rnu $_ghostty_fd \$'\\e[0 q'"
fi

View File

@@ -562,6 +562,7 @@ pub const Config = struct {
env_override: configpkg.RepeatableStringMap = .{},
shell_integration: configpkg.Config.ShellIntegration = .detect,
shell_integration_features: configpkg.Config.ShellIntegrationFeatures = .{},
cursor_blink: ?bool = null,
working_directory: ?[]const u8 = null,
resources_dir: ?[]const u8,
term: []const u8,
@@ -755,6 +756,7 @@ const Subprocess = struct {
try shell_integration.setupFeatures(
&env,
cfg.shell_integration_features,
cfg.cursor_blink orelse true,
);
const force: ?shell_integration.Shell = switch (cfg.shell_integration) {

View File

@@ -188,11 +188,13 @@ test detectShell {
pub fn setupFeatures(
env: *EnvMap,
features: config.ShellIntegrationFeatures,
cursor_blink: bool,
) !void {
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;
n += ":steady".len; // cursor value
break :capacity n;
};
@@ -221,6 +223,10 @@ pub fn setupFeatures(
if (@field(features, name)) {
if (writer.end > 0) try writer.writeByte(',');
try writer.writeAll(name);
if (std.mem.eql(u8, name, "cursor")) {
try writer.writeAll(if (cursor_blink) ":blink" else ":steady");
}
}
}
@@ -241,8 +247,8 @@ test "setup features" {
var env = EnvMap.init(alloc);
defer env.deinit();
try setupFeatures(&env, .{ .cursor = true, .sudo = true, .title = true, .@"ssh-env" = true, .@"ssh-terminfo" = true, .path = true });
try testing.expectEqualStrings("cursor,path,ssh-env,ssh-terminfo,sudo,title", env.get("GHOSTTY_SHELL_FEATURES").?);
try setupFeatures(&env, .{ .cursor = true, .sudo = true, .title = true, .@"ssh-env" = true, .@"ssh-terminfo" = true, .path = true }, true);
try testing.expectEqualStrings("cursor:blink,path,ssh-env,ssh-terminfo,sudo,title", env.get("GHOSTTY_SHELL_FEATURES").?);
}
// Test: all features disabled
@@ -250,7 +256,7 @@ test "setup features" {
var env = EnvMap.init(alloc);
defer env.deinit();
try setupFeatures(&env, std.mem.zeroes(config.ShellIntegrationFeatures));
try setupFeatures(&env, std.mem.zeroes(config.ShellIntegrationFeatures), true);
try testing.expect(env.get("GHOSTTY_SHELL_FEATURES") == null);
}
@@ -259,9 +265,25 @@ test "setup features" {
var env = EnvMap.init(alloc);
defer env.deinit();
try setupFeatures(&env, .{ .cursor = false, .sudo = true, .title = false, .@"ssh-env" = true, .@"ssh-terminfo" = false, .path = false });
try setupFeatures(&env, .{ .cursor = false, .sudo = true, .title = false, .@"ssh-env" = true, .@"ssh-terminfo" = false, .path = false }, true);
try testing.expectEqualStrings("ssh-env,sudo", env.get("GHOSTTY_SHELL_FEATURES").?);
}
// Test: blinking cursor
{
var env = EnvMap.init(alloc);
defer env.deinit();
try setupFeatures(&env, .{ .cursor = true, .sudo = false, .title = false, .@"ssh-env" = false, .@"ssh-terminfo" = false, .path = false }, true);
try testing.expectEqualStrings("cursor:blink", env.get("GHOSTTY_SHELL_FEATURES").?);
}
// Test: steady cursor
{
var env = EnvMap.init(alloc);
defer env.deinit();
try setupFeatures(&env, .{ .cursor = true, .sudo = false, .title = false, .@"ssh-env" = false, .@"ssh-terminfo" = false, .path = false }, false);
try testing.expectEqualStrings("cursor:steady", env.get("GHOSTTY_SHELL_FEATURES").?);
}
}
/// Setup the bash automatic shell integration. This works by