diff --git a/README.md b/README.md index 2651bb0..ea1135c 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/config/dot/quickshell/manual/01-coming-from-another-desktop.md b/config/dot/quickshell/manual/01-coming-from-another-desktop.md new file mode 100644 index 0000000..1ea643a --- /dev/null +++ b/config/dot/quickshell/manual/01-coming-from-another-desktop.md @@ -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. diff --git a/config/dot/quickshell/manual/02-the-keyboard.md b/config/dot/quickshell/manual/02-the-keyboard.md new file mode 100644 index 0000000..bf5b974 --- /dev/null +++ b/config/dot/quickshell/manual/02-the-keyboard.md @@ -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. diff --git a/config/dot/quickshell/manual/03-windows-and-workspaces.md b/config/dot/quickshell/manual/03-windows-and-workspaces.md new file mode 100644 index 0000000..d9761fe --- /dev/null +++ b/config/dot/quickshell/manual/03-windows-and-workspaces.md @@ -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. diff --git a/config/dot/quickshell/manual/04-when-something-breaks.md b/config/dot/quickshell/manual/04-when-something-breaks.md new file mode 100644 index 0000000..af0c09d --- /dev/null +++ b/config/dot/quickshell/manual/04-when-something-breaks.md @@ -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. diff --git a/config/dot/quickshell/manual/05-making-it-yours.md b/config/dot/quickshell/manual/05-making-it-yours.md new file mode 100644 index 0000000..aa18346 --- /dev/null +++ b/config/dot/quickshell/manual/05-making-it-yours.md @@ -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. diff --git a/config/dot/quickshell/modules/settings/ManualPage.qml b/config/dot/quickshell/modules/settings/ManualPage.qml new file mode 100644 index 0000000..0f34b51 --- /dev/null +++ b/config/dot/quickshell/modules/settings/ManualPage.qml @@ -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) + } + } +} diff --git a/config/dot/quickshell/modules/settings/SettingsShell.qml b/config/dot/quickshell/modules/settings/SettingsShell.qml index 41e2bb5..7ad6dff 100644 --- a/config/dot/quickshell/modules/settings/SettingsShell.qml +++ b/config/dot/quickshell/modules/settings/SettingsShell.qml @@ -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 { diff --git a/config/dot/quickshell/modules/settings/SettingsSidebar.qml b/config/dot/quickshell/modules/settings/SettingsSidebar.qml index 31758b7..a52e01f 100644 --- a/config/dot/quickshell/modules/settings/SettingsSidebar.qml +++ b/config/dot/quickshell/modules/settings/SettingsSidebar.qml @@ -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}" } ] diff --git a/config/dot/quickshell/modules/settings/qmldir b/config/dot/quickshell/modules/settings/qmldir index 183f6de..23fa8c5 100644 --- a/config/dot/quickshell/modules/settings/qmldir +++ b/config/dot/quickshell/modules/settings/qmldir @@ -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 diff --git a/config/dot/quickshell/services/SettingsSearch.qml b/config/dot/quickshell/services/SettingsSearch.qml index b9991e4..2707e3c 100644 --- a/config/dot/quickshell/services/SettingsSearch.qml +++ b/config/dot/quickshell/services/SettingsSearch.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" }, diff --git a/config/dot/quickshell/services/ShellState.qml b/config/dot/quickshell/services/ShellState.qml index b11f54b..b699fb3 100644 --- a/config/dot/quickshell/services/ShellState.qml +++ b/config/dot/quickshell/services/ShellState.qml @@ -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; diff --git a/config/local/share/vicinae/scripts/settings-manual b/config/local/share/vicinae/scripts/settings-manual new file mode 100755 index 0000000..e609a2a --- /dev/null +++ b/config/local/share/vicinae/scripts/settings-manual @@ -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 diff --git a/tests/quickshell/manual-contract b/tests/quickshell/manual-contract new file mode 100755 index 0000000..2cd5656 --- /dev/null +++ b/tests/quickshell/manual-contract @@ -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[@]}"