Skip to content

Modules

Per-module settings live under [modules.<name>]. These are global and shared across all panels.

[modules.weather]
unit = "fahrenheit"
refresh_minutes = 30
Key Type Default Description
unit string "fahrenheit" "celsius" or "fahrenheit"
refresh_minutes int 30 How often to re-fetch weather (minutes)
[modules.battery]
poll_seconds = 30
Key Type Default Description
poll_seconds int 30 Polling interval for battery status (seconds)
[modules.clock]
show_date = true
timer_sound = "ding"
timer_volume = 0.7
Key Type Default Description
show_date bool true Whether to show the date alongside time
timer_sound string "ding" What rings when a countdown reaches zero — see below.
timer_volume float 0.7 How loud it rings, from 0 to 1. Clamped.

The shell’s timers live behind the clock: the readout appears beside the time while one runs, the popup under it pauses and stops them, and the calendar overlay’s Timers & stopwatches section is where one is started. So the keys that decide what a finished countdown sounds like are [modules.clock] keys.

A countdown reaching zero does two things, and it does both every time:

  1. It rings. timer_sound is the alarm, and it is what reaches somebody who has walked away from the screen — which is most of the point of setting a timer.
  2. It posts a notification. Timer finished, with the length that ran out, and no timeout on it: it stays on the notification panel until you dismiss it. A sound that has already played tells somebody who missed it nothing, so the notification is what is still there when they come back.

The notification deliberately does not also play the notification chime, whatever that is set to — the timer has already made its own noise, and answering one event with two sounds a frame apart is not an announcement, it is a collision. Setting timer_sound = "none" means a finished timer is silent and notified, not silent and chimed.

timer_sound takes four kinds of answer, tried in this order:

  • One of the shipped alarms — ding, ding-dong, alarm or gong. These are not files: the shell synthesises them from a handful of numbers and writes the result into ~/.cache/moonswing/sounds/ the first time one is wanted, which is what lets them work identically under flutter run, a make install and the snap. It is also what makes them shippable — a recorded kitchen timer is somebody’s sample and somebody’s licence, and these are arithmetic under this project’s own GPL-3.0. ding is the default, one clear strike left to ring; ding-dong is two notes falling, like a doorbell; alarm is two bursts of three quick tones for somebody in the next room; and gong is one low strike with a long tail.
  • none (or off, or silent) — a finished timer notifies without a sound.
  • A path — anything with a / in it, ~ included. Any format mpv can open.
  • A name from the system’s sound theme — anything else, looked for under the XDG sound directories exactly as the notification chime is. timer_sound = "complete" finds /usr/share/sounds/freedesktop/stereo/complete.oga.

A name that resolves to nothing is not silence: the calendar overlay’s timers section says so and names what it could not find. The notification arrives either way, which is why it needs saying.

Several countdowns set to the same minute ring once, not once each — the alarm is longer than a chime and three of them over each other is a noise. Each one still posts its own notification, so nothing about which timers finished is lost.

[modules.media_player]
max_text_width = 200.0
Key Type Default Description
max_text_width float 200.0 Width in pixels before text starts scrolling
[modules.workspaces]
show_app_icons = true
icon_size = 14
max_icons = 4
show_policy_toggle = true
Key Type Default Description
show_app_icons bool true Show the icons of the applications open on each workspace
icon_size int 14 Icon size in pixels (8–64)
max_icons int 4 Icons one workspace shows before the rest collapse into a +N (1–16)
show_policy_toggle bool true Put the tiling/floating switch in a workspace’s right-click menu

With show_app_icons on, each workspace button carries its number or name and the icons of what is open on it, so the buttons are no longer all the same width — a workspace holding three windows is wider than an empty one. The icons come from Miracle’s window tree, and an application is drawn once however many of its windows are on the workspace.

The tree is re-read in response to Miracle’s own window, workspace and output events — never on a timer — and only the window changes that can move a window between workspaces count, so switching focus costs nothing. Nothing is read at all while show_app_icons is off, and one reader serves every panel on every monitor.

Right-clicking a workspace button opens a menu for that workspace — not the focused one, whichever button was clicked. It has two things on it.

With show_policy_toggle on, the first two rows are the workspace’s window placement: Tile new windows and Float new windows, with the one it is currently in marked. Choosing the other sends Miracle workspace <n> policy tile|float, which changes where the next window opens — whatever is already on the workspace stays where it is. Like the urgency flash it costs no extra round-trip: Miracle reports the policy on the GET_WORKSPACES entry the button is already built from, so the menu opens already knowing which row to mark. A Miracle too old to know the command answers with a parse error, and the mark visibly stays where it was rather than reporting a policy nothing took.

Move to output… is the second page of the same menu, and lists every other display Miracle reports — by connector name, with the make and model beside it where Miracle knows them. On a single-monitor session the row is greyed rather than hidden. Miracle’s own move workspace to output acts on the focused workspace and takes no selector, so moving one that is not focused focuses it, moves it and puts the focus back where it was; every hop carries --no-auto-back-and-forth, or a compositor with that option set would read the hop back as “go back” and land somewhere else. The list of displays is read once per connection and refreshed on Miracle’s output event — never on a timer — so the menu answers out of memory rather than on a round-trip.

[modules.dock]
apps = ["firefox", "org.gnome.Nautilus", "kitty", "code"]
icon_size = 24
Key Type Default Description
apps string array [] Desktop entry IDs of apps to show in the dock
icon_size int 24 Icon size in pixels (should fit within panel height)

Each entry in apps is a desktop file ID (the filename without .desktop). For example, "firefox" corresponds to firefox.desktop, and "org.gnome.Nautilus" corresponds to org.gnome.Nautilus.desktop.

Desktop files are looked up from standard XDG application directories. Apps with missing desktop files are silently skipped.

[modules.network]
poll_seconds = 10
Key Type Default Description
poll_seconds int 10 How often to re-check network status (seconds)

A magnifying-glass button that opens the application launcher — the same overlay the open_launcher shortcut opens (see Shortcuts), and only ever one of them at a time.

[modules.launcher]
icon_size = 18
Key Type Default Description
icon_size int 18 Icon size in pixels

The launcher itself lists every installed application, ranks them as you type (matching the name, generic name, Keywords=, and desktop ID, in that order of preference), and launches the selected one with Enter. Applications that declare [Desktop Action …] groups — “New Private Window”, “New Document” — show a chevron; hovering it, or pressing Right with the caret at the end of your query, opens those as a submenu.

Typing a calculation shows its result above the application results:

2^10/4 → 256
(1+2)*3 → 9
sqrt(16)+1 → 5

+ - * / ^ % !, parentheses, and the usual functions (sqrt, sin, cos, tan, ln, log, abs, ceil, floor, …) are understood, along with the constants pi and e. A query has to contain both a digit and an operator to be treated as arithmetic, so searching for an application never turns into a calculation.

Escape or a click on the blurred backdrop dismisses the launcher.

This module is in the default bottom panel, but adding it to an existing config is manual — the default config file is only written when none exists. Add "launcher" to a panel’s layout:

[panels.bottom.layout]
right = ["media_player", "notifications", "launcher"]

The launcher lists the same applications any menu would: those g_app_info_should_show() accepts. An entry with NoDisplay=true, or one restricted with OnlyShowIn=GNOME;, will not appear.

The GitHub mark, the number of unread notifications beside it, and the inbox behind a click: what is waiting for you across every repository you watch, with the reason each thread arrived. Clicking one opens it in your browser and marks it read, the way clicking it on github.com does; the tick beside a row marks it read without opening it, and the double tick in the header marks the whole list.

[modules.github]
refresh_seconds = 60
show_count = true
participating_only = false
include_read = false
mark_read_on_open = true
Key Type Default Description
refresh_seconds int 60 How often the list is re-read (60–3600). GitHub enforces a floor of a minute and may ask for longer
show_count bool true Show the unread count beside the mark
participating_only bool false Only threads you are mentioned in, assigned to or asked to review
include_read bool false Keep threads in the list after they are marked read
mark_read_on_open bool true Mark a notification read as it opens
client_id string the GitHub CLI’s The OAuth app the sign-in runs against — see below
scopes string "notifications" What the sign-in asks for — see below

This module is in the default bottom panel, but adding it to an existing config is manual — the default config file is only written when none exists. Add "github" to a panel’s layout:

[panels.bottom.layout]
left = ["github"]

The GitHub account is linked once for the whole shell, under Settings › Accounts — the module’s popup, while signed out, has a button that goes there. Sign in with GitHub on the GitHub card starts the same device flow gh auth login uses: the shell asks GitHub for an eight character code, shows it on the card, and opens github.com/login/device in your browser. You type the code in there, authorise the app, and the shell takes it from there — your password is never typed into the shell and never reaches it. Closing the settings half-way through loses nothing: the code stays on the card until it expires. Anything else in the shell that reads GitHub uses the same account — the todo board, for one, shows a link to an issue or pull request by its title.

The access token is written to ~/.local/state/moonswing/github-token (mode 0600, in a directory created 0700), not into config.toml. Sign out on the Accounts card deletes it. That signs this machine out; it does not revoke the authorisation, which is done from Settings › Applications on github.com — the key button beside Sign out opens that page.

The consent screen says GitHub CLI, because client_id defaults to that tool’s public client id — a client id is public by construction, and borrowing one is what lets this work without every user registering an app. To use your own instead, register an OAuth app (Developer settings › OAuth Apps) with Enable Device Flow ticked, and name it:

[modules.github]
client_id = "Iv1.0123456789abcdef"

scopes is what that sign-in asks for. The default, notifications, is read access to the notification list plus the two calls that mark a thread read — nothing else, not the contents of a single repository. Notifications from private repositories are not included in that; listing them needs repo, which is full read/write access to every repository you can reach. The same goes for a todo card’s link into a private repository, which is shown by its title only with repo:

[modules.github]
scopes = "notifications repo"

Change either key and the saved token no longer matches what is being asked for: sign out and in again.

There is no push channel for notifications — GitHub’s API is polled — so the module polls it the way the API asks to be polled. Each request carries the previous response’s validator, so an unchanged list comes back as a 304 that costs no rate-limit quota, and GitHub’s own X-Poll-Interval is honoured whenever it asks for longer than refresh_seconds. Nothing is polled at all while no panel carries the module: one poll for the machine, however many bars draw the mark.

Copying the sign-in code needs wl-copy (the wl-clipboard package), as the emoji picker does; without it the code can still be typed out by hand, and the card says so.

Your watchlist crawling along the bar the way the ticker runs along the bottom of a financial news channel: each symbol, its price, and its move today in green or red. Hover the strip to hold it still, or set scroll = false to have it stand still on the first ticker of the list for good. Click it for the list itself, where you can search by symbol or company name and add what you find, reorder the list with the arrows on a row, remove a ticker with its ×, or click a row to open its page on Yahoo Finance.

Quotes come from Yahoo Finance, read exactly the way ticker reads them — the same request, the same session handshake, and the same rule for which price is “the” price: outside the regular session the strip shows the pre-market or after-hours price once anything has traded, and the day’s change includes it. The Stock market desktop widget reads the same quotes from the same poll, so the two never disagree.

[modules.stocks]
symbols = ["^GSPC", "^DJI", "^IXIC", "AAPL", "MSFT", "BTC-USD"]
refresh_seconds = 15
scroll = true
scroll_speed = 40
width = 320
Key Type Default Description
symbols array of strings [] The watchlist, in crawl order, as Yahoo Finance spells each symbol
refresh_seconds int 15 How often prices are re-read while a market on the list is trading (5–3600)
scroll bool true Whether the strip crawls. false shows only the first ticker on the list, standing still
scroll_speed float 40 How fast the strip crawls, in pixels a second (5–400)
width float 320 How wide the crawling strip is in a horizontal bar (80–2000). A watchlist narrower than this sits still

Symbols are Yahoo Finance’s: AAPL for a US stock, ^GSPC for the S&P 500, BRK-B for a class of share, SAP.DE for a stock on another exchange, EURUSD=X for a currency pair, BTC-USD for a coin. Indices are shown by name and currency pairs as EUR/USD. The popup’s add button writes symbols for you, and Settings → Shell → Module Settings → Stock market edits the same list.

A new watchlist starts empty; the popup offers the S&P 500, the Dow and the Nasdaq as a one-click start. While every market on the list is closed, prices are re-read every two minutes instead of every refresh_seconds, and nothing is requested at all while neither the strip nor the widget is on screen. In a left or right bar, where there is no room to crawl, the module is a chart icon coloured by the first ticker’s move.

The module is not in the default layout — add "stocks" to a panel’s left, center or right list, or use Settings → Shell → Panels & Layout.

A bell that shakes and shows a count when a notification arrives, and opens the notification panel on click — a full-height surface that sweeps in from the right edge over everything else on screen, and back out the same way when dismissed, listing what has arrived with each notification’s actions, a mark everything read button and a Clear all. The shell is the desktop’s notification daemon, so this is where notifications from every application land.

[modules.notifications]
sound = "chime"
sound_volume = 0.7
Key Type Default What it does
sound string "chime" What plays when a notification arrives — see below.
sound_volume float 0.7 How loud it plays, from 0 to 1. Clamped.

sound takes four kinds of answer, tried in this order:

  • One of the shipped sounds — chime, ping, glass, bell or knock. These are not files: the shell synthesises them from a handful of numbers and writes the result into ~/.cache/moonswing/sounds/ the first time one is wanted, which is what lets them work identically under flutter run, a make install and the snap. chime is the default.
  • none (or off, or silent) — notifications arrive without a sound.
  • A path — anything with a / in it, ~ included. Any format mpv can open.
  • A name from the system’s sound theme — anything else. Looked for under the XDG sound directories (~/.local/share/sounds, /usr/share/sounds, and whatever XDG_DATA_DIRS names), both directly and in the <theme>/<profile>/ layout the sound-theme-freedesktop package installs. sound = "message" finds /usr/share/sounds/freedesktop/stereo/message.oga.

A name that resolves to nothing is not silence: the notification panel’s Notification sound row says so, names the key, and previews whatever is configured when you click it.

Two things the chime deliberately does not do. It does not play while notifications are silenced — silencing is about interruption, and a sound is the most interrupting thing the shell does. And a burst of notifications arriving together plays once rather than once each, which is why the count on the bell is what says how many there were.

The chime belongs to this module, so it plays only while a notifications module is in one of your panels. With the module in no panel there is nothing holding it open, and the panel’s sound row says that too.

This module is in the default bottom panel, but adding it to an existing config is manual — the default config file is only written when none exists. Add "notifications" to a panel’s layout:

[panels.bottom.layout]
right = ["media_player", "notifications", "launcher"]

The panel ignores the bars’ exclusive zones and draws on the overlay layer, so it covers the full height of the output and passes over the panels rather than being pushed between them.

The panel’s two clearing buttons are not the same act. The ✓✓ button marks everything read: nothing is removed, the messages stay on the list to be read again, and what stops is the shell asking — the bell’s count, the floating card in the corner of every output, and the chime. Clear all empties the list.

Opening the panel does not mark anything read. Reading a notification is something you do, not something a window being mapped does for you.

No configurable settings.

Drives both the panel module (CPU, memory, and temperature, with a popup) and the System tab in the overlay, which adds usage graphs, swap, load average, network throughput, disk usage, and a sortable process table you can kill from. The two share one sampler, so these settings apply to both.

[modules.system_monitor]
poll_seconds = 1
temp_unit = "celsius"
history_samples = 120
cpu_percent_mode = "machine"
show_kernel_threads = false
confirm_kill = true
kill_grace_seconds = 5
disk_poll_seconds = 30
Key Type Default Description
poll_seconds int 1 How often stats are re-read (1–60)
temp_unit string "celsius" "celsius" or "fahrenheit"
history_samples int 120 How many samples the graphs keep (10–600). At the default cadence, 120 is two minutes
cpu_percent_mode string "machine" "machine": 0–100% of the whole machine, so the process rows sum to the total. "core": top-style, where 100% is one saturated core
show_kernel_threads bool false Show kernel threads in the process table
confirm_kill bool true Ask before quitting a process. A force quit always confirms regardless
kill_grace_seconds int 5 How long a process gets to honour the request to quit before the row offers to force it (1–60)
disk_poll_seconds int 30 How often filesystem usage is re-read (5–600). Separate because it shells out to df

Nothing is read until something needs it: the shell only samples /proc while the panel module is on a panel or the System tab is open, and the per-process scan runs only while that tab is actually the visible one.

Two processes can never be killed from here, whatever the config says: the shell itself, and init.

The camera in the bar: click it, choose an area, a window or a screen, and the PNG is on the disk and on the clipboard. The same settings apply to the screenshot_area shortcut, which takes the shot without the module being in any panel.

[modules.screenshot]
directory = "~/Pictures/Screenshots"
filename_prefix = "Screenshot"
copy_to_clipboard = true
delay_seconds = 0
show_cursor = false
shutter_sound = "shutter"
shutter_volume = 0.6
Key Type Default What it does
directory string ~/Pictures/Screenshots Where the PNG is written. ~ and $HOME are expanded.
filename_prefix string "Screenshot" What each file is named before the date and time.
copy_to_clipboard bool true Also put the shot on the clipboard, via wl-copy.
delay_seconds int 0 A pause before the shutter, for getting a menu on screen first (0–60).
show_cursor bool false Paint the pointer into the frame.
shutter_sound string "shutter" What plays once the file has been written — see below.
shutter_volume float 0.6 How loud it plays, from 0 to 1. Clamped.

The sound plays when the screenshot has been saved, not when you choose what to capture — so it follows delay_seconds, and it is the thing that tells you the file exists. A capture that failed makes no sound, because nothing was photographed.

shutter_sound takes four kinds of answer, tried in this order:

  • One of the shipped shutters — shutter, snap, clack or tick. These are not files: the shell synthesises them from a handful of numbers and writes the result into ~/.cache/moonswing/sounds/ the first time one is wanted, which is what lets them work identically under flutter run, a make install and the snap. It is also what makes them shippable — a camera click recording is almost always somebody’s licensed sample, and these are arithmetic under this project’s own GPL-3.0, with no third party in them. shutter is the default: a reflex camera, mirror up and mirror down. snap is one bright click, clack a heavier mechanism with the body ringing under it, and tick a small dry tick for somebody who wants the confirmation without the theatre.
  • none (or off, or silent) — screenshots are saved without a sound.
  • A path — anything with a / in it, ~ included. Any format mpv can open.
  • A name from the system’s sound theme — anything else, looked for under the XDG sound directories exactly as the notification chime is. shutter_sound = "camera-shutter" finds /usr/share/sounds/freedesktop/stereo/camera-shutter.oga.

A name that resolves to nothing is not silence: the screenshot menu says so and names what it could not find. The file is written either way, which is why it needs saying — a shutter that has quietly stopped working looks exactly like one you turned off.

There is deliberately no shutter for a recording. A recording announces itself by the readout in the bar for as long as it runs, and a camera click at the end of one would be describing a photograph nobody took.

The note in the bar is the window manager’s scratchpad: a place to stash a window off every workspace and call it back over whatever you are doing. A click shows what is on the scratchpad, centred on the screen you are on, or hides it again. Hovering it names the two shortcuts that go with it — toggle_scratchpad, which does what the click does, and move_to_scratchpad, which stashes the window you are in. That half has no button, because clicking the bar is not the window you meant to stash.

The module has no settings of its own. It shows no count of what is stashed, because the window manager does not report one: a stashed window is taken out of its window tree altogether. Every stashed window is shown and hidden together.

If the shell cannot reach the window manager, the icon dims and its label says why; the next click tries again.

The checklist icon opens the todo board on the screen whose bar you clicked. It is a board of five columns — Inbox, Todo, In Progress, Finished and Abandoned — and a card is moved between them by dragging it. The + at the top of a column adds a card there; clicking a card opens it to edit its title, details, column, due date and repetition, and to read its history. Every time a card changes column, the date and time is recorded in that history.

Overdue cards sit at the top of Inbox, Todo and In Progress, above the rest, which keep the order you dragged them into. Finished shows what was finished today; everything finished earlier is folded under the day it was finished, and Abandoned folds every day, today’s included. Click a day to open or close it. While you search, every day holding a match is open.

When the shell starts, and again at each midnight, it posts one notification listing what is due that day, followed by anything still open from an earlier day. The number beside the icon counts the same things. Finished and abandoned cards are never counted.

A card can repeat every so many days, weeks or months, starting on a day you choose. On each scheduled day a copy of it is added to the top of Todo, due that day, and the repetition moves to the copy — so the newest copy is the one to edit or delete to change or stop it. A monthly card started on the 31st lands on the last day of shorter months and goes back to the 31st after them. If the shell was not running on a scheduled day, one copy is made when it next starts, for the most recent day missed.

The search field at the top of the board filters every column down to the cards that match, and highlights what matched. It looks for any piece of text — the middle of a word counts, and case does not — in a card’s title and details; with several words, a card has to contain all of them. Ctrl+F returns to the field, and Escape clears it before it closes the board.

A web address in a card’s title or details is a link: clicking it opens the address in your browser rather than the card. While a GitHub account is signed in (Settings › Accounts), a link to a GitHub issue or pull request is drawn as a chip instead, with its title, its number and an icon for whether it is open, a draft, merged or closed; hovering over it says which repository it is in, and clicking it still opens the page. A title is read when the board first shows the card and read again when the board is opened more than ten minutes later, so the chips catch up with merges. A link GitHub will not answer for keeps its number, with a warning icon and the reason on hover — a link into a private repository needs the repo scope (see GitHub).

The megaphone at the top of the board opens your standup summaries, and New summary there writes one: what you moved to Finished since the last summary, everything in In Progress (marked started if it moved there since then), everything in Todo with its due date, overdue first, and anything you moved to Abandoned since then. The first time, it covers the last 24 hours. Opening the megaphone only shows the summaries already taken; it never writes a new one. It is plain text, for pasting into a chat: select it where it is (drag, double-click, or Ctrl+A) and copy with Ctrl+C, which needs wl-clipboard. A web address in a card’s title shows up as a link, and clicking it opens it. Inbox is left out, and so are calendar events the Google sync put on the board — a meeting is not work to report. Nothing is sent anywhere.

The board is kept in an SQLite database, ~/.local/share/moonswing/notes.db (or under $XDG_DATA_HOME), not in config.toml, and saved as you go. SQLite comes with the shell, so nothing needs installing. A board saved by an older version of the shell as todo.json is imported the first time, and the file is renamed to todo.json.imported rather than deleted. If the board cannot be read, it says why and refuses to save anything over it until it can. The module has no settings in config.toml.

Every change is also written to a plain JSON backup, ~/.local/share/moonswing/backup/todo-backup.json, and the line along the bottom of the board says where it is. If notes.db is missing when the shell starts, the board is restored from that file; if it is damaged, it is renamed to notes.db.damaged-<time> — never deleted — and rebuilt from the file. Either way the board says so, since anything changed after the backup was last written is not on it. A database written by a newer version of the shell is not damaged, and is left alone.

A backup on the same disk goes when the disk does, so keep a copy of that file somewhere else. Its folder holds nothing else, which makes it easy to keep in Git:

Terminal window
cd ~/.local/share/moonswing/backup
git init
git add todo-backup.json
git commit -m "Todo backup"

The file is rewritten only when the board changes, pretty-printed with cards in board order and notes in the order they were made, so each commit is exactly what changed. Replacing the file with a symlink into a repository you already keep works too: it is written through the link.

Backups… at the bottom of the board (or the cloud icon beside the search field) shows where the file is, and Restore from this file… replaces the board with it — for when you have put back an older one — after saving the current board beside it.

The board can be kept in step with a task list on a CalDAV server — Radicale, Baïkal, Nextcloud, ownCloud, Fastmail, iCloud and the like — so a phone (DAVx⁵ with jtx Board or Tasks.org) or another computer (Thunderbird, Evolution) can read and change the same cards. The board itself stays where it is: the database above is still what is on screen, it works offline, search still covers every card and note, and the backup file is still written.

Sign in once under Settings › Accounts › CalDAV with the server’s address (or the address of one calendar or task list), your user name and a password — an app password, where the server offers them. The shell asks the server for your calendars and task lists before keeping anything, so a wrong address or password is said there and then. Add account signs in to another server (or as another user on the same one) beside it; signing in again with an address and user name already listed replaces that account rather than listing it twice. The addresses and passwords are kept in ~/.local/state/moonswing/caldav-account.json (or under $XDG_STATE_HOME), readable only by you, not in config.toml. The same accounts’ calendars of events can be shown on the Calendar tab; see Calendar.

The server’s certificate is checked against the system’s trust store (/etc/ssl/certs). A server that store cannot vouch for — self-signed, signed by your own CA, or serving its certificate without the intermediate that links it to a public one — is refused with CERTIFICATE_VERIFY_FAILED, and the form shows the certificate’s SHA-256 fingerprint. Compare it with openssl x509 -in cert.pem -noout -fingerprint -sha256 on the server; if they match, Trust certificate and connect pins that one certificate to the account (for an account already signed in, the card offers Trust beside it instead). Only it is trusted, not certificates in general, so a renewed self-signed certificate is refused again until it is trusted in turn. The alternative that needs no pin is to make the machine trust the server: have it serve its full chain, or add your CA to the system trust store (update-ca-certificates on Debian and Ubuntu, trust anchor on Fedora and Arch).

Then open Backups… on the board, pick a list and Link. With more than one account signed in, every account’s lists are offered, each named with its account, and the sync signs in with the account the list was found on. Linking merges: every card is sent to the list and every task on it comes to the board. With the switch beside it turned on it replaces the board instead — the cards are removed and the list’s tasks take their place, after the board is saved beside the backup file as a restore does. Unlink stops the syncing and leaves both sides as they are.

What a task list holds of a card is its title, its notes, its column, its due day and a simple repeat. The columns are the task’s status — Todo is needs action, In Progress in process, Finished completed and Abandoned cancelled; an Inbox card is needs action with a marker other apps keep and ignore. A repeat of every so many days, weeks, months or years goes both ways; anything more (say, every Monday and Wednesday) is left on the server as it is, and the card does not repeat on the board. The move history and the order within a column stay on this machine, and everything a task has that a card does not — alarms, categories, another app’s own fields — stays on the server untouched: only the fields that changed are written back. Meetings the Google calendar sync put on the board are never sent.

The board syncs a few seconds after you change it, when you open it, when you press Sync now, and every five minutes while a list is linked — nothing at all when none is. A card changed on both sides since the last sync keeps each side’s change where they touched different fields; where both changed the same field, the server’s version is kept and the board lists the cards it happened to. A task deleted on the server takes its card with it, unless the card was changed here since, in which case it is sent back as a new task; a card deleted here is deleted there, unless the task was changed there since. A server that cannot be reached is shown in red along the bottom of the board, and nothing is lost: whatever did not get through goes on the next sync that does.