Everything EEditor does, in the order you're likely to need it.
A workspace is an ordinary folder. EEditor shows what's in it, edits the Markdown files it
finds, and writes nothing else there except one hidden folder, .eeditor/, for
your keybindings.
On first launch EEditor picks a default folder and opens it. To use a different one, press open… at the top of the file tree and choose any directory — a git repository, a synced folder, an external disk. The choice survives restarts.
Because the workspace is just a folder, everything you already do with folders keeps working: back it up, version it, sync it, or open the same notes in another editor. EEditor takes no lock and keeps no separate index that could drift from the files.
What EEditor stores outside your notes: the agenda database (items, categories, rules) and any tables you create in EELisp live in an SQLite file in the application's own data directory — not in your workspace. Your notes are never inside it.
The file tree is the left column. Click a file to open it. The two buttons at the top create a new file and a new folder at the root; to create inside a folder, right-click it (long-press on a touch screen) and use the menu, which also offers Rename… and Delete.
Renaming and deleting keep open tabs honest: a renamed file follows its tab, and deleting a file or folder closes the tabs underneath it and moves you to a neighbour rather than leaving a dead editor.
Files beginning with a dot are hidden from the tree, which is why your keybindings live in
.eeditor/keybindings.eelisp — present, editable, out of the way.
⌘D opens today's note, YYYY-MM-DD.md at the root of the workspace,
creating it with a date heading if it doesn't exist. It never overwrites an existing one, so
pressing it repeatedly through the day just takes you back to the same note.
The editor is CodeMirror 6 with Markdown syntax highlighting. Edits save automatically about a second after you stop typing; ⌘S saves immediately.
⌘E (or the preview button) swaps the editor for rendered Markdown. Wiki links become clickable there. Press it again to go back.
The ☀/☾ button toggles between a light theme (Solarized) and a dark one (One Dark). Your choice is remembered.
PDF renders the current note and opens the system print dialog, where Save as PDF produces a file. Headings, lists, tables and code blocks all come across.
Put EELisp in a fenced block and press ⌘⇧↵ with the caret inside it. The result appears in the REPL below — a live notebook without leaving the document.
```eelisp
(items :category "work" :when-before (today))
```
Opening a second note puts it in a new tab rather than replacing what you had. Click a tab to
switch, click its × to close. ⌘⌥→ and ⌘⌥← move between
tabs if you bind them — see keybindings for the
next-tab, prev-tab and close-tab commands.
A tab marked ↗ is a file from outside the workspace; see §7.
Fuzzy matching over every file in the workspace. Type fragments in any order —
agmt finds agenda-meeting.md. Arrow keys move, ↵ opens,
Esc dismisses.
Searches the contents of every note and lists each hit with its line. Choosing one opens the note at that line.
Any #hashtag in a note is indexed and appears in the tags panel
in the sidebar, with a count. Clicking a tag opens full-text search filtered to it, which is
the fastest way to see everything you marked #idea or #invoice.
Write [[Note name]] to link to another note. Typing [[ opens an
autocomplete list of existing notes, so links stay spelled the way the files are.
In preview, a wiki link is a plain click. In the editor, ⌘-click it. Either way you land in the linked note.
Under the editor, a bar lists every note that links to the one you're reading — ↩ 3 backlinks: index.md, projects.md, 2026-08-09.md. Click one to follow it. The bar hides itself when nothing points here.
Notes opened from outside the workspace are not part of the link graph, so they show no backlinks and aren't offered in autocomplete.
There are three ways a foreign file arrives:
However it arrives, EEditor asks once what you want:
An in-place file is deliberately not part of the workspace: it won't appear in the tree, in search results, in the tag index or in backlinks. If you decide you want it after all, press copy in in the header and it becomes a real note — including whatever unsaved edits are on screen, which also makes this the escape hatch for a read-only original.
EEditor registers itself for .md, .markdown, .txt,
.eelisp and .lisp, as an alternative handler rather than stealing
those types from your existing editor.
The agenda is a personal information manager in the Lotus Agenda tradition: you throw text at it, and structure emerges from rules rather than from forms you fill in.
The box at the top of the agenda panel takes plain language and parses it — call Bob tomorrow !! becomes an item due tomorrow at high priority. Dates, priorities and people are recognised as you write them.
Click one to expand its editor, which holds:
Categories are hierarchical: work/clients/acme is a child of
work/clients. Assigning a child implies its parents, and an exclusive parent
allows only one of its children at a time — so an item can't be both
status/open and status/done.
Rules are the part that makes the agenda feel alive. A rule is a condition and an action; run them and every matching item is filed automatically. Open the setup panel to define categories and rules, then apply them.
(defrule invoices
:when (str-matches text "invoice|bill|payment")
:assign "finance/invoices")
Inside a condition the item is in scope: text, notes,
id, categories and props are bound, along with
(get "field"), (has-category "work"), (overdue?) and
(match n) for the capture groups of the last regular expression. Because rules
are stored as EELisp and re-evaluated, anything the language can express, a rule can decide.
A view is a saved filter with optional grouping — everything overdue, grouped by
category. Define with defview, list with views, display with
show.
The calendar button opens a month grid of dated items. Drag an item to another day to reschedule it; the change is written straight back.
A recurring item regenerates when completed. every handles the date arithmetic —
daily, weekly and monthly, with intervals like every 2 weeks — without depending on
an external calendar.
⌘J shows and hides the EELisp REPL. Type an expression, press ⌘↵, and the result appears above the prompt.
Results are not text dumps. A query renders as a real table; defform and
edit render as a real form with working fields, including computed ones. This is
dBASE III's interaction model, rebuilt.
λ (deftable notes (title:string body:string))
λ (insert notes {:title "Kickoff" :body "Ship by Friday"})
λ (browse notes) ; → a table widget
λ (edit notes 1) ; → a form widget
The snippets button loads the standard EELisp bundle — ten modules and over a hundred and fifty functions covering text, maths, dates, conversion, crypto, reference tables, networking, world clocks and Bitcoin. Once loaded they're callable from the REPL and from your keybindings alike.
The full language is documented at eelisp.app.
⌘⇧K opens .eeditor/keybindings.eelisp, writing the documented defaults
into it the first time. That file is the shortcut table. Save it and it reloads at
once; delete it and the built-in defaults apply again.
(bind "Mod-i" body…)
Modifiers join with - or +. Mod is ⌘ on
macOS and Ctrl elsewhere, and Cmd, Ctrl, Alt
(Option) and Shift can be named directly: "Mod-Shift-f",
"Ctrl-Alt-t". Every binding needs at least one modifier — function keys and
Esc may go bare. A later binding wins the same key, and unbinding one hands the key
back to the editor's own behaviour.
The body is EELisp, evaluated on the engine when the key fires. It returns editor commands as data, and a binding may list several — they run in order against the live document.
| Command | Effect |
|---|---|
(ed-cmd "save") | run a built-in command (table below) |
(ed-insert text) | insert at the caret, caret after the text |
(ed-insert-at pos text) | insert at an absolute offset |
(ed-goto pos) | caret to an absolute offset |
(ed-goto-line n) | caret to the start of line n (1-based) |
(ed-select from to) | select a range |
(ed-replace text) | replace the selection |
(ed-replace-range from to text) | replace an explicit range |
(ed-set-buffer text) | replace the whole document |
(ed-open "notes/x.md") | open a file in a tab |
(ed-new path)(ed-new path text) | create the note if it doesn't exist, then open it — never overwrites |
(ed-message text) | show a toast |
(ed-nothing) | do nothing, useful as a cond branch |
These are defined fresh before every run:
| Variable | Is |
|---|---|
*file* | path of the current note |
*cursor* | caret offset |
*line* / *col* | caret line and column, both 1-based |
*line-text* | text of the caret's line |
*lines* | number of lines in the document |
*sel-from* / *sel-to* | selection bounds |
*selection* | the selected text |
*buffer* | the whole document |
*date* | "YYYY-MM-DD", local time |
*time* | "HH:MM" |
*now* | "YYYY-MM-DD HH:MM:SS" |
*selection* and *buffer* are only computed when your binding
mentions them, so a shortcut stays fast in a large document.
(on-start …) runs the same kind of commands once, after the workspace loads. With
no on-start form at all, EEditor opens welcome.md, or the first note
it finds.
(on-start (ed-open "todo.md")) ; always this note
(on-start (ed-cmd "daily-note")) ; today's note, created if needed
(on-start (ed-new (str *date* ".md"))) ; the same, spelled out
(on-start) ; nothing — an empty editor
An empty (on-start) is deliberately different from leaving the form out: one says
"open nothing", the other says "decide for me".
Anything here can be named by (ed-cmd "…"):
| Command | Does |
|---|---|
save | save the current note |
quick-open | fuzzy file finder |
search | full-text search |
daily-note | open (or create) today's note |
new-file / new-folder | create at the workspace root, asking for a name |
open-file | pick any file; asks whether to copy it in or edit in place |
copy-into-workspace | turn the current ↗ note into a copy in the workspace |
open-keys / reload-keys | edit or re-read this config |
toggle-repl / focus-repl | show/hide the REPL, or jump into it |
focus-editor | put the caret back in the document |
toggle-preview | editor ⇄ rendered Markdown |
toggle-theme | light ⇄ dark |
export-pdf | print the current note to PDF |
snippets | load the standard EELisp bundle |
calendar | open the month grid |
agenda-setup | categories and rules panel |
close-tab / next-tab / prev-tab | tab management |
;; A timestamped heading at the caret, on its own line
(bind "Mod-Shift-i"
(ed-insert (str (cond (= *col* 1) "" true "\n") "## " *time* "\n\n")))
;; Wrap the selection in bold
(bind "Mod-b" (ed-replace (str "**" *selection* "**")))
;; A task line
(bind "Mod-Shift-t" (ed-insert "- [ ] "))
;; Link back to today's note
(bind "Mod-Shift-b" (ed-insert (str "[[" *date* "]]")))
;; File this note under a dated folder and tell me about it
(bind "Mod-Alt-n"
(ed-new (str "journal/" *date* ".md") (str "# " *date* "\n\n"))
(ed-message "journal entry ready"))
Mistakes are reported, not swallowed: a binding that fails prints its error in the REPL and shows a toast naming the key, and problems in the config itself are listed when it reloads.
Below 900 pixels the layout becomes a single pane with a tab bar — Files, Editor, REPL — so the same app works on a phone without pretending to be a desktop.
The workspace is the app's Documents folder, exposed in Files → On My iPhone → EEditor. Anything you put there appears in EEditor, and anything EEditor writes is visible to other apps and to whatever backup or sync you already use.
file… imports a single document through the system picker. folder… points EEditor at an external folder — an iCloud Drive directory, for instance — and remembers it across launches.
The defaults. ⌘ is Ctrl on Windows and Linux.
| Key | Does |
|---|---|
| ⌘S | Save |
| ⌘P | Quick open |
| ⌘⇧F | Full-text search |
| ⌘D | Today's note |
| ⌘I | New note stamped with the minute |
| ⌘⇧I | Timestamped heading at the caret |
| ⌘⇧N | New file |
| ⌘E | Preview ⇄ edit |
| ⌘J | Show/hide the REPL |
| ⌘⇧K | Edit these keybindings |
| ⌘⇧↵ | Run the eelisp block at the caret |
| ⌘↵ | Evaluate in the REPL |
| ⌘-click | Follow a wiki link from the editor |
Every one of these is a line in your keybindings file, and every one can be changed.