mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 15:56:13 +03:00
Merge pull request #839 from rockorager/no-cursor
shell-integration: implement "no-cursor" option
This commit is contained in:
123
src/cli/args.zig
123
src/cli/args.zig
@ -10,6 +10,9 @@ const ErrorList = @import("../config/ErrorList.zig");
|
||||
// - Only `--long=value` format is accepted. Do we want to allow
|
||||
// `--long value`? Not currently allowed.
|
||||
|
||||
// For trimming
|
||||
const whitespace = " \t";
|
||||
|
||||
/// The base errors for arg parsing. Additional errors can be returned due
|
||||
/// to type-specific parsing but these are always possible.
|
||||
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
|
||||
@field(dst, field.name) = switch (Field) {
|
||||
[]const u8 => value: {
|
||||
@ -239,7 +230,19 @@ fn parseIntoField(
|
||||
value orelse return error.ValueRequired,
|
||||
) 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;
|
||||
@ -249,6 +252,42 @@ fn parseIntoField(
|
||||
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 {
|
||||
const t = &[_][]const u8{ "1", "t", "T", "true" };
|
||||
const f = &[_][]const u8{ "0", "f", "F", "false" };
|
||||
@ -462,6 +501,63 @@ test "parseIntoField: enums" {
|
||||
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" {
|
||||
const testing = std.testing;
|
||||
var arena = ArenaAllocator.init(testing.allocator);
|
||||
@ -582,7 +678,6 @@ pub fn LineIterator(comptime ReaderType: type) type {
|
||||
} orelse return null;
|
||||
|
||||
// Trim any whitespace around it
|
||||
const whitespace = " \t";
|
||||
const trim = std.mem.trim(u8, entry, whitespace);
|
||||
if (trim.len != entry.len) {
|
||||
std.mem.copy(u8, entry, trim);
|
||||
|
@ -9,6 +9,7 @@ pub const Keybinds = Config.Keybinds;
|
||||
pub const MouseShiftCapture = Config.MouseShiftCapture;
|
||||
pub const NonNativeFullscreen = Config.NonNativeFullscreen;
|
||||
pub const OptionAsAlt = Config.OptionAsAlt;
|
||||
pub const ShellIntegrationFeatures = Config.ShellIntegrationFeatures;
|
||||
|
||||
// Alternate APIs
|
||||
pub const CAPI = @import("config/CAPI.zig");
|
||||
|
@ -536,6 +536,19 @@ keybind: Keybinds = .{},
|
||||
/// The default value is "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.
|
||||
/// 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
|
||||
@ -2189,6 +2202,11 @@ pub const ShellIntegration = enum {
|
||||
zsh,
|
||||
};
|
||||
|
||||
/// Shell integration features
|
||||
pub const ShellIntegrationFeatures = packed struct {
|
||||
cursor: bool = true,
|
||||
};
|
||||
|
||||
/// OSC 10 and 11 default color reporting format.
|
||||
pub const OSCColorReportFormat = enum {
|
||||
none,
|
||||
|
@ -41,8 +41,10 @@ function __ghostty_precmd() {
|
||||
PS2=$PS2'\[\e]133;B\a\]'
|
||||
|
||||
# Cursor
|
||||
if test "$GHOSTTY_SHELL_INTEGRATION_NO_CURSOR" != "1"; then
|
||||
PS1=$PS1'\[\e[5 q\]'
|
||||
PS0=$PS0'\[\e[0 q\]'
|
||||
fi
|
||||
|
||||
# Command
|
||||
PS0=$PS0'$(__ghostty_get_current_command)'
|
||||
|
@ -51,12 +51,17 @@ status --is-interactive || ghostty_exit
|
||||
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"
|
||||
# Check if we are setting cursors
|
||||
set --local no_cursor "$GHOSTTY_SHELL_INTEGRATION_NO_CURSOR"
|
||||
|
||||
if test -z $no_cursor
|
||||
# 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
|
||||
end
|
||||
|
||||
# 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
|
||||
# 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
|
||||
return
|
||||
end
|
||||
@ -93,7 +98,9 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration"
|
||||
set --global fish_handle_reflow 1
|
||||
|
||||
# Initial calls for first prompt
|
||||
__ghostty_set_cursor_beam
|
||||
if test -z $no_cursor
|
||||
__ghostty_set_cursor_beam
|
||||
end
|
||||
__ghostty_mark_prompt_start
|
||||
__update_cwd_osc
|
||||
end
|
||||
|
@ -200,21 +200,23 @@ _ghostty_deferred_init() {
|
||||
functions[_ghostty_preexec]+="
|
||||
builtin print -rnu $_ghostty_fd \$'\\e]2;'\"\${(V)1}\"\$'\\a'"
|
||||
|
||||
# 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'"
|
||||
if [[ "$GHOSTTY_SHELL_INTEGRATION_NO_CURSOR" != 1 ]]; 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
|
||||
# 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'"
|
||||
fi
|
||||
|
||||
# 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
|
||||
|
@ -820,6 +820,7 @@ const Subprocess = struct {
|
||||
final_path,
|
||||
&env,
|
||||
force,
|
||||
opts.full_config.@"shell-integration-features",
|
||||
);
|
||||
};
|
||||
if (shell_integrated) |shell| {
|
||||
|
@ -1,5 +1,6 @@
|
||||
const std = @import("std");
|
||||
const EnvMap = std.process.EnvMap;
|
||||
const config = @import("../config.zig");
|
||||
|
||||
const log = std.log.scoped(.shell_integration);
|
||||
|
||||
@ -18,23 +19,31 @@ pub fn setup(
|
||||
command_path: []const u8,
|
||||
env: *EnvMap,
|
||||
force_shell: ?Shell,
|
||||
features: config.ShellIntegrationFeatures,
|
||||
) !?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;
|
||||
}
|
||||
const shell: Shell = shell: {
|
||||
if (std.mem.eql(u8, "fish", exe)) {
|
||||
try setupFish(resource_dir, env);
|
||||
break :shell .fish;
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, "zsh", exe)) {
|
||||
try setupZsh(resource_dir, env);
|
||||
return .zsh;
|
||||
}
|
||||
if (std.mem.eql(u8, "zsh", exe)) {
|
||||
try setupZsh(resource_dir, env);
|
||||
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
|
||||
|
Reference in New Issue
Block a user