# cui-cui

A terminal user interface for visualizing and debugging CI pipelines
(GitLab CI, GitHub Actions).

Point it at a `.gitlab-ci.yml` or `.github/workflows/` directory and get an
interactive DAG view of your pipeline, with fully resolved job configurations
and generated shell scripts for local execution.

## Features

- **Pipeline DAG visualization** -- stages as columns, jobs as boxes, dependency
  arrows between them (both explicit `needs:` and implicit stage ordering)
- **Extends/anchor resolution** -- fully resolves `extends` chains, YAML anchors,
  `!reference` tags, and `default:` blocks so you see the final merged config
- **Variable expansion** -- expands `$VAR` and `${VAR:-default}` across global
  and job-level variables, with CI predefined variables from real git context
- **Local script generation** -- builds a shell script from
  `before_script` + `script` + `after_script` with exported variables,
  ready to copy and run locally
- **Container launch** -- press `c` to drop into an interactive container
  matching the job's image, with variables set and the script available to run
- **Four detail tabs** -- Config (resolved YAML), Script (generated bash),
  Variables (all expanded key-value pairs), Debug (extends chain provenance,
  key-level origin tracking, variable origins)
- **Child pipeline tabs** -- press `Enter` on a `trigger:include:local` job
  to open the child pipeline in a new tab; `[`/`]` to switch tabs,
  `Backspace` to close
- **Remote includes** -- fetches `include:project`, `include:remote`, and
  `include:template` via the GitLab API
- **`include:local`** support -- merges locally included files
- **Mouse support** -- click to select jobs/tabs, scroll to navigate,
  drag to resize the detail panel
- **Light and dark themes** -- auto-detected from terminal background;
  override with `--light` or `--dark`
- **Live reload** -- press `F5` to re-parse the file without restarting

## Installation

Download a pre-built Linux binary from the
[releases page](https://gitlab.freedesktop.org/bentiss/cui-cui/-/releases).

Or install from [crates.io](https://crates.io/crates/cui-cui):

```bash
cargo install cui-cui
```

Or build from source:

```bash
cargo build --release
./target/release/cui-cui
```

## Usage

```bash
# Auto-detect CI system from current directory
# (looks for .gitlab-ci.yml, then .github/workflows/)
cui-cui

# Or specify a path
cui-cui path/to/.gitlab-ci.yml
cui-cui path/to/.github/workflows/ci.yml

# Specify a GitLab instance (auto-detected from git remote otherwise)
cui-cui --gitlab-url https://gitlab.example.com .gitlab-ci.yml

# Override theme (auto-detected by default)
cui-cui --light
cui-cui --dark

# Forward host environment variables into containers
cui-cui --pass-env GITHUB_TOKEN --pass-env DOCKER_PASSWORD

# Disable overlay mount (use volume copy instead)
cui-cui --no-overlay

# Use CI config from a separate repo (project dir is the positional arg)
cui-cui --ci-config ~/Src/kernel-ci/ ~/Src/linux

# Headless modes (no TUI, for scripting/automation)
cui-cui --list-jobs                    # List all jobs grouped by stage
cui-cui --dump-script build            # Print the shell script for a job
cui-cui --run build                    # Run a job non-interactively (like r/R in TUI)
cui-cui --run build --privileged       # Same, with privileged mode
cui-cui --container build              # Drop into an interactive container (like c/C in TUI)

# Dispatch mode: run the full pipeline locally
cui-cui --dispatch --no-tags .                   # Run all untagged jobs
cui-cui --dispatch --tags kvm,x86_64 .           # Run jobs matching these tags
cui-cui --dispatch --no-tags --tags kvm .         # Run untagged + kvm-tagged jobs
cui-cui --dispatch --no-tags --skip container-prep .  # Skip a job (treat as passed)
cui-cui --dispatch --no-tags --skip stage=build .    # Skip an entire stage
cui-cui --dispatch --no-tags --skip job=lint .        # Skip only matching job names
cui-cui --dispatch --git-image docker.io/alpine/git:latest .  # Use a custom git image for checkout

# Allow unresolved extends (for remote includes with missing templates)
cui-cui --allow-unresolved path/to/.gitlab-ci.yml

# Generate shell completions
cui-cui --completions bash > /etc/bash_completion.d/cui-cui
cui-cui --completions zsh > ~/.zfunc/_cui-cui
cui-cui --completions fish > ~/.config/fish/completions/cui-cui.fish
```

Job names in `--dump-script`, `--run`, `--container`, and `--skip` support fuzzy matching:
exact, case-insensitive, then substring. Hyphens and underscores are treated as
equivalent (`test_suite` matches `test-suite`). `--skip` also matches stage names
(all jobs in the stage are skipped). Use `stage=` or `job=` prefixes to
restrict matching. Ambiguous matches show available options.

## Key Bindings

| Key | Action |
|-----|--------|
| `h`/`l` or Left/Right | Navigate between stages |
| `j`/`k` or Down/Up | Navigate between jobs within a stage |
| `Tab` / `Shift+Tab` | Cycle detail tabs (Config / Script / Variables / Debug) |
| `Enter` | Open child pipeline (trigger jobs) or toggle focus |
| `Backspace` | Close current child pipeline tab |
| `[` / `]` | Switch between pipeline tabs |
| `PgUp` / `PgDn` | Scroll detail panel |
| `H`/`L`/`K`/`J` | Scroll the DAG view |
| `c` | Launch interactive container for selected job |
| `C` | Launch privileged container (for jobs with kvm tags) |
| `r` | Run selected job non-interactively (shows pass/fail result) |
| `R` | Run selected job non-interactively with privileged mode |
| `F5` | Reload the file |
| `q` / `Esc` | Quit |

### Dispatch mode (`--dispatch`)

When running in a terminal, dispatch shows a full-screen interactive UI with
a pipeline status panel and a scrollable log pane.

| Key | Action |
|-----|--------|
| `j`/`k` or Down/Up | Scroll log output |
| `g` / `G` | Jump to top / re-enable auto-scroll |
| `PgUp` / `PgDn` | Scroll by page |
| Mouse wheel | Scroll log output |
| `/` | Search logs (Enter to commit, `n`/`N` to navigate hits, Esc to clear) |
| `f` | Filter logs grep-style (only matching lines shown, Esc to clear) |
| `q` / `Ctrl-C` | Quit (signals running containers to stop) |

The TUI stays open after dispatch finishes so you can browse logs.
Piped output (`--dispatch | cat`) uses a plain inline display.

## Supported GitLab CI Features

- `stages` ordering
- `extends` (single and multi-parent, with deep merge)
- YAML anchors and aliases
- `!reference` tags
- `include:local`, `include:project`, `include:remote`, `include:template`,
  `include:component`
- `include:inputs` and `spec:inputs` (with `$[[ inputs.* ]]` interpolation)
- `include:rules` (static `when: never` skipping)
- `needs` (DAG dependencies, optional needs, `$[[ matrix.* ]]` in
  `needs:parallel:matrix`)
- `parallel:matrix` (job expansion)
- `variables` (global and per-job, with expansion)
- `inherit:default`, `inherit:variables`
- `before_script`, `script`, `after_script`
- `run:` steps (GitLab Functions -- local `script:`, local `func:`, OCI `func:`)
- `image`, `services`, `tags`
- `rules`, `when`, `allow_failure`
- `artifacts` (paths, exclude, expire_in, when, reports), `cache`
- Hidden jobs (`.name` used as templates)
- `default:` block

## Supported GitHub Actions Features

- Workflow `on:` triggers
- Job `needs:` (DAG dependencies)
- `strategy.matrix` (job expansion with `${{ matrix.* }}` expressions)
- `env:` variables (workflow-level and job-level, with `${{ env.* }}` expansion)
- `${{ github.* }}` context expressions (auto-detected from local git repo)
- `runs-on:` labels
- `container:` image
- Per-step metadata (`uses`, `run`, `with`, `env`, `if`)
- `uses:` action expansion -- fetches action repos, caches them, and expands
  node/composite actions into executable script lines
- `${{ }}` expression evaluation (comparisons, boolean operators, ternary
  patterns like `runner.os == 'Windows' && 'win-val' || 'linux-val'`)
- Step output propagation (`${{ steps.X.outputs.Y }}` resolved as bash
  variables via `GITHUB_OUTPUT` sourcing between steps)
- Step-level `if:` conditions (with static evaluation where possible)
- `timeout-minutes`
- Job-level `if:` conditions
- GitHub predefined variables (`GITHUB_*`, `RUNNER_*`, `CI`)

## Configuration

cui-cui supports layered config files in TOML format. Settings are merged in
order (later wins):

1. **User config**: `$XDG_CONFIG_HOME/cui-cui/config.toml` (defaults to
   `~/.config/cui-cui/config.toml`)
2. **Repo config**: `.cui-cui.toml` in the target directory
3. **CLI arguments** (highest priority)

Example `.cui-cui.toml`:

```toml
# Theme: "light", "dark", or "auto" (default)
theme = "dark"

# GitLab instance URL
gitlab-url = "https://gitlab.freedesktop.org"

# Disable overlay mount for containers
no-overlay = true

# Forward host environment variables into containers
pass-env = ["GITHUB_TOKEN", "DOCKER_PASSWORD"]

# CI config from a separate repo
ci-config = "~/Src/kernel-ci/"

# Jobs/stages to skip in --dispatch mode (treated as passed for dependencies)
dispatch-skip = ["container-prep", "stage=deploy"]

# Container image for git operations (worktree checkout, apply) in dispatch mode
git-image = "docker.io/alpine/git:latest"

# Allow unresolved extends (missing templates skipped with warning)
allow-unresolved = true
```

All fields are optional. Missing files are silently ignored.

## Remote Runners

cui-cui can dispatch jobs to remote hosts via SSH. Declare runners in
`.cui-cui.toml` with tags, and when a job's tags match a runner, the job is
executed on that host instead of in a local container.

### Runner declaration

```toml
[[runner]]
name = "linux-builder"         # human-readable name (required)
host = "build-server.local"    # SSH host (omit for local runners)
tags = ["kvm", "x86_64"]       # tags this runner handles
no-tags = true                 # also pick up untagged jobs (default: false)
concurrency = 4                # max parallel jobs on this runner (default: 1)
user = "ci"                    # SSH user (default: current user)
port = 2222                    # SSH port (default: 22)
working-dir = "/opt/ci/trees"  # remote worktree base (default: ~/.cui-cui/worktrees)
binary = "/usr/local/bin/cui-cui"  # path to cui-cui on remote (default: "cui-cui" in PATH)
```

### Tag matching

A runner matches a job when the runner's tags are a **superset** of the job's
tags (case-insensitive). Untagged jobs only match runners with `no-tags = true`.
The first matching runner wins.

When runners are configured, their tags are automatically added to the dispatch
tag filter so tagged jobs are not skipped.

### Execution modes

**Binary mode** (no `exec-prefix`): Requires the cui-cui binary installed on the
remote. Code is pushed via `git push`, then `cui-cui --run <job>` is invoked
over SSH. The remote reuses the full container/volume strategy (identical to
local dispatch).

**Exec-prefix mode** (with `[runner.custom]`): No binary needed on the remote.
A wrapper shell script is generated locally, shipped to the remote, and
executed. The custom command replaces the container runtime:

```toml
[[runner]]
name = "vng-tester"
tags = ["kvm"]

[runner.custom]
command = "vng --run --"       # becomes: vng --run -- ./.cui-cui-job.sh
```

### Lifecycle hooks

`prepare` and `cleanup` run before and after the job. Cleanup is guaranteed
via shell `trap EXIT` (runs on success, failure, and signals). All job
variables are exported before any phase, so prepare/cleanup can reference
them (`$CI_JOB_IMAGE`, `$CI_JOB_NAME`, custom variables, etc.).

```toml
[[runner]]
name = "mac-tester"
host = "mac-mini.local"
tags = ["macos"]

[runner.custom]
command = "limactl shell $CI_JOB_NAME --"
prepare = "limactl start $CI_JOB_NAME"
cleanup = "limactl stop $CI_JOB_NAME"
```

Use `@file` to reference a local script instead of inline commands. The path
is relative to `.cui-cui.toml` (or absolute):

```toml
[runner.custom]
command = "limactl shell $INSTANCE --"
prepare = "@runners/lima-prepare.sh"
cleanup = "@runners/lima-cleanup.sh"
```

(Here `$INSTANCE` is exported by the prepare script; see
[`examples/lima-runner/`](examples/lima-runner/) for reference scripts.)

### Container tuning

For binary-mode runners, the `[runner.container]` section configures the
container runtime:

```toml
[[runner]]
name = "linux-builder"
host = "build-server.local"
tags = ["kvm", "x86_64"]

[runner.container]
kernel = "$LIMA_KEXEC_ARTIFACT"  # --kernel flag (expanded from job variables)
memory = "8G"                    # -m flag
cpus = 4                         # --cpus flag
```

### Artifacts

Artifacts transfer automatically between runners. The local host acts as hub:

1. After a remote job succeeds, its artifacts are downloaded to the local cache
2. Before a remote job runs, needed artifacts from previous jobs are uploaded

Binary mode uses the remote's artifact cache; exec-prefix mode transfers files
directly to/from the remote worktree.

### SSH requirements

- SSH agent must be running (or key-based auth without passphrase)
- `BatchMode=yes` is used -- no interactive password prompts
- The remote user needs write access to `~/.cui-cui/worktrees/` (or the
  configured `remote-dir`)

### Local runners

Omit `host` for a local runner. Local runners require a custom command
(without it, just use regular container dispatch):

```toml
[[runner]]
name = "vng-local"
tags = ["kvm"]

[runner.custom]
command = "vng --run --"
```

## Kernel Testing with Lima

A common use case for remote runners is Linux kernel CI: build the kernel on a
remote Linux server, then test the compiled kernel by booting it in a
[Lima](https://lima-vm.io/) VM (e.g. on macOS or another host with
hardware-specific capabilities).

### Example `.gitlab-ci.yml`

```yaml
stages:
  - build
  - test

build-kernel:
  stage: build
  tags: [kvm, x86_64]
  image: fedora:latest
  script:
    - make -j$(nproc) vmlinux modules
  artifacts:
    paths:
      - vmlinux
      - modules/

kernel-test:
  stage: test
  tags: [macos, arm64]
  image: registry.example.com/fedora-bootc:41
  needs: [build-kernel]
  variables:
    LIMA_KEXEC_ARTIFACT: "vmlinux@build-kernel"
  script:
    - make -C tools/testing/selftests run_tests
```

### Example `.cui-cui.toml`

```toml
ci-config = "~/Src/kernel-ci/"

# Build the kernel on a remote Linux server (binary mode -- cui-cui required)
[[runner]]
name = "linux-builder"
host = "build-server.local"
tags = ["kvm", "x86_64"]
no-tags = true
concurrency = 2

# Test the kernel in a Lima VM on macOS (custom runtime -- no binary needed).
# The prepare script converts the job's bootc image to qcow2 (cached),
# boots a Lima VM, and optionally kexecs into a compiled kernel.
[[runner]]
name = "mac-tester"
host = "mac-mini.local"
tags = ["macos", "arm64"]

[runner.custom]
command = "limactl shell $INSTANCE --"
prepare = "@runners/lima-prepare.sh"
cleanup = "@runners/lima-cleanup.sh"

# Alternative: test locally in a virtme-ng VM (no SSH, no binary needed)
[[runner]]
name = "vng-tester"
tags = ["kvm"]

[runner.custom]
command = "vng --run --"
```

### Workflow

1. `cui-cui --dispatch .` resolves the pipeline and runs jobs in order
2. `build-kernel` matches `linux-builder` -- code is pushed via SSH, the
   remote cui-cui builds the kernel in a container, and artifacts (`vmlinux`,
   `modules/`) are downloaded to the local cache
3. `kernel-test` matches `mac-tester` -- artifacts are uploaded, the prepare
   script converts the bootc image (`$CI_JOB_IMAGE`) to a qcow2 disk via
   [bootc-image-builder](https://github.com/osbuild/bootc-image-builder),
   boots a Lima VM, kexecs into the compiled kernel (`$LIMA_KEXEC_ARTIFACT`),
   and runs the test script inside the VM via `limactl shell`.
   Cleanup deletes the VM (guaranteed via `trap EXIT`)

See [`examples/lima-runner/`](examples/lima-runner/) for reference prepare and
cleanup scripts.

## License

This program is free software; you can redistribute it and/or modify it under
the terms of the GNU General Public License version 2 as published by the Free
Software Foundation. See [COPYING](COPYING) for the full license text.
