UNPKG

vde-layout

Version:

Terminal multiplexer layout management tool for VDE (Vibe Coding Development Environment)

320 lines (279 loc) 15.7 kB
# vde-layout vde-layout is a CLI that reproduces terminal layouts (tmux or WezTerm) from YAML presets. Define the panes you need once, then bring them back with a single command. ## Key Capabilities - Keep reusable presets for development, monitoring, reviews, and more. - Build nested horizontal/vertical splits with ratio-based sizing and fixed-cell panes. - Launch commands in each pane with custom working directories, environment variables, delays, and titles. - Preview every tmux step in dry-run mode before you apply a preset. - Target tmux or WezTerm backends with the same preset definitions. - Switch between configuration files by flag or environment variables. ## Installation ```bash npm install -g vde-layout # or pnpm add -g vde-layout ``` ## Development ```bash pnpm install pnpm run build pnpm run format:check pnpm run typecheck pnpm run lint pnpm run test # run all checks in sequence pnpm run ci ``` ## Quick Start 1. Create a YAML file at `~/.config/vde/layout/config.yml` (legacy `~/.config/vde/layout.yml` is also supported; see "Configuration Search Order"). 2. Paste a preset definition: ```yaml presets: web-dev: name: Web Development description: Editor, server, and logs layout: type: horizontal ratio: ["90c", 2] panes: - name: editor command: nvim focus: true - type: vertical ratio: [2, 1] panes: - name: server command: npm run dev cwd: ~/projects/app env: NODE_ENV: development - name: logs command: tail -f logs/app.log title: Logs delay: 500 monitor: name: Monitor command: htop ``` 3. Start tmux and run: ```bash vde-layout web-dev ``` ## CLI Commands - `vde-layout [preset]` - Apply the named preset. When omitted, vde-layout uses the `default` preset; if none exists it lists available presets and exits. - `vde-layout list` - Show available presets with descriptions. - `vde-layout --select` - Open an interactive preset selector (auto mode; currently uses `fzf`). - `vde-layout --select --select-ui fzf` - Force the selector backend (`auto` or `fzf`). - `vde-layout --select --select-surface tmux-popup` - Render selector in a tmux popup (`fzf --tmux`). - `vde-layout --select --select-tmux-popup-opts "80%,70%"` - Pass popup sizing/placement to `fzf --tmux=<opts>`. - `vde-layout --select --fzf-arg "--cycle" --fzf-arg "--info=inline"` - Pass additional argument(s) to `fzf` (repeatable). - `vde-layout dev --dry-run` - Display the tmux steps without executing them. - `vde-layout dev --verbose` - Print informational logs, including resolved presets and plan details. - `vde-layout dev --backend wezterm` - Use the WezTerm backend (defaults to `tmux` when omitted). - `vde-layout dev --current-window` - Reuse the current tmux window (or active WezTerm tab) after confirming that other panes can be closed. - `vde-layout dev --new-window` - Force creation of a new tmux window or WezTerm tab even when presets or defaults request reuse. - `vde-layout --config /path/to/config.yml` - Load presets from a specific file. - `vde-layout --help` - Show usage. - `vde-layout --version` / `vde-layout -v` - Print package version. > **Note:** Applying a preset (without `--dry-run`) must be done inside an active tmux session when using the tmux backend. For the WezTerm backend, ensure a WezTerm window is running and focused so the CLI can discover it. > > **Selector UI note:** `--select` requires an interactive terminal and `fzf` on `$PATH`. `--select-surface tmux-popup` requires running inside tmux (`fzf --tmux`, tmux 3.3+ recommended). ## Terminal Backends vde-layout resolves backends in the following order: CLI flag (`--backend`), preset configuration, then defaults to `tmux`. - **tmux (default)** - Requires an active tmux session for non-dry runs. `--current-window` closes other panes in the selected window after confirmation; `--new-window` always creates a new tmux window. - **WezTerm** - Requires the `wezterm` CLI to be available (nightly channel recommended). Start WezTerm beforehand so at least one window exists. - `--current-window` targets the active tab and confirms before closing other panes. - `--new-window` spawns a new tab in the active window when one is available, otherwise creates a fresh window. ## Configuration Search Order When no `--config` flag is provided, vde-layout checks candidate files in this order for `findConfigFile()`: 1. Project scope discovered by walking up from the current directory; for each directory, vde-layout checks `.vde/layout/config.yml` first, then `.vde/layout.yml`. 2. `$VDE_CONFIG_PATH/layout.yml` (if `VDE_CONFIG_PATH` is set). 3. `$XDG_CONFIG_HOME/vde/layout/config.yml` (or `~/.config/vde/layout/config.yml` when `XDG_CONFIG_HOME` is unset). 4. `$XDG_CONFIG_HOME/vde/layout.yml` fallback (or `~/.config/vde/layout.yml`). For `loadConfig()`, vde-layout merges shared scopes first and project scope last: 1. `$VDE_CONFIG_PATH/layout.yml` 2. XDG scope (`.../vde/layout/config.yml` or fallback `.../vde/layout.yml`; first existing file only) 3. Project scope (`<project-root>/.vde/layout/config.yml` or fallback `<project-root>/.vde/layout.yml`, discovered by walking up from the current directory) ## Preset Structure Each preset is an object under the `presets` key: ```yaml presets: preset-key: name: "Display Name" # required description: "Summary" # optional backend: wezterm # optional; "tmux" (default) or "wezterm" windowMode: new-window # optional; "new-window" (default) or "current-window" layout: # optional; omit for single command presets # see Layout Structure command: "htop" # optional; used when layout is omitted hooks: # optional; see Hooks afterApply: "vde-tmux-sidebar open" ``` ### Defaults Structure Global/project defaults can be defined under `defaults`: ```yaml defaults: windowMode: new-window selector: ui: auto # auto | fzf surface: auto # auto | inline | tmux-popup tmuxPopupOpts: "80%,70%" # passed to fzf as --tmux=<value> fzf: extraArgs: # additional arguments passed to fzf - --cycle - --info=inline ``` ### Layout Structure ```yaml layout: type: horizontal | vertical # required ratio: ["90c", 2, 1] # required; number weight or "<positive-integer>c" panes: # required - name: "left" # required for terminal panes command: "npm run start" # optional cwd: "~/project" # optional env: # optional API_BASE_URL: http://localhost:3000 focus: true # optional; only one pane should be true delay: 500 # optional; wait (ms) before running command title: "Server" # optional; tmux pane title ephemeral: true # optional; close pane after command completes closeOnError: false # optional; if ephemeral, close on error (default: false) - type: vertical # nested split ratio: [1, 1] panes: - name: "tests" - name: "shell" ``` ### Template Tokens You can reference dynamically-assigned pane IDs within pane commands using template tokens. These tokens are resolved after the layout finishes splitting panes but before commands execute: - **`{{this_pane}}`** - References the current pane receiving the command - **`{{focus_pane}}`** - References the pane that will receive focus - **`{{pane_id:<name>}}`** - References a specific pane by its name - **`{{window_id}}`** - References the real window ID the layout was applied into (e.g. tmux's `@5`). Only resolved in `hooks.afterApply` (see [Hooks](#hooks)) — using it in a pane `command` raises a template token error rather than resolving, the same as referencing an unknown `{{pane_id:<name>}}`. Example: ```yaml presets: cross-pane-demo: name: Cross Pane Coordination layout: type: vertical ratio: [2, 1] panes: - name: editor command: 'echo "Editor pane ID: {{this_pane}}"' focus: true - name: terminal command: 'echo "I can reference the editor pane: {{pane_id:editor}}"' ``` **Common use cases:** - Send commands to other panes: `tmux send-keys -t {{pane_id:editor}} "npm test" Enter` - Display pane information for debugging: `echo "Current: {{this_pane}}, Focus: {{focus_pane}}"` - Coordinate tasks across multiple panes within your preset configuration ### Hooks `hooks.afterApply` runs an arbitrary host command once, after a preset has been applied successfully. It is a general-purpose hook (not specific to any tool); one intended use is idempotently opening a sidebar tool such as [vde-tmux-sidebar](https://github.com/yuki-yano/vde-tmux-sidebar) once the layout has finished building — the sidebar itself is managed by that separate tool, not defined as a layout pane in vde-layout's preset. ```yaml presets: dev: name: Dev layout: type: horizontal ratio: [3, 1] panes: - name: editor command: nvim focus: true - name: repl command: node hooks: afterApply: "vde-tmux-sidebar open" ``` - Runs exactly once, only after `applyPlan` succeeds; it never runs during `--dry-run` (dry-run instead prints the unresolved command as a planned step). - Executes as a host shell command (equivalent to `sh -c "<command>"`, so pipes/args/redirection work) in the directory vde-layout was invoked from (the CLI's `cwd`), not inside any particular tmux pane. - If the command fails, or if a template token inside it cannot be resolved, vde-layout logs a warning and the preset apply is still reported as successful (exit code is unaffected). - The command is killed and treated as a failure (logged as a warning) if it runs for longer than 30 seconds. - Template tokens are supported, but `{{pane_id:<name>}}` only resolves against the pane names created by *this apply's* own `layout` — it cannot address an existing external pane such as a sidebar. This is especially easy to get wrong in `current-window` mode: the reused current pane is bound to the layout tree's first pane name, so a hook that tries `{{pane_id:sidebar}}` for a preset pane named `sidebar` would silently resolve to the current pane, not the real sidebar. Because the hook doesn't run "in" any specific pane, `{{this_pane}}` and `{{focus_pane}}` both resolve to the pane that ended up focused after the apply. - `{{window_id}}` resolves to the real window ID the layout was applied into (tmux's `@5`-style ID, or wezterm's window ID). This is the recommended way to hand off to an external tool such as [vde-tmux-sidebar](https://github.com/yuki-yano/vde-tmux-sidebar) that needs to target the applied window — tmux's `-t` does not expand formats, so vde-layout resolves it to a literal before running the hook: ```yaml hooks: afterApply: "command -v vde-tmux-sidebar >/dev/null 2>&1 && vde-tmux-sidebar layout-applied --window '{{window_id}}' || true" ``` If `{{window_id}}` is used but the window ID could not be resolved, the hook is skipped (logged as a warning) rather than run with a blank value. ### Ephemeral Panes Ephemeral panes automatically close after their command completes. This is useful for one-time tasks like builds, tests, or initialization scripts. ```yaml panes: - name: build command: npm run build ephemeral: true # Pane closes when command finishes ``` **Error handling:** - By default, ephemeral panes remain open if the command fails, allowing you to inspect errors - Set `closeOnError: true` to close the pane regardless of success or failure ```yaml panes: - name: quick-test command: npm test ephemeral: true closeOnError: false # Default: stays open on error - name: build-and-exit command: npm run build ephemeral: true closeOnError: true # Closes even if build fails ``` **Combining with template tokens:** ```yaml panes: - name: editor command: nvim - name: test-runner command: 'tmux send-keys -t {{pane_id:editor}} ":!npm test" Enter' ephemeral: true # Run once and close ``` ### Ratio and Fixed Cells - `ratio` supports `number` (weight) and `"<positive-integer>c"` (fixed cells). - Fixed-cell entries are reserved first, then the remaining cells are distributed by numeric weights. - Each split must include at least one numeric weight. - `ratio.length` must match `panes.length`. Examples: - `[1, 1]` -> equal split - `["90c", 2, 1]` -> first pane fixed to 90 cells, remaining cells split as 2:1 - `[1, "40c", 1]` -> middle pane fixed to 40 cells, sides split equally from the remaining cells Constraints: - `["90c", "40c"]` is invalid (no numeric weight) - `0c`, `1.5c`, `"90"` are invalid - If runtime pane size is too small to satisfy fixed cells plus minimum remaining panes, execution fails with `SPLIT_SIZE_RESOLUTION_FAILED` ### Single Command Presets If you omit `layout`, the preset runs a single command in one pane (or opens the default shell when `command` is omitted): ```yaml presets: shell: name: Default Shell build: name: Build Script command: npm run build ``` ### Window Mode Selection - `defaults.windowMode` sets the default behavior for presets that omit `windowMode`. Allowed values are `new-window` (default) and `current-window`. - Each preset may override the default by specifying its own `windowMode`. - CLI flags (`--current-window` / `--new-window`) take highest precedence and override both presets and defaults. - When `current-window` mode is used during an actual run, vde-layout prompts for confirmation before closing panes other than the pane running the command. Dry-run mode prints the intended closures without prompting. - **Sidebar protection:** panes with the tmux pane user option `@vde_sidebar` set to `1` (as set by tools like [vde-tmux-sidebar](https://github.com/yuki-yano/vde-tmux-sidebar)) are treated as protected sidebar panes in `current-window` mode. They are never killed when reusing the window, and the preset's layout is built in the remaining, non-sidebar area of the window instead of the sidebar pane itself. ## Runtime Behavior - Dry-run mode prints every tmux command and preserves the execution order you would see in a real run. - Applying a preset creates (or reuses) a tmux window, splits panes according to the plan, sets environment variables, changes directories, and runs commands sequentially. - If an error occurs (for example, a tmux command fails or the configuration is invalid), vde-layout returns a structured error with the failing step and guidance. ## Environment Variables - `VDE_CONFIG_PATH` - Override the base directory for configuration files. - `XDG_CONFIG_HOME` - XDG base directory root; defaults to `~/.config` when unset. - `VDE_DEBUG=true` - Enable debug-level logs (includes stack traces). - `VDE_VERBOSE=true` - Enable info-level logs without full debug output. - `TMUX` - Automatically set by tmux. vde-layout checks this to ensure execution happens inside a session. ## Requirements - Node.js 22 or higher - tmux 2.0 or higher (required for the tmux backend) - WezTerm nightly build with `wezterm` on `$PATH` (required for the WezTerm backend) ## Contributing Please submit bug reports and feature requests through [GitHub Issues](https://github.com/yuki-yano/vde-layout/issues). ## License MIT