Write the manual for the person using the desktop

docs/ is engineering artifacts -- design specs, plans, an upstream
ledger -- and none of it is written for the person who has to live
here. Five chapters that are: coming from another desktop, the
keyboard, windows and workspaces, when something breaks, and making it
yours.

Rendered inside Settings rather than opened in a browser, so a chapter
that says "the Displays page reverts after fifteen seconds" is one
click from the Displays page. Qt's markdown renderer does the work;
one chapter per Text, because Text has an implicit texture size limit
and a document long enough to hit it goes blank rather than
complaining.

The chapters live beside the shell in manual/ rather than at the
repository root, which departs from the plan. The reason is the path:
the shell finds them through Quickshell.shellDir, which is correct
wherever the repository is, whereas walking upward out of the shell
directory is only correct by accident. The contract fails that pattern
if it comes back.

The contract also pins the set both ways -- every chapter listed
exists, every chapter that exists is listed -- because a renamed file
shows an error card where a chapter should be, which reads as a broken
manual rather than as a moved file.
This commit is contained in:
Gabriel Brown
2026-08-22 01:11:46 -04:00
parent 9202697734
commit 202b5b89ac
14 changed files with 459 additions and 2 deletions
+2 -1
View File
@@ -108,12 +108,13 @@ setup/
packages/ One package per line; extras/ holds the optional categories
scripts/ Run in order by ./install
tests/ Contracts. See below
manual/ The user manual, rendered in Settings
docs/ Settings reference, and the design specs behind the work
```
## Tests
142 of them, under `tests/`. Run the lot, or a subset by pattern:
143 of them, under `tests/`. Run the lot, or a subset by pattern:
```sh
panama test # everything
@@ -0,0 +1,53 @@
# Coming from another desktop
Panama is Hyprland with a shell built to replace a GNOME session, so most of
what you know still applies. The differences worth knowing on day one are
below. Nothing here is a preference you have to accept: every one of them is
adjustable in Settings.
## Windows arrange themselves
The biggest change. You do not drag windows into position. Open a second
window and the screen splits; open a third and it splits again. This is
called tiling, and after a day of it, dragging windows feels like making
your own bed in a hotel.
Two things follow from it:
- **There is no minimise.** A minimised window is a window you have hidden
from a layout that was managing it for you. Use `Super + Shift + X` to
push a window to the scratchpad and `Super + X` to bring it back, which is
the same idea without the layout losing track of it.
- **Floating still exists.** `Super + Ctrl + C` makes a window float above
the layout, for the dialog that was never meant to be tiled.
## Workspaces replace the taskbar
Instead of many windows on one screen, use many screens. `Alt + 1` through
`Alt + 0` jump straight to a workspace; `Alt + Shift + 1` sends the window
you are looking at to workspace one. `Super + backtick` shows all of them at
once.
If you came from macOS, this is Spaces with the friction removed. If you came
from Windows, it is virtual desktops that are actually worth using because
switching is one key rather than three.
## The launcher is the menu
There is no applications menu to browse. `Super + Space` opens a search box
that finds applications, files, clipboard history, emoji, and does
arithmetic. Type three letters of what you want.
This replaces the Start menu, Spotlight, the GNOME overview's app grid, and
the run dialog, and it is faster than all of them because it never asks you
to aim at anything.
## What stayed the same
`Super + Tab` still switches windows. The bar is still along the top with a
clock in the middle and status on the right. There is still a dock. Volume
and brightness keys still work, and still work on the lock screen. Copy and
paste are unchanged. Your files are where you left them.
GNOME Settings is still installed for the few panels Panama deliberately does
not own, and Panama's own Settings hands off to it where that applies.
@@ -0,0 +1,46 @@
# The keyboard
Press `Super + /` at any time to see every shortcut this machine has. That
list is generated from the live keymap, so it is never out of date, and it
includes anything you have rebound.
This chapter is the shape behind that list, which is the part worth learning
once.
## Three modifiers, three jobs
| Modifier | Acts on |
|---|---|
| `Super` | The window you are looking at |
| `Alt` | Workspaces |
| `Super + Ctrl` | The layout itself: splitting, floating, swapping |
Almost every shortcut follows from this. `Super + H` moves focus left.
`Alt + H` moves to the workspace on the left. `Super + Ctrl + H` swaps the
window leftward in the layout.
## Directions
`H`, `J`, `K`, `L` mean left, down, up, right, as they do in vim. The arrow
keys work everywhere the letters do, so you never have to learn them if you
would rather not.
Add `Shift` to move the window rather than the focus. `Super + Shift + L`
takes the current window and moves it to the right.
## The handful worth memorising first
| Keys | What it does |
|---|---|
| `Super + Space` | Find and open anything |
| `Super + T` | Terminal |
| `Super + Q` | Close the window |
| `Super + backtick` | Every window, every workspace |
| `Super + I` | Settings |
| `Super + /` | Every other shortcut |
## Changing them
Settings has a Shortcuts page listing every bind, each of which can be
reassigned. A rebind moves the shortcut and cannot change what it does, so
there is no way to make a key do something unexpected by editing it.
@@ -0,0 +1,48 @@
# Windows and workspaces
## How the split is chosen
A new window splits the space of the window that had focus. Whether it splits
beside or below depends on which way that space is longer, which usually
matches what you were about to do anyway.
When it does not, `Super + Ctrl + G` flips the split of the focused window,
and `Super + Ctrl + Z` and `Super + Ctrl + V` decide the direction the *next*
window will take before you open it.
## Resizing
`Super + Shift` plus `Y`, `O`, `B`, `M` adjusts width; `I`, `U`, `N` and
comma adjust height. These are inverted from what their letters suggest, on
purpose, and the reason is written in `config/dot/hypr/keybinds.lua` next to
the binds.
For anything finer, hold `Super` and drag with the right mouse button.
## Fullscreen and floating
`Super + U` makes the focused window fullscreen. `Super + Ctrl + C` lifts it
out of the layout to float, and `Super + Ctrl + Shift + C` pins it so it
follows you between workspaces. A floating window can be moved by holding
`Super` and dragging with the left button.
## Workspaces
Ten of them, on `Alt + 1` through `Alt + 0`. `Alt + H` and `Alt + L` step
between neighbours, and `Alt + Shift` plus a number sends the current window
somewhere without following it.
On a machine with more than one display, workspaces belong to whichever
screen has focus. If you would rather pin workspaces one through ten to your
main display and give the second screen its own, that is a switch on the
Desktop settings page.
## Projects
A project is the set of windows you open together: which applications, which
workspace each was on, and for a terminal, which directory it was in.
Arrange things the way you want them, then run **Save Layout as Project**
from the launcher. **Open Project** lays it out again on free workspaces, so
it never lands on top of what you are already doing. An application that
refuses to open twice is moved into place rather than launched again.
@@ -0,0 +1,56 @@
# When something breaks
## Start here
Run **Check System Health** from the launcher, or open Settings and go to
System Health. It reports what is actually running rather than what was
installed, and it can repair several things itself.
From a terminal, the same check is `panama doctor`.
## The shell is gone
If the bar and dock disappear, the Quickshell process died. **Restart Shell**
from the launcher brings it back. If the launcher is also gone, press
`Super + Shift + R` for the fallback launcher, which depends on nothing but
itself and exists for exactly this.
From a terminal: `qs -d` starts the shell again.
## A shortcut stopped working
Check `Super + /` first: if the shortcut shows a different chord than you
expect, something rebound it. The Shortcuts settings page can reset any bind,
or all of them at once.
If the whole keymap is missing from that list, the compositor config failed
to load. `Hyprland --verify-config` says why without touching your session.
## The screen resolution is wrong
Settings has a Displays page. Every change there reverts itself after fifteen
seconds unless you confirm it, so a mode your monitor cannot show cannot
strand you. If you are already stranded, `hyprctl monitors` from a terminal
shows what is applied.
## Something asked for a password and I do not know why
Panama's password prompt names the action that asked. If it was started by an
agent or a script through `panama-sudo`, it also shows a stated reason,
labelled as an unverified claim, beside the real action.
If you did not expect it at all, cancel. Nothing that needs privileges is
harmed by being refused.
## After an update, something is not right
`panama migrate` applies repairs this machine has not had yet. It is safe to
run at any time and does nothing when there is nothing to do.
If an update went badly, the Snapshots settings page can roll the system back
where btrfs snapshots are configured.
## Reporting it
`panama doctor` produces a redacted summary suitable for pasting into an
issue. It deliberately omits tokens, hostnames and account details.
@@ -0,0 +1,42 @@
# Making it yours
## Settings
`Super + I`. Thirty-odd pages covering displays, sound, network, appearance,
power, printers, users, sharing, storage, updates and the rest. The search
box at the top searches the settings themselves rather than just page names,
so if you know what you want to change you can type it.
## Appearance
Panama ships Tokyo Night Moon in a light and a dark variant, with eight
accent colours. Changing either repaints the shell, the terminal, GTK
applications, the lock screen and the system monitor at once.
The wallpaper is per-monitor if you want it to be.
## Applications
`panama apps` in a terminal offers the optional application categories the
installer asked about, so a machine can acquire Slack in March without having
wanted Discord in January.
`panama app` builds the handful of applications no repository carries. It is
deliberately not part of installing, because a source build is slow and
depends on an upstream that moves.
## Autostart and defaults
The Applications settings page controls which application opens which kind of
file, and what starts when you log in.
## Going further
The compositor is configured in Lua under `~/.config/hypr`, and every file
there explains what it does and why. `overrides.lua` is loaded last and is
the intended place for per-machine changes, so you can adjust anything
without editing a file the repository will update.
The shell is QML under `~/.config/quickshell`. Both directories are symlinks
into the Panama repository, so an edit is a change to your checkout and
`panama update` will offer to commit it.
@@ -0,0 +1,96 @@
// The manual.
//
// Panama's docs/ directory is engineering artifacts: design specs, plans, an
// upstream ledger. None of it is written for the person using the desktop,
// and the person using the desktop is the one with questions.
//
// So the manual is prose, in chapters, rendered here rather than opened in a
// browser. Keeping it in Settings means it is reachable from the same place
// as everything else it talks about, and a link to a settings page can just
// be a settings page.
//
// The chapters live beside the shell in manual/, not at the repository root.
// They are read at runtime through Quickshell.shellDir, which resolves whether
// or not the repository is where it usually is; a path walked upward out of
// the shell directory does not.
//
// One Text per chapter, never the whole manual at once: Text has an implicit
// texture size limit, and a document long enough to hit it fails by going
// blank rather than by complaining.
import Quickshell
import Quickshell.Io
import QtQuick
import qs.config
import qs.services
SettingsPage {
id: root
title: "Manual"
lede: "How this desktop works, for the person using it."
// Chapter files are numbered so their order is their filename. The title
// shown here is the first heading of each, read from the file, so a
// chapter cannot be renamed in one place and not the other.
readonly property var chapters: [
{ file: "01-coming-from-another-desktop.md", label: "Coming from another desktop" },
{ file: "02-the-keyboard.md", label: "The keyboard" },
{ file: "03-windows-and-workspaces.md", label: "Windows and workspaces" },
{ file: "04-when-something-breaks.md", label: "When something breaks" },
{ file: "05-making-it-yours.md", label: "Making it yours" }
]
property int current: 0
// A deep link picks the chapter once and then lets go, so arriving at a
// chapter does not pin you to it. Same consume-once idiom ShellState uses
// for settings sections.
Component.onCompleted: {
const section = ShellState.takeSettingsSection();
for (let index = 0; index < root.chapters.length; index++) {
if (root.chapters[index].file.indexOf(section) >= 0 && section !== "") {
root.current = index;
break;
}
}
}
SettingsTabs {
tabs: root.chapters.map((chapter, index) => ({
value: String(index),
label: chapter.label
}))
current: String(root.current)
onSelected: value => root.current = parseInt(value, 10)
}
SettingsCard {
FileView {
id: chapterFile
path: Quickshell.shellDir + "/manual/" + root.chapters[root.current].file
printErrors: false
onLoaded: body.text = this.text()
onLoadFailed: body.text =
"This chapter could not be read.\n\nIt should be at `"
+ chapterFile.path + "`."
}
Text {
id: body
width: parent.width
textFormat: Text.MarkdownText
wrapMode: Text.WordWrap
color: Theme.fgDim
linkColor: Theme.accent
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSize
// Markdown's own line height is tight for a wall of prose at this
// size; a document read once needs to be easy on the first pass.
lineHeight: 1.35
onLinkActivated: link => Qt.openUrlExternally(link)
}
}
}
@@ -131,6 +131,7 @@ Rectangle {
case "containers": return containersPage;
case "ssh-keys": return sshKeysPage;
case "services": return healthPage;
case "manual": return manualPage;
case "about": return aboutPage;
default: return homePage;
}
@@ -195,6 +196,7 @@ Rectangle {
Component { id: regionPage; RegionPage {} }
Component { id: onlineAccountsPage; OnlineAccountsPage {} }
Component { id: healthPage; HealthPage {} }
Component { id: manualPage; ManualPage {} }
Component { id: aboutPage; AboutPage {} }
Shortcut {
@@ -51,6 +51,7 @@ Rectangle {
{ page: "snapshots", label: "Snapshots", icon: "\u{F0954}" },
{ page: "users", label: "Users", icon: "\u{F0004}" },
{ page: "services", label: "System Health", icon: "\u{F0493}" },
{ page: "manual", label: "Manual", icon: "\u{F02D4}" },
{ page: "about", label: "About", icon: "\u{F02FD}" }
]
@@ -1,5 +1,6 @@
module qs.modules.settings
AboutPage 1.0 AboutPage.qml
ManualPage 1.0 ManualPage.qml
AppearancePage 1.0 AppearancePage.qml
AvatarPicker 1.0 AvatarPicker.qml
ConnectivityPage 1.0 ConnectivityPage.qml
@@ -61,6 +61,10 @@ Singleton {
// them rather than Panama. Without these, searching "timezone" would fail
// on a settings app that plainly has one.
readonly property var extraEntries: [
{ label: "Manual", detail: "How this desktop works, in chapters", page: "manual" },
{ label: "Getting started", detail: "Coming from GNOME, macOS or Windows", page: "manual" },
{ label: "Troubleshooting", detail: "When something breaks", page: "manual" },
{ label: "Help", detail: "The manual", page: "manual" },
{ label: "Timezone", detail: "Set the system timezone", page: "datetime" },
{ label: "Network time", detail: "Synchronize the clock with a time server", page: "datetime" },
{ label: "Wi-Fi", detail: "Managed by GNOME Settings", page: "connectivity" },
@@ -118,7 +118,7 @@ Singleton {
}
function showSettings(page: string): void {
const allowed = ["home", "appearance", "displays", "connectivity", "home-phone", "desktop", "sound", "gaming", "notifications", "screen-intelligence", "shortcuts", "mouse", "privacy", "region", "accounts", "accessibility", "power", "datetime", "applications", "updates", "storage", "snapshots", "users", "sharing", "firewall", "printers", "containers", "ssh-keys", "services", "about"];
const allowed = ["home", "appearance", "displays", "connectivity", "home-phone", "desktop", "sound", "gaming", "notifications", "screen-intelligence", "shortcuts", "mouse", "privacy", "region", "accounts", "accessibility", "power", "datetime", "applications", "updates", "storage", "snapshots", "users", "sharing", "firewall", "printers", "containers", "ssh-keys", "services", "manual", "about"];
root.settingsPage = allowed.indexOf(page) >= 0 ? page : "home";
DesktopPreferences.set("lastPage", root.settingsPage);
root.settingsOpen = true;
+10
View File
@@ -0,0 +1,10 @@
#!/usr/bin/env bash
# Generated by scripts/panama-settings-commands -- do not edit by hand.
# @vicinae.schemaVersion 1
# @vicinae.title Settings: Manual
# @vicinae.mode silent
# @vicinae.icon ../../icons/hicolor/scalable/apps/panama-settings.svg
# @vicinae.description Open Manual in Settings.
# @vicinae.keywords ["settings", "manual", "getting started", "troubleshooting", "help"]
exec "$HOME/.config/quickshell/scripts/panama-action" settings-page manual
+97
View File
@@ -0,0 +1,97 @@
#!/usr/bin/env bash
# The manual.
#
# docs/ in this repository is engineering artifacts: design specs, plans, an
# upstream ledger. None of it is written for the person using the desktop, and
# that person is the one with questions. The manual is the answer, and it is
# rendered inside Settings so a chapter can point at a settings page and have
# that mean something.
#
# What must hold:
#
# 1. Every chapter the page lists exists, and every chapter file is listed.
# A renamed file shows an error card in place of a chapter, which looks
# like the manual is broken rather than like somebody moved a file.
# 2. Chapters render one at a time. Text has an implicit texture size limit,
# and a document long enough to hit it goes blank rather than complaining.
# 3. Links leave the desktop rather than doing nothing.
# 4. The page is registered everywhere a settings page has to be, or it
# silently redirects to Home.
set -uo pipefail
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
manual_dir="$repo_dir/config/dot/quickshell/manual"
page="$repo_dir/config/dot/quickshell/modules/settings/ManualPage.qml"
sidebar="$repo_dir/config/dot/quickshell/modules/settings/SettingsSidebar.qml"
shell_ui="$repo_dir/config/dot/quickshell/modules/settings/SettingsShell.qml"
qmldir="$repo_dir/config/dot/quickshell/modules/settings/qmldir"
state="$repo_dir/config/dot/quickshell/services/ShellState.qml"
search="$repo_dir/config/dot/quickshell/services/SettingsSearch.qml"
findings=()
note() { findings+=("$1"); }
[[ -d "$manual_dir" ]] || { printf 'manual contract: %s is missing\n' "$manual_dir" >&2; exit 1; }
# ── 1. The chapters on disk and the chapters listed are the same set ─────────
mapfile -t on_disk < <(find "$manual_dir" -maxdepth 1 -name '*.md' -printf '%f\n' | sort)
(( ${#on_disk[@]} > 0 )) || note 'the manual has no chapters'
mapfile -t listed < <(grep -oE 'file: "[^"]+\.md"' "$page" | sed 's/file: "//; s/"//' | sort)
(( ${#listed[@]} > 0 )) || note 'the manual page lists no chapters'
for file in "${on_disk[@]}"; do
printf '%s\n' "${listed[@]}" | grep -qx "$file" \
|| note "$file exists but the manual page never shows it"
done
for file in "${listed[@]}"; do
[[ -r "$manual_dir/$file" ]] \
|| note "the manual page lists $file, which does not exist, so that chapter renders an error"
done
# A chapter that is only a heading is a chapter somebody forgot to write.
for file in "${on_disk[@]}"; do
lines="$(grep -c . "$manual_dir/$file" || true)"
(( lines > 10 )) || note "$file has $lines lines; it reads as unfinished"
head -1 "$manual_dir/$file" | grep -q '^# ' \
|| note "$file does not begin with a heading, so it has no title of its own"
done
# ── 2 & 3. How it renders ────────────────────────────────────────────────────
grep -q 'textFormat: Text.MarkdownText' "$page" \
|| note 'chapters are not rendered as markdown, so the source appears verbatim'
grep -q 'root.chapters\[root.current\]' "$page" \
|| note 'the page does not render one chapter at a time; a single long Text goes blank rather than erroring'
grep -q 'onLinkActivated' "$page" \
|| note 'links in the manual do nothing when clicked'
grep -q 'onLoadFailed' "$page" \
|| note 'a chapter that cannot be read fails silently instead of saying so'
# The chapters are reached through the shell directory, not by walking upward
# out of it: that path is only correct when the repository is where it usually
# is, and the shell directory is a symlink.
grep -q 'Quickshell.shellDir + "/manual/"' "$page" \
|| note 'the manual is not located through the shell directory, so it would break on a clone elsewhere'
grep -q '\.\./\.\./\.\.' "$page" \
&& note 'the manual path walks upward out of the shell directory, which is only correct by accident'
# ── 4. Registered in all five places ─────────────────────────────────────────
grep -q '{ page: "manual"' "$sidebar" || note 'the manual has no sidebar entry'
grep -q 'case "manual": return manualPage;' "$shell_ui" || note 'SettingsShell does not route to the manual'
grep -q 'Component { id: manualPage; ManualPage {} }' "$shell_ui" || note 'SettingsShell never declares the manual component'
grep -q '^ManualPage 1.0 ManualPage.qml$' "$qmldir" || note 'ManualPage is not registered in the settings qmldir'
grep -q '"manual"' "$state" || note 'the manual is not in the allowed settings pages, so openSettings would redirect to Home'
grep -q 'page: "manual"' "$search" || note 'the manual is not searchable from the settings search box'
if (( ${#findings[@]} > 0 )); then
printf 'manual contract: %d finding(s)\n' "${#findings[@]}" >&2
printf ' - %s\n' "${findings[@]}" >&2
exit 1
fi
printf 'manual contract: PASS (%d chapters)\n' "${#on_disk[@]}"