DEV Community

Paul
Paul

Posted on

A Fig That Works Anywhere — FaiTerm

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() })),
      },
    },
  }],
};
Enter fullscreen mode Exit fullscreen mode

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, loadSpec links, and the command and splitOn of script generators — 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 }
  }]
}
Enter fullscreen mode Exit fullscreen mode
  • 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:

  1. A JSON Schema (schema/completion-spec.schema.json) — what the data looks like;
  2. A physical layout convention — flat commands at <first-letter>/<command>.json, namespaced commands at <first-letter>/<namespace>/.../command.json, with index.json mapping logical names to files only;
  3. A handler contract — {"handler": id, "version": n}, where the field's position determines the callback signature (custom / postProcess / trigger / generateSpec / alias / script each 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 version field 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 { ... }
Enter fullscreen mode Exit fullscreen mode

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, ...);
}
Enter fullscreen mode Exit fullscreen mode

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:

  1. The cursor's on-screen coordinates — only available through macOS's Accessibility API; Windows and Linux have no equivalent.
  2. 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:

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
Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)