[READ-ONLY] Mirror of https://github.com/plttn/tide. 🌊 The ultimate Fish prompt.
tide AGENTS.md
8.8 kB
Markdown
at main

AGENTS.md #

This file provides guidance to agents when working with code in this repository.

What this is #

Tide is an asynchronous prompt for the Fish shell, installed as a Fisher plugin. This is plttn/tide, a fork of IlanCosman/tide. All work here targets plttn/tide — never open PRs against upstream IlanCosman/tide.

Everything is written in Fish script (.fish); there is no build step or compiled artifact.

Commands #

All tasks are run via mise (use mise, not make):

  • mise run fmt — format all .fish files with fish_indent
  • mise run lint — syntax-check all .fish files via fish --no-execute
  • mise run install — install Tide + test deps (fisher, clownfish) into the current Fish config
  • mise run test [FILE...] — run the littlecheck suite, or only the given test files (scripts/test.fish)
  • mise run test-clean — wipe the cached local test HOME (scripts/test_clean.fish) if the test env gets into a bad state

Running a single test #

Tests are littlecheck .fish files under tests/, each a self-contained script with # CHECK: comments asserting stdout. Pass one or more to mise run test to run only those:

mise run test tests/_tide_item_node.test.fish

Never run python3 littlecheck.py on a test file yourself. Test files set universal variables (set -U tide_left_prompt_items ...) and funcsave helpers, so outside the test HOME they overwrite the real fish config and break the user's prompt. mise run install also writes to the real config, so don't use it to verify changes either.

A local run reuses a persistent HOME ($XDG_CACHE_HOME/tide-test-home or ~/.cache/tide-test-home) so fisher/clownfish aren't reinstalled every run; each run still installs the working copy into it. In CI (GITHUB_ACTIONS=true) this caching is skipped and everything runs in a clean environment.

Run the suite as a non-root user. tests/_tide_item_pwd.test.fish expects / to be unwritable (so it gets the unwritable-directory icon), and that check fails as root, e.g. in some containers and sandboxes.

Test files mock external commands (e.g. node, git) via clownfish's mock function — see tests/_tide_item_node.test.fish for the pattern. tests/test_setup.fish defines _tide_decolor (strips ANSI codes for assertions) and sets _tide_side for right-prompt items.

Debugging the async render path #

Timing bugs in the render path — a stale prompt, a missed repaint, two renders racing — usually can't be seen by calling fish_prompt from a script, since the bug is in when fish renders rather than in what one render prints. scripts/prompt_probe/probe.py drives a real interactive fish in a pty (isolated HOME, keystrokes, window resizes) and can log every render, dispatch and signal handler run. It is a manual tool, not part of mise run test; see scripts/prompt_probe/README.md.

Architecture #

Item-based prompt composition #

The prompt is built from items — one function per prompt segment, named _tide_item_<name>.fish in functions/. Each item function:

  • Reads config from user-exposed tide_<item>_* variables (colors, icons, thresholds)
  • Detects whether it's relevant (e.g. _tide_item_node checks $_tide_parent_dirs for package.json and that node is on PATH) and prints nothing if not
  • Calls _tide_print_item <name> <content...> (functions/_tide_print_item.fish) to emit its segment, which handles background/foreground coloring and separators between adjacent items based on tide_<side>_prompt_items

Which items appear and in what order is controlled by the universal variables tide_left_prompt_items / tide_right_prompt_items, set by the configuration wizard or by hand.

Rendering pipeline #

functions/fish_prompt.fish is the entry point Fish calls to render a prompt. On each render it:

  1. Kicks off a background job (a second fish -c ... subprocess) that computes the actual prompt content by calling _tide_1_line_prompt or _tide_2_line_prompt (chosen at load time based on whether newline is in $_tide_left_items), renders it into a private scratch tmpfile named after its own pid, atomically renames it into place, then signals the parent with SIGUSR1
  2. Immediately returns a (possibly stale) previous render so the shell never blocks
  3. _tide_refresh_prompt (an --on-signal SIGUSR1 handler) reads the tmpfile into a session-local global variable (_tide_prompt_$fish_pid) and calls commandline -f repaint, which re-renders fish_prompt/fish_right_prompt with the fresh content

If SIGUSR1 delivery can't be confirmed at shell init (some sandboxes/containers swallow signals — checked once via a SIGUSR2 self-test), rendering falls back to fully synchronous instead of spawning a background job that could never notify completion.

This async design is why Tide can afford expensive checks (e.g. full git status with untracked/modified/deleted counts) that would make a synchronous prompt feel slow. fish_prompt.fish itself is built with a large eval "..." block assembled once at shell-init time (varies by one-line vs two-line, frame-enabled vs not) rather than branching on every render, since load time matters more than per-render branching cost here.

_tide_1_line_prompt.fish / _tide_2_line_prompt.fish iterate the configured items and concatenate their _tide_print_item output per side.

VCS item internals #

_tide_item_vcs.fish dispatches between git and jj (Jujutsu) backends:

  • _tide_internal_jj_git.fish walks up from $PWD looking for a .jj directory to decide if this is a jj repo (and whether .disable-jj-prompt opts back out)
  • If in a jj repo and the jj binary is available, _tide_internal_vcs_jj.fish renders; otherwise _tide_internal_vcs_git.fish handles git (and jj-repos-backed-by-git without the jj CLI)

The git backend minimizes subprocess spawns (currently 3 git invocations) since each is a real fork/exec cost inside an already-forked background job.

Configuration wizard #

tide configure (functions/_tide_sub_configure.fish) is a self-contained interactive TUI, not part of the render path. It walks a decision tree defined by .fish files under functions/tide/configure/choices/{all,lean,classic,powerline,rainbow}/** and functions/tide/configure/functions/, previewing changes live using fake_columns/fake_lines and no-op stand-ins for every _tide_item_* function. See ARCHITECTURE.md for the full choice flowchart. Chosen values are written out via functions/tide/configure/configs/.

Subcommands #

tide (functions/tide.fish) dispatches to _tide_sub_<name> functions (configure, reload, bug-report) — adding a subcommand means adding a new _tide_sub_<name>.fish.

Init / lifecycle hooks #

conf.d/_tide_init.fish wires up Fisher's _tide_init_install / _tide_init_update / _tide_init_uninstall events — first-install onboarding, version-migration shims (e.g. _tide_migrate_vcs_prompt_items), and cleanup of all tide_*/_tide_* functions and universal variables on uninstall.

Conventions #

(Full detail in CONTRIBUTING.md — key points below.)

  • Style: prefer test over [ ]; prefer &&/|| over and/or for simple conditionals, if/else/else if for anything more complex; prefer piping over command substitution when it doesn't require extra commands.
  • Naming: everything snake_case. User-facing names (variables, functions, files) are prefixed tide_; internal-only names are prefixed _tide_. Items live in _tide_item_<name>.fish; subcommands in _tide_sub_<name>.fish.
  • PR titles must follow Conventional Commits (<type>[(scope)][!]: <description>) — PRs are squash-merged, so the PR title becomes the commit message on main, and release-please reads it to generate the changelog. This is CI-enforced.
  • Breaking changes (!) should be very rare, since they bump the major version. A new default look is a feat with a callout in the PR description. Where possible, give new defaults only to new installs: set them in functions/tide/configure/configs/ and keep the old behavior when the variable is unset, so updates don't change existing setups.
  • Releases are fully automated by release-please; don't hand-edit functions/tide.fish's version string or the floating vN/vN.M tags.