repos / zmx

session persistence for terminal processes
git clone https://github.com/neurosnap/zmx.git

commit
3e5f982
parent
68dfbef
author
Josh Bainbridge
date
2026-01-01 18:39:56 -0500 EST
feat: add completions command to output shell completion scripts

Adding auto-completion support for a limited number of commonly used
shells (bash, zsh, fish) would allow for greater shell integration. This
can be done by using completions command that is then evaluated in the
shell config.

Add a new completions module that holds completion scripts for zsh, bash
and fish. Then include a new command called completions that takes a
shell name and outputs the requested script.

This can then be added to a shell config such as .zshrc like so:

eval “$(zmx completions zsh)"

For automatic shell integration.
4 files changed,  +153, -0
M CHANGELOG.md
+1, -0
1@@ -9,6 +9,7 @@ Use spec: https://common-changelog.org/
2 - New flag `--vt` for `zmx [hi]story` which prints raw ansi escape codes for terminal session
3 - New flag `--html` for `zmx [hi]story` which prints html representation of terminal session
4 - New list flag `zmx [l]ist [--list]` that lists all session names with no extra information
5+- New command `zmx [c]ompletions <shell>` that outputs auto-completion scripts for a given shell
6 
7 ### Fixed
8 
M README.md
+1, -0
1@@ -58,6 +58,7 @@ Commands:
2   [r]un <name> [command...]      Send command without attaching, creating session if needed
3   [d]etach                       Detach all clients from current session  (ctrl+\ for current client)
4   [l]ist [--short]               List active sessions
5+  [c]ompletions <shell>          Completion scripts for shell integration
6   [k]ill <name>                  Kill a session and all attached clients
7   [hi]story <name> [--vt|--html] Output session scrollback (--vt or --html for escape sequences)
8   [v]ersion                      Show version information
A src/completions.zig
+137, -0
  1@@ -0,0 +1,137 @@
  2+const std = @import("std");
  3+
  4+pub const Shell = enum {
  5+    bash,
  6+    zsh,
  7+    fish,
  8+
  9+    pub fn fromString(s: []const u8) ?Shell {
 10+        if (std.mem.eql(u8, s, "bash")) return .bash;
 11+        if (std.mem.eql(u8, s, "zsh")) return .zsh;
 12+        if (std.mem.eql(u8, s, "fish")) return .fish;
 13+
 14+        return null;
 15+    }
 16+
 17+    pub fn getCompletionScript(self: Shell) []const u8 {
 18+        return switch (self) {
 19+            .bash => bash_completions,
 20+            .zsh => zsh_completions,
 21+            .fish => fish_completions,
 22+        };
 23+    }
 24+};
 25+
 26+const bash_completions =
 27+    \\_zmx_completions() {
 28+    \\  local cur prev words cword
 29+    \\  COMPREPLY=()
 30+    \\  cur="${COMP_WORDS[COMP_CWORD]}"
 31+    \\  prev="${COMP_WORDS[COMP_CWORD-1]}"
 32+    \\
 33+    \\  local commands="attach run detach list completions kill history version help"
 34+    \\
 35+    \\  if [[ $COMP_CWORD -eq 1 ]]; then
 36+    \\    COMPREPLY=($(compgen -W "$commands" -- "$cur"))
 37+    \\    return 0
 38+    \\  fi
 39+    \\
 40+    \\  case "$prev" in
 41+    \\    attach|run|kill|history)
 42+    \\      local sessions=$(zmx list --short 2>/dev/null | tr '\n' ' ')
 43+    \\      COMPREPLY=($(compgen -W "$sessions" -- "$cur"))
 44+    \\      ;;
 45+    \\    completions)
 46+    \\      COMPREPLY=($(compgen -W "bash zsh fish" -- "$cur"))
 47+    \\      ;;
 48+    \\    list)
 49+    \\      COMPREPLY=($(compgen -W "--short" -- "$cur"))
 50+    \\      ;;
 51+    \\  esac
 52+    \\}
 53+    \\
 54+    \\complete -o bashdefault -o default -F _zmx_completions zmx
 55+;
 56+
 57+const zsh_completions =
 58+    \\_zmx() {
 59+    \\  local context state state_descr line
 60+    \\  typeset -A opt_args
 61+    \\
 62+    \\  _arguments -C \
 63+    \\    '1: :->commands' \
 64+    \\    '2: :->args' \
 65+    \\    '*: :->trailing' \
 66+    \\    && return 0
 67+    \\
 68+    \\  case $state in
 69+    \\    commands)
 70+    \\      local -a commands
 71+    \\      commands=(
 72+    \\        'attach:Attach to session, creating if needed'
 73+    \\        'run:Send command without attaching'
 74+    \\        'detach:Detach all clients from current session'
 75+    \\        'list:List active sessions'
 76+    \\        'completions:Shell completion scripts'
 77+    \\        'kill:Kill a session'
 78+    \\        'history:Output session scrollback'
 79+    \\        'version:Show version'
 80+    \\        'help:Show help message'
 81+    \\      )
 82+    \\      _describe 'command' commands
 83+    \\      ;;
 84+    \\    args)
 85+    \\      case $words[2] in
 86+    \\        attach|a|kill|k|run|r|history|hi)
 87+    \\          _zmx_sessions
 88+    \\          ;;
 89+    \\        completions|c)
 90+    \\          _values 'shell' 'bash' 'zsh' 'fish'
 91+    \\          ;;
 92+    \\        list|l)
 93+    \\          _values 'options' '--short'
 94+    \\          ;;
 95+    \\      esac
 96+    \\      ;;
 97+    \\    trailing)
 98+    \\      # Additional args for commands like 'attach' or 'run'
 99+    \\      ;;
100+    \\  esac
101+    \\}
102+    \\
103+    \\_zmx_sessions() {
104+    \\  local -a sessions
105+    \\
106+    \\  local local_sessions=$(zmx list --short 2>/dev/null)
107+    \\  if [[ -n "$local_sessions" ]]; then
108+    \\    sessions+=(${(f)local_sessions})
109+    \\  fi
110+    \\
111+    \\  _describe 'local session' sessions
112+    \\}
113+    \\
114+    \\compdef _zmx zmx
115+;
116+
117+const fish_completions =
118+    \\complete -c zmx -f
119+    \\
120+    \\set -l subcommands attach run detach list completions kill history version help
121+    \\set -l no_subcmd "not __fish_seen_subcommand_from $subcommands"
122+    \\
123+    \\complete -c zmx -n $no_subcmd -a attach -d 'Attach to session, creating if needed'
124+    \\complete -c zmx -n $no_subcmd -a run -d 'Send command without attaching'
125+    \\complete -c zmx -n $no_subcmd -a detach -d 'Detach all clients from current session'
126+    \\complete -c zmx -n $no_subcmd -a list -d 'List active sessions'
127+    \\complete -c zmx -n $no_subcmd -a completions -d 'Shell completion scripts'
128+    \\complete -c zmx -n $no_subcmd -a kill -d 'Kill a session'
129+    \\complete -c zmx -n $no_subcmd -a history -d 'Output session scrollback'
130+    \\complete -c zmx -n $no_subcmd -a version -d 'Show version'
131+    \\complete -c zmx -n $no_subcmd -a help -d 'Show help message'
132+    \\
133+    \\complete -c zmx -n "__fish_seen_subcommand_from attach run kill history" -a '(zmx list --short 2>/dev/null)' -d 'Session name'
134+    \\
135+    \\complete -c zmx -n "__fish_seen_subcommand_from completions" -a 'bash zsh fish' -d 'Shell'
136+    \\
137+    \\complete -c zmx -n "__fish_seen_subcommand_from list" -l short -d 'Short output'
138+;
M src/main.zig
+14, -0
 1@@ -5,6 +5,7 @@ const build_options = @import("build_options");
 2 const ghostty_vt = @import("ghostty-vt");
 3 const ipc = @import("ipc.zig");
 4 const log = @import("log.zig");
 5+const completions = @import("completions.zig");
 6 
 7 pub const version = build_options.version;
 8 pub const git_sha = build_options.git_sha;
 9@@ -349,6 +350,10 @@ pub fn main() !void {
10     } else if (std.mem.eql(u8, cmd, "list") or std.mem.eql(u8, cmd, "l")) {
11         const short = if (args.next()) |arg| std.mem.eql(u8, arg, "--short") else false;
12         return list(&cfg, short);
13+    } else if (std.mem.eql(u8, cmd, "completions") or std.mem.eql(u8, cmd, "c")) {
14+        const arg = args.next() orelse return;
15+        const shell = completions.Shell.fromString(arg) orelse return;
16+        return printCompletions(shell);
17     } else if (std.mem.eql(u8, cmd, "detach") or std.mem.eql(u8, cmd, "d")) {
18         return detachAll(&cfg);
19     } else if (std.mem.eql(u8, cmd, "kill") or std.mem.eql(u8, cmd, "k")) {
20@@ -452,6 +457,14 @@ fn printVersion() !void {
21     try w.interface.flush();
22 }
23 
24+fn printCompletions(shell: completions.Shell) !void {
25+    const script = shell.getCompletionScript();
26+    var buf: [8192]u8 = undefined;
27+    var w = std.fs.File.stdout().writer(&buf);
28+    try w.interface.print("{s}\n", .{script});
29+    try w.interface.flush();
30+}
31+
32 fn help() !void {
33     const help_text =
34         \\zmx - session persistence for terminal processes
35@@ -463,6 +476,7 @@ fn help() !void {
36         \\  [r]un <name> [command...]     Send command without attaching, creating session if needed
37         \\  [d]etach                      Detach all clients from current session (ctrl+\ for current client)
38         \\  [l]ist [--short]              List active sessions
39+        \\  [c]ompletions <shell>         Completion scripts for shell integration
40         \\  [k]ill <name>                 Kill a session and all attached clients
41         \\  [hi]story <name> [--vt|--html] Output session scrollback (--vt or --html for escape sequences)
42         \\  [v]ersion                     Show version information