Merge pull request #839 from rockorager/no-cursor

shell-integration: implement "no-cursor" option
This commit is contained in:
Mitchell Hashimoto
2023-11-07 17:07:09 -08:00
committed by GitHub
8 changed files with 181 additions and 46 deletions

View File

@ -10,6 +10,9 @@ const ErrorList = @import("../config/ErrorList.zig");
// - Only `--long=value` format is accepted. Do we want to allow // - Only `--long=value` format is accepted. Do we want to allow
// `--long value`? Not currently allowed. // `--long value`? Not currently allowed.
// For trimming
const whitespace = " \t";
/// The base errors for arg parsing. Additional errors can be returned due /// The base errors for arg parsing. Additional errors can be returned due
/// to type-specific parsing but these are always possible. /// to type-specific parsing but these are always possible.
pub const Error = error{ pub const Error = error{
@ -191,18 +194,6 @@ fn parseIntoField(
} }
} }
switch (fieldInfo) {
.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
@field(dst, field.name) = switch (Field) { @field(dst, field.name) = switch (Field) {
[]const u8 => value: { []const u8 => value: {
@ -239,7 +230,19 @@ fn parseIntoField(
value orelse return error.ValueRequired, value orelse return error.ValueRequired,
) catch return error.InvalidValue, ) catch return error.InvalidValue,
else => unreachable, else => switch (fieldInfo) {
.Enum => std.meta.stringToEnum(
Field,
value orelse return error.ValueRequired,
) orelse return error.InvalidValue,
.Struct => try parsePackedStruct(
Field,
value orelse return error.ValueRequired,
),
else => unreachable,
},
}; };
return; return;
@ -249,6 +252,42 @@ fn parseIntoField(
return error.InvalidField; return error.InvalidField;
} }
fn parsePackedStruct(comptime T: type, v: []const u8) !T {
const info = @typeInfo(T).Struct;
assert(info.layout == .Packed);
var result: T = .{};
// We split each value by ","
var iter = std.mem.splitSequence(u8, v, ",");
loop: while (iter.next()) |part_raw| {
// Determine the field we're looking for and the value. If the
// field is prefixed with "no-" then we set the value to false.
const part, const value = part: {
const negation_prefix = "no-";
const trimmed = std.mem.trim(u8, part_raw, whitespace);
if (std.mem.startsWith(u8, trimmed, negation_prefix)) {
break :part .{ trimmed[negation_prefix.len..], false };
} else {
break :part .{ trimmed, true };
}
};
inline for (info.fields) |field| {
assert(field.type == bool);
if (std.mem.eql(u8, field.name, part)) {
@field(result, field.name) = value;
continue :loop;
}
}
// No field matched
return error.InvalidValue;
}
return result;
}
fn parseBool(v: []const u8) !bool { fn parseBool(v: []const u8) !bool {
const t = &[_][]const u8{ "1", "t", "T", "true" }; const t = &[_][]const u8{ "1", "t", "T", "true" };
const f = &[_][]const u8{ "0", "f", "F", "false" }; const f = &[_][]const u8{ "0", "f", "F", "false" };
@ -462,6 +501,63 @@ test "parseIntoField: enums" {
try testing.expectEqual(Enum.two, data.v); try testing.expectEqual(Enum.two, data.v);
} }
test "parseIntoField: packed struct" {
const testing = std.testing;
var arena = ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
const Field = packed struct {
a: bool = false,
b: bool = true,
};
var data: struct {
v: Field,
} = undefined;
try parseIntoField(@TypeOf(data), alloc, &data, "v", "b");
try testing.expect(!data.v.a);
try testing.expect(data.v.b);
}
test "parseIntoField: packed struct negation" {
const testing = std.testing;
var arena = ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
const Field = packed struct {
a: bool = false,
b: bool = true,
};
var data: struct {
v: Field,
} = undefined;
try parseIntoField(@TypeOf(data), alloc, &data, "v", "a,no-b");
try testing.expect(data.v.a);
try testing.expect(!data.v.b);
}
test "parseIntoField: packed struct whitespace" {
const testing = std.testing;
var arena = ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
const Field = packed struct {
a: bool = false,
b: bool = true,
};
var data: struct {
v: Field,
} = undefined;
try parseIntoField(@TypeOf(data), alloc, &data, "v", " a, no-b ");
try testing.expect(data.v.a);
try testing.expect(!data.v.b);
}
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);
@ -582,7 +678,6 @@ pub fn LineIterator(comptime ReaderType: type) type {
} orelse return null; } orelse return null;
// Trim any whitespace around it // Trim any whitespace around it
const whitespace = " \t";
const trim = std.mem.trim(u8, entry, whitespace); const trim = std.mem.trim(u8, entry, whitespace);
if (trim.len != entry.len) { if (trim.len != entry.len) {
std.mem.copy(u8, entry, trim); std.mem.copy(u8, entry, trim);

View File

@ -9,6 +9,7 @@ pub const Keybinds = Config.Keybinds;
pub const MouseShiftCapture = Config.MouseShiftCapture; pub const MouseShiftCapture = Config.MouseShiftCapture;
pub const NonNativeFullscreen = Config.NonNativeFullscreen; pub const NonNativeFullscreen = Config.NonNativeFullscreen;
pub const OptionAsAlt = Config.OptionAsAlt; pub const OptionAsAlt = Config.OptionAsAlt;
pub const ShellIntegrationFeatures = Config.ShellIntegrationFeatures;
// Alternate APIs // Alternate APIs
pub const CAPI = @import("config/CAPI.zig"); pub const CAPI = @import("config/CAPI.zig");

View File

@ -536,6 +536,19 @@ keybind: Keybinds = .{},
/// The default value is "detect". /// The default value is "detect".
@"shell-integration": ShellIntegration = .detect, @"shell-integration": ShellIntegration = .detect,
/// Shell integration features to enable if shell integration itself is enabled.
/// The format of this is a list of features to enable separated by commas.
/// If you prefix a feature with "no-" then it is disabled. If you omit
/// a feature, its default value is used, so you must explicitly disable
/// features you don't want.
///
/// Available features:
///
/// - "cursor" - Set the cursor to a blinking bar at the prompt.
///
/// Example: "cursor", "no-cursor"
@"shell-integration-features": ShellIntegrationFeatures = .{},
/// Sets the reporting format for OSC sequences that request color information. /// Sets the reporting format for OSC sequences that request color information.
/// Ghostty currently supports OSC 10 (foreground) and OSC 11 (background) queries, /// Ghostty currently supports OSC 10 (foreground) and OSC 11 (background) queries,
/// and by default the reported values are scaled-up RGB values, where each component /// and by default the reported values are scaled-up RGB values, where each component
@ -2189,6 +2202,11 @@ pub const ShellIntegration = enum {
zsh, zsh,
}; };
/// Shell integration features
pub const ShellIntegrationFeatures = packed struct {
cursor: bool = true,
};
/// OSC 10 and 11 default color reporting format. /// OSC 10 and 11 default color reporting format.
pub const OSCColorReportFormat = enum { pub const OSCColorReportFormat = enum {
none, none,

View File

@ -41,8 +41,10 @@ function __ghostty_precmd() {
PS2=$PS2'\[\e]133;B\a\]' PS2=$PS2'\[\e]133;B\a\]'
# Cursor # Cursor
if test "$GHOSTTY_SHELL_INTEGRATION_NO_CURSOR" != "1"; then
PS1=$PS1'\[\e[5 q\]' PS1=$PS1'\[\e[5 q\]'
PS0=$PS0'\[\e[0 q\]' PS0=$PS0'\[\e[0 q\]'
fi
# Command # Command
PS0=$PS0'$(__ghostty_get_current_command)' PS0=$PS0'$(__ghostty_get_current_command)'

View File

@ -51,12 +51,17 @@ status --is-interactive || ghostty_exit
function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration" function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration"
functions -e __ghostty_setup functions -e __ghostty_setup
# Change the cursor to a beam on prompt. # Check if we are setting cursors
function __ghostty_set_cursor_beam --on-event fish_prompt -d "Set cursor shape" set --local no_cursor "$GHOSTTY_SHELL_INTEGRATION_NO_CURSOR"
echo -en "\e[5 q"
end if test -z $no_cursor
function __ghostty_reset_cursor --on-event fish_preexec -d "Reset cursor shape" # Change the cursor to a beam on prompt.
echo -en "\e[0 q" 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
end end
# Setup prompt marking # Setup prompt marking
@ -82,7 +87,7 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration"
# Report pwd. This is actually built-in to fish but only for terminals # Report pwd. This is actually built-in to fish but only for terminals
# that match an allowlist and that isn't us. # that match an allowlist and that isn't us.
function __update_cwd_osc --on-variable PWD -d 'Notify capable terminals when $PWD changes' function __update_cwd_osc --on-variable PWD -d 'Notify capable terminals when $PWD changes'
if status --is-command-substitution || set -q INSIDE_EMACS if status --is-command-substitution || set -q INSIDE_EMACS
return return
end end
@ -93,7 +98,9 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration"
set --global fish_handle_reflow 1 set --global fish_handle_reflow 1
# Initial calls for first prompt # Initial calls for first prompt
__ghostty_set_cursor_beam if test -z $no_cursor
__ghostty_set_cursor_beam
end
__ghostty_mark_prompt_start __ghostty_mark_prompt_start
__update_cwd_osc __update_cwd_osc
end end

View File

@ -200,21 +200,23 @@ _ghostty_deferred_init() {
functions[_ghostty_preexec]+=" functions[_ghostty_preexec]+="
builtin print -rnu $_ghostty_fd \$'\\e]2;'\"\${(V)1}\"\$'\\a'" builtin print -rnu $_ghostty_fd \$'\\e]2;'\"\${(V)1}\"\$'\\a'"
# Enable cursor shape changes depending on the current keymap. if [[ "$GHOSTTY_SHELL_INTEGRATION_NO_CURSOR" != 1 ]]; then
# This implementation leaks blinking block cursor into external commands # Enable cursor shape changes depending on the current keymap.
# executed from zle. For example, users of fzf-based widgets may find # This implementation leaks blinking block cursor into external commands
# themselves with a blinking block cursor within fzf. # executed from zle. For example, users of fzf-based widgets may find
_ghostty_zle_line_init _ghostty_zle_line_finish _ghostty_zle_keymap_select() { # themselves with a blinking block cursor within fzf.
case ${KEYMAP-} in _ghostty_zle_line_init _ghostty_zle_line_finish _ghostty_zle_keymap_select() {
# Blinking block cursor. case ${KEYMAP-} in
vicmd|visual) builtin print -nu "$_ghostty_fd" '\e[1 q';; # Blinking block cursor.
# Blinking bar cursor. vicmd|visual) builtin print -nu "$_ghostty_fd" '\e[1 q';;
*) builtin print -nu "$_ghostty_fd" '\e[5 q';; # Blinking bar cursor.
esac *) builtin print -nu "$_ghostty_fd" '\e[5 q';;
} esac
# Restore the blinking default shape before executing an external command }
functions[_ghostty_preexec]+=" # Restore the blinking default shape before executing an external command
builtin print -rnu $_ghostty_fd \$'\\e[0 q'" functions[_ghostty_preexec]+="
builtin print -rnu $_ghostty_fd \$'\\e[0 q'"
fi
# Some zsh users manually run `source ~/.zshrc` in order to apply rc file # 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 # changes to the current shell. This is a terrible practice that breaks many

View File

@ -820,6 +820,7 @@ const Subprocess = struct {
final_path, final_path,
&env, &env,
force, force,
opts.full_config.@"shell-integration-features",
); );
}; };
if (shell_integrated) |shell| { if (shell_integrated) |shell| {

View File

@ -1,5 +1,6 @@
const std = @import("std"); const std = @import("std");
const EnvMap = std.process.EnvMap; const EnvMap = std.process.EnvMap;
const config = @import("../config.zig");
const log = std.log.scoped(.shell_integration); const log = std.log.scoped(.shell_integration);
@ -18,23 +19,31 @@ pub fn setup(
command_path: []const u8, command_path: []const u8,
env: *EnvMap, env: *EnvMap,
force_shell: ?Shell, force_shell: ?Shell,
features: config.ShellIntegrationFeatures,
) !?Shell { ) !?Shell {
const exe = if (force_shell) |shell| switch (shell) { const exe = if (force_shell) |shell| switch (shell) {
.fish => "/fish", .fish => "/fish",
.zsh => "/zsh", .zsh => "/zsh",
} else std.fs.path.basename(command_path); } else std.fs.path.basename(command_path);
if (std.mem.eql(u8, "fish", exe)) { const shell: Shell = shell: {
try setupFish(resource_dir, env); if (std.mem.eql(u8, "fish", exe)) {
return .fish; try setupFish(resource_dir, env);
} break :shell .fish;
}
if (std.mem.eql(u8, "zsh", exe)) { if (std.mem.eql(u8, "zsh", exe)) {
try setupZsh(resource_dir, env); try setupZsh(resource_dir, env);
return .zsh; break :shell .zsh;
} }
return null; return null;
};
// Setup our feature env vars
if (!features.cursor) try env.put("GHOSTTY_SHELL_INTEGRATION_NO_CURSOR", "1");
return shell;
} }
/// Setup the fish automatic shell integration. This works by /// Setup the fish automatic shell integration. This works by