This is a fork (
BlueCheeseburger/cardmirror) ofant981228/cardmirror. Changes added on top of upstream, newest first:
- The update chip shows download progress (desktop only) — the status-bar pill fills in as the update downloads and its text names the percent, instead of only appearing once the download is done. (main)
- Each pane in a multi-pane workspace now shows its own cloud-sync pill, in that pane's footer, instead of one shared pill that left it ambiguous which pane's document it applied to — compact, provider-logo-only while synced, no "Cloud" text label. (main)
- Right-click anywhere on the ribbon to name (or rename) a window (desktop only) — a named window shows that name as its title instead of the document filename, so windows are easier to tell apart at a glance; persists across mode-switch reloads. (main)
- A pane's speech-doc marking now shows through even while you're focused on it (amber wins over the focused-pane blue highlight instead of being hidden by it), Send to Speech explains itself when the cursor isn't somewhere sendable instead of silently doing nothing, and a filename with a macOS Finder-typed "/" now displays the same way it does in Finder instead of showing the raw on-disk ":" underneath it. (main)
- Custom dash can convert "--" and "---" independently, at the same time instead of picking just one; opening a moved/deleted Recent file now offers to locate it instead of just forgetting it; a reading-speed test link was added to the readers setting; Option+backtick no longer captures as "Unidentified" when rebinding a shortcut on macOS. (main)
- Rebinding a shortcut on macOS warns when it overlaps a system shortcut (Quit, Hide, Log Out, the screenshot keys, Mission Control, Full Screen — checked against the real Ctrl/Cmd keys you pressed, so a two-key combo like Control-Command-F isn't confused for a plain Cmd-F); settings/tooltips that name a shortcut now say "Cmd" instead of "Ctrl" on macOS wherever that's what actually works; and Send to Speech now tells you why it did nothing instead of silently no-opping when the speech doc is the only document open and you try to send from it to itself. (main)
- New Document and opening a file from Finder/Dock now ask which pane or window in a three-pane workspace, instead of guessing (New always spawned a whole window even with an empty pane sitting right there; external-open always went to the last-focused window even when a different one had the room). (main)
- Synced with upstream through its 1.8.0 release — pulls in the disk-conflict guard rework and cloud pill (Dropbox/OneDrive/Google Drive/iCloud folders show sync status, and a save that would overwrite a teammate's edit writes a conflicted copy instead of silently clobbering it), the single-owner flashcard store (fixes cards vanishing with two windows open), and every 1.7.0 co-editing fix. This fork's own multi-pane autosave now uses the same conflicted-copy path on a changed-on-disk write. (main)
- Save (ribbon button or Mod-S) only saves the current document again, and in three-pane mode the Save and Autosave buttons move out of the ribbon into each pane's own title chip — each pane now has its own Save/Autosave controls acting only on that pane's doc, instead of one shared pair of ribbon buttons whose target depended on which pane happened to be focused. (main)
- Ctrl/Cmd+K auto-fills the "Link to" field when the selected text already looks like a URL (has a scheme, a
www.prefix, or a recognized domain ending) — no more retyping a pasted link into the hyperlink dialog after selecting it as the display text. (main)- Closing with unsaved changes in multiple panes now shows one named dialog per document, all at once, instead of a single unnamed "You have unsaved changes" prompt repeated one at a time with no indication which doc it was about. A doc whose saved file can no longer be found also gets a "Bind new filepath…" button in place of plain Save. (main)
- The autosave-failure notice offers a "Save As…" button when the file has gone missing or its folder refused the write, instead of only telling you to do that yourself. Works for a background pane in three-pane mode too — it brings that doc into view first. (main)
- Autosave is on by default, instead of off until turned on per file — turn it off per file from the ribbon toggle (or per-pane chip in three-pane mode) same as before. (main)
- Three-pane workspace autosave now flashes the Save button on a successful save, matching single-doc mode and manual saves — it used to save silently there. (main)
- Home screen suggests reopening a whole three-pane workspace, not just individual files — closing a multi-pane window with 2+ saved docs open remembers that set, so one click puts all of them back in the same pane layout. Desktop only. (main)
- Ctrl/Cmd+K adds or removes a hyperlink — select text and press it for a small dialog (display text + URL); press it again on an existing link to remove it. (main)
- Three-pane workspace toggle in the ribbon, independently per window — no more Settings-dialog-only switch. Opening a document also now offers "New window" alongside the three slots, so a full workspace never forces you to replace an open pane. (main)
- CardMirror registers as a
.docxhandler on macOS/Windows — it now shows up (and can be set as default) in "Open With" for Word documents, not just its own native.cmirformat. (main)- Paced auto-scroll — a hands-free teleprompter that scrolls the document at your actual reading pace, slowing for dense highlighted text and speeding through everything else.
- Autosave for
.docxfiles, not just.cmir— plus a time-boxed, logged fallback for the zip worker and a ribbon button that distinguishes "saving" from "paused" (e.g. a doc with a live view / linked copy Word can't hold).- Independent document background theme — the editor surface's light/dark appearance can now be set separately from the app chrome's theme.
- Independent multi-window three-pane workspace — additional windows no longer have to mirror the main window's pane layout.
- Google Gemini as a third AI provider, alongside Anthropic and OpenRouter.
CardMirror is a debate text editor focused on high school and college
policy debate. It's a standalone
replacement for the editor side of
Verbatim, the
Microsoft Word add-in most US debate teams use: same organizational structure,
same F-key shortcuts, same send-to-speech
workflow, same Word-compatible .docx files — but with no Word, no
macros, and no add-in. That keeps it fast on the multi-megabyte
evidence files debaters work in, and enables cross-platform feature parity.
Run it as a desktop app for tournament-day work, or as a web preview in any browser for trying it out or working from a Chromebook or locked-down school machine.
Keep backups. CardMirror is stable and used daily, but no editor is bug-free — keep a copy of anything critical in a second format.
Desktop builds are unsigned, so Windows and macOS warn you the first time you launch. The one-time workaround for each is in the install steps below.
See MANUAL.md for the full user
manual; PRIVACY.md for how CardMirror handles your data;
TERMS.md for the terms of use;
CHANGELOG.md for release notes;
ARCHITECTURE.md for the full design;
PROJECT.md for project orientation.
You can find a video walkthrough of CardMirror's basic functions here.
The core CardMirror editor is free, and built and maintained by one person. If it's useful to you, two ways to help keep it going:
- Subscribe at Debate Decoded — recurring support that funds ongoing development, and the best way to keep the project moving.
- Tip once on Ko-fi — a one-time contribution, no account needed. (Or use the Sponsor button at the top of the repo.)
Desktop builds live on the Releases page. Pick the file for your operating system, run the installer, and launch CardMirror like any other app.
-
Download
CardMirror-x.x.x-universal.dmg— one download that runs natively on both Apple Silicon (M1 and later) and Intel Macs. -
Open the
.dmg, drag CardMirror to your Applications folder. -
First launch only. Gatekeeper refuses to open unsigned apps. You'll see one of two messages depending on your macOS version:
-
"can't be opened because Apple cannot check it for malicious software" → Open Finder → Applications, right-click (or Control-click) CardMirror → Open. Click Open in the confirmation dialog. From then on, normal double-click works.
-
"is damaged and can't be opened" → macOS has hard-quarantined the app and the right-click workaround doesn't suffice. Open a terminal (Spotlight → "Terminal") and run:
sudo xattr -cr /Applications/CardMirror.app
You'll be asked for your password. After that, normal double-click works.
-
- Download
CardMirror Setup x.x.x.exe. - Run the installer (Next → Install → Finish).
- First launch only. SmartScreen shows "Windows protected your PC." Click More info → Run anyway.
Two options for installation, depending upon distribution:
-
AppImage (works on every modern distro): download
cardmirror-x.x.x.AppImage. In a terminal, in the download folder, run:chmod +x cardmirror-x.x.x.AppImage ./cardmirror-x.x.x.AppImage
For most graphical desktop environments with modern file managers, you should be able to run the AppImage by double-clicking after the
chmod +xcommand. -
Arch and Arch-based distributions — use the AUR:
yay -S cardmirror-bin # or with paru: paru -S cardmirror-binOr grab
cardmirror-x.x.x.pacmanfrom the release directly and:sudo pacman -U cardmirror-x.x.x.pacman
Updates specifically pull from BlueCheeseburger/cardmirror, not upstream cardmirror.
Help → Check for Updates… checks manually: it tells you you're current, or announces an available update and downloads it in the background.
CardMirror also checks for updates automatically — silently at launch and once a day, speaking up only when a new version is actually ready. Turn this off (or pause it for a week — handy at a tournament) in Settings → General → "About this install" → Check for updates automatically.
Updates never interrupt you: when a new version has finished
downloading, a small chip appears in the status bar ("Update x.y.z
ready — restart to install"), and nothing installs until you click it.
On Windows and Linux, quitting the app normally also applies a
downloaded update on the way out. On macOS, clicking the chip restarts
straight into the new version; if your install can't be updated in
place (for example the app isn't in a writable folder), the chip opens
the releases page instead so you can grab the new .dmg.
Going to a tournament? Pause update checks for 1 week (Settings → General → "About this install") stops all automatic checks and downloads until the shown resume date.
Linux users who installed via the AUR can update with yay -Syu
instead.
A few features call out to an AI model:
- AI-formatted citations from a pasted URL or freeform quote.
- AI repair of OCR / PDF extraction errors in a selection (Mod-Shift-R).
- AI image alt-text and table-from-image (right-click an image).
- AI commenting / explain features in the comments column.
Two providers are supported; pick either one:
- Anthropic talks directly to Anthropic's Claude API. Get an API key from console.anthropic.com (you'll need to top up a small amount of credit — Anthropic doesn't have a free tier for the API).
- Gemini talks directly to Google's Gemini API. Get an API key from aistudio.google.com/api-keys (you'll need to top up a small amount of credit — Anthropic doesn't have a free tier for the API).
- OpenRouter talks to openrouter.ai,
which fronts models from many labs behind one key — use it if you
already have OpenRouter credit or want a specific non-Claude
model. You choose the model by its id (e.g.
anthropic/claude-sonnet-4.6oropenai/gpt-4o) in the OpenRouter model field.
To enable them:
- Get an API key from the provider you picked.
- In CardMirror, click the ⚙ gear icon in the ribbon.
- Toggle AI features on, choose your provider under AI provider, and paste your key into that provider's key field (Anthropic API key, Gemini API key, or OpenRouter API key).
The key is stored locally on your machine and is sent directly to the provider you picked when you trigger an AI feature. It doesn't travel through a third-party server.
Translation (Mod-Shift-T on a selection → copied to the clipboard) also has an Anthropic backend, but it works without any AI setup: the default MyMemory backend needs no key. You can also plug in a Google Cloud Translation key. Configure it under Settings → Editing → Translation.
You only need this if you want to build CardMirror yourself (contribute, run a development branch, or use the editor on a platform we don't publish binaries for). For day-to-day use, download a release above.
CardMirror is built with JavaScript / TypeScript and needs Node.js to run. Node is a regular desktop installer.
- macOS — open nodejs.org in your browser
and click the blue "LTS" download button. Open the
.pkgfile from Downloads and click through the installer. - Windows — open nodejs.org and click the
blue "LTS" download button. Open the
.msifile from Downloads and click through the installer. - Linux — the easiest path is the official installer at nodejs.org/en/download — pick your distro and follow the few commands it shows.
You don't need to verify the install — if the next step works, Node is installed.
- Open the CardMirror page on GitHub in your browser.
- Click the green
<> Codebutton near the top of the file list. - Click "Download ZIP" at the bottom of the dropdown.
- Unzip the download. You'll get a folder called
cardmirror-main. Move it somewhere you can find later — your Desktop or Documents is fine. - Open the
cardmirror-mainfolder and look inside. Some unzippers double-wrap. You want the folder that directly containspackage.json,README.md,index.html, andsrc/— if you only see anothercardmirror-mainfolder, that's the wrapper; open it.
A "terminal" is a window where you type commands. You're going to open one already pointing at the CardMirror folder.
- macOS — enable Finder → right-click → Services → New Terminal at Folder once via System Settings → Keyboard → Keyboard Shortcuts → Services → Files and Folders, then right-click the folder.
- Windows — open File Explorer in the folder, click the address
bar, type
cmd, press Enter. - Linux — right-click inside the folder and pick Open Terminal
Here (Nautilus / Dolphin / Thunar all offer it), or open in terminal and
cdto the filepath.
To make sure you're in the right directory, type ls (macOS / Linux)
or dir (Windows) and press Enter. You should see package.json,
README.md, src, apps. If you don't, your terminal is likely one
folder too high up — verify your current folder with the pwd command
(macOS / Linux / Windows PowerShell) or by typing echo %cd% (Windows, non-PowerShell).
npm installDownloads everything CardMirror needs. Takes 30 seconds to a couple
of minutes. Deprecation warnings are normal; only red error lines
indicate trouble.
npm run devAfter a few seconds, open http://localhost:5173/ in your browser.
npm run desktop:devThis builds the Electron main process, starts the Vite dev server, and launches the desktop window. Same code, same renderer — but in a native window with file-system access.
Open a terminal in the same folder; run npm run dev (or
npm run desktop:dev) again. To pick up newer code, download a
fresh ZIP and rerun npm install in it.
These all run inside the terminal pointed at the CardMirror folder (same setup as the install steps above):
npm test # run all tests
npm run test:bench # performance benchmarks
npm run typecheck # strict TypeScript check
The round-trip test suite and the round-trip benchmark both walk a
folder of .docx fixtures and run universal preservation checks on
each one (text length, heading IDs, mark counts, indent / spacing
multisets, etc.). Point them at any folder by setting
CARDMIRROR_DOCS_DIR:
CARDMIRROR_DOCS_DIR="/path/to/your/docx/files" npm test
CARDMIRROR_DOCS_DIR="/path/to/your/docx/files" npm run test:bench
When the variable isn't set, the suite looks under
reference-docs/example docs/ (the project owner's local corpus).
When that folder doesn't exist either — the default state on a fresh
clone — the file-dependent tests skip cleanly and the rest of the
suite still runs.
The CLI imports a Verbatim/Advanced-Verbatim docx, normalizes it through our schema, and re-exports a fresh docx:
npm run round-trip path/to/input.docx [path/to/output.docx]The output is fully native to Verbatim — same canonical style ids, same
direct-formatting conventions. Stylepox and other non-Verbatim cruft is
dropped on import (per ARCHITECTURE.md §3).
import {
schema, // the ProseMirror schema
fromDocx, // .docx bytes → ProseMirror doc
toDocx, // ProseMirror doc → .docx bytes
exportDoc, // schema doc → { documentXml, relsXml }
importDoc, // document.xml → schema doc
newHeadingId, // generate a fresh stable heading UUID
} from 'cardmirror';import { fromDocx, toDocx } from 'cardmirror';
import { readFile, writeFile } from 'node:fs/promises';
const buf = await readFile('input.docx');
const doc = await fromDocx(buf);
// `doc` is a ProseMirror Node — walk it, transform it, edit it...
console.log(`${doc.nodeSize} chars in tree`);
const out = await toDocx(doc);
await writeFile('output.docx', out);doc: sequence of block-level kinds
pocket: Heading 1 paragraph (with stable id)
hat: Heading 2 paragraph (with stable id)
block: Heading 3 paragraph (with stable id)
card: structured: tag (card_body | undertag | cite_paragraph | analytic | table)*
tag: Heading 4 (only inside card)
cite_paragraph, card_body: body paragraphs inside cards
analytic: outline-4 paragraph (Analytic style; can be standalone or in-card)
undertag: Undertag-styled paragraph
paragraph: unstyled body text (first-class — can sit between any nodes)
table: table_row+ (at doc level OR inside a card / analytic_unit)
table_row: (table_cell | table_header)+
table_cell: paragraph+
image: inline atom (base64 bytes + EMU dimensions + alt; round-trips through .docx)
Every paragraph-like textblock carries round-trip-only attrs
indent (left indent in OOXML dxa) and spacing (verbatim
<w:spacing> map). Tables carry rawTblPr (table-level borders /
style / shading captured opaquely); cells carry rawTcPr
(per-cell borders, shading, vAlign).
Marks: cite_mark, underline_mark, underline_direct,
emphasis_mark, undertag_mark, analytic_mark, plus direct
formatting bold, italic, strikethrough, superscript,
subscript, link, highlight, font_color, font_size,
shading, pilcrow_marker, font_family, comment_range
(anchors a thread to a range of text).
See src/schema/ for full specs and
ARCHITECTURE.md §4 for design rationale.
The desktop app can load plugins — one GitHub repo per plugin, publishing a manifest + built bundle as release assets. A plugin registers commands that appear in the command palette, the keybindings editor, and the printed shortcut reference like native ones, and can extract structured card data from the current selection, jump back to a card's source, and exchange messages with companion apps (such as a flowing app) over the local cardmirror-bridge — a loopback HTTP handshake, never the network.
Two things to know before writing one:
- Plugins are full-trust code. They run inside the editor with the
same access the editor has, so installs are limited to a curated
allowlist (served by the relay; self-hosted relay operators curate
their own via
RELAY_PLUGIN_ALLOWLIST). For development, use "Load plugin from file…" in Settings → Plugins, or unlock arbitrary repos with__plugins('community-on')in the developer console. - The plugin-facing API surface is a draft (a sandboxed v2 may change it); the bridge handshake and HTTP routes are frozen.
The full contract — manifest fields, install flow, the capability API,
and the bridge protocol — lives in
reference-docs/cardmirror-plugin-api.md.
CardMirror is built on ProseMirror, the modular rich-text editor framework created and maintained by Marijn Haverbeke. Nearly every editor primitive CardMirror leans on — the schema-validated transactions, the typed-tree document model, NodeViews, plugin state, the keymap and history modules — is ProseMirror's.
The idea to use ProseMirror, as well as credit for dozens of course-corrections along the way, belongs to text editor wizard Slim Lim.
If you're curious how ProseMirror works under the hood, Marijn's launch post is the best high-level introduction, and the ProseMirror docs cover the APIs in depth.
Thank you to Marijn and the ProseMirror community for the years of careful library design that made this project tractable. If ProseMirror has been useful to you too, Marijn's work is supported directly at https://marijnhaverbeke.nl/fund/.
CardMirror's voice control was shaped by ideas pioneered in the hands-free-editing community — Talon, Cursorless, and Pokey Rule.
Features and code have been contributed by Shreeram Modi, Q Cooper, cora, and Neo Cai. Each contribution is credited where it shipped in the changelog.
Special thanks to Q Cooper and Missouri State debate for beta testing.
The app's interface icons are from the
Untitled UI free icons,
used under their free license. See
THIRD-PARTY-NOTICES.md for the full
third-party attributions and license terms.
CardMirror is licensed under the
PolyForm Noncommercial License 1.0.0. You can read,
fork, modify, and share the source for any noncommercial purpose
(personal use, hobby projects, debate-team and academic use,
research, government use, charitable / public-interest
organizations); commercial use requires a separate license. See
LICENSE for the full terms.
Underlying dependencies (ProseMirror and friends) ship under their
own permissive licenses, preserved in node_modules/.
