Tuna’s core: what it is and how to use it
This guide explains Tuna’s core to someone who knows Emacs. It shows every form and procedure a plugin author or a user writes, each beside its closest Emacs equivalent, and says plainly where Tuna differs and why. Nothing here is built yet: it describes the approved design.
Before you start
How to read the guide:
- Code side by side. Where a Tuna form has an Emacs counterpart, the Tuna code is on the left and the Emacs Lisp on the right. On a narrow screen the Emacs code comes below.
- Tags on each entry say where a name comes from: core is given by the core to every plugin; plugin is offered by a plugin’s contract, not the core, and is shown because authors meet it constantly; user is for your
init.ssandM-:; worker is only inside a worker; proposed is not yet approved. - Step-through diagrams in What happens when… light up one step at a time. Press Next. Without JavaScript the steps are listed in full.
- The button at the top of the contents switches between the system’s colours, light and dark.
Tuna is written in Chez Scheme, version 10.4.1. Its code reads like any Scheme. Two things look unusual at first: names written buffers:insert! (a name insert! from the contract buffers), and keywords written for: or depth: (a word ending in a colon, which names the argument after it).
What the core is
The core is the part of Tuna that every other part runs on. It does seven things:
- Runs plugins. It starts, stops, reloads and swaps them, and undoes everything a plugin set up when it stops.
- Checks plugins. A plugin can name only what it imports; the build checks every name before anything runs.
- Keeps state. It holds the user’s data, the lists plugins add entries to, and settings, each change recorded with its owner.
- Runs the command loop. One thread, the editor thread, runs all plugin code: keys, commands, timers.
- Runs heavy work on workers, other threads that cannot touch editor state.
- Pauses commands at a prompt without stopping the editor, and stops runaway code on
C-g. - Lets you change things live:
M-:,C-M-x,reload-config, and a record of who changed what.
What the core is not. The core knows nothing about keys, buffers, windows, commands, files, the minibuffer, the screen or the browser. Each of those is a plugin. Even writing saved data to disk is a plugin (journal-store): the core keeps the data and says when it must be written; the storage plugin decides where and how. Without a storage plugin Tuna still runs; it saves nothing. The same is true of the process service that runs your shells.
Where Emacs differs: Emacs’s core is written in C and holds buffers, windows, keymaps, redisplay and the command loop. Tuna’s core holds none of these. The gain is that each of them can be replaced, reloaded or turned off like any plugin; the cost is that a working editor needs about sixteen plugins listed (see defaults.ss, proposed).
The pieces
Everything in Tuna is made of these pieces. Learn them and the rest follows.
| Piece | One line | Example | Closest in Emacs |
|---|---|---|---|
| Core | The plugin runtime and its rules; no editor features. | starting and stopping plugins, workers | the C core and subr.el, without the editor parts |
| Plugin | A folder with a plugin.ss file (and maybe a contract.ss) that does one job. Its file runs top to bottom when it turns on. | buffers, minibuffer, highlight, terminal | a package, loaded with require |
| Contract | A plugin’s public interface: the names others may use. Others write (import (buffers)) and call buffers:insert!. A contract offers verbs. | (buffers), (text-store) | a package’s public functions, by naming convention only |
| Saved data | The user’s data: one plain value per name, checked, written to disk after every command, kept across reloads, crashes and restarts. | buffer text, undo history, prompt history | savehist, desktop, auto-save files |
| Option | A setting. Each owner (a plugin, an init.ss line, your session) has its own layer; the newest wins. A layer may be scoped to a buffer or a mode. | fill-column | defcustom, setq-local |
| Hook | A list of procedures others add to; each entry belongs to whoever added it. | buffers:after-change-hook | a hook variable |
| Table | A list of named entries others add to through verbs; each entry belongs to whoever added it and goes when they go. | key bindings, commands, faces on a buffer | keymaps, overlays, auto-mode-alist |
| Worker | Heavy work on its own thread. It gets plain read-only data, gives back plain data, and can reach nothing else. | fuzzy matching, highlighting a big file | a subprocess with a filter |
A plugin’s ordinary variables (a counter, a cache, written with plain define) are none of these: they are the plugin’s alone, nobody else can reach them, and they start fresh each time the plugin starts.
Words used everywhere
These words appear on every page. Each is also in the glossary.
- Turning on, turning off. A plugin is on while its file’s effects are in force. Turning it off undoes them all.
- Run. One turning-on of a plugin: its file ran once from the top. A reload ends one run and starts the next. Everything the file set up through the core belongs to that run.
- Owner. Whoever made a change: a plugin’s run, one line of
init.ss, or yourM-:session. When an owner ends, its changes are undone. - Receipt. The core’s list, for each owner, of every change it made: each table entry, option layer, hook entry, timer, job, tie and end action. Ending an owner means undoing its receipt, newest first.
- Provider and caller. The provider is the plugin whose contract is called (
buffersforbuffers:insert!). The caller is the plugin,init.ssline or session whose code contains the call. - Verb. A contract procedure or form that says what you mean:
keys:bind!,buffers:set-faces!,commands:define. A verb makes its entries for its caller, so they go when the caller goes. This is how an ordinary plugin registers anything. - Entry. One key and value in a table or hook, owned by whoever added it.
- Layer. One owner’s value for an option (or for a function, when redefined or advised). The newest layer wins; removing an owner removes its layer and the one below shows again.
- Scope. Where a layer or hook entry applies: everywhere, one buffer (
(buffer . 7)) or one mode ((mode . scheme-mode)). The scoped layer wins only there. Emacs’ssetq-localis a layer scoped to one buffer. - Current buffer. The buffer that text procedures (
buffers:insert!,buffers:point) act on, as in Emacs. Each command, even a paused one, has its own. - Plain data. Numbers, characters, symbols, booleans, strings, bytevectors, vectors, persistent maps, and lists of these. No procedures, records, hashtables or ports. It is what saved data and workers accept.
- Persistent map. A key-to-value map that is never changed in place: “setting” a key gives a new map and leaves the old one as it was. Cheap, because the two share almost everything. Keyed saved data uses it.
- Id. A number naming a thing another plugin owns (a buffer, a marker). Plugins hold each other’s things by id, never by the thing itself. Ids come from
new-idand are never reused. - Job. One run of a worker on its own thread, started by
start-job. - Command and task. A command is what a key runs. The core runs each command as a task on the editor thread; a task can pause (at a prompt) and resume later.
- Editor thread. The one thread that runs all plugin code. Nothing else touches editor state, so it needs no locks.
- View. A buffer a plugin makes to show its own output (
*git-status*, the minibuffer’s prompt). It goes when that plugin turns off. The opposite is the user’s buffer (a file, a shell), which no plugin closes. - Tie. A note on a caller’s receipt that says “when this caller ends, make this call” (for example, kill this view). Made by
on-caller-end. - Swap. Replacing the plugin that provides a contract with another that provides the same contract.
- Reload. Ending a plugin’s run and starting a new one from its (changed) file.
- Rerun. The core reloading a plugin by itself because something it used while starting has changed.