Everything CMeDit does, in one page. If you want the one-screen version instead, press F1 in the editor; a shorter copy of this manual also ships inside the binary under Help ▸ Manual.
Start here
CMeDit is a modeless terminal text editor. There is no insert mode and no command mode: typing types, Ctrl+S saves, Ctrl+F finds, Ctrl+Q quits. It has a drop-down menu bar, full mouse support, the real system clipboard, a file explorer panel, project-wide search and replace, syntax highlighting, external linting, crash recovery and per-folder session restore, and view modes for spreadsheets, images, PDFs, RTF and Word documents, workbooks, e-books and ZIP archives.
It is written from first principles in Haskell with no TUI framework: raw-mode
terminal control, the input parser, the diff renderer, the menus and the dialogs are
built directly on termios and ANSI/VT escape sequences. Its only
dependencies are the libraries that ship with the compiler, which is why it builds
offline and runs as a single binary you can drop onto any machine.
Start here
CMeDit builds with a plain make. You need GHC 9.0 or newer; nothing
else is fetched, so the build works on a machine with no network and no Hackage
index.
Copy the resulting cmedit binary anywhere on your PATH.
It has no runtime dependencies of its own.
There is a native Windows port: the same codebase with a Windows implementation of
the platform layer (hand-rolled kernel32 FFI, no extra packages). With GHC installed
through ghcup and make from
MSYS2:
It needs a console that speaks VT, which means Windows 10 1809 or later. Windows
Terminal is recommended and is the default on Windows 11; legacy conhost
gets the portable fallback path like any other minimal terminal. On any other
operating system, make windows-check typechecks the whole program against
the Windows platform layer without linking, which is how the port is kept honest from
Linux. WSL and ssh from Windows Terminal run the POSIX build
unchanged.
For full system-clipboard integration, install any one of xclip,
wl-clipboard (wl-copy/wl-paste) or
xsel on Linux; macOS already has pbcopy/pbpaste.
Without a helper, CMeDit falls back to the OSC 52 escape sequence, which is what
makes copy-and-paste work over SSH in the first place.
Tip. cabal build and cabal run cmedit
also work in an environment whose Hackage index cache exists. On a fully offline
machine prefer make, which drives ghc --make directly and
needs no index.
Start here
Each FILE is opened for editing and created on save if it does not
exist. A DIR argument opens as a workspace folder in the explorer panel,
so cmedit . opens the current directory that way. With no argument,
CMeDit starts with an empty buffer.
| Option | Meaning |
|---|---|
-h, --help | Show the built-in help, including the full list of config keys, and exit. |
-v, --version | Show the version and exit. |
-t N, --tab-width N | Tab width in columns. Default 4, valid range 1 to 16. |
--tabs | Indent with real tab characters. This is the default. |
--spaces | Indent with spaces. |
--line-numbers | Show the line-number gutter. |
--no-line-numbers | Hide the gutter. This is the default. |
--no-auto-indent | Do not copy indentation onto new lines. |
--readonly | Open the file read-only. The status bar shows [RO]. |
--restore | Restore this folder's session — the files you had open here last time, in the same order, at the same cursors. See Sessions. |
--convert | Convert instead of editing, even when stdout is a terminal. See Converting files. |
--sheet N | Which sheet a workbook conversion exports, counted from 1. Implies --convert. |
--stats-on-exit | Print a session summary (frames, memory, garbage collection) when the editor exits. |
Flags override the configuration file, which is read first. See Settings and config file for the persistent equivalents.
Start here
Every reading view turns an awkward format into text a terminal can show, so the same machinery makes CMeDit a converter — and a converter is what you want when the terminal is not there. No flag is needed: the editor draws on stdout, so a redirected stdout cannot mean "open the editor" and can only mean "give me the text".
The converted content goes to stdout and one line describing it to stderr, which is the only arrangement that survives a redirect:
--convert forces conversion when stdout is a terminal. An
image (no text to give) or a file too large to load (already plain text) says so and
exits non-zero.
Start here
From top to bottom, the screen is: the menu bar, the text area (with the explorer panel on its left when a folder is open, the line-number gutter next to it, and the vertical scroll bar on the right), the horizontal scroll bar, the status bar and the shortcut hint bar.
Press F10 to open the menus, or Alt plus a title's underlined letter (Alt+F for File, Alt+E Edit, Alt+I Find, Alt+V View, Alt+W Window, Alt+H Help). Arrows move, Enter runs the highlighted item, Esc closes. Every item shows its keyboard shortcut next to it, which makes the menus the gentlest way to learn the keyboard. Clicking works everywhere.
Menus adapt to context: Table View only appears for a .csv or
.tsv file, Formatted View only for an .rtf, Archive Contents
only for a container that has both a reading view and a listing, Revert only when the
file can be reverted, Delete only with a selection, Save All only when more than one
file is open with unsaved changes, and the in-file find entries are hidden in views
that have no cursor to search from.
The left side shows the active file, a bullet when it has unsaved changes, and its position in the list of open files. The right side is clickable:
| Zone | Click action |
|---|---|
Ln 24, Col 41 | Opens Go to Line (or Go to Page in the PDF view). |
INS / OVR | Toggles overwrite mode. |
UTF-8 / UTF-8-BOM | Toggles the byte-order mark written on save. |
LF / CRLF | Switches the line ending written on save. |
ruff: 2E 1W | Jumps to the first diagnostic. Only shown when a linter has reported something. |
In other views the right side reports what matters there instead: the cell reference and table size in CSV, the image format and pixel size in the image view, the page number in a PDF, the sheet in a workbook, the chapter in an e-book, the line position in the paged viewer.
The bottom line lists the shortcuts that apply right now, and changes with the focus: the editor shows save, open, find and quit; the explorer shows new, rename and delete; the search panel shows its toggles; the CSV, image and PDF views show their own keys.
A vertical bar occupies the rightmost column whenever the content overflows, with a proportional thumb you can drag; clicking the track jumps. A horizontal bar sits directly above the status bar for views that can scroll sideways (plain text without word wrap, and CSV tables). Either can be turned off in Settings, which also frees the column or row they occupy. In a table the horizontal drag is character-granular, so a wide first column can be scrolled partly out of view.
Tab moves between fields, options and buttons; Enter presses the highlighted button; Esc always cancels. Value pickers (the rows in Settings, and the theme picker) cycle with ←/→, Space or a click; Enter deliberately does not confirm the dialog while a picker has focus, so you cannot dismiss it by accident while changing a value. Informational boxes, including the F1 help card and About, close on a click anywhere outside them; confirmations stay modal.
Using it
.. to go above the root, . to
toggle hidden files and Enter to open.apco finds app/controllers.py.Opening a file that is already open switches to it rather than making a second copy, and re-opening a file you had open before restores the cursor position you left it at.
CMeDit keeps every open file in one window. Alt+. and Alt+, cycle forward and back (Ctrl+PageDown / Ctrl+PageUp do the same), Alt+1 to Alt+9 jump straight to a file by number, and the Window menu (Alt+W) lists everything open. Ctrl+N starts a new empty buffer; Ctrl+W closes the active file.
| Ctrl+S | Save the active file. |
| Ctrl+Shift+S | Save As: write a copy under a new name and continue editing that. In the read-only reading views the same key is an export — see below. |
| File ▸ Save All | Write every modified document at once. |
| File ▸ Revert | Reload the file from disk, confirming first if you have unsaved edits. |
Two optional save-time cleanups are available in Settings: trimming trailing whitespace and ensuring a final newline. They are applied as ordinary undoable edits so you can see and undo exactly what changed, and they never touch CSV or image documents.
Quitting with unsaved files prompts for each one. With a large batch (more than eight) it asks once, offering Save All, Discard All or Cancel.
CMeDit re-stats open documents every couple of seconds while the terminal is
focused, and immediately when you switch back to it. A file that changed underneath
you (a git pull, say) gets a diamond marker in the explorer, a notice on
the status bar when it is the active file, and a File ▸ Revert entry to reload it.
Nothing happens automatically.
Files are read and written as UTF-8. View ▸ Line Endings flips between LF and CRLF, and View ▸ UTF-8 BOM toggles the byte-order mark; both apply on the next save, and both keep the file marked as modified until you save. Status bar clicks do the same thing in one step. A file is written back exactly as it came in unless you change these yourself.
Every open route funnels through the same guards, so a huge or hostile file can never be decoded into millions of junk lines:
paged-view = off
to have them refused instead.[RO] on the status bar. Save is refused;
Save As still lets you write a copy elsewhere.Using it
Open a folder (File ▸ Open Folder, cmedit somedir, or
Ctrl+B) and a project tree docks on the left. Ctrl+B moves focus
into the panel and back to the editor; on a collapsed panel it expands it first.
| Key | Action |
|---|---|
| ↑ ↓ | Move the selection. |
| → / ← | Expand / collapse a directory. |
| Enter or click | Open the selected file. |
| Ins or Ctrl+N | Create a file in the selected directory. End the name with / to create a folder instead. |
| F2 | Rename. Open buffers follow the new path, even under a renamed directory. |
| Del | Delete, after a confirmation with Cancel preselected. |
| . | Show or hide dot-files. |
| Ctrl+B | Return focus to the editor. |
Open files are highlighted, a bullet marks unsaved changes and a diamond marks a file that changed on disk since you opened it. Unopened file names are tinted by type: source code green, Markdown and HTML cyan, data formats such as JSON, YAML and CSV yellow, known binary blobs dimmed grey, and displayable images get a magenta glyph before the name. Large files show their size, and files too big to edit are dimmed.
Drag the divider to resize the panel. The chevron button (or dragging the divider all the way left) collapses it to a one-column strip you can click to bring back. The cross button closes the folder after a confirmation. The tree keeps itself current: expanding a directory always re-lists it, and a background poll re-lists any expanded directory whose modification time changed, so work done in another window shows up on its own.
Using it
Arrows, Home, End, PageUp and PageDown behave as they do everywhere else. Ctrl+← and Ctrl+→ move by word, Ctrl+Home and Ctrl+End go to the start and end of the document. The mouse wheel scrolls; Shift+wheel (or a horizontal wheel or touchpad gesture) pans sideways when word wrap is off.
Hold Shift with any movement key, or drag with the mouse. Double-click selects a word, triple-click a line, and Ctrl+A selects everything. The status bar shows the selected character count.
Ctrl+X, Ctrl+C and Ctrl+V use the real system
clipboard through xclip, wl-copy/wl-paste,
xsel or pbcopy/pbpaste when one is available,
and the OSC 52 escape sequence otherwise, so copying inside an SSH session can
still land on your laptop's clipboard. Pasting large amounts of text is handled in
chunks, so a multi-megabyte paste does not stall the editor.
Why Ctrl+C copies instead of interrupting. CMeDit puts the terminal in raw mode with signal generation disabled, so Ctrl+C is an ordinary keystroke. The same applies to Ctrl+S and Ctrl+Q, which would otherwise be swallowed by terminal flow control.
Ctrl+Z and Ctrl+Y. Runs of typing coalesce into sensible steps rather than one character each, held line moves collapse into a single undo step, and the CSV table view keeps its own undo history. Undo snapshots share their unchanged text, so deep history stays cheap even in large files, and the history is bounded so it cannot grow for the length of a session.
Tab and Shift+Tab indent and unindent: the whole selection at
once when there is one, otherwise the current line. Whether that inserts a tab
character or spaces, and how wide a tab is, are configurable. New lines copy the
previous line's indentation when auto-indent is on. Insert toggles
overwrite mode, shown as OVR on the status bar.
| Ctrl+D | Duplicate the current line, or the selected lines. |
| Shift+Alt+↑ / Shift+Alt+↓ | Copy the line or selection one row up or down. |
| Alt+↑ / Alt+↓ | Move the line or selection up or down. Held moves undo as one step. |
| Alt+J | Join lines, collapsing the whitespace at the seam to a single space. |
| Ctrl+Shift+K | Delete the line or selected lines. |
| Ctrl+K | Cut the line to the clipboard. |
| Ctrl+Backspace / Ctrl+Del | Delete the word to the left / right of the cursor. |
A selection that ends at column 0 does not include that last line, which is what you want when you have selected downward through whole lines.
Ctrl+/ comments or uncomments the current line or selection using the
file type's own syntax: #, //, -- and so on,
with HTML, CSS, Markdown, FreeMarker and Jinja wrapping the span in a block comment.
JSON gets JSONC-style //, since toggling a comment is an explicit request.
The prefix is aligned at the block's indentation and blank lines are skipped.
Ctrl+Space completes the identifier at the cursor from the words of every open buffer, with occurrences nearest the cursor ranked first. No language server is involved, so it works offline and in any file type. Keep typing to narrow the list, Tab or Enter accepts, Esc dismisses, and any other key dismisses the popup and is passed through to the buffer. A single remaining candidate is inserted immediately, and the whole thing counts as one undo step.
When the cursor sits on one of ()[]{} its partner is highlighted, and
Ctrl+] (Find ▸ Go to Bracket) jumps between the two. The scan is bounded,
so an unmatched bracket in a huge file cannot stall a repaint.
http:// and https:// URLs are recognised in the visible
text. Hovering one shows a hand pointer and a status-bar hint; Ctrl+Click
or a right-click opens it in your browser through xdg-open,
open or rundll32. On terminals that support OSC 8
hyperlinks, URLs in the document and file names in the explorer, search results and
status bar are real links your terminal can also act on.
Using it
Ctrl+F opens Find, Ctrl+R opens Replace. Both have match-case and whole-word options, and both remember what you typed.
Ctrl+G goes to a line number. The same dialog goes to a page in the PDF view, a chapter in an e-book, a sheet in a workbook — and in the paged viewer it jumps to a line of a file far too large to load, which is the point of that view.
Using it
F4 opens the workspace search panel over the whole open folder; F6 opens it with the replace field showing. (Ctrl+Shift+F and Ctrl+Shift+H are aliases, but many terminals intercept those, which is why the function keys are the documented way in. The Find menu always works.)
Results stream in grouped by file with match counts and snippet lines as the walk proceeds. Select a result with the keyboard or click it to jump straight to that spot in the file. Files already open are searched from their in-memory contents, so unsaved edits are included.
| Key | Action |
|---|---|
| Tab | Move between the search, replace and glob fields (and the Replace All button). |
| ↑ ↓ | Move through results. |
| ← → | Fold and unfold a file's results. |
| Enter | Run the search, or open the selected result. |
| Alt+C / Alt+W / Alt+X | Toggle match case, whole word, regular expression. |
| Alt+D | Also search inside documents (PDF, Word, workbooks, e-books). See below. |
| Alt+H | Show or hide the replace field. |
| Alt+R | Replace all matches in every file. |
| Ctrl+Enter | Replace in just the selected file. |
| Del | Dismiss the selected result row (or a whole file's results) from the list. |
| Esc | Return to the editor. The results stay, so you can come back to them. |
The include and exclude fields take glob patterns, comma separated:
*.py, src/**/*.ts, !vendor/**. The walker also
prunes on its own: dot directories, node_modules, common build
directories, symbolic links, binaries (by extension where possible, otherwise by
sniffing the first 8 KiB) and files over 8 MB.
Replacing across a workspace is deliberately staged rather than written straight to disk:
A replace touching more than ten files asks for confirmation first. A very large replace (more than fifty files) falls back to writing directly to disk, preserving each file's byte-order mark and line endings, since opening hundreds of documents would help nobody.
The walk runs on background threads with a small pool of workers, so a search over thousands of files stays responsive and a new search supersedes a running one instantly. Results are capped at 2000 matches per file and 20000 in total. A finished search posts a desktop notification if your terminal is unfocused.
Using it
The same panel can look inside PDFs, Word and OpenDocument files, workbooks and
e-books, decoding each one through the very reader that would display it — so
anything found is something you can then go and look at. Toggle it with
Alt+D (or the [Doc] chip). It is off by default, because
decoding one document costs roughly what grepping a hundred source files costs.
Hits are addressed by something intrinsic to the document rather than by a line
number — these views reflow to your window, so a stored row would point somewhere else
in a wider one: a PDF says p.7, an e-book ch.3, a Word or
OpenDocument file a paragraph, a workbook the cell (B4).
Enter opens the document at that unit and highlights the term there. A
workbook is searched cell by cell, so a phrase spanning two columns is deliberately
not a match.
Replace never touches a document. None of these formats can be written back, so they are excluded from every replace path, and the panel reports how many it left alone rather than skipping them silently.
Using it
Turn on the regex toggle (Alt+X) in the workspace search. The engine is
written from scratch as a Thompson NFA executed by a Pike VM, which means matching is
linear in the length of the line: a pathological pattern such as (a+)+b
cannot hang the search, and no match is ever dropped to a step budget. It is
line-oriented, so a pattern matches within a single line.
| Syntax | Meaning |
|---|---|
. | Any character. |
[abc] [a-z] [^a-z] | Character class, ranges, negated class. |
\d \D \w \W \s \S | Digit, non-digit, word, non-word, whitespace, non-whitespace. |
\b \B | Word boundary, non-boundary. |
^ $ | Start and end of the line. |
(…) | Capturing group. |
(?:…) | Non-capturing group. |
a|b | Alternation. |
* + ? | Zero or more, one or more, optional. Greedy. |
{n} {n,} {n,m} | Bounded repetition. |
*? +? ?? {n,m}? | The same quantifiers, lazy. |
In the replacement field, $1 to $9 insert capture groups
and $0 (or $&) inserts the whole match. An invalid
pattern is reported in the panel rather than silently matching nothing.
Views
CMeDit looks at a file's bytes, not its name. Images, PDFs and ZIP archives are
recognised by their magic bytes, so a mislabelled file still opens correctly — and a
ZIP container is classified by what is inside it (a
word/document.xml, a workbook part, an e-book's
container.xml, an OpenDocument content.xml), so a Word file
with the wrong extension still opens as a document. CSV and RTF views are chosen by
extension, because both are text and only the name distinguishes them from any other
text file. The paged viewer is chosen by size.
| View | Chosen for | Editable | Toggle |
|---|---|---|---|
| Text | Everything else | Yes | Default |
| CSV table | .csv, .tsv | Yes | Alt+T |
| Formatted RTF | .rtf | No, the markup is | Alt+T |
| Word document | DOCX / ODT contents | No; select & copy | Alt+T to the listing |
| Workbook grid | XLSX / ODS contents | No; select & copy | Alt+T to the listing |
| E-book | EPUB contents | No; select & copy | Alt+T to the listing |
| PDF reader | %PDF- magic | No; select & copy | None |
| Archive listing | ZIP magic | No | Alt+T where a reading view exists |
| Image | Image magic bytes | No | None |
| Paged viewer | Text over 100 MB | No | paged-view |
Views
.csv and .tsv files open as a spreadsheet grid with
column letters and row numbers. Alt+T (View ▸ Table View) switches to the
raw text and back.
| Key | Action |
|---|---|
| Arrows, Tab | Move between cells. |
| Enter or F2 | Edit the current cell. Typing a character also starts a fresh value. |
| Shift+Enter | Insert a line break inside a cell. |
| Alt+← Alt+→ | Insert a column before or after the cursor. |
| Alt+↑ Alt+↓ | Insert a row above or below the cursor. |
| Ctrl+Del | Delete the current row. |
| Alt+Backspace | Delete the current column. |
| Alt+S | Sort by the current column. Press again to reverse. |
| Ctrl+Z / Ctrl+Y | Undo and redo, in the table's own history. |
A cell can hold several lines; a row's height is that of its tallest cell, capped at three lines, and a taller cell scrolls while you edit it. ↑ and ↓ inside a cell move between its lines before they move between cells.
Selection is rectangular: Shift+arrows or a mouse drag grow a box. Copy yields a mini-CSV of the box (or the raw value for a single cell), and paste is shape-aware: one value pasted over a box fills the box, a copied box pasted over a single cell spreads out from it, and a box pasted over a box of the same shape overwrites it. Cut and Del clear the selected cells.
The first row is pinned below the column letters while you scroll, because
spreadsheets almost always have a header. View ▸ Freeze Header Row turns that off for
the session, and freeze-header = off in the config makes that
permanent.
Columns are sized to fit their content. Drag a column border in the lettered header row to set your own width, and double-click the border to fit it to the content again. Overrides move with the column when you insert or delete columns, and are remembered per document.
Alt+S sorts by the column the cursor is in. Values that look like numbers sort numerically (so 9 comes before 10), text sorts case-insensitively, and empty cells go last. A frozen header stays pinned, the cursor follows its own row to its new position, and one undo restores the previous order.
While the table view is showing, the table is the document. Saving, or switching back to text, writes RFC 4180 CSV with correct quoting and preserves the file's line ending. The original file is only rewritten when you save.
Large tables. A table with hundreds of thousands of rows stays responsive: column widths are cached and updated per edited cell rather than rescanned, and the modified flag is computed by pointer comparison rather than by comparing content.
Views
A .docx, .xlsx, .odt, .ods or
.epub is a ZIP full of XML, so instead of stopping at the archive listing
CMeDit reads it. Only the members a format needs are ever decompressed, so a 1 GB
.docx full of photographs costs the members its text lives in and no
more. Any failure at any stage falls back to the plain
archive listing with a note saying why, so a damaged file
still opens — and Alt+T swaps between the reading view and that listing at
any time.
A Word document shows as a document: headings, bold, italic, underline, colour, alignment, indents, bullets and tables on tab stops, reflowed to your window.
A workbook opens in the spreadsheet grid, with [ / ] turning
the sheets and Ctrl+G jumping to one by number. Gaps in a sheet are real
and are shown, shared strings and inline strings are resolved, and formulas show the
value Excel last calculated. Where a workbook was written by a library rather
than by Excel — openpyxl, xlsxwriter,
pandas.to_excel — there is no cached value to show, so CMeDit evaluates
the formula itself: around fifty functions (SUM, AVERAGE,
IF, COUNT/COUNTIF,
MIN/MAX, ROUND, SUMIF,
VLOOKUP, the text and maths families), the full operator set, ranges
including whole columns, cross-sheet references and chains of formulas. A value the
file already gives is never recomputed, so nothing CMeDit works out can
contradict the program that wrote the file; a formula it cannot parse or has no
function for is left blank and counted, and the status bar reports both totals.
Number formats are not applied, so a date reads as its stored serial number, and the
status bar says so.
OpenDocument files — what LibreOffice writes — go through the same two views, with
one difference in their favour: an .ods stores each cell's
displayed text as well as its value, so dates and currencies read as
15/01/2024 and $1,234.50 rather than as the numbers
underneath them. (.odp and .odg are positioned shapes rather
than documents, and fall back to the listing.)
An e-book shows its chapters in reading order, with [ / ] turning chapters and Ctrl+G going to one by number. The container, package document and spine are followed properly, so the chapters are the ones the book actually orders.
The document views select and copy exactly as the RTF and PDF ones do — drag, double-click a word, triple-click a line, Shift+arrows, Ctrl+A, then Ctrl+C — and a workbook copies a rectangular block of cells the way a CSV file does. Ctrl+Shift+S exports the sheet you are looking at as a CSV file (and, in the document views, the document as plain text) — an export, not a Save As: it writes a copy and leaves the file itself open and untouched.
Everything the readers do not model is skipped rather than mis-rendered, and all three views are read-only: there is no serialiser back to any of these formats, and could not be.
Views
.rtf files open as the document rather than as its markup. Bold,
italic, underline, strike-through and text colour map onto the terminal's own
attributes; paragraphs wrap to the window with their real alignment, indents and
hanging bullets; \'hh and \uN escapes, curly quotes and
dashes decode properly. Font tables, style sheets, embedded pictures and every other
{\*\…} destination are skipped rather than mis-rendered, which is a
property of the format: RTF defines those groups as safely ignorable.
You can select text with the mouse (double-click for a word, triple for a line, Shift+click to extend, or Shift+arrows) and Ctrl+C copies it, so quoting a paragraph out of a document you are reading is one gesture; Ctrl+A takes the whole thing and Esc clears.
Alt+T (View ▸ Formatted View) switches to the raw markup, which edits and saves like any other text file, with the control words syntax-highlighted.
The formatted view is read-only by design. It is a projection of the text buffer, and it is never written back. A word-processor file also carries style sheets, tables, embedded pictures and revision marks that the reader does not model, so if the view could save, those could be lost. Because it cannot, they cannot. Editing an RTF means editing its markup.
The view shows no cursor until you select, since the buffer cursor is a position in the markup and showing it would blink somewhere arbitrary in the rendered text. It re-derives itself whenever the buffer changes underneath it, so undo and staged replaces are reflected immediately. Document colours are honoured only when they would actually read against your theme, because nearly every RTF file specifies black body text, which would be invisible on a dark background. Text set in a large font renders bold, since a terminal has only one font size.
Views
Opening a PDF shows the document, not the file. A PDF is glyphs at coordinates with no paragraphs and no reading order, so the reader reconstructs one: columns from the gaps in the page's horizontal occupancy, lines from the vertical clustering of glyphs, spaces from the gap between glyph runs measured in ems, and paragraphs from lines that reach the right margin under a consistent leading. Words hyphenated across the original line breaks are rejoined. Bold, italic and monospaced runs are inferred from the fonts and drawn with the terminal's own attributes, while lines whose horizontal positions carry meaning (table rows, code listings) are placed by column instead of being re-wrapped.
| Key | Action |
|---|---|
| [ / ] | Previous / next page. p and n do the same. |
| Ctrl+G | Go to a page number. |
| Arrows, PageUp/PageDown, wheel | Scroll. |
| Ctrl+Home / Ctrl+End | Start and end of the document. |
| Drag, double/triple-click, Shift+arrows | Select text. Ctrl+C copies it; Esc clears. |
| Ctrl+F, F3 / Shift+F3 | Search the document; every match highlights while the dialog is open, and the hit becomes the selection, so finding and copying are one gesture. |
The text reflows when you resize the terminal. Page boundaries are drawn as a rule, and the status bar shows which page you are on out of how many.
What the reader deliberately does not do: decrypt encrypted files (it says so instead), show embedded images or vector graphics, or reproduce the page's typesetting. It is for reading a document, not for looking at it. A scanned page with no text layer reports that rather than showing you nothing, and the view is read-only, so a PDF you have open can never be saved over. Ctrl+Shift+S exports the reconstructed text as a plain-text copy.
Robustness. Objects are found by scanning the file for
N G obj rather than by following the cross-reference table, so broken,
patched and incrementally updated files all read: a later definition of an object
number wins, which is exactly what an appended update means. Compressed object
streams, universal since PDF 1.5, are expanded afterwards under the same rule.
Views
Opening a .zip shows what is inside it: an indented file tree with
each member's size, how much compression saved and when it was last modified, above a
summary of the whole archive. Everything built on ZIP gets at least this treatment —
.jar, .war, .apk, .whl,
.pptx, .cbz, .vsix and friends list as a tree,
while .docx, .xlsx, .odt, .ods and
.epub go further and open as
documents, workbooks and e-books, with Alt+T dropping
to this listing. As with images, the file is recognised by its content rather than its
name.
Only the archive's table of contents is read, never the members themselves. That has two happy consequences: the listing is as quick on a multi-gigabyte archive as on a tiny one, because the central directory lives at the end of the file and is proportional to the number of entries rather than the size of the data; and encryption does not get in the way, because names, sizes and dates are not the part that is encrypted. Encrypted members are listed and flagged on their own row, as are members using an unusual compression method.
The listing is an ordinary read-only document, so Ctrl+F searches it,
Ctrl+G jumps to a line, word wrap works, and you can select and copy from
it. What you cannot do is save it: there is no serialiser back to ZIP, and Save As
offers a .txt name so that you cannot replace an archive with a
description of itself by accident.
Views
Opening a PNG, JPEG (baseline or progressive), GIF, WebP (lossless VP8L or lossy
VP8), BMP or Netpbm file shows the picture in the terminal, scaled to fit. Every
decoder is written from first principles on the boot libraries: the DEFLATE
inflate, GIF's LZW, JPEG's Huffman tables and IDCT, WebP's prefix codes,
transforms and boolean arithmetic decoder.
| Key or gesture | Action |
|---|---|
| a | Switch between 24-bit colour half-blocks and a monochrome ASCII ramp. |
| Drag a rectangle | Zoom into that region of the source pixels. |
| Click, or Esc | Zoom back out to the whole image. |
| Ctrl+W | Close the image. |
On a terminal that supports the kitty graphics protocol or sixel, the picture is drawn at true pixel resolution, aspect-corrected using the terminal's real cell size and capped at native resolution so a small image sits centred at 1:1 rather than being blown up. Everywhere else it is drawn with half-block characters, two pixels per cell in 24-bit colour, which needs no terminal capability at all. Both paths use the same geometry, so the picture occupies the same box either way. Resizing the terminal re-scales the picture; scrolling and moving around do not, because the scaled result is cached.
Animated GIFs play in place. On kitty itself the whole animation is uploaded once with the placement and the terminal loops it, so the editor never ticks. On terminals that speak the static kitty protocol but drop animation, every frame is pre-uploaded and the editor swaps the visible placement, which costs a few dozen bytes per frame. Elsewhere the frames are stepped by the editor and drawn as cells. A still image, or an animation in a background file, costs nothing at all. Zooming a kitty-protocol placement deliberately freezes on the current frame, since re-uploading a cropped frame per tick would be a full-cost transmission.
Views
A text file too large to hold as an editable buffer (over 100 MB) opens in a read-only paged viewer whose memory does not depend on the file's size. A 281 MB log of four million lines opens at about 32 MB resident, and stays there after forty page-downs; a 120 MB file that is one single line opens at about 30 MB.
Two structures make that work. One streaming pass indexes a byte offset every thousand lines (and sniffs the byte-order mark and line ending, so a CR-only file pages correctly), and only a window of lines around the viewport is ever decoded. Every read is bounded three ways: the stride of skipped lines, the number of lines requested, and 64 KiB per line.
Scrolling, Ctrl+Home, Ctrl+End and Ctrl+G (Go To Line) are all instant, including a jump to line 2,000,000 of a log. Syntax highlighting still applies to each visible line, lexed from the initial state, since the state before it would mean reading from the top of a multi-gigabyte file; a construct opened earlier is therefore not carried in. The view is read-only by construction, so editing keys are swallowed with a note and Save and Revert are refused. The index is a snapshot, so a log that keeps growing keeps its extent until you reopen it.
Set paged-view = off to have oversized files refused instead.
Setup
CMeDit can run your project's own linters as you type. It does not implement any checks itself and there is no language server involved.
At startup, whenever Settings opens, and when you open a folder, CMeDit looks for
the supported tools on your PATH. Node tools are also looked for in the
workspace's node_modules/.bin; in a Yarn Plug'n'Play workspace with no
node_modules, the workspace's own copy is run through
.pnp.cjs via node, so the project's exact tool and plugins
apply.
| Tool | Files | Default | Install |
|---|---|---|---|
| ruff | .py, .pyi | on | pip install ruff |
| flake8 | .py, .pyi | on, skipped when ruff runs | pip install flake8 |
| eslint | .js .jsx .ts .tsx .mjs .cjs | on | npm install -D eslint |
| stylelint | .css .scss .sass .less | on | npm install -D stylelint |
| pyright | .py, .pyi | off, and only on save | pip install pyright |
| shellcheck | .sh, .bash | on | apt install shellcheck |
About half a second after you stop typing, the active file is sent to its linter on
standard input, so unsaved changes are checked rather than what happens to be on disk.
The working directory is the workspace root, so project settings such as
pyproject.toml, eslint.config.mjs or
.shellcheckrc apply exactly as they would from the command line. Saving
re-lints immediately, including the save-time-only tools. Only one pass runs at a
time: a superseded pass is cancelled, there is a whole-pass timeout, and a rate floor
keeps a burst of typing from starting a queue of processes.
ruff: 3E 1W. Click it to jump to the first problem.File ▸ Settings has a Linting section: a master switch, then one row per tool.
Installed tools show their version, and anything missing shows the exact command that
would install it. Saving Settings writes lint plus one
lint-<name> key per tool, which you can also edit by hand.
Setup
View ▸ Theme opens a picker; moving the focus previews each theme live behind the
dialog, so Esc restores what you had simply because nothing was written.
The choice applies to the session; set theme in the config file to make
it permanent.
| Theme | Description |
|---|---|
auto | Follows your terminal's background colour, re-checked whenever the terminal regains focus, so a system light/dark switch follows you. Falls back to dark when the terminal never answers. This is the default. |
dark-terminal | Dark palette that keeps your terminal's own background. (dark is accepted as a legacy spelling.) |
light-terminal | Light palette that keeps your terminal's own background. (light is accepted as a legacy spelling.) |
cherry-blossom | Soft pink, painted on every cell. |
flashbang | Blinding pure white, for the sun-drenched. |
midnight | Deep navy, for terminals whose dark is not dark enough. |
graphite | Neutral near-black with the colour left to the syntax, and IDE-style chrome that sits below the page. |
The two terminal themes restyle the chrome and tokens only. The other four paint their own background on every cell, so they look identical whatever your terminal's palette is. The cursor colour is matched to the theme too, and restored when CMeDit exits.
Highlighting is chosen by file extension and covers SQL (PostgreSQL flavour), Python, JavaScript and TypeScript (including JSX and TSX), CSS, SCSS and Less, HTML and XML, FreeMarker, Jinja, shell, Haskell, Markdown, JSON, YAML, TOML, INI and conf files, CSV, and RTF markup. Multi-line constructs stay correct no matter where you scroll, including dollar-quoted SQL bodies, Python docstrings, JavaScript template literals, fenced Markdown code and HTML comments. Lines longer than 20,000 characters are rendered unstyled, so one minified line cannot dominate a frame.
| Alt+Z | Word wrap. Long lines wrap to the window instead of scrolling sideways. |
| Alt+L | The line-number gutter. |
| View ▸ Whitespace | Whitespace markers: spaces and tabs drawn as glyphs. No shortcut — Alt+W opens the Window menu. |
| Ctrl+B | The explorer panel. |
All four are also in the View menu, and all four have config keys so a session starts the way you like it.
Setup
File ▸ Settings (Ctrl+,) lists every option as an arrow-key value picker, grouped into Editing, Display, Files and Linting, with a one-line hint for the highlighted row. Move with the arrows, change a value with ←/→, Space, Enter or a click. Changes apply immediately behind the dialog so you can see them. Save writes them to the config file; Cancel or Esc puts everything back the way it was. The dialog scrolls when it does not fit the terminal.
Writing is surgical: existing keys are updated in place, your comments and any unfamiliar lines are preserved, and a key is only appended when its value differs from the default.
The same settings can be edited by hand in ~/.config/cmedit/config
(XDG_CONFIG_HOME is honoured). One key = value per line;
# starts a comment. Booleans accept true, yes,
on or 1 and false, no,
off or 0. Anything unrecognised is reported once on the
status line rather than silently ignored, and one bad line never takes the rest of the
file down with it. Command-line flags override the file.
| Key | Values | Default | Meaning |
|---|---|---|---|
tab-width | 1 to 16 | 4 | Columns per tab stop. |
indent | tabs, spaces | tabs | What pressing Tab inserts. |
auto-indent | boolean | on | New lines copy the previous line's indentation. |
word-wrap | boolean | off | Start with word wrap on. |
line-numbers | boolean | off | Show the line-number gutter. |
whitespace | boolean | off | Show whitespace markers. |
scrollbar-vertical | boolean | on | Reserve the right-hand column for the vertical scroll bar. |
scrollbar-horizontal | boolean | on | Reserve a row for the horizontal scroll bar. |
trim-trailing-whitespace | boolean | off | Strip line-end whitespace on save, as an undoable edit. |
final-newline | boolean | off | Ensure the file ends with a newline on save. |
freeze-header | boolean | on | Pin a CSV table's first row while scrolling. |
theme | see above | auto | Colour theme. |
paged-view | boolean | on | Open files too large to edit in the paged viewer instead of refusing them. |
journal | boolean | on | Crash-recovery journal and clean-exit snapshots. Off means CMeDit caches no file content anywhere. See Sessions. |
restore-session | boolean | off | Have a bare cmedit restore this folder's session, as --restore does. |
lint | boolean | on | Master switch for external linters. |
lint-ruff … lint-shellcheck | boolean | on, except lint-pyright | Per-tool switches: lint-ruff, lint-flake8, lint-eslint, lint-stylelint, lint-pyright, lint-shellcheck. |
debug-stats | boolean | off | Show live session counters (frame time, heap, background jobs) on the status bar. |
Setup
Unsaved changes are journalled every couple of seconds to
~/.cache/cmedit/journal (a directory created mode 0700; a
very large file a little less often, and always on a background thread), so an SSH
drop, a closed terminal or an OOM kill does not take the last hour with it. The next
time CMeDit starts it lists what it found and offers to Recover,
Discard or Keep for later; recovered files come back as unsaved buffers,
so nothing is written over your file until you save it. A journal is deleted as soon
as its document is saved or closed, and on a clean exit.
The journal holds file content in your cache directory — and, so a
restored session can offer your files back as you left them, a clean exit also writes
a copy of every open document (up to 4 MB each) to
~/.cache/cmedit/snapshots. One switch covers both:
journal = off in the config (or the row turned off in File ▸ Settings)
and CMeDit caches no file content anywhere — no journals while you work, no snapshots
when you quit. That is the setting to reach for if you edit secrets.
cmedit --restore reopens the files you had open in this
folder last time — in the same order, with the same cursor positions, the same
workspace folder and the same file in front. Each folder gets its own session file
under ~/.config/cmedit/sessions, so alternating between two projects in
two terminals does not have the second one overwrite the first one's list;
--restore takes the session for the directory you start in, or — if that
folder has none — the most recently written session of any folder, with the status
line naming the folder it came from. A run with no folder open keeps using
~/.config/cmedit/session, and older session files still restore.
Put restore-session = true in the config (or turn on "Restore session
on start" in File ▸ Settings) to have a bare cmedit do it; naming files
on the command line still just opens those, and files named alongside
--restore open on top of the restored session. Session files hold paths,
cursors and the times the files were last seen — never file content.
The File menu also lists your last few sessions ("website (6 files)", up to four): choosing one restores it into the running editor, adding its folder and files to what you already have open — an already-open file is switched to, not duplicated — and closing nothing.
If a restored file changed on disk since the session ended, a "Files Changed Since This Session" dialog lists them (◆) and offers Latest on Disk (the default; Esc means it too, and it is a no-op since the files are already open at that version) or As You Left Them, which brings back the contents from the session's clean exit as unsaved, modified buffers marked ◆ — nothing is written over the newer file until you save it yourself. One answer covers the whole list.
Files over 4 MB, and sessions that ended in a crash (or ran with
journal = off), have no saved copy: those are annotated in the list, stay
at their newest version, and if none of the listed files has a copy the dialog just
reports what changed with a single button. Files deleted since the session are skipped
with a note ("Restored 4 of 5 files"). Restore composes with crash recovery: restore
runs first and the journal is applied last, so unsaved changes from a crash come back
in the restored files and always have the final word.
Setup
| Path | Contents |
|---|---|
~/.config/cmedit/config | Your settings. Written by File ▸ Settings ▸ Save, and safe to edit by hand. |
~/.config/cmedit/recent | Recently opened files with their cursor positions, most recent first, capped at 50 entries. This is what restores your cursor when you reopen a file. |
~/.config/cmedit/history | Find and replace terms, capped at 50 each, escaped so multi-line terms survive. |
~/.config/cmedit/session, ~/.config/cmedit/sessions/ | Per-folder session lists: paths, cursors and last-seen times — never file content. Capped at 50 files per session. |
~/.cache/cmedit/journal/ | The crash-recovery journal: unsaved buffer content, written every couple of seconds, deleted on save, close or clean exit. Created mode 0700. |
~/.cache/cmedit/snapshots/ | Clean-exit copies of open documents (up to 4 MB each), which back the "As You Left Them" restore choice. |
XDG_CONFIG_HOME and XDG_CACHE_HOME are honoured. Delete
any of them to start fresh; none is required for CMeDit to run.
journal = off stops both cache directories from receiving any file
content. Recents are persisted when the list order changes and once more on exit,
including on an external kill, so positions are not lost.
Reference
| Ctrl+N | New file | Ctrl+O | Open file |
| Ctrl+P | Go to file (fuzzy) | Ctrl+Shift+P | Command palette |
| Ctrl+S | Save | Ctrl+Shift+S | Save As (export in reading views) |
| Ctrl+W | Close file | Ctrl+Q | Quit |
| Ctrl+B | Explorer panel | Ctrl+, | Settings |
| Alt+. / Alt+, | Next / previous file | Ctrl+PgDn / Ctrl+PgUp | Next / previous file |
| Alt+1…Alt+9 | Go to file N | F10, Alt+letter | Menus |
| Ctrl+Z / Ctrl+Y | Undo / redo | Ctrl+X Ctrl+C Ctrl+V | Cut, copy, paste |
| Ctrl+A | Select all | Shift + movement | Extend the selection |
| Tab / Shift+Tab | Indent / unindent | Insert | Overwrite mode |
| Ctrl+D | Duplicate line | Ctrl+K | Cut line |
| Alt+↑ / Alt+↓ | Move line | Shift+Alt+↑ / Shift+Alt+↓ | Copy line up / down |
| Alt+J | Join lines | Ctrl+Shift+K | Delete line |
| Ctrl+/ | Toggle comment | Ctrl+Space | Complete word |
| Ctrl+Backspace | Delete word left | Ctrl+Del | Delete word right |
| Ctrl+← / Ctrl+→ | By word | Ctrl+Home / Ctrl+End | Document ends |
| Ctrl+F | Find | F3 / Shift+F3 | Find next / previous |
| Ctrl+R | Replace | Ctrl+G | Go to line (page, chapter, sheet) |
| F4 | Find in files | F6 | Replace in files |
| F12 | Go to definition | Ctrl+] | Matching bracket |
| F8 | Next problem | Alt+← / Alt+→ | Back / forward |
| Alt+Z | Word wrap | Alt+L | Line numbers |
| Alt+T | Table view / formatted view / archive contents | View ▸ Whitespace | Whitespace markers (no shortcut) |
| F1 | Keyboard help card | F10 | Menu bar |
| ↑ ↓ | Move | Enter | Open |
| Ins / Ctrl+N | New file, or folder with a trailing / | F2 | Rename |
| Del | Delete | . | Show hidden files |
| Tab | Next field | Enter | Search, or open a result |
| Alt+C | Match case | Alt+W | Whole word |
| Alt+X | Regular expression | Alt+D | Search inside documents |
| Alt+H | Show the replace field | Alt+R | Replace all |
| Ctrl+Enter | Replace in this file | Del | Dismiss the selected result |
| Enter / F2 | Edit the cell | Tab | Next cell |
| Alt+↑ Alt+↓ | Insert a row | Alt+← Alt+→ | Insert a column |
| Ctrl+Del | Delete the row | Alt+Backspace | Delete the column |
| Alt+S | Sort by column | Alt+T | Back to text |
| [ / ] | Page (PDF), chapter (EPUB), sheet (workbook) | Ctrl+G | Go to page / chapter / sheet |
| Drag, double/triple-click, Shift+arrows | Select (PDF, RTF, DOCX, ODT, EPUB) | Ctrl+C | Copy the selection |
| a | Colour or ASCII rendering (image) | Drag | Zoom to a rectangle (image) |
| Ctrl+Shift+S | Export as text / CSV | Ctrl+F | Search the document |
Reference
At startup CMeDit asks the terminal what it can do, then upgrades itself feature by feature. A terminal that answers nothing gets the portable escape stream, which is the same stream CMeDit has always emitted. Every upgrade is opt-in by evidence, so a plain terminal is never corrupted.
XTVERSION, the terminal's name and version.theme = auto. It
is asked again whenever the terminal regains focus, so a system light/dark switch
follows you.REP repeat sequence: print two
characters, ask the terminal to repeat, then ask where the cursor is. The answer is
the truth, whatever the terminal claims.REP.CMeDit enables the kitty keyboard protocol's disambiguate flag at startup, which is
what lets modified keys legacy encodings cannot express, such as
Ctrl+Enter and Shift+Enter, arrive intact. Terminal focus
reporting is enabled too, which is how the freshness poll and the automatic theme know
when you have come back. Both are turned off again on exit, along with mouse
reporting, the alternate screen and the raw-mode terminal settings; a
SIGTERM or SIGHUP still runs that cleanup.
Reference
Every one of these exists because something without it went wrong on a real file.
| Limit | Value | What it protects |
|---|---|---|
| Editable file size | 100 MB | Larger text files open in the paged viewer instead. |
| Background load threshold | 2 MB | Above this a file loads on a background thread with a spinner. |
| Binary sniff | first 8 KiB | A NUL byte means the file is binary and is not decoded as text. |
| Paged viewer line cap | 64 KiB per line | A file with no line separators cannot be concatenated into memory. |
| Paged index stride | 1000 lines | The index stays small on a file of any size. |
| PDF size / pages / text | 128 MB, 2000 pages, 4M characters | Extraction cannot run away on a hostile file. |
| Searched file size | 8 MB | Bigger files are skipped by the workspace search. |
| Searched document size | 64 MB | Bigger PDFs, workbooks and e-books are skipped by the document search. |
| Matches | 2000 per file, 20000 total | A broad search over a huge tree stays usable. |
| Staged replace | 50 files | Above this, replace writes to disk directly instead of opening documents. |
| Replace confirmation | 10 files | Above this, Replace All asks first. |
| Highlighted line length | 20,000 characters | A minified line renders unstyled rather than dominating the frame. |
| Bracket scan | 200,000 characters | An unmatched bracket cannot stall a repaint. |
| CSV cell height | 3 lines | A cell with many newlines cannot swallow the screen. |
| Exit snapshot | 4 MB per document | Snapshots stay cheap; larger files restore from disk. |
| Recent files / search history | 50 each | Bounded structurally, so neither grows with session length. |
Two more behaviours are worth knowing: the event loop collects memory after 30 seconds of inactivity, so an editor left open all day does not keep the high-water mark of everything it has ever opened; and background work (searches, definition scans, lint passes, loads) is superseded rather than queued, so the newest request always wins.
Reference
Your terminal is intercepting it, which several do. Use F4 and F6, or the Find menu.
Install xclip, wl-clipboard or xsel. Without
one, CMeDit falls back to OSC 52, which your terminal must be configured to
allow; many terminals disable clipboard writes from applications by default.
Automatic theming needs the terminal to answer the background colour query. If
yours does not, set theme = dark-terminal or light-terminal
explicitly, or pick one of the four painted themes, which look the same
everywhere.
That is the fallback rendering. True pixels need kitty graphics or sixel support; the detection is by probe reply, so a terminal that does not answer gets half-blocks. An animation played by the editor rather than the terminal is normal on terminals that support static kitty graphics but not its animation actions.
Check the message on the status bar. Binary files are refused unless they are a
known image, PDF or archive; files over 100 MB open in the paged viewer unless
paged-view is off; and a corrupt image or an encrypted PDF reports what
went wrong rather than dumping bytes at you. A damaged office file or e-book falls
back to its archive listing with a note saying why.
Open File ▸ Settings and look at the Linting section: each row shows either the detected version or the command that would install the tool. Check that the master switch is on, that the file's extension is one the tool handles, and that the document has a real path on disk.
Turn on debug-stats = on for live frame time, heap and background job
counters on the status bar, or run with --stats-on-exit for a one-shot
session summary.
Reference
Copyright © 2026 Benjamin Marsh. CMeDit is free software under the GNU General Public License, version 3 (GPL-3.0-only). You may use, study, share and modify it under those terms; distributed derivative works must remain under the GPL. It comes with no warranty.
Contributions are welcome; merging one requires agreeing to the Contributor License Agreement in the repository.