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:
@@ -113,7 +113,7 @@ docs/ Settings reference, and the design specs behind the work
|
|||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
|
|
||||||
141 of them, under `tests/`. Run the lot, or a subset by pattern:
|
142 of them, under `tests/`. Run the lot, or a subset by pattern:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
panama test # everything
|
panama test # everything
|
||||||
|
|||||||
@@ -1445,6 +1445,12 @@ Singleton {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// ── Internal ────────────────────────────────────────────────────────
|
// ── 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",
|
key: "lastPage", type: "string", def: "home", group: "internal",
|
||||||
internal: true,
|
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": "[",
|
"bracketleft": "[",
|
||||||
"bracketright": "]",
|
"bracketright": "]",
|
||||||
"grave": "`",
|
"grave": "`",
|
||||||
|
"slash": "/",
|
||||||
|
"backslash": "\\",
|
||||||
|
"period": ".",
|
||||||
|
"comma": ",",
|
||||||
|
"equal": "=",
|
||||||
|
"minus": "-",
|
||||||
|
"semicolon": ";",
|
||||||
|
"apostrophe": "'",
|
||||||
|
"Return": "Enter",
|
||||||
|
"Escape": "Esc",
|
||||||
|
"space": "Space",
|
||||||
"Print": "Print Screen",
|
"Print": "Print Screen",
|
||||||
"left": "←",
|
"left": "←",
|
||||||
"right": "→",
|
"right": "→",
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ Singleton {
|
|||||||
id: root
|
id: root
|
||||||
|
|
||||||
// Exactly one of these may be non-empty at a time.
|
// 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: ""
|
property string activeOverlay: ""
|
||||||
|
|
||||||
readonly property bool overviewOpen: activeOverlay === "overview"
|
readonly property bool overviewOpen: activeOverlay === "overview"
|
||||||
@@ -28,6 +28,7 @@ Singleton {
|
|||||||
readonly property bool activityOpen: activeOverlay === "activity"
|
readonly property bool activityOpen: activeOverlay === "activity"
|
||||||
readonly property bool powerMenuOpen: activeOverlay === "powermenu"
|
readonly property bool powerMenuOpen: activeOverlay === "powermenu"
|
||||||
readonly property bool cheatsheetOpen: activeOverlay === "cheatsheet"
|
readonly property bool cheatsheetOpen: activeOverlay === "cheatsheet"
|
||||||
|
readonly property bool welcomeOpen: activeOverlay === "welcome"
|
||||||
|
|
||||||
// Settings is a normal application window rather than a transient overlay.
|
// Settings is a normal application window rather than a transient overlay.
|
||||||
// It can stay open while Quick Settings or the notification center appears.
|
// It can stay open while Quick Settings or the notification center appears.
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import qs.modules.notifications
|
|||||||
import qs.modules.capture
|
import qs.modules.capture
|
||||||
import qs.modules.powermenu
|
import qs.modules.powermenu
|
||||||
import qs.modules.cheatsheet
|
import qs.modules.cheatsheet
|
||||||
|
import qs.modules.welcome
|
||||||
import qs.modules.clipboard
|
import qs.modules.clipboard
|
||||||
import qs.modules.datemenu
|
import qs.modules.datemenu
|
||||||
import qs.modules.focus
|
import qs.modules.focus
|
||||||
@@ -100,6 +101,7 @@ ShellRoot {
|
|||||||
DateMenu { id: dateMenu }
|
DateMenu { id: dateMenu }
|
||||||
ClipboardPanel {}
|
ClipboardPanel {}
|
||||||
Cheatsheet {}
|
Cheatsheet {}
|
||||||
|
Welcome {}
|
||||||
CaptureOverlay {}
|
CaptureOverlay {}
|
||||||
IntelligenceResult {}
|
IntelligenceResult {}
|
||||||
ActivityPanel {}
|
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 {
|
IpcHandler {
|
||||||
target: "overview"
|
target: "overview"
|
||||||
function toggle(): void { ShellState.toggle("overview"); }
|
function toggle(): void { ShellState.toggle("overview"); }
|
||||||
|
|||||||
+9
@@ -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
|
||||||
@@ -77,7 +77,7 @@ declare -a standalone=(
|
|||||||
lock-screen suspend-system log-out reboot-system power-off
|
lock-screen suspend-system log-out reboot-system power-off
|
||||||
remind-me list-reminders pick-color
|
remind-me list-reminders pick-color
|
||||||
switch-window force-quit-window kill-process ssh-hosts recent-files
|
switch-window force-quit-window kill-process ssh-hosts recent-files
|
||||||
copy-password keyboard-shortcuts
|
copy-password keyboard-shortcuts show-welcome
|
||||||
)
|
)
|
||||||
|
|
||||||
# Generated commands must match their source. A stale command dispatches to a
|
# Generated commands must match their source. A stale command dispatches to a
|
||||||
|
|||||||
Executable
+133
@@ -0,0 +1,133 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# The first thing a new machine shows.
|
||||||
|
#
|
||||||
|
# Panama has thirty settings pages, which is the opposite of the usual problem:
|
||||||
|
# somebody arriving from another desktop cannot tell which few things matter.
|
||||||
|
# This is those few things, once.
|
||||||
|
#
|
||||||
|
# What must hold:
|
||||||
|
#
|
||||||
|
# 1. The chords come from the live keymap. A welcome screen that teaches the
|
||||||
|
# wrong keys is worse than none, and it is the one surface read by
|
||||||
|
# somebody with no way to tell it is wrong.
|
||||||
|
# 2. It is shown once and dismissing marks it. A desktop that reintroduces
|
||||||
|
# itself every login has failed to take no for an answer.
|
||||||
|
# 3. Dismissing by ANY route counts, including Escape.
|
||||||
|
# 4. It stays reachable afterwards. The moment somebody wants it again is
|
||||||
|
# exactly when a one-shot has thrown it away.
|
||||||
|
# 5. It does NOT close on a click outside, unlike every other surface here.
|
||||||
|
# A stray click in the first thirty seconds would throw away the only
|
||||||
|
# explanation on offer.
|
||||||
|
#
|
||||||
|
# The live half opens the real surface. It deliberately does not exercise the
|
||||||
|
# dismiss path, because that would write to the running machine's settings and
|
||||||
|
# there is no honest way to put it back.
|
||||||
|
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
module="$repo_dir/config/dot/quickshell/modules/welcome"
|
||||||
|
welcome="$module/Welcome.qml"
|
||||||
|
shell_qml="$repo_dir/config/dot/quickshell/shell.qml"
|
||||||
|
state="$repo_dir/config/dot/quickshell/services/ShellState.qml"
|
||||||
|
schema="$repo_dir/config/dot/quickshell/config/PreferenceSchema.qml"
|
||||||
|
command_file="$repo_dir/config/local/share/vicinae/scripts/show-welcome"
|
||||||
|
|
||||||
|
findings=()
|
||||||
|
note() { findings+=("$1"); }
|
||||||
|
|
||||||
|
[[ -r "$welcome" ]] || { printf 'welcome contract: %s is missing\n' "$welcome" >&2; exit 1; }
|
||||||
|
|
||||||
|
# ── The module ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
grep -q '^Welcome 1.0 Welcome.qml$' "$module/qmldir" \
|
||||||
|
|| note 'Welcome is not registered in its qmldir, so the shell would fail to load entirely'
|
||||||
|
|
||||||
|
# ── 1. The keys it teaches are the keys you have ─────────────────────────────
|
||||||
|
|
||||||
|
grep -q 'Keybinds.binds' "$welcome" \
|
||||||
|
|| note 'the welcome screen does not read the live keymap, so it could teach chords this machine does not have'
|
||||||
|
grep -q 'function chordFor' "$welcome" \
|
||||||
|
|| note 'there is no lookup from a bind description to its current chord'
|
||||||
|
# A fallback is correct -- the keymap may not have loaded yet -- but it must be
|
||||||
|
# a fallback rather than the source.
|
||||||
|
grep -q 'fallback' "$welcome" \
|
||||||
|
|| note 'no fallback chord, so the screen would be blank until the keymap loads'
|
||||||
|
|
||||||
|
# ── 2 & 3. Once, and dismissing counts ───────────────────────────────────────
|
||||||
|
|
||||||
|
grep -q 'key: "welcomeSeen"' "$schema" \
|
||||||
|
|| note 'there is no welcomeSeen preference, so the welcome cannot be shown once'
|
||||||
|
grep -A2 'key: "welcomeSeen"' "$schema" | grep -q 'internal: true' \
|
||||||
|
|| note 'welcomeSeen is not internal, so it would appear in settings search as something to toggle'
|
||||||
|
grep -q 'DesktopPreferences.set("welcomeSeen", true)' "$welcome" \
|
||||||
|
|| note 'dismissing does not mark the welcome as seen, so it would return every login'
|
||||||
|
grep -q 'Keys.onEscapePressed: root.dismiss()' "$welcome" \
|
||||||
|
|| note 'Escape does not go through dismiss(), so closing that way would show it again next login'
|
||||||
|
|
||||||
|
# ── 4. Still reachable ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
grep -q 'target: "welcome"' "$shell_qml" \
|
||||||
|
|| note 'there is no welcome IPC target, so it cannot be reopened'
|
||||||
|
[[ -x "$command_file" ]] || note 'there is no launcher command to show the welcome again'
|
||||||
|
|
||||||
|
# ── 5. A stray click must not dismiss it ─────────────────────────────────────
|
||||||
|
#
|
||||||
|
# Every other overlay closes on an outside click. This one must not, and the
|
||||||
|
# check is that the scrim's MouseArea swallows clicks without acting.
|
||||||
|
|
||||||
|
python3 - "$welcome" <<'PY' || note 'the welcome scrim closes on an outside click, so a stray click would throw away the only explanation on offer'
|
||||||
|
import re, sys
|
||||||
|
text = open(sys.argv[1], encoding="utf-8").read()
|
||||||
|
# The scrim is the MouseArea filling the dimming Rectangle. Anything that
|
||||||
|
# reacts to a click there dismisses the surface.
|
||||||
|
scrim = re.search(r"Rectangle\s*\{[^{}]*anchors\.fill:\s*parent.*?MouseArea\s*\{(.*?)\}", text, re.S)
|
||||||
|
if scrim and re.search(r"onClicked|onPressed|onTapped", scrim.group(1)):
|
||||||
|
raise SystemExit(1)
|
||||||
|
PY
|
||||||
|
|
||||||
|
# ── Wiring ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
grep -q 'import qs.modules.welcome' "$shell_qml" \
|
||||||
|
|| note 'shell.qml does not import the welcome module'
|
||||||
|
grep -qE '^\s*Welcome \{\}' "$shell_qml" \
|
||||||
|
|| note 'shell.qml never instantiates the welcome screen'
|
||||||
|
grep -q 'welcomeOpen: activeOverlay === "welcome"' "$state" \
|
||||||
|
|| note 'ShellState does not track the welcome screen'
|
||||||
|
|
||||||
|
# ── Live ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
if ! command -v qs >/dev/null 2>&1 || ! qs ipc call welcome status >/dev/null 2>&1; then
|
||||||
|
if (( ${#findings[@]} > 0 )); then
|
||||||
|
printf 'welcome contract: %d finding(s)\n' "${#findings[@]}" >&2
|
||||||
|
printf ' - %s\n' "${findings[@]}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
printf 'welcome contract: PASS (static; no running shell)\n'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
was_open="$(qs ipc call welcome status | jq -r .open)"
|
||||||
|
|
||||||
|
qs ipc call welcome open >/dev/null
|
||||||
|
sleep 1
|
||||||
|
[[ "$(qs ipc call welcome status | jq -r .open)" == "true" ]] \
|
||||||
|
|| note 'the welcome screen did not open'
|
||||||
|
hyprctl layers -j | grep -q 'qs-popover-welcome' \
|
||||||
|
|| note 'the welcome screen reports open but its layer never mapped'
|
||||||
|
|
||||||
|
qs ipc call welcome close >/dev/null
|
||||||
|
sleep 1
|
||||||
|
[[ "$(qs ipc call welcome status | jq -r .open)" == "false" ]] \
|
||||||
|
|| note 'the welcome screen did not close'
|
||||||
|
|
||||||
|
[[ "$was_open" == "true" ]] && qs ipc call welcome open >/dev/null
|
||||||
|
|
||||||
|
if (( ${#findings[@]} > 0 )); then
|
||||||
|
printf 'welcome contract: %d finding(s)\n' "${#findings[@]}" >&2
|
||||||
|
printf ' - %s\n' "${findings[@]}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf 'welcome contract: PASS\n'
|
||||||
Reference in New Issue
Block a user