twin

Gem Version Tests MIT License

Sync configuration folders between two Macs from self-documenting Markdown files.

Sync entries are defined in Markdown files with YAML blocks — human-readable, self-documenting, and queryable via grubber. Selection is interactive via fzf, with a Markdown preview rendered by apex.

Why?

Sync-definitions in Markdown + YAML are three things at once:

  • Human-readable. Plain Markdown, no twin-specific syntax to learn. The Markdown frame documents why a path is synced, not just what, so you can read your own sync-files in a year and still understand them.
  • Machine-readable. The YAML blocks are queryable via grubber, so any tool (twin, but also future ones) can act on the same source of truth.
  • AI-writable. LLMs handle Markdown + YAML well. You can ask an assistant to add new entries or refactor existing ones, and the result stays valid for both humans and grubber.

Screenshots

Stage 1 — program picker. One row per program, color-coded status, indented paths underneath:

Stage 1 — program picker

Stage 2 — multi-select over the paths of one program. The right pane shows a compact preview of the relevant sync-file section, rendered by apex:

Stage 2 — Fish Shell paths with apex preview

Installation

1. Install grubber

twin parses sync-files via grubber, a small Go binary. Download the latest release for your platform from github.com/rhsev/grubber/releases and put it somewhere in your PATH (e.g. /usr/local/bin/grubber).

2. Install twin

gem install grubber-twin

Or from source:

git clone https://github.com/rhsev/grubber-twin.git
cd grubber-twin
gem build grubber-twin.gemspec
gem install ./grubber-twin-*.gem

3. Other tools

Also required in PATH: rsync (preinstalled on macOS), fzf (brew install fzf). For the stage-2 preview, one of apex, glow, or bat is recommended (falls back in that order; cat if none are present).

Quickstart

  1. Pick a directory for sync-files (anywhere; this example uses ~/Sync):
   mkdir -p ~/Sync
  1. Create the config at ~/.config/twin/config.yaml:
   sync_dir: ~/Sync
   global_excludes:
     - .DS_Store
     - .git/
  1. Drop a sync-file into ~/Sync. The simplest starting point is to copy one of the examples and adapt the frontmatter:
   cp examples/home.md ~/Sync/
   $EDITOR ~/Sync/home.md   # edit Source: and Target:
  1. Run twin:
   twin

Pick a program, then the paths to sync, hit Enter.

Usage

twin                         # picker — all programs across all sync-files
twin home.md                 # picker — one sync-file in sync_dir (by name)
twin /abs/path/to/file.md    # picker — any sync-file by absolute path
twin ./relative/dir/         # picker — all sync-files in a directory
twin list                    # plain listing
twin status                  # listing with source/target mtimes
twin sync -p grubber         # sync one program by name pattern
twin sync --file=repos       # sync all programs from a sync-file
twin sync --dry-run          # preview without writing
twin --help                  # show usage

File argument resolution:

  • bare name (no /) → looked up by substring in sync_dir
  • contains / → resolved as path (absolute or relative); file or directory both work

Configuration

~/.config/twin/config.yaml:

sync_dir: /path/to/sync-files

global_excludes:
  - .DS_Store
  - .git/

# Optional preview rendering (apex):
# apex_theme: default
# apex_width: 80
# apex_code_highlight: monokai
# apex_code_highlight_theme: dark

Environment overrides: TWIN_SYNC_DIR, TWIN_CONFIG.

Sync-files

Each Markdown file represents one sync relationship. Frontmatter defines the relationship (Source/Target); YAML blocks define individual paths.

See examples/home.md and examples/repos.md for ready-to-adapt templates.

Minimal example:

---
Active: 1
Label: mac-mini → macbook
Source: /Users/admin
Target: /Volumes/macbook/Users/admin
---

## Fish Shell

Configuration for the fish shell, including completions and abbreviations.

```yaml
Program: Fish Shell
Path: .config/fish
Description: Fish Shell configuration
Exclude: conf.d/local.fish
```

Frontmatter fields (Active, Label, Source, Target) are merged into every block by grubber. Multiple blocks can share the same Program — twin groups them and treats the program as the unit of selection.

The optional Cmd field runs a shell command after a successful sync (useful for reloading services or notifying companions).

Design

Sync instructions and context in one place — the same Markdown file holds both the Path: directives and the prose explaining them. No TUI framework: fzf does the interactive part, apex the rendering.

See ARCHITECTURE.md for the data model and internals.

Tests

rake test