Skip to content

Latest commit

 

History

75 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

wf

CI Go License

"I know I ran this exact nmap command three days ago. I just can't remember the flags, the target, or which one of my 4,000 shell history lines it's hiding in."

wf is the fix: a terminal UI for saving, organizing, and running parameterized shell commands — think Warp's Workflows feature, minus the rest of Warp. Workflows are plain YAML files, sorted into folders, browsable with a keyboard or a mouse, and every run gets logged so "did that actually work?" is a keystroke away instead of a guess.

Built on Bubble Tea, Lip Gloss, Bubbles, and Huh — all the good Charm stuff.


The pitch, in one screen

┌─ Workflows ────────────────────────────────────────────┐
│ 📁 infra                                               │
│ 📁 backups                                             │
│ ⚡ deploy prod          ✓ 2h ago  ·  the scary one     │
│    nmap discovery scan  ✗ 6d ago  ·  finally, an audit │
│    update os                                           │
└────────────────────────────────────────────────────────┘
  ↑/↓ navigate   n new   N new folder   enter run/open
  h history   m move   d delete   q quit

Save a command once. Give its {{placeholders}} names. Never type it from memory again — and never wonder whether last Tuesday's run actually succeeded, because it's sitting right there in history, exit code and all.

Install / run

go install github.com/itheCreator1/wf/cmd/wf@latest

That drops a wf binary in $(go env GOPATH)/bin — add it to your PATH if it isn't already, and wf is a command like any other. Working from a clone instead:

go run ./cmd/wf

Workflows live in $HOME/.config/wf/workflows — one fixed location no matter which directory you launch wf from, so "wait, which set of workflows am I looking at" is never a question you have to ask. Override with -dir or WF_WORKFLOWS_DIR when you mean it (fixtures, testing, a second brain):

go run ./cmd/wf -dir ~/my-workflows
# or
WF_WORKFLOWS_DIR=~/my-workflows go run ./cmd/wf

Or build it once and forget go run exists:

go build -o wf ./cmd/wf
./wf

Platforms. Linux and macOS are the supported targets — both are built and tested in CI. wf compiles and runs on Windows too, with one caveat: there's no PTY layer there, so an interactive: true workflow still runs correctly against the real terminal and still reports its exit code, but its output transcript comes back empty rather than being recorded to history.

The tour

List view — home base

Folders first, workflows after, both alphabetical. Everything below works from the keyboard and the mouse — click a row to select it, click an already-selected one to open it, or just grab a workflow and drag it onto a folder to relocate it.

Key Does what it says
↑/↓ (j/k) navigate — or just click
/ fuzzy filter
enter / r run the workflow, or step into the folder
backspace step back out
n new workflow (lands wherever you're currently standing)
b new notebook
N new folder
e edit the selected workflow or notebook
m move it to another folder (or just drag it) — works for workflows and notebooks alike
h its run history — workflows only; a notebook has no single history of its own, so this shows a note pointing you at the workflow(s) it runs instead
d delete — folders must be empty, workflows and notebooks both ask nicely first
q / ctrl+c quit

Edit view — the paperwork

Key Does what it says
tab / shift+tab hop between fields
ctrl+s save
esc cancel (twice, if you'd actually lose something)
ctrl+a add an argument row — auto-fills its name if your command already has an undeclared {{placeholder}} sitting there, unclaimed
ctrl+x remove the focused argument row
ctrl+↑ / ctrl+↓ reorder it

Type a {{placeholder}} you haven't declared and it gets flagged immediately, right under the command field — no surprise at run time, no "why didn't this substitute."

Run view — the moment of truth

Got arguments? A tiny form asks for them first, defaults pre-filled, enter to go. No arguments? Straight to execution.

Output streams live, stderr picked out in red, ctrl+c aborts mid-flight, esc/q closes once it's done. Mark a workflow interactive (⚡ in the list) and wf hands your actual terminal over instead — sudo prompts, ssh, pagers, and editors all behave exactly like they would if you'd typed the command yourself, because for a moment, you basically did.

Either way, the run gets recorded — command, exit code, timing, and the output itself, PTY sessions included (captured through a real pseudo-terminal; if that ever fails to allocate, the workflow still runs, just without the transcript — a graceful shrug, not a dead end).

History view — the receipts

Press h on a workflow to see every run it's ever had, freshest first: status, timestamp, how long it took, the exact command that fired, and what it printed — collapsed to a few lines by default, o to unfold the whole thing. The list itself shows a quick ✓ 2m ago / ✗ 6d ago badge per workflow, so you don't even have to ask. The last 200 runs stick around; after that, the oldest quietly retire.

Press h on a notebook and you'll get a note instead of a history list — deliberately: a notebook has no single history identity, since it can reference zero or many different workflows via {workflow=slug} blocks (each recorded under that workflow's own slug), and ad-hoc {run} blocks are never recorded at all. Check the referenced workflow's own history instead.

Folders — because "workflows" plural gets messy fast

Real subdirectories on disk. No sidecar metadata file to fall out of sync, no migration if you've never made one — an install with zero subfolders just is the root folder, and always was. N makes one, enter walks into it, m (or a drag) moves a workflow or notebook in, d clears it out once it's empty.

Notebooks — when one command isn't the whole story

A workflow is one command. A notebook (📓 in the list, .md file instead of .yaml) is a runbook: prose explaining what you're doing, interleaved with the actual commands to do it, each one individually runnable right where it sits.

  📓 Deploy Runbook

  # Deploy Runbook

  First, check current status:

  ╭──────────────────────╮
  │ run                  │
  │ echo checking status │
  ╰──────────────────────╯

  Then deploy:

  ╭───────────────────────────╮
  │ workflow: Deploy Prod     │
  │ echo deploying to {{env}} │
  ╰───────────────────────────╯

  ↓/tab next block   enter run   p preview   esc/q back

A fenced code block becomes runnable by adding an attribute right after its language, inside { }:

```sh {run}
echo just run this literally
```

```sh {run interactive}
sudo systemctl restart nginx
```

```sh {workflow=deploy-prod}
```
  • {run} — runs the block's own body. {{placeholder}} tokens work here too, same argument-fill form as a workflow with arguments.
  • {run interactive} — same, but hands the real terminal over (sudo, ssh, pagers, editors), exactly like a workflow's interactive: true.
  • {workflow=<slug>} — runs an existing saved workflow by its slug instead. The fence body is left empty on purpose: the real command lives in that workflow's own file, so it can't drift out of sync with what actually runs. This is the only kind of block whose run gets recorded to history, same as running it from the list — an ad-hoc {run} block doesn't, on the theory that a notebook is a scratchpad of commands, not itself a saved, named thing worth a history entry.
  • Any other fenced block — no attribute at all — is just an example for reading, never runnable. The attribute is required; a bare command typed as plain paragraph text (no fence) is treated as prose too, with no visual cue that it's inert. If you paste a command in and it isn't doing anything, this is almost always why.

↑/↓ (or j/k/tab/shift+tab) jump between runnable blocks only — prose is for reading, not stopping on. enter runs whichever one's focused. p toggles a fully-rendered markdown preview of the whole document (via Glamour) if you just want to read it. e opens the raw markdown source in wf's own built-in editor — no $EDITOR handover, just a plain text box — to write or restructure the whole thing; alt+p there splits the editor side-by-side with a live-updating rendered-markdown pane, so you can keep typing on the left and watch the right pane update as you go (unlike browse mode's p, this doesn't replace the editor or swallow keystrokes — alt+p again closes the split back to a full-width editor), ctrl+s saves.

Since it's just markdown with an attribute in a code-fence's info string — not a comment, not a custom syntax — a notebook renders perfectly normally on GitHub, in VS Code, anywhere: the attribute is invisible noise to every other tool, and the fence still gets full syntax highlighting since its language tag is untouched.

The workflow file itself

One YAML file per workflow, named after a slug of its name, living wherever you filed it:

name: Docker Tail Logs
description: Tail logs for a running container
command: docker logs -f --tail {{lines}} {{container}}
arguments:
  - name: container
    description: Name or ID of the container
    default: ""
  - name: lines
    description: Number of lines to show before tailing
    default: "100"
tags:
  - docker
interactive: false
  • name and command are the only required fields.
  • arguments — each one declares a {{name}} your command can reference, with an optional description and default.
  • tags — free-form, currently just fodder for the fuzzy filter.
  • interactive — flip this on for anything that needs to talk to the terminal: sudo, ssh, pagers, editors, confirmation prompts.

Commands run via $SHELL -c '<command>' (or /bin/sh if $SHELL is a mystery), so pipelines and builtins work fine — but -c doesn't source your rc-file aliases and functions, interactive or not. That flag's job is stdin: a normal run gets /dev/null (fine, until it isn't), an interactive one gets the real terminal, prompts and all.

History lives separately, at ~/.config/wf/history.jsonl, right next to whichever workflows directory is currently in play.

The twist — yes, you can make wf read its own documentation

Since every shell command is fair game, nothing's stopping you from onboarding yourself with wf itself. Drop this in your workflows directory:

name: how do I use this thing
description: read the manual, but make it an interactive workflow
command: less README.md
interactive: true

Save it, run it, and wf will suspend itself, hand you a pager, and let you read exactly this sentence from inside the tool it's describing. It's turtles, but they're helpful turtles.

Project layout

cmd/wf/              entrypoint — flags, env, store/history init, bootstrap
internal/workflow/   the domain model: validation, placeholders, no I/O
internal/notebook/   the notebook domain model: markdown parsing, block/
                        attribute grammar, no I/O
internal/store/      YAML + Markdown persistence — load/save/delete, folders,
                        atomic writes, for both workflows and notebooks
internal/history/    the run log — JSON lines, retention, per-workflow lookup
internal/runner/     non-interactive execution — streaming output, cancellation
internal/ptyrun/     interactive execution via a real PTY, with output capture
                        and a graceful fallback if a PTY can't be had
internal/ui/         the Bubble Tea app — state machine, mouse handling,
                        history/folder views; list/, edit/, run/, notebook/
                        hold the rest
workflows/           seed workflows for development — not your real ones

Development

go build ./...
go vet ./...
go test ./...
gofmt -l .

CI runs exactly these four on every push and pull request, across Linux and macOS — see .github/workflows/ci.yml.

internal/workflow, internal/notebook, internal/store, internal/history, internal/runner, and internal/ptyrun are unit tested. The TUI itself, including internal/ui/run and internal/ui/notebook, is verified the old-fashioned way — actually running it — since Bubble Tea rendering logic and unit tests don't have much to say to each other.


Now go save that nmap command before you have to reverse-engineer it from your history again.

About

Terminal UI for saving, organizing, and running parameterized shell commands

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages