An advanced automation utility to script, play back, and capture frame-accurate GUI applications and terminal environments under a virtual Sway compositor. Eliminate flake, compile fast, and update READMEs automatically in CI/CD.
Installs the v0.1.0 static binary to ~/.local/bin —
.deb / .rpm / .tar.gz packages on
Codeberg Releases.
Click nodes to inspect data flows, socket symlinks, and background processes.
Wayreel configuration is completely declarative. It parses both structured JSON files and a simplified, human-friendly VHS-like DSL format.
Configurations are deeply nested and composable: you can declare global default configs in wayreel.json, then override specific output files, themes, and shell environments in individual .json or .reel files.
# DSL Reel script
REEL main
mode = tui
theme = dark
output = reels/demo.webm
T echo "Compiling..."
Enter
P 2s
Select a pre-recorded demo reel, and inspect the code side-by-side.
Loading DSL configuration...
Standard terminal recorders force you to wait in real time. If a setup compilation takes 60 seconds, standard tools take 60 seconds of CPU and runner wall-clock time.
Wayreel solves this via Fast Mode: it accelerates virtual keyboard keystrokes and script pauses inside the compositor by a speed factor (e.g. 3.0x), records frames at a matching high rate (90 FPS), and re-encodes back to normal speed (30 FPS) using ffmpeg timestamp manipulation.
Write Wayreel DSL script commands, compile instantly to JSON, and load it into our desktop simulator workspace.
Loading compilation...
Wayreel's native Go implementation bypasses sandboxing restrictions, ensures file safety, and handles cleanup.
To isolate recorded window actions, Wayreel creates a custom temporary runtime folder. However, Sway needs to open windows nested inside your host session.
Wayreel achieves this using a socket symlink bridge: it links the active host compositor socket (e.g. wayland-0) to the sandboxed runtime, letting Sway run nested, while assigning target applications to Sway's newly created nested socket (e.g. wayland-1).
// Go logic to link compositor sockets
hostWayland := os.Getenv("WAYLAND_DISPLAY")
if hostWayland != "" {
src := filepath.Join(baseRuntime, hostWayland)
dst := filepath.Join(runtimeDir, hostWayland)
err = os.Symlink(src, dst)
if err != nil {
log.Warn("Failed to bridge socket: %v", err)
}
}
Security mechanisms in Wayland prevent applications from sniffing or injecting global keyboard events.
Wayreel works around this by utilizing wtype, which connects directly to the compositor using the wlr-virtual-keyboard-v1 protocol. It feeds keyboard symbols step-by-step with safety buffer pauses (e.g. 10ms to 50ms) to prevent characters from dropping or lagging.
// wtype virtual keyboard interface invocation
// Triggered on the isolated WAYLAND_DISPLAY socket
func TypeString(disp string, keys string, speed float64) {
cmd := exec.Command("wtype", "-d", "40", "-s", "10", keys)
cmd.Env = append(os.Environ(), "WAYLAND_DISPLAY=" + disp)
cmd.Run()
}
Running headless graphical servers involves launching Xvfb displays, Sway composers, and foot terminals. If a recording fails, orphaned processes can lock ports or leak memory.
Wayreel sets process-group IDs (Setpgid: true) on all child launches. An active Signal listener captures interrupts (like Ctrl+C) and terminates the entire process group hierarchy recursively.
// Process group process monitoring
cmd := exec.Command(binary, args...)
cmd.SysProcAttr = &syscall.SysProcAttr{
Setpgid: true, // Assign process group leader
}
trackPID(cmd.Process.Pid)
// Triggered on termination interrupts
func KillProcessGroup(pid int) {
syscall.Kill(-pid, syscall.SIGKILL) // Recurse group
}
A clear overview of system requirements, dependencies, and known edge-cases.
wtype (keyboard injection), wf-recorder (high frame capture), ffmpeg (stretch & scaling output), grim (Wayland screenshot capture, required for IMG steps).IMG / I)IMG or I steps anywhere in a reel script to capture PNG screenshots alongside video recording.mode=fullscreen (default) captures the entire nested display; mode=app crops to the focused window via swaymsg geometry.path= for an exact output path, prefix= for a directory with auto-generated filename, or base= for a filename in CWD. Omitting all uses an auto name in CWD.$ts expands to the capture timestamp (yyyy-mm-dd-hhmmss); $desc expands to the step description. Useful for unique filenames: path="shots/$ts-$desc.png".base="latest.png" to always overwrite the same file — ideal for README badges and live documentation.T "git log --oneline -5"
K <cr>
P 1s
# app window crop, timestamped
I mode=app prefix="shots" desc=git-log
# always overwrite for badges
I base="shots/latest.png"