Rationale
Why TryIt looks the way it does. Short prose explainer for contributors, covering the non-obvious design calls.
The chdir problem
A child process cannot change its parent's current working directory. When you run cd /tmp in bash, bash changes its own CWD. When you run some-program cd-me /tmp, some-program's chdir(2) affects only some-program — bash's CWD is unaffected.
This is a hard Unix invariant. Every shell navigation tool has to work around it somehow: z, autojump, fasd, zoxide, and the Ruby try-cli all converge on the same trick.
The eval trick
The standard answer is to print a shell command on stdout and let the user's shell eval it. Concretely, TryIt prints
cd '/home/user/work/tries/2026-04-19-my-idea'and tryit init's shell function wraps the invocation in eval "$(...)":
tryit() {
local __try_cmd
__try_cmd=$(julia ... -- "$@") || return $?
[ -n "$__try_cmd" ] && eval "$__try_cmd"
}The eval runs in the parent shell's context, so the cd actually moves the user.
Why stdout is reserved
Because the shell function evals whatever we print, every byte on stdout matters. A stray println("hello") during selector rendering would become a syntax error — or worse, an executed command — when the shell tried to eval it.
Principle I of the project constitution nails this down: stdout carries only the final cd line (or nothing, if the user cancels). Diagnostics, prompts, progress, error messages — all go to stderr. The selector's full-screen UI draws through an alternate-screen buffer, which the TUI library (Tachikoma.jl) sets up and tears down; it never competes with the UB4 channel.
Why exit codes matter
Shell users often chain invocations:
tryit "$idea" && code .If tryit returns non-zero, the user expects the && branch to be skipped. We therefore adopt POSIX-like codes:
0— success; acdline was emitted (or the user voluntarily cancelled with Esc).2— permission error (e.g.,TRY_PATHis not writable).64— usage error (empty slug, no TTY in a non-TTY context, unknown argv).127— a required external dependency is missing (typicallygit).130— SIGINT received.
Non-zero codes are documented per command, and the test suite asserts them. Downstream scripts can rely on the distinction between "the user said no" and "the tool said no".
Why git (the binary), not LibGit2.jl
v0.2 added tryit clone and tryit worktree. We shell out to the user's installed git rather than linking a Julia git library for three reasons:
- Credential forwarding. The user's SSH agent, Git Credential Manager, and hosting-provider PATs are already wired up for the
gitbinary. Reimplementing that surface in Julia is a losing battle. - Worktree semantics.
git worktreeis a first-class CLI feature and stable across git versions; libgit2's equivalent has trailing edges. - Runtime-dep budget. SPEC NF1 pins the runtime graph to
Tachikoma.jl,PrecompileTools.jl, and stdlib. A git library would mean either a heavy binary (libgit2) or a broader Julia dep graph — neither justified by the thin surface we actually need.
Because git is an external binary, the FR-030 / UN5 contract ("missing git → exit 127 with the canonical error line") protects scripted users from mysterious failures.
Why Tachikoma.jl (not Term.jl)
v0.1 briefly considered Term.jl before swapping to Tachikoma.jl. Two properties mattered:
- Elm-inspired Model/update/view. Our
SelectorSessionis a pure-dataModelwe mutate viaupdate!(model, event). This maps cleanly onto property-based tests (feed scripted key events, inspect the resultingModelstate) without needing a real TTY at test time. - Built-in virtual-terminal testing. Tachikoma ships a headless backend. For
TryIt's contract (stdout reserved for thecdline, all rendering in the alt-screen), this means CI tests can exercise the full selector loop without stdin/stdout gymnastics.
Why precompile
SPEC NF17 mandates PrecompileTools.@compile_workload — without it, a fresh shell's first tryit invocation pays the full Julia module-inference cost on top of whatever work the user actually asked for. Our workload warms the pure paths (slug, TriesPath, SelectorSession construction + one update!). We deliberately do not precompile clone_into or worktree_at — they spawn git, which is brittle inside precompile sandboxes.
PrecompileTools.jl is the one runtime-dep addition beyond Tachikoma.jl, reconciled into SPEC NF1 in the v0.3 change set.