Merge 736bb4d800 into 6246c288ae
commit
701fecf94b
|
|
@ -6,6 +6,7 @@ const formatter_file = @import("config/formatter_file.zig");
|
|||
pub const Config = @import("config/Config.zig");
|
||||
pub const conditional = @import("config/conditional.zig");
|
||||
pub const io = @import("config/io.zig");
|
||||
pub const shell = @import("config/shell.zig");
|
||||
pub const string = @import("config/string.zig");
|
||||
pub const edit = @import("config/edit.zig");
|
||||
pub const url = @import("config/url.zig");
|
||||
|
|
@ -40,7 +41,8 @@ pub const RepeatableString = Config.RepeatableString;
|
|||
pub const RepeatableStringMap = @import("config/RepeatableStringMap.zig");
|
||||
pub const RepeatablePath = Config.RepeatablePath;
|
||||
pub const Path = Config.Path;
|
||||
pub const ShellIntegrationFeatures = Config.ShellIntegrationFeatures;
|
||||
pub const ShellIntegration = shell.ShellIntegration;
|
||||
pub const ShellIntegrationFeatures = shell.ShellIntegrationFeatures;
|
||||
pub const WindowDecoration = Config.WindowDecoration;
|
||||
pub const WindowPaddingColor = Config.WindowPaddingColor;
|
||||
pub const BackgroundImagePosition = Config.BackgroundImagePosition;
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ const conditional = @import("conditional.zig");
|
|||
const Conditional = conditional.Conditional;
|
||||
const file_load = @import("file_load.zig");
|
||||
const formatterpkg = @import("formatter.zig");
|
||||
const shellpkg = @import("shell.zig");
|
||||
const themepkg = @import("theme.zig");
|
||||
const url = @import("url.zig");
|
||||
pub const Key = @import("key.zig").Key;
|
||||
|
|
@ -862,9 +863,11 @@ palette: Palette = .{},
|
|||
/// q`). Shell configurations will often request specific cursor styles.
|
||||
///
|
||||
/// Note that shell integration will automatically set the cursor to a bar at
|
||||
/// a prompt, regardless of this configuration. You can disable that behavior
|
||||
/// by specifying `shell-integration-features = no-cursor` or disabling shell
|
||||
/// integration entirely.
|
||||
/// a prompt, regardless of this configuration. You can customize the shell
|
||||
/// integration cursor style using `shell-integration-features` (e.g.,
|
||||
/// `shell-integration-features = cursor:block` or `cursor:underline:steady`).
|
||||
/// You can also disable the cursor feature entirely with
|
||||
/// `shell-integration-features = no-cursor`.
|
||||
///
|
||||
/// Valid values are:
|
||||
///
|
||||
|
|
@ -2810,7 +2813,7 @@ keybind: Keybinds = .{},
|
|||
/// * `bash`, `elvish`, `fish`, `nushell`, `zsh` - Use this specific shell injection scheme.
|
||||
///
|
||||
/// The default value is `detect`.
|
||||
@"shell-integration": ShellIntegration = .detect,
|
||||
@"shell-integration": shellpkg.ShellIntegration = .detect,
|
||||
|
||||
/// Shell integration features to enable. These require our shell integration
|
||||
/// to be loaded, either automatically via shell-integration or manually.
|
||||
|
|
@ -2824,7 +2827,20 @@ keybind: Keybinds = .{},
|
|||
///
|
||||
/// Available features:
|
||||
///
|
||||
/// * `cursor` - Set the cursor to a bar at the prompt.
|
||||
/// * `cursor` - Set the cursor style at the prompt. By default, this sets a bar
|
||||
/// cursor using the cursor-style-blink configuration value.
|
||||
///
|
||||
/// Available cursor shapes: `bar`, `block`, `underline`
|
||||
/// Available cursor styles: `blink`, `steady`, or omit to use cursor-style-blink
|
||||
///
|
||||
/// Examples:
|
||||
/// - `cursor` or `cursor:bar` - bar cursor (respects cursor-style-blink)
|
||||
/// - `cursor:block` - block cursor (respects cursor-style-blink)
|
||||
/// - `cursor:underline:steady` - steady underline cursor
|
||||
/// - `cursor:block:blink` - blinking block cursor
|
||||
///
|
||||
/// For convenience, `cursor:blink` and `cursor:steady` are supported as
|
||||
/// shortcuts for `cursor:bar:blink` and `cursor:bar:steady`.
|
||||
///
|
||||
/// * `sudo` - Set sudo wrapper to preserve terminfo.
|
||||
///
|
||||
|
|
@ -2855,7 +2871,7 @@ keybind: Keybinds = .{},
|
|||
/// when both `ssh-env` and `ssh-terminfo` are enabled, Ghostty will install its
|
||||
/// terminfo on remote hosts and use `xterm-ghostty` as TERM, falling back to
|
||||
/// `xterm-256color` with environment variables if terminfo installation fails.
|
||||
@"shell-integration-features": ShellIntegrationFeatures = .{},
|
||||
@"shell-integration-features": shellpkg.ShellIntegrationFeatures = .{},
|
||||
|
||||
/// Custom entries into the command palette.
|
||||
///
|
||||
|
|
@ -8657,27 +8673,6 @@ pub const MiddleClickAction = enum {
|
|||
ignore,
|
||||
};
|
||||
|
||||
/// Shell integration values
|
||||
pub const ShellIntegration = enum {
|
||||
none,
|
||||
detect,
|
||||
bash,
|
||||
elvish,
|
||||
fish,
|
||||
nushell,
|
||||
zsh,
|
||||
};
|
||||
|
||||
/// Shell integration features
|
||||
pub const ShellIntegrationFeatures = packed struct {
|
||||
cursor: bool = true,
|
||||
sudo: bool = false,
|
||||
title: bool = true,
|
||||
@"ssh-env": bool = false,
|
||||
@"ssh-terminfo": bool = false,
|
||||
path: bool = true,
|
||||
};
|
||||
|
||||
pub const SplitPreserveZoom = packed struct {
|
||||
navigation: bool = false,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,325 @@
|
|||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const cli = @import("../cli.zig");
|
||||
const formatterpkg = @import("formatter.zig");
|
||||
|
||||
/// Shell integration values
|
||||
pub const ShellIntegration = enum {
|
||||
none,
|
||||
detect,
|
||||
bash,
|
||||
elvish,
|
||||
fish,
|
||||
nushell,
|
||||
zsh,
|
||||
};
|
||||
|
||||
/// Shell integration features
|
||||
pub const ShellIntegrationFeatures = struct {
|
||||
pub const Cursor = struct {
|
||||
shape: Shape = .bar,
|
||||
style: Style = .default,
|
||||
|
||||
pub const Shape = enum {
|
||||
disabled,
|
||||
bar,
|
||||
block,
|
||||
underline,
|
||||
};
|
||||
|
||||
pub const Style = enum {
|
||||
default, // cursor-style-blink
|
||||
blink,
|
||||
steady,
|
||||
};
|
||||
};
|
||||
|
||||
cursor: Cursor = .{},
|
||||
path: bool = true,
|
||||
@"ssh-env": bool = false,
|
||||
@"ssh-terminfo": bool = false,
|
||||
sudo: bool = false,
|
||||
title: bool = true,
|
||||
|
||||
pub fn parseCLI(input: ?[]const u8) !ShellIntegrationFeatures {
|
||||
const v = input orelse return error.ValueRequired;
|
||||
var result: ShellIntegrationFeatures = .{};
|
||||
|
||||
// Handle "true" or "false" to toggle all features
|
||||
if (std.mem.eql(u8, v, "true") or std.mem.eql(u8, v, "false")) {
|
||||
const b = std.mem.eql(u8, v, "true");
|
||||
result.cursor = if (b) .{} else .{ .shape = .disabled };
|
||||
inline for (@typeInfo(ShellIntegrationFeatures).@"struct".fields) |field| {
|
||||
if (field.type == bool) {
|
||||
@field(result, field.name) = b;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
var iter = std.mem.splitSequence(u8, v, ",");
|
||||
loop: while (iter.next()) |part_raw| {
|
||||
const trimmed = std.mem.trim(u8, part_raw, cli.args.whitespace);
|
||||
|
||||
// Handle cursor[:shape[:style]] syntax
|
||||
if (std.mem.startsWith(u8, trimmed, "cursor")) {
|
||||
var cursor_iter = std.mem.splitScalar(u8, trimmed, ':');
|
||||
_ = cursor_iter.next(); // skip "cursor"
|
||||
|
||||
// Parse shape (if present)
|
||||
if (cursor_iter.next()) |shape| {
|
||||
// For convenience, "blink" or "steady" alone implies bar (default)
|
||||
if (std.mem.eql(u8, shape, "blink")) {
|
||||
result.cursor.shape = .bar;
|
||||
result.cursor.style = .blink;
|
||||
} else if (std.mem.eql(u8, shape, "steady")) {
|
||||
result.cursor.shape = .bar;
|
||||
result.cursor.style = .steady;
|
||||
} else {
|
||||
result.cursor.shape = std.meta.stringToEnum(Cursor.Shape, shape) orelse return error.InvalidValue;
|
||||
}
|
||||
|
||||
// Parse style (if present)
|
||||
if (cursor_iter.next()) |style| {
|
||||
result.cursor.style = std.meta.stringToEnum(Cursor.Style, style) orelse return error.InvalidValue;
|
||||
}
|
||||
} else {
|
||||
result.cursor = .{};
|
||||
}
|
||||
continue;
|
||||
} else if (std.mem.eql(u8, trimmed, "no-cursor")) {
|
||||
result.cursor.shape = .disabled;
|
||||
continue;
|
||||
}
|
||||
|
||||
const name, const value = part: {
|
||||
const negation_prefix = "no-";
|
||||
const trimmed_name = std.mem.trim(u8, part_raw, cli.args.whitespace);
|
||||
if (std.mem.startsWith(u8, trimmed, negation_prefix)) {
|
||||
break :part .{ trimmed_name[negation_prefix.len..], false };
|
||||
} else {
|
||||
break :part .{ trimmed_name, true };
|
||||
}
|
||||
};
|
||||
|
||||
inline for (@typeInfo(ShellIntegrationFeatures).@"struct".fields) |field| {
|
||||
if (field.type == bool and std.mem.eql(u8, field.name, name)) {
|
||||
@field(result, field.name) = value;
|
||||
continue :loop;
|
||||
}
|
||||
}
|
||||
|
||||
// No field matched
|
||||
return error.InvalidValue;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
pub const FormatMode = enum {
|
||||
/// Human-readable format for config output (e.g., "cursor:block:blink")
|
||||
config,
|
||||
/// Format for GHOSTTY_SHELL_FEATURES environment variable (e.g., "cursor:5")
|
||||
env,
|
||||
};
|
||||
|
||||
pub fn format(self: ShellIntegrationFeatures, writer: *std.Io.Writer, mode: FormatMode) anyerror!void {
|
||||
const fields = comptime fields: {
|
||||
const all_fields = @typeInfo(ShellIntegrationFeatures).@"struct".fields;
|
||||
var sorted: [all_fields.len]std.builtin.Type.StructField = all_fields[0..].*;
|
||||
const SortContext = struct {
|
||||
fn lessThan(_: @This(), a: std.builtin.Type.StructField, b: std.builtin.Type.StructField) bool {
|
||||
return std.ascii.orderIgnoreCase(a.name, b.name) == .lt;
|
||||
}
|
||||
};
|
||||
std.mem.sortUnstable(std.builtin.Type.StructField, &sorted, SortContext{}, SortContext.lessThan);
|
||||
break :fields sorted;
|
||||
};
|
||||
|
||||
inline for (fields) |field| {
|
||||
const enabled = switch (field.type) {
|
||||
bool => @field(self, field.name),
|
||||
Cursor => @field(self, field.name).shape != .disabled,
|
||||
else => @compileError("unexpected field type in ShellIntegrationFeatures"),
|
||||
};
|
||||
if (enabled) {
|
||||
if (writer.end > 0) try writer.writeByte(',');
|
||||
try writer.writeAll(field.name);
|
||||
switch (field.type) {
|
||||
Cursor => {
|
||||
const cursor = @field(self, field.name);
|
||||
switch (mode) {
|
||||
// DECSCUSR codes
|
||||
.env => {
|
||||
const decscusr: u8 = switch (cursor.shape) {
|
||||
.disabled => unreachable,
|
||||
.bar => switch (cursor.style) {
|
||||
.default => unreachable,
|
||||
.blink => 5,
|
||||
.steady => 6,
|
||||
},
|
||||
.block => switch (cursor.style) {
|
||||
.default => unreachable,
|
||||
.blink => 1,
|
||||
.steady => 2,
|
||||
},
|
||||
.underline => switch (cursor.style) {
|
||||
.default => unreachable,
|
||||
.blink => 3,
|
||||
.steady => 4,
|
||||
},
|
||||
};
|
||||
try writer.print(":{d}", .{decscusr});
|
||||
},
|
||||
// enum tag names
|
||||
.config => {
|
||||
try writer.writeByte(':');
|
||||
try writer.writeAll(@tagName(cursor.shape));
|
||||
if (cursor.style != .default) {
|
||||
try writer.writeByte(':');
|
||||
try writer.writeAll(@tagName(cursor.style));
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn formatEntry(self: ShellIntegrationFeatures, formatter: formatterpkg.EntryFormatter) !void {
|
||||
var buf: [128]u8 = undefined;
|
||||
var writer: std.Io.Writer = .fixed(&buf);
|
||||
try self.format(&writer, .config);
|
||||
try formatter.formatEntry([]const u8, buf[0..writer.end]);
|
||||
}
|
||||
|
||||
pub fn clone(self: ShellIntegrationFeatures, _: Allocator) error{}!ShellIntegrationFeatures {
|
||||
return self;
|
||||
}
|
||||
|
||||
pub fn equal(self: ShellIntegrationFeatures, other: ShellIntegrationFeatures) bool {
|
||||
return std.meta.eql(self, other);
|
||||
}
|
||||
|
||||
test "parseCLI" {
|
||||
const testing = std.testing;
|
||||
|
||||
// Test that we can parse each bool field by name.
|
||||
inline for (@typeInfo(ShellIntegrationFeatures).@"struct".fields) |field| {
|
||||
if (comptime field.type == bool) {
|
||||
const result = try ShellIntegrationFeatures.parseCLI(field.name);
|
||||
try testing.expect(@field(result, field.name));
|
||||
}
|
||||
}
|
||||
|
||||
// Test that we can parse each bool field with "no-" prefix.
|
||||
inline for (@typeInfo(ShellIntegrationFeatures).@"struct".fields) |field| {
|
||||
if (comptime field.type == bool) {
|
||||
const result = try ShellIntegrationFeatures.parseCLI("no-" ++ field.name);
|
||||
try testing.expect(!@field(result, field.name));
|
||||
}
|
||||
}
|
||||
|
||||
// Test "true" enables all features
|
||||
{
|
||||
const result = try ShellIntegrationFeatures.parseCLI("true");
|
||||
inline for (@typeInfo(ShellIntegrationFeatures).@"struct".fields) |field| {
|
||||
switch (field.type) {
|
||||
bool => try testing.expect(@field(result, field.name)),
|
||||
Cursor => {
|
||||
try testing.expectEqual(Cursor.Shape.bar, @field(result, field.name).shape);
|
||||
try testing.expectEqual(Cursor.Style.default, @field(result, field.name).style);
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test "false" disables all features
|
||||
{
|
||||
const result = try ShellIntegrationFeatures.parseCLI("false");
|
||||
inline for (@typeInfo(ShellIntegrationFeatures).@"struct".fields) |field| {
|
||||
switch (field.type) {
|
||||
bool => try testing.expect(!@field(result, field.name)),
|
||||
Cursor => try testing.expectEqual(Cursor.Shape.disabled, @field(result, field.name).shape),
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test all comma-separated field names.
|
||||
const all_input = comptime blk: {
|
||||
const fields = @typeInfo(ShellIntegrationFeatures).@"struct".fields;
|
||||
var buf: []const u8 = fields[0].name;
|
||||
for (fields[1..]) |field| buf = buf ++ "," ++ field.name;
|
||||
break :blk buf;
|
||||
};
|
||||
const all_features = try ShellIntegrationFeatures.parseCLI(all_input);
|
||||
inline for (@typeInfo(ShellIntegrationFeatures).@"struct".fields) |field| {
|
||||
const value = @field(all_features, field.name);
|
||||
switch (field.type) {
|
||||
bool => try testing.expect(value),
|
||||
Cursor => {
|
||||
try testing.expectEqual(Cursor.Shape.bar, value.shape);
|
||||
try testing.expectEqual(Cursor.Style.default, value.style);
|
||||
},
|
||||
else => @compileError("unexpected field type in ShellIntegrationFeatures"),
|
||||
}
|
||||
}
|
||||
|
||||
// Cursor shapes and styles
|
||||
inline for (@typeInfo(Cursor.Shape).@"enum".fields) |shape_field| {
|
||||
const shape = @field(Cursor.Shape, shape_field.name);
|
||||
if (comptime shape != .disabled) {
|
||||
inline for (@typeInfo(Cursor.Style).@"enum".fields) |style_field| {
|
||||
const style = @field(Cursor.Style, style_field.name);
|
||||
const style_name = if (comptime style == .default) "" else ":" ++ style_field.name;
|
||||
const input = "cursor:" ++ shape_field.name ++ style_name;
|
||||
const result = try ShellIntegrationFeatures.parseCLI(input);
|
||||
try testing.expectEqual(shape, result.cursor.shape);
|
||||
try testing.expectEqual(style, result.cursor.style);
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
const result = try ShellIntegrationFeatures.parseCLI("cursor:blink");
|
||||
try testing.expectEqual(Cursor.Shape.bar, result.cursor.shape);
|
||||
try testing.expectEqual(Cursor.Style.blink, result.cursor.style);
|
||||
}
|
||||
{
|
||||
const result = try ShellIntegrationFeatures.parseCLI("cursor:steady");
|
||||
try testing.expectEqual(Cursor.Shape.bar, result.cursor.shape);
|
||||
try testing.expectEqual(Cursor.Style.steady, result.cursor.style);
|
||||
}
|
||||
}
|
||||
|
||||
test "format" {
|
||||
const testing = std.testing;
|
||||
|
||||
const testFormat = struct {
|
||||
fn f(features: ShellIntegrationFeatures, mode: FormatMode, expected: []const u8) !void {
|
||||
var buf: [128]u8 = undefined;
|
||||
var writer: std.Io.Writer = .fixed(&buf);
|
||||
try features.format(&writer, mode);
|
||||
try testing.expectEqualStrings(expected, buf[0..writer.end]);
|
||||
}
|
||||
}.f;
|
||||
|
||||
// .config format
|
||||
try testFormat(.{ .cursor = .{ .shape = .bar, .style = .steady }, .title = true }, .config, "cursor:bar:steady,path,title");
|
||||
try testFormat(.{ .cursor = .{ .shape = .bar, .style = .blink }, .sudo = true }, .config, "cursor:bar:blink,path,sudo,title");
|
||||
try testFormat(.{ .cursor = .{ .shape = .disabled }, .title = true }, .config, "path,title");
|
||||
try testFormat(.{ .cursor = .{ .shape = .block, .style = .blink }, .title = true }, .config, "cursor:block:blink,path,title");
|
||||
try testFormat(.{ .cursor = .{ .shape = .underline, .style = .default } }, .config, "cursor:underline,path,title");
|
||||
try testFormat(.{ .cursor = .{ .shape = .bar, .style = .default } }, .config, "cursor:bar,path,title");
|
||||
|
||||
// .env format
|
||||
try testFormat(.{ .cursor = .{ .shape = .bar, .style = .steady }, .title = true }, .env, "cursor:6,path,title");
|
||||
try testFormat(.{ .cursor = .{ .shape = .bar, .style = .blink }, .sudo = true }, .env, "cursor:5,path,sudo,title");
|
||||
try testFormat(.{ .cursor = .{ .shape = .disabled }, .title = true }, .env, "path,title");
|
||||
try testFormat(.{ .cursor = .{ .shape = .block, .style = .blink }, .title = true }, .env, "cursor:1,path,title");
|
||||
try testFormat(.{ .cursor = .{ .shape = .underline, .style = .steady } }, .env, "cursor:4,path,title");
|
||||
}
|
||||
};
|
||||
|
|
@ -158,9 +158,9 @@ function __ghostty_precmd() {
|
|||
fi
|
||||
|
||||
# Cursor
|
||||
if [[ "$GHOSTTY_SHELL_FEATURES" == *"cursor"* ]]; then
|
||||
builtin local cursor=5 # blinking bar
|
||||
[[ "$GHOSTTY_SHELL_FEATURES" == *"cursor:steady"* ]] && cursor=6 # steady bar
|
||||
if [[ "$GHOSTTY_SHELL_FEATURES" == *"cursor:"* ]]; then
|
||||
builtin local cursor="${GHOSTTY_SHELL_FEATURES#*cursor:}"
|
||||
cursor="${cursor%%[, ]*}"
|
||||
|
||||
[[ "$PS1" != *"\[\e[${cursor} q\]"* ]] && PS1=$PS1"\[\e[${cursor} q\]"
|
||||
[[ "$PS0" != *'\[\e[0 q\]'* ]] && PS0=$PS0'\[\e[0 q\]' # reset
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
use platform
|
||||
use re
|
||||
use str
|
||||
|
||||
# Clean up XDG_DATA_DIRS by removing GHOSTTY_SHELL_INTEGRATION_XDG_DIR
|
||||
|
|
@ -100,15 +101,13 @@
|
|||
set edit:after-readline = (conj $edit:after-readline $mark-output-start~)
|
||||
set edit:after-command = (conj $edit:after-command $mark-output-end~)
|
||||
|
||||
if (str:contains $E:GHOSTTY_SHELL_FEATURES "cursor") {
|
||||
var cursor = "5" # blinking bar
|
||||
if (has-value $features cursor:steady) {
|
||||
set cursor = "6" # steady bar
|
||||
}
|
||||
if (str:contains $E:GHOSTTY_SHELL_FEATURES "cursor:") {
|
||||
var match = (re:find 'cursor:(\d+)' $E:GHOSTTY_SHELL_FEATURES)
|
||||
var cursor = $match[groups][1][text]
|
||||
|
||||
fn beam { printf "\e["$cursor" q" }
|
||||
fn set-cursor { printf "\e["$cursor" q" }
|
||||
fn reset { printf "\e[0 q" }
|
||||
set edit:before-readline = (conj $edit:before-readline $beam~)
|
||||
set edit:before-readline = (conj $edit:before-readline $set-cursor~)
|
||||
set edit:after-readline = (conj $edit:after-readline {|_| reset })
|
||||
}
|
||||
if (and (has-value $features path) (has-env GHOSTTY_BIN_DIR)) {
|
||||
|
|
|
|||
|
|
@ -72,12 +72,10 @@ 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 string match -q 'cursor*' -- $features
|
||||
set -l cursor 5 # blinking bar
|
||||
contains cursor:steady $features && set cursor 6 # steady bar
|
||||
if string match -q 'cursor:*' -- $features
|
||||
set -l cursor (string replace -r '.*cursor:(\d+).*' '$1' -- $GHOSTTY_SHELL_FEATURES)
|
||||
|
||||
# Change the cursor to a beam on prompt.
|
||||
function __ghostty_set_cursor_beam --on-event fish_prompt -V cursor -d "Set cursor shape"
|
||||
function __ghostty_set_cursor --on-event fish_prompt -V cursor -d "Set cursor shape"
|
||||
if not functions -q fish_vi_cursor_handle
|
||||
echo -en "\e[$cursor q"
|
||||
end
|
||||
|
|
@ -168,8 +166,8 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration"
|
|||
set --global fish_handle_reflow 1
|
||||
|
||||
# Initial calls for first prompt
|
||||
if string match -q 'cursor*' -- $features
|
||||
__ghostty_set_cursor_beam
|
||||
if string match -q 'cursor:*' -- $features
|
||||
__ghostty_set_cursor
|
||||
end
|
||||
__ghostty_mark_prompt_start
|
||||
__update_cwd_osc
|
||||
|
|
|
|||
|
|
@ -248,17 +248,21 @@ _ghostty_deferred_init() {
|
|||
builtin print -rnu $_ghostty_fd \$'\\e]2;'\"\${1//[[:cntrl:]]}\"\$'\\a'"
|
||||
fi
|
||||
|
||||
if [[ "$GHOSTTY_SHELL_FEATURES" == *"cursor"* ]]; 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
|
||||
# themselves with a blinking block cursor within fzf.
|
||||
_ghostty_zle_line_init _ghostty_zle_line_finish _ghostty_zle_keymap_select() {
|
||||
builtin local steady=0
|
||||
[[ "$GHOSTTY_SHELL_FEATURES" == *"cursor:steady"* ]] && steady=1
|
||||
builtin local cursor="${GHOSTTY_SHELL_FEATURES#*cursor:}"
|
||||
cursor="${cursor%%[, ]*}"
|
||||
|
||||
# vi command mode uses block cursor (1=blink, 2=steady)
|
||||
builtin local block=$(( 1 + (cursor % 2 == 0) ))
|
||||
|
||||
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
|
||||
vicmd|visual) builtin print -nu "$_ghostty_fd" "\e[${block} q" ;;
|
||||
*) builtin print -nu "$_ghostty_fd" "\e[${cursor} q" ;;
|
||||
esac
|
||||
}
|
||||
# Restore the default shape before executing an external command
|
||||
|
|
|
|||
|
|
@ -564,8 +564,8 @@ pub const Config = struct {
|
|||
command: ?configpkg.Command = null,
|
||||
env: EnvMap,
|
||||
env_override: configpkg.RepeatableStringMap = .{},
|
||||
shell_integration: configpkg.Config.ShellIntegration = .detect,
|
||||
shell_integration_features: configpkg.Config.ShellIntegrationFeatures = .{},
|
||||
shell_integration: configpkg.ShellIntegration = .detect,
|
||||
shell_integration_features: configpkg.ShellIntegrationFeatures = .{},
|
||||
cursor_blink: ?bool = null,
|
||||
working_directory: ?[]const u8 = null,
|
||||
resources_dir: ?[]const u8,
|
||||
|
|
|
|||
|
|
@ -188,47 +188,16 @@ test detectShell {
|
|||
pub fn setupFeatures(
|
||||
env: *EnvMap,
|
||||
features: config.ShellIntegrationFeatures,
|
||||
cursor_blink: bool,
|
||||
cursor_blink_default: 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;
|
||||
};
|
||||
|
||||
var buf: [capacity]u8 = undefined;
|
||||
var buf: [128]u8 = undefined;
|
||||
var writer: std.Io.Writer = .fixed(&buf);
|
||||
|
||||
// Sort the fields so that the output is deterministic. This is
|
||||
// done at comptime so it has no runtime cost
|
||||
const fields_sorted: [fields.len][]const u8 = comptime fields: {
|
||||
var fields_sorted: [fields.len][]const u8 = undefined;
|
||||
for (fields, 0..) |field, i| fields_sorted[i] = field.name;
|
||||
std.mem.sortUnstable(
|
||||
[]const u8,
|
||||
&fields_sorted,
|
||||
{},
|
||||
(struct {
|
||||
fn lessThan(_: void, lhs: []const u8, rhs: []const u8) bool {
|
||||
return std.ascii.orderIgnoreCase(lhs, rhs) == .lt;
|
||||
}
|
||||
}).lessThan,
|
||||
);
|
||||
break :fields fields_sorted;
|
||||
};
|
||||
|
||||
inline for (fields_sorted) |name| {
|
||||
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");
|
||||
}
|
||||
}
|
||||
var resolved = features;
|
||||
if (resolved.cursor.style == .default) {
|
||||
resolved.cursor.style = if (cursor_blink_default) .blink else .steady;
|
||||
}
|
||||
try resolved.format(&writer, .env);
|
||||
|
||||
if (writer.end > 0) {
|
||||
try env.put("GHOSTTY_SHELL_FEATURES", buf[0..writer.end]);
|
||||
|
|
@ -242,16 +211,16 @@ test "setup features" {
|
|||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
|
||||
// Test: all features enabled
|
||||
// Default 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 }, true);
|
||||
try testing.expectEqualStrings("cursor:blink,path,ssh-env,ssh-terminfo,sudo,title", env.get("GHOSTTY_SHELL_FEATURES").?);
|
||||
try setupFeatures(&env, .{}, true);
|
||||
try testing.expectEqualStrings("cursor:5,path,title", env.get("GHOSTTY_SHELL_FEATURES").?);
|
||||
}
|
||||
|
||||
// Test: all features disabled
|
||||
// No features (all disabled)
|
||||
{
|
||||
var env = EnvMap.init(alloc);
|
||||
defer env.deinit();
|
||||
|
|
@ -260,29 +229,36 @@ test "setup features" {
|
|||
try testing.expect(env.get("GHOSTTY_SHELL_FEATURES") == null);
|
||||
}
|
||||
|
||||
// Test: mixed features
|
||||
// Cursor defaults (bar with style from config)
|
||||
{
|
||||
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 }, true);
|
||||
try testing.expectEqualStrings("ssh-env,sudo", env.get("GHOSTTY_SHELL_FEATURES").?);
|
||||
var features: config.ShellIntegrationFeatures = std.mem.zeroes(config.ShellIntegrationFeatures);
|
||||
features.cursor = .{};
|
||||
try setupFeatures(&env, features, true);
|
||||
try testing.expectEqualStrings("cursor:5", env.get("GHOSTTY_SHELL_FEATURES").?);
|
||||
try setupFeatures(&env, features, false);
|
||||
try testing.expectEqualStrings("cursor:6", env.get("GHOSTTY_SHELL_FEATURES").?);
|
||||
}
|
||||
|
||||
// Test: blinking cursor
|
||||
// Cursor with explicit shape
|
||||
{
|
||||
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").?);
|
||||
var features: config.ShellIntegrationFeatures = std.mem.zeroes(config.ShellIntegrationFeatures);
|
||||
features.cursor = .{ .shape = .block, .style = .default };
|
||||
try setupFeatures(&env, features, true);
|
||||
try testing.expectEqualStrings("cursor:1", env.get("GHOSTTY_SHELL_FEATURES").?);
|
||||
}
|
||||
|
||||
// Test: steady cursor
|
||||
// Cursor with explicit shape and style
|
||||
{
|
||||
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").?);
|
||||
var features: config.ShellIntegrationFeatures = std.mem.zeroes(config.ShellIntegrationFeatures);
|
||||
features.cursor = .{ .shape = .underline, .style = .steady };
|
||||
try setupFeatures(&env, features, true);
|
||||
try testing.expectEqualStrings("cursor:4", env.get("GHOSTTY_SHELL_FEATURES").?);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue