demo-it runs transcript-driven demonstrations and presentations via CLI and nvim plugin frontends.
It provides:
demo-itddaemondemo-itCLI frontendDemoIt*nvim plugin
flake.nix+devenv.nixprovide a reproducible Go shell.envrcintegratesdirenv+devenv- common checks:
fmt,lint,tests,tests-e2e,build,ci - shared formatting config:
treefmt.toml(used by bothnix fmtandfmt) devenv uprunsdemo-itdwith auto-reload via.air.toml- inside the devenv shell, project-local shims for
demo-itanddemo-itdare placed ahead of user-profile installs onPATH, andDEMO_IT_SOCKETdefaults to$DEVENV_ROOT/.devenv/demo-itd.sockwithDEMO_IT_REQUIRE_LOCAL_DAEMON=1
demo-it exports a Home Manager module as homeManagerModules.default (alias: homeManagerModules.demo-it) so you can run demo-itd as a user service.
- Add the flake input:
# flake.nix
inputs.demo-it.url = "github:dejanr/demo-it";- Wire the module into Home Manager.
If you use Home Manager through NixOS/nix-darwin, add it to sharedModules:
home-manager.sharedModules = [
inputs.demo-it.homeManagerModules.default
];If you use standalone Home Manager, import it in your home config:
{
imports = [ inputs.demo-it.homeManagerModules.default ];
}- Enable the service in your home configuration:
{
services.demo-it = {
enable = true;
};
}When enabled, the module:
- installs
demo-itanddemo-itd - starts
systemd --userservicedemo-itd - exports
DEMO_IT_SOCKET(by default) so CLI commands target the same daemon
The flake also exports a Neovim plugin package as packages.<system>.demo-it-nvim.
Example usage from another flake:
extraPlugins = [
inputs.demo-it.packages.${pkgs.stdenv.hostPlatform.system}.demo-it-nvim
];This is the recommended way to share the plugin with nixvim/Home Manager setups.
Use start to reset/create deterministic tmux sessions:
When using the devenv shell, demo-it expects the local daemon socket and will fail fast with a run 'devenv up' first hint if it is not running.
demo-it start # bootstrap from current working directory
demo-it start ./examples/tmux-splits
demo-it start ./examples/no-slidesUse record to open an isolated <workspace>-record tmux session and emit a demo-it block from captured pane-management actions and typed keys:
demo-it record --title "Recorded split flow"
demo-it record --yes > test.md
demo-it record --yes -f test.mdrecord is intended for authoring demo-it.md blocks from short live tmux interactions.
- starts an isolated
<workspace>-recordsession - asks for Enter confirmation before handoff to the record session (
--yes/-yskips this) - captures pane semantics via tmux hooks (
split-pane,split-pane-vertical,kill-pane/killall-pane) - captures typed shell input as
key-macroactions - groups key streams into one macro until
Enter(then starts a new macro) - applies action-level
delay_ms: 500at the start of each generated macro and preserves per-keydelay_mswhen timing is available - finishes when recording shells exit (
Ctrl-D) and prints a fenceddemo-itblock to stdout (or writes it with-f <file.md>)
Example generated action shape:
- kind: key-macro
delay_ms: 500
keys:
- key: C-r
delay_ms: 240
- key: e
- key: c
- key: h
- key: o
- key: EnterPath mode is still supported and is equivalent to demo-it start <workspace-path> (or demo-it start <transcript-file.md>):
demo-it ./examples/tmux-splits/
demo-it ./examples/tmux-splits/demo-it.mdFor examples/tmux-splits, this creates:
tmux-splits-demotmux-splits-notes
It opens/switches to tmux-splits-demo; open tmux-splits-notes manually when needed. Workspace bootstrap also starts the daemon run context so demo-it run-status and demo-it next are available immediately. In single-workspace mode, demo-it start first kills previously managed demo-it tmux sessions, then creates the new workspace sessions. demo-it start accepts either a workspace directory (expects <workspace>/demo-it.md) or a transcript file path directly (for example ./test.md). Slide assets are optional: action-only transcripts (for shell demos) also work, see examples/no-slides.
When demo-it opens slides in Neovim panes, it now also runs :DemoItPresentationEnable (silently when available) so presentation-friendly UI defaults are applied automatically. Demo tmux sessions also start with status off for a cleaner stage view.
Session utilities:
demo-it statusshows the active managed workspace/session statusdemo-it run-statusshows daemon run-state JSONdemo-it notesopens the notes session for the active workspacedemo-it showopens the demo session for the active workspacedemo-it trace-nexttraces active demo-pane output while executingnextand writes.demo-it/traces/*.logplus normalized.txtsnapshotsdemo-it record [--title <text>] [--yes] [-f <file.md>] [workspace-path]opens<workspace>-record, captures tmux pane actions plus typed shell keys, and emits ademo-itblock when recording shell exitsdemo-it killkills managed demo-it tmux sessions- inside managed demo-it tmux sessions,
C-sthennrunsdemo-it next, andC-sthenprunsdemo-it prev
Inside a -record session, you can drive normal tmux/shell workflows (including split-window -h / split-window -v) and then exit with Ctrl-D; record will print a generated block you can paste into demo-it.md, or write it directly via -f (use --yes to skip the handoff confirmation prompt).
For action-level debugging, set DEMO_IT_DEBUG_LOG to a file path before running commands:
DEMO_IT_DEBUG_LOG=/tmp/demo-it.log demo-it start ./examples/key-macro
DEMO_IT_DEBUG_LOG=/tmp/demo-it.log demo-it nextSet DEMO_IT_DEBUG_LOG on start so tmux keybindings inherit the same debug log path.
The log records tmux commands, pane targeting resolution, and key-macro step playback.
For pane-level tracing, run:
demo-it trace-nextThis taps the active pane in the latest demo session using tmux pipe-pane, runs next, writes raw output to <workspace>/.demo-it/traces/<timestamp>-next-pane-<id>.log, and writes a normalized textual snapshot to the same path with .txt extension.
Snapshot coverage for this flow is available via:
tests-e2e
# equivalent:
go test -tags=e2e ./test/e2eFor local development, you can force tmux keybindings to use a specific binary via DEMO_IT_PATH on start:
DEMO_IT_PATH=./bin/demo-it demo-it start ./examples/key-macroOn start, demo-it also writes tmux global environment values (DEMO_IT_PATH, DEMO_IT_RUN_ID, DEMO_IT_SOCKET) so prefix bindings can execute with matching runtime context.
If DEMO_IT_PATH is empty, keybindings use demo-it from PATH.
Minimal tmux prefix bindings (for custom tmux configs), including inline error messages:
bind -N "demo-it next" Space run-shell -b 'out=$("${DEMO_IT_PATH:-demo-it}" next 2>&1); code=$?; [ "$code" -eq 0 ] || tmux display-message "demo-it next failed: $(printf "%s" "$out" | tr "\n" " ")"'
bind -N "demo-it prev" BSpace run-shell -b 'out=$("${DEMO_IT_PATH:-demo-it}" prev 2>&1); code=$?; [ "$code" -eq 0 ] || tmux display-message "demo-it prev failed: $(printf "%s" "$out" | tr "\n" " ")"'If demo-it.md contains demo-it fenced blocks, bootstrap runs the first block immediately. Slides are optional: blocks can be pure tmux/shell actions, or they can use slide: ... / open-slide (which auto-start Neovim in the demo pane and open that slide, so insert-text: nvim/key:return is no longer required). speaker_notes are intended as post-action guidance before moving to the next block, and are rendered in demo-notes (Neovim scratch buffer) and refreshed as demo-it next/demo-it prev move through steps for the latest workspace without writing workspace notes files. Use auto_slide_in_ms inside a block to auto-trigger next after the given delay (with a step guard so manual navigation cancels the pending auto-advance). Use killall-pane to kill all extra panes and keep only the initial pane (lowest pane index). Use split-pane (right) or split-pane-vertical (down) for tmux layout changes while keeping the presenter pane focused; use kill-pane to close the latest pane (highest pane index) in the target session; use key-macro for timed key playback (interval_ms + per-key delay_ms) with optional pane targeting (pane: active|last|left|right|<index>|<pane-id>); and use slide shorthands (slide: ..., open-slide, or key with slide) to open markdown files in a Neovim pane of the demo session.
Example key-macro action:
- kind: key-macro
pane: last
interval_ms: 80
keys:
- key: i
- key: h
- key: return
delay_ms: 250The repository now includes a local plugin runtime:
plugin/demo-it.lualua/demo-it/init.lua
To iterate quickly from Neovim:
:set rtp+=/home/dejanr/projects/demo-it:lua require("demo-it").setup():DemoItReload
Available commands:
:DemoItStart [workspace-path](defaults to current working directory; enables presentation mode on success):DemoItStatus,:DemoItNext,:DemoItPrev:DemoItRerun,:DemoItReloadState:DemoItJump <id|index>:DemoItFocus <present|return|none>:DemoItPresentationEnable,:DemoItPresentationDisable,:DemoItPresentationToggle:DemoItPreview,:DemoItPreviewStop:DemoIt <raw cli args...>
DemoItPresentation mode borrows the simple parts of zen/editing plugins: it reduces UI noise (line numbers/signs/status UI), adds light reading padding (winbar, scrolloff, sidescrolloff, extra fold column), turns on wrapping for easier reading, suppresses markdown diagnostics while active, and switches Markdown conceal between rendered (Normal) and raw (Insert) editing modes.
DemoItPreview prefers :LivePreview start when available and falls back to :MarkdownPreview.
If Neovim runs inside Kitty with remote control enabled (KITTY_LISTEN_ON), presentation mode also bumps terminal font size and restores it when disabled.
You can configure presentation font sizing when setting up the plugin:
require("demo-it").setup({
presentation = {
disable_markdown_diagnostics = true, -- default: true
font = {
neovide_scale = 1.25, -- multiply current neovide_scale_factor
kitty_delta = 3, -- kitty @ set-font-size +3 while enabled
},
},
})Formatting alignment tip:
- configure Neovim formatters to call
treefmt --stdin <file> --quietso editor formatting matchesnix fmt/fmt.
This project is strongly inspired by Howard Abrams' demo-it.
I watched his presentation a long time ago, and it stayed with me.
Many ideas here are a direct nod to his work.
This project is licensed under the MIT License (see LICENSE).