Introduce the desktop to somebody who has just met it

Thirty settings pages is the opposite of the usual problem: a person
arriving from GNOME, macOS or Windows cannot tell which few things
matter. This is those few, once, on the first start.

Not a tour. Nobody reads a tour, and a multi-step wizard on a desktop
somebody just installed is one more thing between them and using it.
One card, five keys, and a way out.

The chords come from the live keymap rather than being written here, so
a machine whose owner has already rebound something teaches what they
actually have. A welcome screen is the one surface read by somebody
with no way to tell it is wrong, which is exactly why it must not be.

Two deliberate departures from how every other surface behaves. It does
not close on a click outside, because a stray click in the first thirty
seconds would throw away the only explanation on offer. And dismissing
by any route marks it seen, Escape included, because a desktop that
reintroduces itself every login has failed to take no for an answer.
It stays reachable from the launcher afterwards, since the moment
somebody wants it again is exactly when a one-shot has thrown it away.

Also teaches the keymap to spell punctuation: slash, period, comma and
the rest were rendering as their raw keysym names, so the welcome
screen offered "Super + slash" and the cheatsheet agreed with it.
This commit is contained in:
Gabriel Brown
2026-08-22 01:00:18 -04:00
parent 9fbbdd902b
commit 9202697734
10 changed files with 462 additions and 3 deletions
@@ -1445,6 +1445,12 @@ Singleton {
},
// ── Internal ────────────────────────────────────────────────────────
{
key: "welcomeSeen", type: "bool", def: false, group: "internal",
internal: true,
label: "Welcome shown",
detail: "Set once the first-run welcome has been dismissed. Restoring defaults shows it again, which is intended: a reset machine is one somebody wants introduced to them"
},
{
key: "lastPage", type: "string", def: "home", group: "internal",
internal: true,
@@ -0,0 +1,281 @@
// The first thing a new machine shows.
//
// Panama has thirty settings pages, which is the opposite of the usual
// problem: somebody arriving from GNOME, macOS or Windows cannot tell which
// four things matter. This is those four things, once, on the first start.
//
// Deliberately not a tour. Nobody reads a tour, and a multi-step wizard on a
// desktop somebody just installed is another thing standing between them and
// using it. One screen, the handful of keys that unlock everything else, and a
// way out.
//
// The chords are read from the live keymap rather than written here, so a
// machine whose owner has already rebound something shows what they actually
// have -- and so this cannot drift the way a hand-written list would.
//
// Shown once, tracked by the `welcomeSeen` preference. It stays reachable
// afterwards from the launcher and from Settings > About, because the moment
// somebody wants it again is exactly the moment a one-shot has thrown it away.
import Quickshell
import Quickshell.Wayland
import QtQuick
import qs.config
import qs.services
import qs.widgets
import qs.modules.settings
PanelWindow {
id: root
readonly property bool open: ShellState.welcomeOpen
// What to teach, in the order it becomes useful. Each names a bind by the
// description hypr/keybinds.lua gives it, so the chord shown is whatever
// that bind currently answers to.
readonly property var lessons: [
{
find: "Launcher",
fallback: "Super + Space",
title: "Find anything",
detail: "Applications, files, clipboard history, emoji, a calculator. Start typing."
},
{
find: "Terminal",
fallback: "Super + T",
title: "Open a terminal",
detail: "Windows tile themselves as they open. Nothing needs arranging."
},
{
find: "Overview",
fallback: "Super + `",
title: "See every window",
detail: "All your workspaces at once, and search across them."
},
{
find: "Settings",
fallback: "Super + I",
title: "Change anything",
detail: "Displays, sound, network, appearance, and everything else."
},
{
find: "Keyboard shortcuts",
fallback: "Super + /",
title: "The rest of the keys",
detail: "Every shortcut this desktop has, whenever you want it."
}
]
function chordFor(description: string, fallback: string): string {
for (const bind of Keybinds.binds) {
if (bind.description === description)
return bind.chord;
}
return fallback;
}
anchors { top: true; bottom: true; left: true; right: true }
color: "transparent"
exclusiveZone: 0
exclusionMode: ExclusionMode.Ignore
WlrLayershell.namespace: "qs-popover-welcome"
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.keyboardFocus: root.open
? WlrKeyboardFocus.Exclusive
: WlrKeyboardFocus.None
property bool mapped: false
visible: root.mapped
onOpenChanged: {
if (root.open) {
unmapTimer.stop();
root.mapped = true;
Keybinds.refresh();
} else {
unmapTimer.restart();
}
}
Timer {
id: unmapTimer
interval: Theme.durNormal
onTriggered: root.mapped = false
}
// Dismissing is what marks it seen. Closing by any route counts: somebody
// who pressed Escape has seen it, and showing it again next login would be
// the desktop failing to take no for an answer.
function dismiss(): void {
DesktopPreferences.set("welcomeSeen", true);
ShellState.close();
}
// First start on a fresh install. Waits for the keymap so the chords are
// real rather than fallbacks, and gives the rest of the shell a moment to
// finish coming up -- appearing over a half-drawn desktop reads as a crash.
Timer {
id: firstStart
interval: 2500
running: !DesktopPreferences.get("welcomeSeen")
onTriggered: {
if (!DesktopPreferences.get("welcomeSeen") && !ShellState.anyOverlayOpen)
ShellState.open("welcome");
}
}
Rectangle {
anchors.fill: parent
color: Theme.alpha(Theme.bgDark, Theme.overlayAlpha)
opacity: root.open ? 1 : 0
Behavior on opacity { NumberAnimation { duration: Theme.durNormal; easing.type: Easing.OutCubic } }
// No click-outside-to-close. Everything else on this desktop dismisses
// that way, but this is the one surface where a stray click during the
// first thirty seconds would throw away the only explanation on offer.
MouseArea { anchors.fill: parent }
}
Rectangle {
id: card
anchors.centerIn: parent
width: Math.min(root.width - 120, 640)
height: Math.min(root.height - 120, content.implicitHeight + 64)
radius: Theme.popoverRadius
color: Theme.alpha(Theme.bgPopover, Theme.popoverAlpha)
border.width: 1
border.color: Theme.alpha(Theme.fg, 0.1)
opacity: root.open ? 1 : 0
scale: root.open ? 1 : 0.98
Behavior on opacity { NumberAnimation { duration: Theme.durNormal; easing.type: Easing.OutCubic } }
Behavior on scale { NumberAnimation { duration: Theme.durNormal; easing.type: Easing.OutCubic } }
PrismEdge {
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
inset: Theme.popoverRadius
}
Column {
id: content
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: 32
spacing: 22
Column {
width: parent.width
spacing: 6
Text {
text: "Welcome to Panama"
color: Theme.fg
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeTitle
font.weight: Font.DemiBold
}
Text {
width: parent.width
text: "Five keys, and you have the whole desktop."
color: Theme.fgDim
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSize
wrapMode: Text.WordWrap
}
}
Column {
width: parent.width
spacing: 14
Repeater {
model: root.lessons
Item {
required property var modelData
width: parent.width
height: Math.max(chordPill.height, lessonText.implicitHeight)
Rectangle {
id: chordPill
width: 150
height: chordLabel.implicitHeight + 12
radius: Theme.pillRadius
color: Theme.alpha(Theme.fg, 0.07)
Text {
id: chordLabel
anchors.centerIn: parent
text: root.chordFor(modelData.find, modelData.fallback)
color: Theme.accent
font.family: Theme.fontMono
font.pixelSize: Theme.fontSizeSmall
font.features: Theme.tabularFigures
}
}
Column {
id: lessonText
anchors.left: chordPill.right
anchors.leftMargin: 16
anchors.right: parent.right
anchors.verticalCenter: chordPill.verticalCenter
spacing: 2
Text {
width: parent.width
text: modelData.title
color: Theme.fg
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSize
font.weight: Font.Medium
}
Text {
width: parent.width
text: modelData.detail
color: Theme.fgDim
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
wrapMode: Text.WordWrap
}
}
}
}
}
Row {
anchors.right: parent.right
spacing: 8
SettingsButton {
text: "Show every shortcut"
onClicked: {
DesktopPreferences.set("welcomeSeen", true);
ShellState.open("cheatsheet");
}
}
SettingsButton {
text: "Start using it"
tone: "accent"
onClicked: root.dismiss()
}
}
}
}
Item {
anchors.fill: parent
focus: true
Keys.onEscapePressed: root.dismiss()
}
}
@@ -0,0 +1,2 @@
module qs.modules.welcome
Welcome 1.0 Welcome.qml
@@ -47,6 +47,17 @@ Singleton {
"bracketleft": "[",
"bracketright": "]",
"grave": "`",
"slash": "/",
"backslash": "\\",
"period": ".",
"comma": ",",
"equal": "=",
"minus": "-",
"semicolon": ";",
"apostrophe": "'",
"Return": "Enter",
"Escape": "Esc",
"space": "Space",
"Print": "Print Screen",
"left": "←",
"right": "→",
@@ -17,7 +17,7 @@ Singleton {
id: root
// Exactly one of these may be non-empty at a time.
// "" | "overview" | "quicksettings" | "notifications" | "clipboard" | "capture" | "activity" | "powermenu" | "cheatsheet"
// "" | "overview" | "quicksettings" | "notifications" | "clipboard" | "capture" | "activity" | "powermenu" | "cheatsheet" | "welcome"
property string activeOverlay: ""
readonly property bool overviewOpen: activeOverlay === "overview"
@@ -28,6 +28,7 @@ Singleton {
readonly property bool activityOpen: activeOverlay === "activity"
readonly property bool powerMenuOpen: activeOverlay === "powermenu"
readonly property bool cheatsheetOpen: activeOverlay === "cheatsheet"
readonly property bool welcomeOpen: activeOverlay === "welcome"
// Settings is a normal application window rather than a transient overlay.
// It can stay open while Quick Settings or the notification center appears.
+16
View File
@@ -35,6 +35,7 @@ import qs.modules.notifications
import qs.modules.capture
import qs.modules.powermenu
import qs.modules.cheatsheet
import qs.modules.welcome
import qs.modules.clipboard
import qs.modules.datemenu
import qs.modules.focus
@@ -100,6 +101,7 @@ ShellRoot {
DateMenu { id: dateMenu }
ClipboardPanel {}
Cheatsheet {}
Welcome {}
CaptureOverlay {}
IntelligenceResult {}
ActivityPanel {}
@@ -146,6 +148,20 @@ ShellRoot {
}
}
// Shown once on a fresh machine, and reachable afterwards -- the moment
// somebody wants it again is exactly when a one-shot has thrown it away.
IpcHandler {
target: "welcome"
function open(): void { ShellState.open("welcome"); }
function close(): void { ShellState.close(); }
function status(): string {
return JSON.stringify({
open: ShellState.welcomeOpen,
seen: DesktopPreferences.get("welcomeSeen") === true
});
}
}
IpcHandler {
target: "overview"
function toggle(): void { ShellState.toggle("overview"); }
+9
View File
@@ -0,0 +1,9 @@
#!/usr/bin/env bash
# @vicinae.schemaVersion 1
# @vicinae.title Welcome to Panama
# @vicinae.mode silent
# @vicinae.icon ../../icons/hicolor/scalable/apps/panama-settings.svg
# @vicinae.description The five keys that unlock the desktop, shown again.
# @vicinae.keywords ["welcome", "getting started", "help", "intro", "tour", "new"]
exec qs ipc call welcome open