diff --git a/config/dot/quickshell/modules/settings/ApplicationsPage.qml b/config/dot/quickshell/modules/settings/ApplicationsPage.qml index 864da62..0dea3cb 100644 --- a/config/dot/quickshell/modules/settings/ApplicationsPage.qml +++ b/config/dot/quickshell/modules/settings/ApplicationsPage.qml @@ -14,13 +14,13 @@ SettingsPage { property string expandedRole: "" readonly property var applications: DesktopEntries.applications.values readonly property var roles: [ - { key: "browser", label: "Browser", detail: "Web links and HTML pages", categories: ["webbrowser"], terms: ["browser", "web"] }, - { key: "mail", label: "Mail", detail: "Email links", categories: ["email"], terms: ["mail", "email"] }, - { key: "files", label: "Files", detail: "Folders and file locations", categories: ["filemanager"], terms: ["file manager", "files"] }, - { key: "terminal", label: "Terminal", detail: "Terminal links and command-line handoffs", categories: ["terminalemulator"], terms: ["terminal", "console"] }, - { key: "music", label: "Music", detail: "MP3 audio", categories: ["audio", "player"], terms: ["music", "audio player"] }, - { key: "images", label: "Images", detail: "PNG images", categories: ["graphics", "viewer"], terms: ["image", "photo", "picture"] }, - { key: "video", label: "Video", detail: "MP4 video", categories: ["video", "player"], terms: ["video", "media player"] } + { key: "browser", label: "Browser", detail: "Web links and HTML pages", categorySets: [["webbrowser"]], terms: ["web browser", "browser"] }, + { key: "mail", label: "Mail", detail: "Email links", categorySets: [["email"]], terms: ["mail client", "email client"] }, + { key: "files", label: "Files", detail: "Folders and file locations", categorySets: [["filemanager"]], terms: ["file manager"] }, + { key: "terminal", label: "Terminal", detail: "Terminal links and command-line handoffs", categorySets: [["terminalemulator"]], terms: ["terminal emulator", "terminal"] }, + { key: "music", label: "Music", detail: "MP3 audio", categorySets: [["music"], ["audio", "player"]], terms: ["music player", "audio player"] }, + { key: "images", label: "Images", detail: "PNG images", categorySets: [], terms: ["image viewer", "image editor", "photo viewer", "photo editor", "picture viewer"] }, + { key: "video", label: "Video", detail: "MP4 video", categorySets: [["video"]], terms: ["video player", "movie player"] } ] function desktopId(entry: var): string { @@ -42,13 +42,21 @@ SettingsPage { } function matchesRole(entry: var, role: var): bool { - const categories = Array.isArray(entry.categories) - ? entry.categories.join(" ").toLowerCase() - : String(entry.categories ?? "").toLowerCase(); - const metadata = [entry.name, entry.genericName, entry.comment] + const rawCategories = Array.isArray(entry.categories) + ? entry.categories + : [String(entry.categories ?? "")]; + const categories = []; + for (const rawCategory of rawCategories) { + for (const value of String(rawCategory).split(";")) { + const category = value.trim().toLowerCase(); + if (category !== "") + categories.push(category); + } + } + const metadata = [entry.name, entry.genericName] .map(value => String(value ?? "").toLowerCase()) .join(" "); - return role.categories.some(category => categories.includes(category)) + return role.categorySets.some(set => set.every(category => categories.includes(category))) || role.terms.some(term => metadata.includes(term)); } @@ -62,7 +70,7 @@ SettingsPage { TextRow { visible: DefaultApps.lastError !== "" - label: "Could not apply the change" + label: "Application settings need attention" detail: DefaultApps.lastError value: "" divider: false @@ -88,9 +96,11 @@ SettingsPage { SettingRow { label: roleBlock.modelData.label detail: roleBlock.modelData.detail - value: roleBlock.selectedEntry - ? root.displayName(roleBlock.selectedEntry) - : (root.currentHandler(roleBlock.modelData.key) || "Not set") + value: DefaultApps.busy ? "Loading…" : ( + roleBlock.selectedEntry + ? root.displayName(roleBlock.selectedEntry) + : (root.currentHandler(roleBlock.modelData.key) || "Not set") + ) activatable: roleBlock.choices.length > 0 && !DefaultApps.busy divider: root.expandedRole !== roleBlock.modelData.key && roleBlock.index < root.roles.length - 1 onActivated: { @@ -137,7 +147,7 @@ SettingsPage { subtitle: "These desktop entries live in your user configuration. Select a row to toggle it." TextRow { - visible: DefaultApps.autostartEntries.length === 0 + visible: !DefaultApps.busy && DefaultApps.autostartEntries.length === 0 label: "No user autostart entries" detail: "Applications can add entries to ~/.config/autostart." value: "" @@ -168,7 +178,7 @@ SettingsPage { subtitle: "Panama starts these from Hyprland configuration. They are read-only here." TextRow { - visible: DefaultApps.luaAutostartEntries.length === 0 + visible: !DefaultApps.busy && DefaultApps.luaAutostartEntries.length === 0 label: "No compositor entries found" detail: "No hl.exec_cmd entries were found in config/dot/hypr/autostart.lua." value: "" diff --git a/tests/quickshell/applications-settings-contract.sh b/tests/quickshell/applications-settings-contract.sh index a351adf..d3b5ef6 100755 --- a/tests/quickshell/applications-settings-contract.sh +++ b/tests/quickshell/applications-settings-contract.sh @@ -38,10 +38,84 @@ 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