```
Everything else is CSS. `packages/renderer/src/terminalogue.css` declares the palette as
custom properties (`--tlg-bg`, `--tlg-fg`, `--tlg-prompt`, `--tlg-accent`, `--tlg-cursor`,
the control tokens, and the window-decoration tokens `--tlg-dot-*` and `--tlg-mark*`) on
`.tlg`, and each theme is one block of
overrides keyed off `.tlg[data-theme='…']`. `dark` needs no block at all: it *is* the base
palette, which is why an untouched pre-v0.3 document looks untouched. No theme adds,
removes or restyles an element, so there is exactly one DOM to reason about — and there is
no theme code in either host adapter (a test asserts that too).
`packages/marp` is the third adapter, and it is as thin as the other two. It is a
markdown-it plugin and a Marp CLI functional engine, in about two hundred lines: the fence
rule runs `parseTerminalogue` and writes the resulting document into an inert placeholder,
and one core rule appends the stylesheet and a browser runtime whose only job is to call
the same `mountTerminalogue`. There is no terminal DOM, no animation and no playback logic
anywhere in it.
```ts
export const terminalogueEngine = ({ marp }) => marp.use(terminaloguePlugin);
```
The split is deliberate: the parser runs once, in Node, while Marp is converting, and the
renderer runs in the browser, on the slide. What crosses between them is a
`TerminalogueDocument` as JSON, percent-encoded into a data attribute — never markup.
The host adapters are thin, and none of them reimplements any terminal DOM, CSS or
animation: the VS Code preview script, the Obsidian code block processor and the Marp
browser runtime all call the same `mountTerminalogue`, and all three ship a byte-identical
copy of `packages/renderer/src/terminalogue.css` (tests assert this).
---
## Development
Requires Node 20+ and pnpm.
```bash
pnpm install
pnpm build # builds core, renderer and marp, then the three apps
pnpm test # vitest for the shared packages, node:test for the host adapters
pnpm lint
pnpm check # build + lint + typecheck + test
pnpm lint:obsidian # what Obsidian's plugin review says about this repository
```
`pnpm lint:obsidian` is a second, slower pass with
[`eslint-plugin-obsidianmd`](https://github.com/obsidianmd/eslint-plugin) — the same rules
the community directory's review runs, including the type-aware ones. It is deliberately
not part of `pnpm check`: `eslint.config.js` says what this repository has decided, and
`eslint.obsidian.config.mjs` says what Obsidian will say about it, which is a question worth
asking before a submission rather than on every commit. It reads the root `manifest.json`
the way the reviewer does, so `isDesktopOnly: false` there holds every file in the
workspace — Terminalogue Presenter's included — to the mobile rules.
The shared packages are TypeScript project references, which is what lets that pass, and an
editor, resolve `@terminalogue/core` in a fresh clone before anything has been built. Import
a workspace package from a new one and add it to that package's `references` too, or its
types silently become `any`.
Watch mode for the hosts:
```bash
pnpm --filter terminalogue-vscode watch
pnpm --filter terminalogue-obsidian watch
pnpm --filter terminalogue-obsidian-presenter watch
```
Deploy either Obsidian plugin into a vault for manual testing. Call the script directly —
`pnpm --filter … deploy` collides with pnpm's own reserved `deploy` command:
```bash
node apps/obsidian/scripts/deploy.mjs "/path/to/your/vault"
node apps/obsidian-presenter/scripts/deploy.mjs "/path/to/your/vault"
```
Two files are generated at build time and are not in the repository:
`packages/marp/src/generated/assets.ts` (the shared stylesheet and the bundled browser
runtime, as string constants) and `apps/obsidian-presenter/src/generated/engine-source.ts`
(the bundled Marp engine). Both are produced by their package's build script, which
`pnpm build`, `pnpm typecheck` and `pnpm test` all run first.
### Publishing the VS Code extension
`apps/vscode` carries everything the Marketplace listing needs: `icon.png`, `LICENSE`,
its own `README.md` (the listing body) and `CHANGELOG.md` (the Changelog tab), plus the
`publisher`, `repository`, `homepage` and `bugs` fields. Packaging reports no warnings.
```bash
cd apps/vscode
npx @vscode/vsce login
# once, with a Personal Access Token
npx @vscode/vsce publish --no-dependencies
```
`"private": true` stays in `apps/vscode/package.json` on purpose: `vsce` ignores it, while
it keeps the package from ever being published to npm by accident.
### Regenerating the README animations
Both GIFs under `docs/images` are recordings of the real thing: the shared parser, the
shared renderer and the shared stylesheet. Each one is driven by a block in
[`scripts/demo`](scripts/demo), and both scripts take the demo's name:
| GIF | Block | Name |
| --- | --- | --- |
| `terminalogue.gif` | [`demo.termlogue`](scripts/demo/demo.termlogue) | `demo` (the default) |
| `terminalogue-typo.gif` | [`typo.termlogue`](scripts/demo/typo.termlogue) | `typo` |
They are worth regenerating when the terminal's appearance changes, and worth nothing at
all if they drift into showing a version of the terminal that no longer exists.
```bash
pnpm build
npx playwright install chromium
node scripts/demo/build-page.mjs typo
node scripts/demo/record.mjs typo
```
Playwright is deliberately not a dependency of this repository — it is fetched for the rare
occasion this is done — and the pages and frames land in the ignored `dist-demo/`. Each
frame records when it was taken, and `frames.txt` carries those durations, so the GIF plays
back at the speed the animation actually ran rather than at a nominal frame rate:
```bash
ffmpeg -y -f concat -safe 0 -i dist-demo/frames/typo/frames.txt -filter_complex "[0:v] fps=12,scale=900:-1:flags=lanczos,split [a][b];[a] palettegen=stats_mode=diff [p];[b][p] paletteuse=dither=bayer:bayer_scale=3:diff_mode=rectangle" -loop 0 docs/images/terminalogue-typo.gif
```
A recording is reproducible rather than lucky. Jitter is pinned by `jitterMin === jitterMax`
and the renderer's randomness comes from a seeded generator in the recording page, with the
seed for each demo named in `build-page.mjs` — so where `@typo` puts its mistakes is a
choice, and the only thing that changes a recording is the block it plays.
One thing to know before adjusting `typo.termlogue`: the recorder manages a frame roughly
every 100ms, and a wrong character is only on screen for one typo beat. That block runs at
`@speed 150ms` precisely so the beat is 270ms and each mistake spans three frames. At a
brisk typing speed the slip is captured once, if at all, and the GIF shows a flicker rather
than a correction. The arithmetic is one line: the beat is the typing speed times 1.8.
### Publishing the Obsidian plugin
Obsidian's community directory installs a plugin from a GitHub repository, and it looks in
two fixed places: `manifest.json` at the **HEAD of the default branch** tells it which
version is current, and the GitHub release **tagged exactly that version** — `0.5.1`, never
`v0.5.1` — carries `manifest.json`, `main.js` and `styles.css` as individual assets.
`versions.json` in the root maps each published version to the Obsidian version it needs, so
an older app can still find a release it can run.
That is why this repository has a root `manifest.json` and `versions.json` at all, and why
`pnpm build` also copies the built `main.js` and `styles.css` there: a plugin repository is
expected to have all four at its root, and Obsidian's review tooling looks for them. The
copies are build output and are ignored by git. They belong to `apps/obsidian`, which stays
the plugin's real home:
```bash
pnpm release:obsidian:sync
```
regenerates both from `apps/obsidian/manifest.json`, then verifies the release and collects
it into `dist-release/obsidian/`. It refuses to collect anything it cannot vouch for — a root
manifest that has drifted, a missing `versions.json` entry, a workspace version that
disagrees with the plugin's, a `styles.css` that is not the shared stylesheet byte for byte,
or a `main.js` older than the sources it was built from. That last pair matters more than it
looks: nothing about a stale bundle looks wrong in a release, and `vsce` and `gh` will both
happily ship one. The Obsidian test suite fails on the same drift, so `pnpm test` catches it
before the release script does.
`pnpm release:obsidian` runs the same checks without touching the root files, which is what
you want in CI or before a release you have not changed anything for. Editing the plugin's
manifest — the author, say — and then running it is the one way to see it refuse: it names
the field that moved and asks for `--sync`.
It publishes nothing: it prints the `gh release create` command and stops.
Submission itself happens once, at
[community.obsidian.md](https://community.obsidian.md) — sign in with an Obsidian account,
connect GitHub so the directory can verify the repository is yours, claim it, then
**Plugins > New plugin** with the repository URL. Every release after that is found from the
manifest and the tag alone, with no further submission. Review feedback is answered by
publishing a **new release at a higher version**, never by moving a tag onto the same one.
One repository advertises one plugin, because there is only one root `manifest.json`. This
one is **Terminalogue**; Terminalogue Presenter needs a repository of its own before it can
be submitted.
The test suite covers the parser (commands, output, all eight directives, escapes, unknown
directives, malformed durations, preserved trailing whitespace, blank lines, CRLF,
transcript and command extraction, plus every theme name, case-insensitive matching, the
`dark` default, unknown themes and duplicate `@theme`), the renderer under fake timers
(typing progression for both `$ command` and `@type`, pause/resume mid-frame, restart,
`@wait`, `@pause` at every speed, the 1× / 2× / 4× / Instant multipliers, clipboard copying
through an injected adapter, and that `destroy()` leaves behind no timer — animation or
copy-feedback — and no observer), the markdown-it plugin, and both Markdown host adapters
running their real built bundles in jsdom — including that re-rendering a document does not
stack a second animation.
The Marp adapter is tested against a real Marp Core instance as well as a bare markdown-it:
that the fence is detected and every other fence is left alone, that the core parser is what
produced the payload, that themes, `@type`, `@pause`, `@wait`, `@clear`, `@speed` and
`@typo` all
survive the conversion, that multiple blocks stay independent, that the runtime is injected
once and only into a deck that has a block, that the stylesheet lands in Marp's own `` are diagnostics, not
styling. Nothing in the DSL generates a `style` attribute or a stylesheet, and the only
value a document contributes to a CSS selector is `data-theme` — which the renderer
re-checks against the allowlist before writing it.
- A size is two integers and nothing else. `@size` accepts `x` within
documented limits and rejects everything else, so `@size 80x24; background:url(…)` is a
diagnostic rather than a declaration. The renderer re-validates the pair it is handed and
writes the two **numbers** into CSS custom properties; the arithmetic that turns them
into a width and a height lives in the stylesheet, so no document text ever reaches a
style. A block with no `@size` gets no `style` attribute at all.
- Block content is never treated as trusted HTML. Text reaches the DOM through
`textContent`; the VS Code placeholder carries the block source percent-encoded in a data
attribute, so `` and `
` render as
literal terminal text — from an output line, a title, a prompt or an `@type` alike. There
are tests for exactly this, and the markup sinks are banned by lint.
- Terminalogue Presenter starts exactly one program: the Marp CLI its setting names. It
does that with `spawn` and `shell: false`, with the executable and its arguments kept
separate, and it never builds a shell command out of a path — see
[Processes and paths](#processes-and-paths). A `termlogue` block is not an input to it:
Terminalogue Presenter converts the *file*, and what Marp does with a `termlogue` block
is render it, exactly as VS Code and Obsidian do.
- A generated Marp presentation makes no network request either. The stylesheet and the
runtime are inlined into the HTML, so a presentation works offline, on a plane, from a
USB stick.
If you are looking for a tool that *runs* the commands in your document, Terminalogue is
deliberately not it.
---
## Not in v0.6
v0.6 adds `@typo` and nothing else, and a typo is deliberately one thing: the key
immediately to the left or right on the same QWERTY row, a Backspace, the intended
character. Everything around that is left out — vertical neighbours, key geometry and
physical distance, AZERTY, QWERTZ, Dvorak, Colemak and Japanese layouts, a configurable
layout at all, Shift and Caps Lock simulation, doubled, dropped or transposed characters,
stray or repeated Backspaces, typo dictionaries, AI-generated or statistically modelled
mistakes, per-key probabilities, typo sounds, an on-screen keyboard, typo history, and any
mark on the finished transcript saying a typo happened. No dependency was added for it, and
no speculative abstraction either.
v0.5's exclusions still stand. It added `@size x` and nothing else, leaving
out `@cols`, `@rows`, `@width`, `@height`, pixel and percentage sizes, `@wrap`, a
horizontal-scrolling mode, terminal resize animation, a runtime resize handle, a GUI size
editor, auto-fitting or responsive font scaling, per-theme size overrides, a
presentation-specific size syntax, size inference from the content, and any way to change
the size during playback.
v0.4's exclusions still stand: PDF, PPTX, PNG, GIF and MP4 export, static (non-animated)
rendering for print, Marp for VS Code preview integration, an embedded Marp editor, a custom
presentation window, Docker-based or remote Marp execution, cloud export, automatic
installation of Marp CLI, and any AI feature at all.
v0.3's exclusions still stand too: user-defined themes, arbitrary colours, arbitrary CSS, a
theme editor, a runtime theme selector, theme-switching animation, theme auto-detection
from the OS or from VS Code's or Obsidian's own theme, further palettes (Solarized,
Dracula, Nord, Gruvbox, macOS Terminal, Git Bash, Windows Terminal), background images,
transparency, configurable fonts and a font-size directive, and logos or vendor icons of
any kind.
Still left out from before: real shell execution, terminal recording, asciinema/VHS import,
syntax highlighting, fullscreen, masked passwords and `@type --masked`, mouse animation,
key simulation such as `@key`, `Ctrl+C`, arrow keys or Tab completion, marker navigation
over `@pause` labels, a seek bar, timeline, progress bar or spinner, an Obsidian-specific
editor UI, and a custom VS Code webview.
Nothing is tightly coupled in a way that would prevent adding any of them later.
---
## License
MIT. See [LICENSE](LICENSE).