Files
Panama/config/dot/quickshell/modules/welcome/Welcome.qml
T
Gabriel Brown 7578348db1 Merge Home & Phone into a three-tab Home that knows your house
Home is now Overview | My Home | Phone. Overview leads with quick-action
tiles (focus, Do Not Disturb, health, snapshots, storage), keeps the
findings card — updates fold in, the reclaim-space prompt is gone on
purpose — and adds glance cards, the next calendar event, and weather.

My Home groups every light by Home Assistant area: the helper gained an
`areas` command (one REST template render, no websocket), and the rooms
degrade to a flat list on setups without areas. The favorites editor and
connection card moved intact. Phone gains a vitals strip — battery and
cell signal read from KDE Connect's plugin D-Bus objects, where absence
is data, not an error — beside ring, clipboard, send-a-file, and the
BlueBubbles handoff.

The retired home-phone id resolves to my-home forever via a new alias
map in SettingsRoutes (with a hasOwnProperty guard so prototype names
cannot leak into settingsPage). Storage no longer claims 0 B free — the
old page read a field the disks helper never emitted.

Contracts updated alongside; per the new workflow, the full suite runs
once at the end of the redesign (see the test backlog note).

Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
2026-08-23 22:06:18 -04:00

284 lines
9.6 KiB
QML

// The first thing a new machine shows.
//
// Panama has fourteen settings categories and thirty-odd pages inside them,
// 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 > System > 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()
}
}