mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-15 00:06:09 +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
|
// - 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);
|
||||||
|
@ -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");
|
||||||
|
@ -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,
|
||||||
|
@ -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)'
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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| {
|
||||||
|
@ -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
|
||||||
|
Reference in New Issue
Block a user