The Problem
Do any of these sound familiar?
- How do I get Fig on Windows?
- When will Fig's command suggestions ever land on Linux?
- Can Fig's completion also use my command history?
- If I work over SSH, how do I get Fig-style completion without installing anything on the remote machine?
Preface
If you spend a lot of time on the command line, you already know the pain: too many commands, too many flags, and no completion worth the name. What you want is something like the candidate list from an input method editor — except for shell commands. That thing was Fig — https://app.fig.io
But Fig only ever shipped a Mac version. Nobody else got to experience that kind of command-line completion. Fig did open-source its completion data, though: https://github.com/withfig/autocomplete — a pile of JavaScript that describes how commands should be suggested.
1. Fig: The Best Terminal Completion There Ever Was
Fig has since been acquired by Amazon and renamed Amazon Q.
Once you had it installed and enabled, you'd type git in iTerm2 and a dropdown would appear at the right of your cursor — add, commit, push, rebase… each with an icon and a one-line description. Keep typing git checkout and it wouldn't list filenames; it would list the branches that actually exist in your repo. Type npm run and it would list the real scripts from your package.json. Type docker and it would list the containers and images actually on your machine.
That was Fig's entire ambition: give the terminal you already use an IDE-style completion experience.
What Fig got right
1. It didn't build another terminal.
This was Fig's smartest move. Every other approach at the time either forced you onto a brand-new terminal emulator (Warp, Tabby) or left you stuck with the weak completion of the day. Fig chose to piggyback instead — it supported macOS Terminal, iTerm2, Tabby, Hyper, Kitty, WezTerm, Alacritty, and even the embedded terminals in VS Code, the JetBrains IDEs, and Android Studio. You changed nothing. You installed an app.
2. Completion quality came from human-written declarative specs, not guesswork.
This was Fig's real moat. It didn't try to guess what should follow git checkout. It defined a completion spec — a declarative schema describing a CLI's subcommands, options, and args. The withfig/autocomplete repo held specs like that for hundreds of popular CLIs, maintained by 400+ contributors. That data was Fig's single most valuable asset, and the part it open-sourced under the MIT license.
2. Fig's Limitations
Fig proved that "declarative specs + a GUI dropdown" was the right direction. But the way it built that direction locked it in.
1. It only supported macOS
This wasn't "macOS only for now" — from 2021 until the day it shut down, Windows and Linux stayed officially "in progress."
The reason:
- It depended on the Accessibility API, which is macOS-specific. Windows and Linux have no equivalent, and "where is the cursor on screen" is simply an unsolvable problem there.
The community pushed for Linux support from day one. The team opened discussion #14 (Linux) and discussion #15 (Windows). Neither shipped before the Fig name disappeared.
2. Recommendations were mostly static
Most of what Fig suggested was fixed. There was a dynamic portion too — branch names for git, directory candidates for cd — but that was about the extent of it.
If you've used fish, you know its history-based command suggestions are a fantastic feature. Or zsh with zsh-autosuggestions, which gives you something similar.
3. Specs were TypeScript code
This is the limitation that made Fig's assets genuinely unreusable by any other language.
Fig's specs were .ts files. They could contain callbacks like custom, postProcess, and generateSpec — real executable code:
// The shape of a Fig spec (illustrative)
const spec: Fig.Spec = {
name: "git",
subcommands: [{
name: "checkout",
args: {
name: "branch",
generators: {
script: ["git", "branch", "--no-color"],
postProcess: (out) => out.split("\n").map(b => ({ name: b.trim() })),
},
},
}],
};
Data and logic were mixed together, and that data had to run inside Node before it could be evaluated.
So when a Go, Rust, or Dart project said "I want Fig's completion too," what it faced wasn't "read a JSON file" — it was "re-translate the function logic across hundreds of TS files."
So I forked it and started this project.
3. autocomplete (Dart): Rebuilding Fig's Most Valuable Part as a Portable Engine
Repo: github.com/littlewrite/autocomplete (a fork of withfig/autocomplete; the new code lives in dart/)
3.1 The assets I inherited
Measured against Fig's repo:
| Metric | Count |
|---|---|
| Logical commands | 1463 |
| Alias entries | 736 |
| Physical JSON documents | 1464 |
| Coverage |
git, docker, kubectl, npm, cargo, brew, terraform, aws, az, gcloud, flutter, codex, claude … |
Most of that repo is object definitions — spec definitions. A small fraction is dynamic code, i.e. spec generators: git branches and cd directory candidates, for instance, are produced by executing code.
3.2 The rewrite: a JSON-first architecture
Since most of autocomplete is TypeScript object definitions, they can be converted into JSON files and deserialized back into objects at use time. That means Java can read it — and so can Go, Rust, and Python. All of them can read the same JSON and deserialize it into spec objects that just work. The dynamic functions are only a small part, and they're straightforward to convert.
This is the fork's biggest design improvement:
-
Static data → pure JSON: command names, descriptions,
options,subcommands,args,template,loadSpeclinks, and the command andsplitOnofscriptgenerators — all of it lives in JSON. - Dynamic behavior → stable handler ID references: wherever code needs to run, the JSON keeps only a reference:
{
"name": "branch",
"generators": [{
"script": ["git", "branch", "--no-color"],
"postProcess": { "handler": "git.branches", "version": 1 }
}]
}
- There is no executable code in the JSON.
4. Improvements Over Fig
Feature 1: JSON abstraction — one dataset, N languages
Once specs become JSON, a completion spec stops being "some TypeScript code" and becomes a language-agnostic data contract. Three things define it:
-
A JSON Schema (
schema/completion-spec.schema.json) — what the data looks like; -
A physical layout convention — flat commands at
<first-letter>/<command>.json, namespaced commands at<first-letter>/<namespace>/.../command.json, withindex.jsonmapping logical names to files only; -
A handler contract —
{"handler": id, "version": n}, where the field's position determines the callback signature (custom/postProcess/trigger/generateSpec/alias/scripteach have an explicit Dart signature).
That makes extensibility remarkably clean:
- Switching languages: any language with a JSON parser — Go, Rust, Java, Kotlin, Swift, C#, JS — can implement its own handler registry and reuse the exact same JSON catalog without changing a single byte. Dart is simply the first implementation that works end to end, not the only one.
-
A handler ID is a versioned API boundary: the
versionfield makes semantic changes traceable. If the semantics don't change, old documents keep working; if they do, you bump the version and old and new implementations can coexist. - Data updates require recompiling no host at all: adding a command to the catalog means adding a JSON file, not editing code in any language.
Reusing Fig's specs in another language would have meant "re-translating the logic" — fully translating several thousand files into the target language, an enormous amount of work. By defining the spec as JSON, that work disappears entirely. What's left is the dynamic functions, and they're only a small fraction, so porting them is far quicker.
Feature 2: A genuinely platform-independent runtime (pure Dart, no Flutter dependency)
The package's lib/ contains no dart:io and does not depend on Flutter. It asks the host for exactly two injection points:
// 1. Where does the data come from — a function that returns a string
typedef JsonAssetReader = Future<String> Function(String relativePath);
// 2. The completion environment — directories, processes, environment variables
abstract class CompleteAdapter { ... }
Feature 3: The adapter abstraction — the completion environment itself is replaceable
For Fig, the completion environment was the local machine. This project abstracts it into an interface:
abstract class CompleteAdapter {
String? getEnv(String key);
Future<String> resolveCwd(String cwd, Shell shell);
Future<List<FileSystemEntry>> listDirectory(String path);
Future<ProcessRunResult> runProcess(List<String> command, ...);
}
So completion can work in three completely different worlds:
| Adapter | Scenario |
|---|---|
Local (dart:io) |
The local shell |
| SSH / SFTP | A remote server: directory listings go over remote SFTP, and a generator's script is executed on the remote host
|
| Noop | Disconnected / unknown host — never silently falls back to the local filesystem |
Add to that a Shell enum covering bash / zsh / fish / pwsh / powershell / cmd / xonsh / nushell, plus a per-session pathSeparator (only native Windows consoles use \; WSL / Git Bash / macOS / Linux / remote Unix all use /) — cross-platform isn't a slogan here, it's semantics nailed down one at a time.
Yes: you can use it from Dart on any platform, any OS — even the Web.
Feature 4: Streaming completion — static results immediately, slow ones later
Completion splits into static and dynamic. With streaming completion, the user gets the static suggestions right away, and as soon as the dynamic code finishes executing, those results keep filling into the list. The whole thing just feels faster.
- Static: the subcommands, options, and args hardcoded in the spec. They're sitting in the local JSON; they come back in microseconds.
-
Dynamic: anything that requires actually running a process.
git branch,docker ps,kubectl get pods, directory listings…
Fig's approach was compute everything, then show it all at once. What the user experienced: type, wait, and only once everything had been generated did the full list of suggestions appear. Over SSH, where you're at the mercy of the network, that experience is nothing short of disastrous.
autocomplete provides SuggestionRequestMode.staticThenFinal, which splits a single computation into progressive frames:
| Frame | Contents | Arrives |
|---|---|---|
staticPartial |
The subcommands / options / args from the spec | Immediately |
staticPartial (second frame) |
New subcommands produced by generateSpec
|
Once generated |
sourcePartial |
The result of one dynamic source (one frame per source) | As each generator finishes |
finalResult |
The authoritative, complete result | Once everything is done |
Which means the experience becomes: by the time you've finished typing, suggestions are already in front of you; while you're reading and choosing, the rest keep filling in. Not "wait for every command to finish computing before anything appears."
5. FaiTerm: A Cross-Platform Command-Completion Terminal
FaiTerm is a terminal GUI built with Flutter. Command completion is built in — no extra configuration needed.
5.1 Why build a terminal
Because I want to use it on Windows and Linux — and on Android and mobile devices too.
Tools like Fig piggyback on someone else's terminal, so they have to obtain two things:
- The cursor's on-screen coordinates — only available through macOS's Accessibility API; Windows and Linux have no equivalent.
- What you're currently typing — only available by inserting a pseudo-terminal passthrough layer between the terminal and the shell.
Those two constraints mean it can never cross platforms.
So flip it around: what if the terminal is mine?
- Where's the cursor? I render it — of course I know.
- What are you typing? I receive the keystrokes, and I maintain the edit buffer.
- What's the shell, and what's the current directory? I spawned the PTY — I know.
Otherwise you're stuck depending on the Accessibility API, a pseudo-terminal passthrough layer, and system accessibility permissions — and cross-platform becomes extraordinarily hard.
5.2 Command history suggestions
On top of autocomplete's dynamic suggestions, FaiTerm also suggests commands based on your history. Compared to zsh-autosuggestions, FaiTerm's history completion gives you multiple choices at once.
I use that plugin myself, and most of the time it's excellent. But there's one annoyance: when two commands are very similar, unless you type something that differentiates them, you can't bring up the other suggestion. That's the pain point I ran into after using the zsh plugin for a long time.
5.3 What FaiTerm can do today
Beyond completion, it's a terminal you can genuinely live in.
| Capability | Notes |
|---|---|
| Multi-tab terminal | Native Pty; local and remote share one interaction model |
| Command completion | spec + history + notes + AI, merged into one list |
| Multi-protocol connections | SSH / SFTP / Telnet / FTP / serial, unified into a single connection list |
| SFTP file management | Dual-pane, drag and drop, transfer queue |
| AI chat and Agent | Can bind to a terminal and execute automatically; dangerous-command blocking + approval modes |
| Knowledge base + RAG | Notes are searchable; # completes straight into the command line |
| Command history | Stored in SQLite, searchable, filterable by window / host |
| Themes | 600+ terminal color schemes, light/dark paired, following the app's appearance |
| Cross-platform | macOS / Linux / Windows / Android / iOS / HarmonyOS |
6. Try It Out
FaiTerm
Website: https://www.faiterm.com
Current version: v1.0.9. Download directly:
| Platform | Artifacts |
|---|---|
| macOS | dmg (Apple Silicon / Intel / universal), pkg |
| Windows | exe, msix (x64 / arm64) |
| Linux | AppImage, deb, rpm, zip |
Install it and it just works — no Fig to install first, no accessibility permissions to grant, no background service to run separately.
Bug reports, complaints, and feature requests are all welcome:
- GitHub: https://github.com/littlewrite/FaTerm
- For the Chinese-speaking community, there's also a QQ group: 1028280069
autocomplete (Dart)
If you're not here for FaiTerm but want to wire a completion engine into your own project, just use the package directly:
Repo: https://github.com/littlewrite/autocomplete — package docs are in dart/README.md.
dependencies:
autocomplete:
git:
url: git@github.com:littlewrite/autocomplete.git
path: dart
ref: feat_dart_v2
It does not depend on Flutter. It runs on the CLI, on desktop, on mobile, and on the Web — all you have to provide is "a function that reads a string" and "a completion environment."
If you work in Go / Rust / Java / Kotlin / Swift / C# / JS, you're especially welcome to come run this JSON catalog.
That's exactly the problem JSON-first set out to solve: the spec data doesn't need rewriting, you just implement the handler contract. Dart is the first implementation to work end to end, not the only one.
Equally welcome is help with the completion specs themselves — is a CLI you use every day incompletely covered, or missing entirely? Come open a PR; adding one JSON file is all it takes.
7. Closing
A huge thank you to Fig and to the other developers who contributed to the community — the declarative completion spec, withfig/autocomplete.
autocomplete-dart builds on that: it takes the most valuable part Fig left behind — that spec asset — and rebuilds it from "code that can only run inside Node" into "a data contract any language and any platform can read." JSON for the data; an abstracted adapter so the completion environment itself is replaceable.
Fig's story ended on macOS.
This story continues in FaiTerm, no longer bound to any single platform.
Appendix: Key Links
- FaiTerm website: https://www.faiterm.com
- github.com/littlewrite/autocomplete — the JSON-first Dart implementation (MIT)
- withfig/autocomplete — Fig's spec repo (now Amazon Q Developer CLI, MIT)
- Amazon acquires Fig (TechCrunch, 2023-08-29)
- inshellisense #258: Fig is sunsetting
- Kiro CLI: Upgrading from Amazon Q Developer CLI
- Design docs:
docs/v3-json-first-architecture.md,docs/json-driven-specs.md,docs/json-spec-generation-rules.md,docs/remote-spec-distribution.md(all in theautocompleterepo)


Top comments (0)