Skip to content
Open
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
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,34 @@ pnpm install -g @johnlindquist/worktree

## Usage

### Shell Autocompletion (bash/zsh)

Enable completion for the current shell session:

```bash
eval "$(wt completion bash)"
```

```zsh
eval "$(wt completion zsh)"
```

To enable it permanently, add the matching line to your shell profile:

```bash
echo 'eval "$(wt completion bash)"' >> ~/.bashrc
```

```zsh
echo 'eval "$(wt completion zsh)"' >> ~/.zshrc
```

Completion coverage includes:
- subcommands
- worktree branches for `merge`, `remove`/`rm`, and `open`
- git branches for `new`, `setup`, and `extract`
- `config` subcommands (`set`, `get`, `clear`, `path`)

### Create a new worktree from Branch Name

```bash
Expand Down
98 changes: 98 additions & 0 deletions build/commands/completion.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { bashCompletionScript } from "../completions/bash.js";
import { zshCompletionScript } from "../completions/zsh.js";
import { getWorktrees, getBranches } from "../utils/git.js";
const SUBCOMMANDS = [
"new",
"setup",
"list",
"ls",
"remove",
"rm",
"merge",
"purge",
"pr",
"open",
"extract",
"config",
"completion",
];
const CONFIG_SUBCOMMANDS = ["set", "get", "clear", "path"];
const WORKTREE_BRANCH_COMMANDS = new Set(["merge", "remove", "rm", "open"]);
const GIT_BRANCH_COMMANDS = new Set(["new", "setup", "extract"]);
/**
* Handle the `wt completion [shell]` command.
* Outputs the shell completion script to stdout.
*/
export async function completionHandler(shell) {
switch (shell) {
case "bash":
console.log(bashCompletionScript());
break;
case "zsh":
console.log(zshCompletionScript());
break;
default:
console.error(`Unsupported shell: ${shell}. Supported shells: bash, zsh`);
process.exit(1);
}
}
/**
* Handle dynamic completion requests from the shell completion script.
*
* Called internally as `wt __complete -- <words...>` where words are the
* current command-line tokens (excluding the program name itself).
*
* Outputs one candidate per line to stdout.
*/
export async function getCompletions(words) {
try {
// Current word being typed (last element, may be empty string)
const current = words.length > 0 ? words[words.length - 1] : "";
// Completed words before the current one
const completed = words.slice(0, -1);
// No completed words → completing the subcommand itself
if (completed.length === 0) {
const matches = SUBCOMMANDS.filter((cmd) => cmd.startsWith(current));
if (matches.length > 0) {
console.log(matches.join("\n"));
}
return;
}
const command = completed[0];
// Worktree branch commands: merge, remove/rm, open
if (WORKTREE_BRANCH_COMMANDS.has(command)) {
const worktrees = await getWorktrees();
const branches = worktrees
.filter((wt) => !wt.isMain && wt.branch)
.map((wt) => wt.branch);
const matches = branches.filter((b) => b.startsWith(current));
if (matches.length > 0) {
console.log(matches.join("\n"));
}
return;
}
// Git branch commands: new, setup, extract
if (GIT_BRANCH_COMMANDS.has(command)) {
const branches = await getBranches();
const matches = branches.filter((b) => b.startsWith(current));
if (matches.length > 0) {
console.log(matches.join("\n"));
}
return;
}
// Config subcommands
if (command === "config") {
if (completed.length === 1) {
const matches = CONFIG_SUBCOMMANDS.filter((cmd) => cmd.startsWith(current));
if (matches.length > 0) {
console.log(matches.join("\n"));
}
}
return;
}
// No completions for other commands (pr, purge, list, etc.)
}
catch {
// Silently return empty results on error
}
}
21 changes: 21 additions & 0 deletions build/completions/bash.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* Generate a Bash completion script for the `wt` CLI.
*
* The script registers a completion function that calls `wt __complete`
* to obtain dynamic completion candidates whenever the user presses Tab.
*/
export function bashCompletionScript() {
return `
_wt_completions() {
local cur="\${COMP_WORDS[COMP_CWORD]}"
local words=("\${COMP_WORDS[@]:1}")

local completions
completions=$(wt __complete -- "\${words[@]}" 2>/dev/null)

COMPREPLY=($(compgen -W "\${completions}" -- "\${cur}"))
}

complete -F _wt_completions wt
`.trimStart();
}
24 changes: 24 additions & 0 deletions build/completions/zsh.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* Generate a Zsh completion script for the `wt` CLI.
*
* The script defines a completion function that calls `wt __complete`
* to obtain dynamic completion candidates whenever the user presses Tab.
*/
export function zshCompletionScript() {
return `
#compdef wt

_wt() {
local -a completions
local words_arr=("\${words[@]:1}")

completions=(\${(f)"$(wt __complete -- "\${words_arr[@]}" 2>/dev/null)"})

if (( \${#completions} > 0 )); then
compadd -a completions
fi
}

compdef _wt wt
`.trimStart();
}
15 changes: 15 additions & 0 deletions build/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { configHandler } from "./commands/config.js";
import { prWorktreeHandler } from "./commands/pr.js";
import { openWorktreeHandler } from "./commands/open.js";
import { extractWorktreeHandler } from "./commands/extract.js";
import { completionHandler, getCompletions } from "./commands/completion.js";
const program = new Command();
program
.name("wt")
Expand Down Expand Up @@ -119,4 +120,18 @@ program
.addCommand(new Command("path")
.description("Show the path to the configuration file.")
.action(() => configHandler("path")));
program
.command("completion")
.argument("[shell]", "Shell type (bash, zsh)", "bash")
.description('Output shell completion script. Run: eval "$(wt completion bash)"')
.action(completionHandler);
program
.command("__complete", { hidden: true })
.allowUnknownOption()
.allowExcessArguments(true)
.action(async () => {
const dashIndex = process.argv.indexOf("--");
const words = dashIndex >= 0 ? process.argv.slice(dashIndex + 1) : [];
await getCompletions(words);
});
program.parse(process.argv);
18 changes: 18 additions & 0 deletions build/utils/git.js
Original file line number Diff line number Diff line change
Expand Up @@ -456,3 +456,21 @@ export async function popStash(cwd = ".") {
return false;
}
}
/**
* Get the list of local branch names
*
* @param cwd - Working directory to run git command from
* @returns Array of local branch names (short form)
*/
export async function getBranches(cwd = ".") {
try {
const { stdout } = await execa("git", ["-C", cwd, "branch", "--format=%(refname:short)"]);
if (!stdout.trim()) {
return [];
}
return stdout.split("\n").map(b => b.trim()).filter(b => b);
}
catch {
return [];
}
}
114 changes: 114 additions & 0 deletions src/commands/completion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { bashCompletionScript } from "../completions/bash.js";
import { zshCompletionScript } from "../completions/zsh.js";
import { getWorktrees, getBranches } from "../utils/git.js";

const SUBCOMMANDS = [
"new",
"setup",
"list",
"ls",
"remove",
"rm",
"merge",
"purge",
"pr",
"open",
"extract",
"config",
"completion",
];

const CONFIG_SUBCOMMANDS = ["set", "get", "clear", "path"];

const WORKTREE_BRANCH_COMMANDS = new Set(["merge", "remove", "rm", "open"]);
const GIT_BRANCH_COMMANDS = new Set(["new", "setup", "extract"]);

/**
* Handle the `wt completion [shell]` command.
* Outputs the shell completion script to stdout.
*/
export async function completionHandler(shell: string): Promise<void> {
switch (shell) {
case "bash":
console.log(bashCompletionScript());
break;
case "zsh":
console.log(zshCompletionScript());
break;
default:
console.error(
`Unsupported shell: ${shell}. Supported shells: bash, zsh`
);
process.exit(1);
}
}

/**
* Handle dynamic completion requests from the shell completion script.
*
* Called internally as `wt __complete -- <words...>` where words are the
* current command-line tokens (excluding the program name itself).
*
* Outputs one candidate per line to stdout.
*/
export async function getCompletions(words: string[]): Promise<void> {
try {
// Current word being typed (last element, may be empty string)
const current = words.length > 0 ? words[words.length - 1] : "";
// Completed words before the current one
const completed = words.slice(0, -1);

// No completed words → completing the subcommand itself
if (completed.length === 0) {
const matches = SUBCOMMANDS.filter((cmd) =>
cmd.startsWith(current)
);
if (matches.length > 0) {
console.log(matches.join("\n"));
}
return;
}

const command = completed[0];

// Worktree branch commands: merge, remove/rm, open
if (WORKTREE_BRANCH_COMMANDS.has(command)) {
const worktrees = await getWorktrees();
const branches = worktrees
.filter((wt) => !wt.isMain && wt.branch)
.map((wt) => wt.branch as string);
const matches = branches.filter((b) => b.startsWith(current));
if (matches.length > 0) {
console.log(matches.join("\n"));
}
return;
}

// Git branch commands: new, setup, extract
if (GIT_BRANCH_COMMANDS.has(command)) {
const branches = await getBranches();
const matches = branches.filter((b) => b.startsWith(current));
if (matches.length > 0) {
console.log(matches.join("\n"));
}
return;
}

// Config subcommands
if (command === "config") {
if (completed.length === 1) {
const matches = CONFIG_SUBCOMMANDS.filter((cmd) =>
cmd.startsWith(current)
);
if (matches.length > 0) {
console.log(matches.join("\n"));
}
}
return;
}

// No completions for other commands (pr, purge, list, etc.)
} catch {
// Silently return empty results on error
}
}
21 changes: 21 additions & 0 deletions src/completions/bash.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* Generate a Bash completion script for the `wt` CLI.
*
* The script registers a completion function that calls `wt __complete`
* to obtain dynamic completion candidates whenever the user presses Tab.
*/
export function bashCompletionScript(): string {
return `
_wt_completions() {
local cur="\${COMP_WORDS[COMP_CWORD]}"
local words=("\${COMP_WORDS[@]:1}")

local completions
completions=$(wt __complete -- "\${words[@]}" 2>/dev/null)

COMPREPLY=($(compgen -W "\${completions}" -- "\${cur}"))
}

complete -F _wt_completions wt
`.trimStart();
}
24 changes: 24 additions & 0 deletions src/completions/zsh.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* Generate a Zsh completion script for the `wt` CLI.
*
* The script defines a completion function that calls `wt __complete`
* to obtain dynamic completion candidates whenever the user presses Tab.
*/
export function zshCompletionScript(): string {
return `
#compdef wt

_wt() {
local -a completions
local words_arr=("\${words[@]:1}")

completions=(\${(f)"$(wt __complete -- "\${words_arr[@]}" 2>/dev/null)"})

if (( \${#completions} > 0 )); then
compadd -a completions
fi
}

compdef _wt wt
`.trimStart();
}
Loading