The profile
You don’t have to write tabs by hand: arrange the panes in zellij and
phosphor keep writes that tab here for you.
~/.config/phosphor/deck.toml (override: PHOSPHOR_PROFILE). phosphor init
writes it, phosphor setup edits it, you can edit it by hand; after a hand
edit run phosphor gen && phosphor restart. Files that gen writes say
“GENERATED BY phosphor gen”: never edit those.
[deck]
Section titled “[deck]”| key | default | what |
|---|---|---|
| session | “deck” | the session’s name; it shows in the tab bar |
| command | “deck” | the one-word command that gets you in (~/.local/bin/<it>) |
| theme | “p31” | p31 green, p3 amber, p4 white, paper (e-ink) |
| mount_root | “~/fleet” | where the fleet’s files appear |
| projects | “~/projects” | where workspaces are made (phosphor workspace) |
| mesh | auto | tailscale, headscale, none (plain ssh), or auto: detect |
| bar | “tabs” | tabs (tab bar with its + menu), compact, full (adds key hints) |
| editor | your $EDITOR, else nano | what panes open files with |
| shell | your login shell | what shell panes, + → shell and zellij’s new panes run (bash, zsh, fish…) |
| connect | “ssh” | how other computers attach: ssh (mouse, touch) or mosh |
| web | false | the deck in a browser, tailnet only (phosphor web on) |
| web_port | 8443 | the HTTPS port tailscale serve publishes it on |
| notifier | false | floating adjutant on every tab; off by default – see notifier below |
| graphs | “braille” | “blocks” draws the graphs with block characters, for fonts without Braille |
| face | “” | adjutant face (see phosphor face) |
| notify_seconds | 8 | how long a floating notice stays (notifier = true) |
| demo | false | fleet fakes its readings instead of polling ssh (what phosphor demo sets) |
notifier opens a floating pane per tab for phosphor notify’s toast. It’s
off by default: zellij 0.45’s screen thread froze twice in real use, both
times right after hide-floating-panes -t ran across every tab – shown or
not – to clear the toast. No isolated repro exists for the freeze itself
(see issue #14), but that “touch every tab, whether it needs it or not”
pattern was the only clue there was, and it’s gone: phosphor notify now
targets only the tab it actually showed the toast on, both to show it (a
bare show-floating-panes, with no tab named, can itself answer “Tab not
found” when called from outside the client – naming the tab fixes that
too) and to hide it again. If you turn notifier on, this is lower-risk
than before, not risk-free.
theme recolors more than zellij and the web client: phosphor gen also
writes it into yazi’s theme.toml, btop’s own phosphor theme (set as
color_theme in btop.conf), and passes it to gping and ctop on their
command line (ctop only tells light from dark, so paper is the one theme
that inverts it). Each is left alone the moment it holds something gen
didn’t write itself – a color_theme you picked, a theme.toml of your
own.
[[hosts]]
Section titled “[[hosts]]”One block per machine.
| key | what |
|---|---|
| name | its name in the deck |
| role | brain (exactly one, and local), work, desktop, storage, node, viewer |
| local | true on the brain |
| ssh | ssh alias or host name |
| user | only if your user there differs |
| ip | optional; gping pings it |
| mount | remote folder shown in ~/fleet/<name> (rclone sftp through your own ssh: ssh config, keys, known_hosts) |
| mounts | the brain’s own folders shown in ~/fleet/<brain>/ (/ → root, ~ → home, /mnt/X → X) |
| fleet | false keeps it in the deck but off the fleet panel |
Viewers are never polled or mounted. @work, @desktop… in a tab mean “the
first host with that role”; @hosts means all of them; @mount_root is
~/fleet.
[[tabs]]
Section titled “[[tabs]]”In order; Alt-1..9 go to them. name, optional split (“rows” or “cols”),
and panes, a list of pane specs:
| key | what |
|---|---|
| cmd | the program (phosphor pulse, yazi, …); empty: a shell |
| args | its arguments (@... tokens expand, see above) |
| ssh | a machine to ssh into instead of a program |
| reconnect | ssh panes: retry when the link drops (default true) |
| size | lines/columns, or a percentage like “55%” |
| cwd | working folder |
| needs_size | wait a second before starting (yazi, ctop read the size once) |
| alt | start on the alternate screen (gping, ctop) |
| split, panes | a container with its own panes |
Every program runs through phosphor run (see keys).
tabs.d: tabs you can share
Section titled “tabs.d: tabs you can share”A [[tabs]] block dropped into ~/.config/phosphor/tabs.d/*.toml (one file,
same shape as above) shows up in the deck without touching your own profile
– pass someone a file, they drop it in, phosphor gen. They come after your
own tabs, in file-name order; a name your profile already uses wins.
Their content is read-only from the tools that edit your profile: Alt-r’s
save and phosphor keep say which tabs.d/*.toml a tab comes from instead
of touching it. Edit that file by hand, then phosphor gen, to change one.
Their position isn’t: phosphor tabs can still move one with K/J, the
same as any tab. The first time, it writes a name-only stub into your
profile – [[tabs]] name = "BUBBLE" and nothing else – marking where it
goes; its content still comes from tabs.d. f on a placed one takes the
stub back out (it returns to the end, in file-name order), not the tab
itself; the file it came from is never touched.
phosphor recipe drops a starter bundle in this same way (see commands).
[[tunnels]]
Section titled “[[tunnels]]”| key | what |
|---|---|
| host | an ~/.ssh/config alias whose LocalForward lines the deck keeps up |
| config | optional: another ssh config file for it |
[keys]
Section titled “[keys]”The deck’s own keys (phosphor shortcuts changes them for you). Anything
left out keeps its default; "" turns one off.
| key | default | what |
|---|---|---|
| edit | “Alt r” | edit this tab: unlock, change, save or put back |
| new_tab | “Alt n” | a new tab (the + menu) in the pane’s folder |
| note | “Alt j” | a note or todo from here, tagged with the tab you’re in |
| zoom | “Alt z” | the focused pane takes the whole tab |
| leave | “Alt x” | leave the deck (Ctrl-q always does too) |
| controls | “Alt g” | take and give back zellij’s controls |
| tabs | “Alt” | this + 1..9 goes to a tab (“Alt”, “Ctrl Alt”, “Ctrl Shift”) |
| panes | “Alt” | this + arrows moves between panes |
[notes]
Section titled “[notes]”Where the shared notebook lives. Without this table it’s private, at
~/.local/share/phosphor/notes.md. phosphor init asks (offering folders
with .obsidian/), and phosphor setup changes it later; either way,
whatever notebook you already had moves into the new folder, never
overwriting one that’s already there.
| key | what |
|---|---|
| folder | a folder you already sync (Obsidian, Syncthing, git); notes.md goes inside it |
Phosphor syncs nothing itself: the folder reaches other machines however you
already sync it. The notebook stays one file (notes.md, plus
notes-archive.md), so it shows in Obsidian as a single long note, not one
note per entry. Editing it from Obsidian and from phosphor note/notes at
the same moment isn’t merged: whichever save lands last wins, same as any
shared file edited from two places at once. Across machines, a real
conflict (both edited while offline from each other) becomes whatever
conflict file your sync tool makes – Phosphor doesn’t resolve it.
[mentions]
Section titled “[mentions]”| key | what |
|---|---|
| prepare | a command that writes briefing notes; the notification comes on stdin |
| by | the name shown in WORK NOTES |
| prompt | replaces the default briefing prompt ({sender}, {message}) |
| timeout | seconds before giving up (default 600) |
[prometheus]
Section titled “[prometheus]”For phosphor prom (and “prometheus” in the + menu, which shows only with
this section). One card per gauge, colored by its thresholds; the error, if a
query fails, shows under it.
| key | default | what |
|---|---|---|
| url | “http://localhost:9090” | your Prometheus |
| interval | 5 | seconds between redraws |
[[prometheus.gauges]]name = "Memory alloc"type = "gauge" # gauge (a bar), arc, sparklinequery = "go_memstats_alloc_bytes / 1024 / 1024"min = 0max = 256warn = 128 # from here the card turns ambercrit = 200 # and redunit = "MB"A query answers with its first series. Without gauges it shows Prometheus watching itself:

For phosphor ci (and “ci” in the + menu, which shows only with this section). Status cards for GitHub Actions and GitLab pipelines: the latest run with its jobs, and under it a history of the five before it (so an idle repo still tells you what happened last). If a refresh fails the card stays, marked “stale”, instead of turning red; a real error says what to do (a private project needs glab auth login or GITLAB_TOKEN).
| key | default | what |
|---|---|---|
| interval | 10 | seconds between redraws |
[[ci.pipelines]]name = "Frontend Build"provider = "github" # github or gitlabrepo = "owner/repo"branch = "main"token = "env:GITHUB_TOKEN" # optional token or env var referenceVoice announcements for notifications (phosphor notify, and anything that
calls it: a fleet host going down or coming back, a chat mention or DM).
| key | default | what |
|---|---|---|
| enabled | false | speak notifications aloud |
| voice | “glados” | voice profile: glados, adjutant, hal, synth, system |
| glados_path | “~/.local/share/phosphor/glados-tts” | path to GLaDOS-TTS installation |
| volume | 1.0 | volume (0.0 to 1.0) |
| fleet_alerts | false | also announce fleet health alerts |
fleet_alerts gates fleet health on top of enabled: with it off (the default),
a host going down or coming back still shows in SYS and pushes to your phone if
[push] is on, it just isn’t spoken – most people want a message meant for
them read aloud, not every blip of a machine nobody’s looking at.
[push]
Section titled “[push]”Notifications on a phone that isn’t attached (phosphor notify sends them, and
so does anything that calls it: a fleet host going down or coming back – at
most one push a minute per host, even if the link flaps – and a chat mention
or DM). The brain is often in a rack with nobody near it, so speaking
([tts]) reaches no one; this sends the notice to an
ntfy topic, and the ntfy app on the phone rings and
vibrates, whether Termux is open or not. Off until you turn it on: the
message text leaves the brain.
| key | default | what |
|---|---|---|
| enabled | false | push every phosphor notify |
| url | “https://ntfy.sh” | the ntfy server: the public one, or your own (reachable from the brain) |
| topic | “” | the topic to publish on; subscribe to the same one in the phone’s ntfy app |
| token | “” | for a protected topic: the token, or env:VAR to read it from the environment |
| priority | “default” | min, low, default, high, max (high and max make the phone sound loudly) |
| title | “phosphor” | the notice’s title; the tab it came from is appended |
[push]enabled = truetopic = "deck-9f3k2x7q" # anyone who knows a public topic can read it: make it long and randomOn the public ntfy.sh a topic is only as private as its name; with your own server (or a token)
it’s yours. Try it with phosphor notify --push "hello": it says why if it can’t send.
Your own server, no cloud
Section titled “Your own server, no cloud”ntfy is one Go binary; no Firebase, no account. Download a release for the brain’s architecture and run it:
./ntfy serve --listen-http <brain's tailscale IP>:8080 --cache-file ~/.local/share/ntfy/cache.dbPoint [push] url at http://<that IP>:8080, and the phone’s ntfy app at
the same address instead of ntfy.sh (its server field, not just the topic).
Plain HTTP is fine over a tailnet: tailscale already encrypts the link, so
there’s no certificate to manage. This is one process, not a phosphor
service – keep it running yourself (a systemd --user unit is the usual
way) so it’s still there after a reboot; phosphor only needs it reachable
at url when phosphor notify --push runs.
[services]
Section titled “[services]”For phosphor services, the SYS panel that lists systemd units and their
state. Phosphor’s own units (the deck’s service and watchdog timer, one
fleet-*.service per mounted host, one per [[tunnels]]) always show up,
straight from what phosphor gen actually writes – nothing to configure
for those. extra adds your own homelab services.
| key | default | what |
|---|---|---|
| interval | 5 | seconds between redraws |
| extra | [] | your own units, by name (system scope; prefix "user:" for one of yours) |
[services]extra = ["nginx.service", "postgresql.service", "user:some-timer.timer"]A system unit’s status is read without sudo (systemctl show, read-only);
a missing or misspelled name shows as “not found” instead of failing the
panel. inactive isn’t shown as a problem – a tunnel you turned off is
supposed to sit there – only failed or a unit that doesn’t exist gets a
loud color.
apps.toml
Section titled “apps.toml”Programs of your own, next to the profile (~/.config/phosphor/apps.toml,
override: PHOSPHOR_APPS). They show first in phosphor store as “yours”
and in the + menu, and open in a tab of their own.
[[apps]]name = "lazygit"cmd = "lazygit"desc = "git in a TUI"| key | what |
|---|---|
| name | its name in the store and the + menu |
| cmd | the program: a name in your PATH, or a path (~ works) |
| args | its arguments |
| desc | a line for the store |
| alt | start on the alternate screen |
| needs_size | wait a second before starting |