Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions src/build/GhosttyResources.zig
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const GhosttyResources = @This();

const std = @import("std");
const builtin = std.builtin;
const assert = std.debug.assert;
const Config = @import("Config.zig");
const RunStep = std.Build.Step.Run;
Expand Down Expand Up @@ -183,6 +184,36 @@ pub fn init(b: *std.Build, cfg: *const Config, deps: *const SharedDeps) !Ghostty
try steps.append(b.allocator, &install_step.step);
}

// PowerShell module
{
const run = b.addRunArtifact(build_data_exe);
run.addArg("+pwsh");
const wf = b.addWriteFiles();
_ = wf.addCopyFile(run.captureStdOut(), "ghostty.psm1");

// Write the module file.
const module_subdir = b.path("share", "pwsh", "modules");
const install_step = b.addInstallDirectory(.{
.source_dir = wf.getDirectory(),
.install_dir = .prefix,
.install_subdir = b.pathJoin(module_subdir, "ghostty"),
});
try steps.append(b.allocator, &install_step.step);

// Add the modules path to PSModulePath.
var module_path = cfg.env.get("PSModulePath");
module_path = if (module_path != null) {
const module_dir = b.pathJoin(b.install_path, module_subdir);
if (builtin.os.tag == .windows) {
module_path + ";" + module_dir;
} else {
module_path + ":" + module_dir;
}
};
const env_step = RunStep.setEnvironmentVariable(run, "PSModulePath", module_path);
try steps.append(b.allocator, env_step);
}

// Vim and Neovim plugin
{
const wf = b.addWriteFiles();
Expand Down
3 changes: 2 additions & 1 deletion src/config/Config.zig
Original file line number Diff line number Diff line change
Expand Up @@ -2790,7 +2790,7 @@ keybind: Keybinds = .{},
///
/// * `detect` - Detect the shell based on the filename.
///
/// * `bash`, `elvish`, `fish`, `nushell`, `zsh` - Use this specific shell injection scheme.
/// * `bash`, `elvish`, `fish`, `nushell`, `pwsh`, `zsh` - Use this specific shell injection scheme.
///
/// The default value is `detect`.
@"shell-integration": ShellIntegration = .detect,
Expand Down Expand Up @@ -8624,6 +8624,7 @@ pub const ShellIntegration = enum {
elvish,
fish,
nushell,
pwsh,
zsh,
};

Expand Down
247 changes: 247 additions & 0 deletions src/extra/pwsh.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
const std = @import("std");

const Config = @import("../config/Config.zig");
const Action = @import("../cli.zig").ghostty.Action;

/// PowerShell completions that contains all available commands and options.
pub const module = comptimeGeneratePwshCompletions();

fn comptimeGeneratePwshCompletions() []const u8 {
comptime {
@setEvalBranchQuota(50000);
var counter: std.Io.Writer.Discarding = .init(&.{});
try writePwshCompletions(&counter.writer);

var buf: [counter.count]u8 = undefined;
var writer: std.Io.Writer = .fixed(&buf);
try writePwshCompletions(&writer);
const final = buf;
return final[0..writer.end];
}
}

fn writePwshCompletions(writer: *std.Io.Writer) !void {
// -- Static header: outer wrapper, Register-ArgumentCompleter, helpers --
try writer.writeAll(
\\ param($ghostty)
\\ $commandName = (Split-Path -Leaf $ghostty) -replace '\.exe$'
\\
\\ Register-ArgumentCompleter -Native -CommandName $commandName,"$commandName.exe" -ScriptBlock {
\\ param($wordToComplete, $commandAst, $cursorPosition)
\\
\\ function Get-Fonts {
\\ & "$ghostty" +list-fonts | Where-Object { $_ -match '^[A-Z]' }
\\ }
\\
\\ function Get-Themes {
\\ & "$ghostty" +list-themes | ForEach-Object { $_ -replace ' \(.*$' }
\\ }
\\
\\ $elements = $commandAst.CommandElements
\\ $action = $null
\\ $prev = $null
\\
\\ for ($i = 1; $i -lt $elements.Count; $i++) {
\\ $t = $elements[$i].ToString()
\\
\\ if ($t.StartsWith('+') -and -not $action) { $action = $t }
\\ if ($i -lt $elements.Count - 1 -or $wordToComplete -eq '') {
\\ $prev = $t
\\ }
\\ }
\\
\\ filter Select-Completion {
\\ if ($_ -like "$wordToComplete*") {
\\ [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
\\ }
\\ }
\\
\\ filter Select-ValueCompletion($key) {
\\ $c = "--$key=$_"
\\ if ($c -like "$wordToComplete*") {
\\ [System.Management.Automation.CompletionResult]::new($c, $_, 'ParameterValue', $_)
\\ }
\\ }
\\
);

// -- Section: config --key=value completion (no action context) --
try writer.writeAll(" if (-not $action -and $wordToComplete -like '--*=*') {\n");
try writer.writeAll(" $key, $val = $wordToComplete -split '=', 2\n");
try writer.writeAll(" $key = $key -replace '^--'\n");
try writer.writeAll(" switch ($key) {\n");

for (@typeInfo(Config).@"struct".fields) |field| {
if (field.name[0] == '_') continue;

if (std.mem.startsWith(u8, field.name, "font-family")) {
try writer.writeAll(" '" ++ field.name ++ "' { Get-Fonts | Select-ValueCompletion $key; return }\n");
} else if (std.mem.eql(u8, "theme", field.name)) {
try writer.writeAll(" 'theme' { Get-Themes | Select-ValueCompletion $key; return }\n");
} else if (std.mem.eql(u8, "working-directory", field.name)) {
try writer.writeAll(" 'working-directory' { Get-ChildItem -Directory -Path \"$val*\" | ForEach-Object { $_.FullName } | Select-ValueCompletion $key; return }\n");
} else if (field.type == Config.RepeatablePath) {
try writer.writeAll(" '" ++ field.name ++ "' { Resolve-Path -Path \"$val*\" -ErrorAction SilentlyContinue | ForEach-Object { $_.Path } | Select-ValueCompletion $key; return }\n");
} else {
switch (@typeInfo(field.type)) {
.bool => {},
.@"enum" => |info| {
try writer.writeAll(" '" ++ field.name ++ "' { @(");
for (info.fields, 0..) |f, i| {
if (i > 0) try writer.writeAll(", ");
try writer.writeAll("'" ++ f.name ++ "'");
}
try writer.writeAll(") | Select-ValueCompletion $key; return }\n");
},
.@"struct" => |info| {
if (!@hasDecl(field.type, "parseCLI") and info.layout == .@"packed") {
try writer.writeAll(" '" ++ field.name ++ "' { @(");
for (info.fields, 0..) |f, i| {
if (i > 0) try writer.writeAll(", ");
try writer.writeAll("'" ++ f.name ++ "', 'no-" ++ f.name ++ "'");
}
try writer.writeAll(") | Select-ValueCompletion $key; return }\n");
}
},
else => {},
}
}
}

try writer.writeAll(" }\n");
try writer.writeAll(" return\n");
try writer.writeAll(" }\n");

// -- Section: action context --
try writer.writeAll(" if ($action) {\n");

// D1: --opt=value completion for action options
try writer.writeAll(" if ($wordToComplete -like '--*=*') {\n");
try writer.writeAll(" $key, $val = $wordToComplete -split '=', 2\n");
try writer.writeAll(" $key = $key -replace '^--'\n");
try writer.writeAll(" switch ($action) {\n");

for (@typeInfo(Action).@"enum".fields) |field| {
const options = @field(Action, field.name).options();
const opt_fields = @typeInfo(options).@"struct".fields;
if (opt_fields.len == 0) continue;

// Check if this action has any completable option values
var has_completable = false;
for (opt_fields) |opt| {
if (opt.name[0] == '_') continue;
switch (@typeInfo(opt.type)) {
.@"enum" => {
has_completable = true;
break;
},
.optional => |optional| {
switch (@typeInfo(optional.child)) {
.@"enum" => {
has_completable = true;
break;
},
else => {},
}
},
else => {
if (std.mem.eql(u8, "config-file", opt.name)) {
has_completable = true;
break;
}
},
}
}
if (!has_completable) continue;

try writer.writeAll(" '+" ++ field.name ++ "' {\n");
try writer.writeAll(" switch ($key) {\n");
for (opt_fields) |opt| {
if (opt.name[0] == '_') continue;
switch (@typeInfo(opt.type)) {
.@"enum" => |info| {
try writer.writeAll(" '" ++ opt.name ++ "' { @(");
for (info.fields, 0..) |f, i| {
if (i > 0) try writer.writeAll(", ");
try writer.writeAll("'" ++ f.name ++ "'");
}
try writer.writeAll(") | Select-ValueCompletion $key; return }\n");
},
.optional => |optional| {
switch (@typeInfo(optional.child)) {
.@"enum" => |info| {
try writer.writeAll(" '" ++ opt.name ++ "' { @(");
for (info.fields, 0..) |f, i| {
if (i > 0) try writer.writeAll(", ");
try writer.writeAll("'" ++ f.name ++ "'");
}
try writer.writeAll(") | Select-ValueCompletion $key; return }\n");
},
else => {},
}
},
else => {
if (std.mem.eql(u8, "config-file", opt.name)) {
try writer.writeAll(" 'config-file' { Resolve-Path -Path \"$val*\" -ErrorAction SilentlyContinue | ForEach-Object { $_.Path } | Select-ValueCompletion $key; return }\n");
}
},
}
}
try writer.writeAll(" }\n");
try writer.writeAll(" }\n");
}

try writer.writeAll(" }\n");
try writer.writeAll(" return\n");
try writer.writeAll(" }\n");

// D2: list action-specific option names
try writer.writeAll(" switch ($action) {\n");
for (@typeInfo(Action).@"enum".fields) |field| {
const options = @field(Action, field.name).options();
const opt_fields = @typeInfo(options).@"struct".fields;
if (opt_fields.len == 0) continue;

try writer.writeAll(" '+" ++ field.name ++ "' { @(");
var count: usize = 0;
for (opt_fields) |opt| {
if (opt.name[0] == '_') continue;
if (count > 0) try writer.writeAll(", ");
switch (opt.type) {
bool, ?bool => try writer.writeAll("'--" ++ opt.name ++ "'"),
else => try writer.writeAll("'--" ++ opt.name ++ "='"),
}
count += 1;
}
try writer.writeAll(", '--help') | Select-Completion }\n");
}
try writer.writeAll(" default { @('--help') | Select-Completion }\n");
try writer.writeAll(" }\n");
try writer.writeAll(" return\n");
try writer.writeAll(" }\n");

// -- Section: top-level completions --
try writer.writeAll(" @('-e', '--help', '--version'");

// Actions
for (@typeInfo(Action).@"enum".fields) |field| {
try writer.writeAll(", '+" ++ field.name ++ "'");
}

// Config keys
for (@typeInfo(Config).@"struct".fields) |field| {
if (field.name[0] == '_') continue;
switch (field.type) {
bool, ?bool => try writer.writeAll(", '--" ++ field.name ++ "'"),
else => try writer.writeAll(", '--" ++ field.name ++ "='"),
}
}

try writer.writeAll(") | Select-Completion\n");

// -- Static footer --
try writer.writeAll(
\\ }.GetNewClosure()
\\
);
}
2 changes: 2 additions & 0 deletions src/main_build_data.zig
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ pub const Action = enum {
// Shell completions
bash,
fish,
pwsh,
zsh,

// Editor syntax files
Expand All @@ -39,6 +40,7 @@ pub fn main() !void {
switch (action) {
.bash => try writer.writeAll(@import("extra/bash.zig").completions),
.fish => try writer.writeAll(@import("extra/fish.zig").completions),
.pwsh => try writer.writeAll(@import("extra/pwsh.zig").module),
.zsh => try writer.writeAll(@import("extra/zsh.zig").completions),
.sublime => try writer.writeAll(@import("extra/sublime.zig").syntax),
.@"vim-syntax" => try writer.writeAll(@import("extra/vim.zig").syntax),
Expand Down
1 change: 1 addition & 0 deletions src/main_ghostty.zig
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ test {
// Extra
_ = @import("extra/bash.zig");
_ = @import("extra/fish.zig");
_ = @import("extra/pwsh.zig");
_ = @import("extra/sublime.zig");
_ = @import("extra/vim.zig");
_ = @import("extra/zsh.zig");
Expand Down
34 changes: 34 additions & 0 deletions src/shell-integration/pwsh/ghostty.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# This script is loaded automatically when shell-integrations are enabled.
# It will load instead of the `$PROFILE` and, unless `-NoProfile` was passed to the process,
# load the `$PROFILE` itself.
#
# To load shell-integrations in other scripts, include the following in your scripts:
#
# if ($env:GHOSTTY_RESOURCES_DIR) {
# source "${env:GHOSTTY_RESOURCES_DIR}/shell-integration/pwsh/ghostty.ps1"
# }

function Test-Feature([string] $Feature) {
($env:GHOSTTY_SHELL_FEATURES -split ',') -contains $Feature
}

function Test-Interactive {
try {
if (-not $Host.UI.RawUI) {
return $false
}

if (-not [Environment]::UserInteractive) {
return $false
}

if ([Console]::IsOutputRedirected) {
return $false
}

$true
}
catch {
$false
}
}
1 change: 1 addition & 0 deletions src/termio/Exec.zig
Original file line number Diff line number Diff line change
Expand Up @@ -777,6 +777,7 @@ const Subprocess = struct {
.elvish => .elvish,
.fish => .fish,
.nushell => .nushell,
.pwsh => .pwsh,
.zsh => .zsh,
};

Expand Down
Loading