Skip to content

Install binary, config, and state to standard XDG / FHS locations #21

@ukanga

Description

@ukanga

Background

Today wt puts everything under one custom directory, ~/.wt/:

File Purpose
~/.wt/wt binary
~/.wt/config.toml global config
~/.wt/sessions.json windows-mode session state

This is non-standard. On Linux (and increasingly on macOS), the expected layout is XDG Base Directory plus the systemd file hierarchy:

Concern Standard location (Linux default)
User binary $HOME/.local/bin/
User config $XDG_CONFIG_HOME (default $HOME/.config/)
User state $XDG_STATE_HOME (default $HOME/.local/state/)

Because ~/.wt/ is not on PATH, install.sh currently has to add a shell-rc alias in ~/.bashrc / ~/.zshrc / ~/.config/fish/config.fish just to make wt callable. Moving the binary onto ~/.local/bin/ (which is on PATH by default on every modern Linux distro and easily added on macOS) removes the alias dance entirely.

What to build

Move wt's on-disk footprint off the custom ~/.wt/ directory and onto standard locations: ~/.local/bin/ for the binary, $XDG_CONFIG_HOME/wt/ for config, $XDG_STATE_HOME/wt/ for state.

Target layout

File Old New (Linux & macOS default)
Binary ~/.wt/wt (with shell alias) ~/.local/bin/wt (on PATH)
Global config ~/.wt/config.toml ~/.config/wt/config.toml
Session state ~/.wt/sessions.json ~/.local/state/wt/sessions.json

XDG_CONFIG_HOME / XDG_STATE_HOME env vars are honoured. Per-repo .wt.toml is unchanged.

macOS gets the same XDG paths, not ~/Library/Application Support/. This matches the convention used by gh, starship, zoxide, and most modern CLI tools, and gives macOS users with mixed Linux/macOS dotfiles a single ~/.config/ tree.

Design

  • Binary default: ~/.local/bin/wt on both platforms.
  • --system flag on install.sh: installs to /usr/local/bin/wt (with sudo where required). Useful for shared machines.
  • Path resolution in code: hand-rolled XDG logic (read XDG_CONFIG_HOME / XDG_STATE_HOME, fall back to dirs::home_dir().join(".config") / home_dir().join(".local/state")). Do not use dirs::config_dir() / dirs::state_dir() — they return Apple paths on macOS, which is the opposite of the chosen design.
  • Migration: read-with-fallback in code plus an explicit migration step in install.sh. Existing users keep working without intervention; running the new installer moves their files cleanly.
  • PATH on macOS: ~/.local/bin/ is not on $PATH by default on macOS. install.sh detects this and prints a one-line instruction for the user to add it themselves — it does not write to rc files (eliminating the alias dance is the point of this issue).

Implementation notes

install.sh

  • Replace the hard-coded INSTALL_DIR="$HOME/.wt" with logic that picks ~/.local/bin/ by default and mkdir -ps it.
  • Add --system. When passed, install to /usr/local/bin/wt using sudo if not already root.
  • After the copy, check whether the chosen install dir is on the user's PATH. If not, print a one-line instruction; do not write a shell-rc alias.
  • Drop the alias-writing block entirely when the binary is on PATH.
  • Migration order (so the user's wt command is never broken mid-flight):
    1. Copy the new binary to the chosen install dir.
    2. Remove the legacy alias from shell rc files (~/.bashrc, ~/.bash_profile, ~/.zshrc, ~/.config/fish/config.fish). This is required, not optional: shell aliases take precedence over PATH, so a stale alias wt='$HOME/.wt/wt' would shadow the new binary and resolve to a path we are about to delete, breaking the wt command. Remove only the exact lines install.sh would have added (the alias line and the # wt - Git worktree orchestrator comment that precedes it), guarded by grep -q to stay idempotent.
    3. Delete ~/.wt/wt.
    4. If ~/.wt/config.toml exists and the new $XDG_CONFIG_HOME/wt/config.toml does not, move it (creating the parent dir as needed).
    5. Same for ~/.wt/sessions.json$XDG_STATE_HOME/wt/sessions.json.
    6. If ~/.wt/ is now empty, remove the directory.
      Print one line per step so the user can audit what changed.

src/config.rs

  • Add a small xdg_config_home() helper: std::env::var_os("XDG_CONFIG_HOME").filter(|p| !p.is_empty()).map(PathBuf::from).or_else(|| dirs::home_dir().map(|h| h.join(".config"))). Same shape for xdg_state_home() (.local/state).
  • Change Config::load() and Config::load_for_repo() to resolve the global path as xdg_config_home().join("wt/config.toml").
  • Add a fallback layer: order is $XDG_CONFIG_HOME/wt/config.toml → legacy ~/.wt/config.toml → defaults. The existing load_layered infrastructure already supports multiple layers. If only the legacy location exists, log a one-line stderr notice the first time per process: wt: notice: reading config from legacy ~/.wt/config.toml; rerun ./install.sh to migrate.
  • Replace Config::ensure_wt_dir with Config::ensure_config_dir() and Config::ensure_state_dir(). Update call sites.

src/session.rs

  • Resolve the path as xdg_state_home().join("wt/sessions.json").
  • Read-with-fallback to legacy ~/.wt/sessions.json, with the same one-line notice on first read.
  • Writes always go to the new location.

README.md

  • Update "Configuration" to reference ~/.config/wt/config.toml (note legacy is still readable).
  • Update "Installation" to drop the alias mention; the binary lands on PATH.
  • Document ./install.sh --system and the migration behaviour.

Acceptance criteria

  • Fresh install (no ~/.wt/ present) puts the binary at ~/.local/bin/wt and writes nothing under ~/.wt/.
  • ./install.sh --system installs to /usr/local/bin/wt (with sudo if necessary).
  • Newly-created config and state files land at $XDG_CONFIG_HOME/wt/config.toml and $XDG_STATE_HOME/wt/sessions.json respectively (defaults ~/.config/wt/config.toml and ~/.local/state/wt/sessions.json).
  • Existing user with ~/.wt/{wt,config.toml,sessions.json} runs the new install.sh and ends up with: files in the new locations, an empty (or removed) ~/.wt/ directory, and the legacy alias wt=... line removed from every shell rc that contained it. which wt in a fresh shell resolves to the new binary path, not an alias.
  • Re-running install.sh after a successful migration is idempotent — no duplicate edits, no errors, exit 0.
  • If a user with only legacy files runs wt directly (without re-running install.sh), the binary still finds and reads their legacy config and state, with a one-time stderr deprecation notice.
  • XDG_CONFIG_HOME and XDG_STATE_HOME env vars are honoured (verified by test).
  • install.sh does not write any shell-rc alias under any code path. If the install dir is not on PATH, it prints a single instruction line.
  • macOS install lands the binary at ~/.local/bin/wt, config at ~/.config/wt/config.toml, state at ~/.local/state/wt/sessions.json — i.e. same XDG paths as Linux, not ~/Library/Application Support/. Documented in README.
  • On macOS, when ~/.local/bin/ is not on $PATH, install.sh prints a single instruction line and does not modify .zshrc / .bash_profile / config.fish.
  • Integration test exercises read-with-fallback by setting HOME to a temp dir and seeding only the legacy paths.
  • README "Configuration" and "Installation" sections updated.

Out of scope

  • A wt --uninstall flow.
  • Packaging for distro repos (deb, rpm, AUR, Homebrew tap). Once paths are standard, packaging becomes feasible — file as a follow-up.
  • Removing legacy-path support in code. Keep it indefinitely until a future minor version explicitly drops it.
  • Windows support.

Blocked by

None — can start immediately.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions