#!/usr/bin/env bash set -euo pipefail fail() { printf 'applications settings contract: %s\n' "$1" >&2 exit 1 } project_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" page="$project_root/config/dot/quickshell/modules/settings/ApplicationsPage.qml" [[ -f "$page" ]] || fail 'Applications page is missing' assert_contains() { rg -F --quiet "$1" "$page" || fail "page is missing: $1" } assert_contains 'SettingsPage {' assert_contains 'objectName: "applications"' assert_contains 'DesktopEntries.applications.values' assert_contains 'DefaultApps' assert_contains 'SettingsCard {' assert_contains 'SettingRow {' assert_contains 'activatable:' assert_contains 'ActionRow {' assert_contains 'TextRow {' for label in Browser Mail Files Terminal Music Images Video; do assert_contains "label: \"$label\"" done assert_contains 'title: "Default applications"' assert_contains 'title: "User autostart"' assert_contains 'title: "Compositor autostart"' assert_contains 'AutostartAppPicker {' assert_contains 'DefaultApps.addAutostart(' assert_contains 'label: "Add an application"' assert_contains 'categories' assert_contains 'genericName' assert_contains '.sort(' assert_contains 'currentEntry' assert_contains 'read-only' assert_contains 'choices.push(currentEntry)' assert_contains 'label: "Application settings need attention"' assert_contains 'DefaultApps.busy ? "Loading…"' assert_contains 'visible: !DefaultApps.busy && DefaultApps.autostartEntries.length === 0' assert_contains 'visible: !DefaultApps.busy && DefaultApps.luaAutostartEntries.length === 0' PAGE_PATH="$page" bun -e ' const source = await Bun.file(process.env.PAGE_PATH).text(); const rolesSource = source.match(/readonly property var roles:\s*(\[[\s\S]*?\n \])/); const matcherSource = source.match(/function matchesRole\(entry: var, role: var\): bool \{([\s\S]*?)\n \}/); if (!rolesSource || !matcherSource) { console.error("applications settings contract: role matcher could not be loaded"); process.exit(1); } const roles = Function(`return (${rolesSource[1]})`)(); const matchesRole = Function("entry", "role", matcherSource[1]); const role = key => roles.find(candidate => candidate.key === key); const fixtures = [ { name: "AudioVideo does not imply music", entry: { name: "Kodi", genericName: "Media Center", comment: "Entertainment hub", categories: "AudioVideo;Player;" }, role: "music", expected: false }, { name: "Graphics does not imply image handler", entry: { name: "Document Scanner", genericName: "Document Scanner", comment: "Scan documents", categories: ["Graphics"] }, role: "images", expected: false }, { name: "Viewer does not imply image handler", entry: { name: "Papers", genericName: "Document Viewer", comment: "Read documents", categories: "Office;Viewer;" }, role: "images", expected: false }, { name: "comment does not nominate a default handler", entry: { name: "Settings", genericName: "System Settings", comment: "Configure your video player", categories: ["System"] }, role: "video", expected: false }, { name: "exact audio player categories match music", entry: { name: "Rhythmbox", genericName: "Music Player", comment: "Play music", categories: "AudioVideo;Audio;Player;" }, role: "music", expected: true }, { name: "exact video category matches video", entry: { name: "Videos", genericName: "Video Player", comment: "Play movies", categories: ["AudioVideo", "Video", "Player"] }, role: "video", expected: true }, { name: "descriptive metadata matches image handler", entry: { name: "Loupe", genericName: "Image Viewer", comment: "Browse pictures", categories: "Graphics;Viewer;" }, role: "images", expected: true } ]; for (const fixture of fixtures) { const actual = matchesRole(fixture.entry, role(fixture.role)); if (actual !== fixture.expected) { console.error(`applications settings contract: ${fixture.name}: expected ${fixture.expected}, got ${actual}`); process.exit(1); } } ' if rg --quiet 'Component\.onCompleted|DesktopEntries\.(byId|heuristicLookup)' "$page"; then fail 'page snapshots or performs a one-time desktop-entry lookup' fi if rg -F --quiet 'label: "Could not apply the change"' "$page"; then fail 'error heading incorrectly describes read failures as apply failures' fi if rg --quiet '#[0-9A-Fa-f]{3,8}' "$page"; then fail 'page introduces a color literal instead of the shared visual system' fi [[ "$(rg --count 'activatable:' "$page")" -ge 2 ]] \ || fail 'default and autostart rows are not both whole-row activatable' picker="$project_root/config/dot/quickshell/modules/settings/AutostartAppPicker.qml" qmldir="$project_root/config/dot/quickshell/modules/settings/qmldir" [[ -f "$picker" ]] || fail 'autostart application picker is missing' rg -Fq 'required property var existing' "$picker" \ || fail 'autostart picker cannot exclude existing entries' rg -Fq 'signal picked(string id)' "$picker" \ || fail 'autostart picker does not emit a validated desktop id' rg -q '^AutostartAppPicker 1\.0 AutostartAppPicker\.qml$' "$qmldir" \ || fail 'autostart picker is not registered in the Settings module' printf 'applications settings contract: PASS\n'