It is the system settings app for this desktop, so it is named the way one is. The window is "Settings", the titlebar is "Settings", the wordmark above the search field is gone, the sidebar's last row is "About", and the status line reads "Desktop is healthy". Prose followed the same rule. Forty-odd strings explained what "Panama" does -- "Panama never animates while idle", "Restore Panama defaults", "Panama looks in ~/Pictures/Wallpapers" -- which is how a product describes itself, not how a settings panel describes a setting. They now say what happens. No user-visible "Panama" remains anywhere in the app. Two consequences worth naming: The SUPER+I shortcut's description is user-visible, because the Shortcuts page is generated from it, so that is renamed too. Rebindings are keyed by shipped chord rather than description, so no existing override is orphaned by this. Three contracts matched the window by title and one matched that shortcut by description; all four are updated. There are no Hyprland window rules keyed on the title, so nothing about the window's placement changes. The desktop entry is now Name=Settings, but the FILE keeps its panama-settings name, as does the icon: the dock pins applications by desktop id, and renaming the file would silently unpin it. dock-pins-contract covers exactly that. The dated design docs under docs/superpowers keep the old name. They are a record of what was decided when, and editing them to agree with the present would make them lie about the past. Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
252 lines
10 KiB
QML
252 lines
10 KiB
QML
// Applications and session startup.
|
|
|
|
import Quickshell
|
|
import QtQuick
|
|
import qs.services
|
|
|
|
SettingsPage {
|
|
id: root
|
|
|
|
objectName: "applications"
|
|
title: "Applications"
|
|
lede: "Choose what opens your files and links, and what starts with your session."
|
|
|
|
property string expandedRole: ""
|
|
readonly property var applications: DesktopEntries.applications.values
|
|
readonly property var roles: [
|
|
{ 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 {
|
|
const entryId = String(entry?.id ?? "");
|
|
return entryId.endsWith(".desktop") ? entryId : entryId + ".desktop";
|
|
}
|
|
|
|
function displayName(entry: var): string {
|
|
return String(entry?.name || entry?.genericName || root.desktopId(entry));
|
|
}
|
|
|
|
function currentHandler(role: string): string {
|
|
return String(DefaultApps.handlers[role] ?? "");
|
|
}
|
|
|
|
function currentEntry(role: string): var {
|
|
const handler = root.currentHandler(role);
|
|
return root.applications.find(entry => root.desktopId(entry) === handler) ?? null;
|
|
}
|
|
|
|
function matchesRole(entry: var, role: var): bool {
|
|
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.categorySets.some(set => set.every(category => categories.includes(category)))
|
|
|| role.terms.some(term => metadata.includes(term));
|
|
}
|
|
|
|
function choicesForRole(role: var): var {
|
|
const choices = root.applications.filter(entry => root.matchesRole(entry, role));
|
|
const currentEntry = root.currentEntry(role.key);
|
|
if (currentEntry && !choices.some(entry => root.desktopId(entry) === root.desktopId(currentEntry)))
|
|
choices.push(currentEntry);
|
|
return choices.sort((left, right) => root.displayName(left).localeCompare(root.displayName(right)));
|
|
}
|
|
|
|
TextRow {
|
|
visible: DefaultApps.lastError !== ""
|
|
label: "Application settings need attention"
|
|
detail: DefaultApps.lastError
|
|
value: ""
|
|
divider: false
|
|
}
|
|
|
|
SettingsCard {
|
|
title: "Default applications"
|
|
subtitle: "Open a row to choose from applications that advertise the matching role."
|
|
|
|
Repeater {
|
|
model: root.roles
|
|
|
|
delegate: Column {
|
|
id: roleBlock
|
|
|
|
required property var modelData
|
|
required property int index
|
|
readonly property var choices: root.choicesForRole(roleBlock.modelData)
|
|
readonly property var selectedEntry: root.currentEntry(roleBlock.modelData.key)
|
|
|
|
width: parent.width
|
|
|
|
SettingRow {
|
|
label: roleBlock.modelData.label
|
|
detail: roleBlock.modelData.detail
|
|
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: {
|
|
root.expandedRole = root.expandedRole === roleBlock.modelData.key
|
|
? ""
|
|
: roleBlock.modelData.key;
|
|
}
|
|
}
|
|
|
|
Column {
|
|
width: parent.width
|
|
visible: root.expandedRole === roleBlock.modelData.key
|
|
|
|
Repeater {
|
|
model: roleBlock.choices
|
|
|
|
delegate: SettingRow {
|
|
id: candidateRow
|
|
|
|
required property var modelData
|
|
required property int index
|
|
|
|
readonly property string candidateId: root.desktopId(candidateRow.modelData)
|
|
readonly property bool selected: candidateRow.candidateId === root.currentHandler(roleBlock.modelData.key)
|
|
|
|
label: root.displayName(candidateRow.modelData)
|
|
detail: String(candidateRow.modelData.genericName || candidateRow.modelData.comment || candidateRow.candidateId)
|
|
value: candidateRow.selected ? "Current" : ""
|
|
activatable: !candidateRow.selected && !DefaultApps.busy
|
|
divider: candidateRow.index < roleBlock.choices.length - 1 || roleBlock.index < root.roles.length - 1
|
|
onActivated: {
|
|
DefaultApps.setDefault(roleBlock.modelData.key, candidateRow.candidateId);
|
|
root.expandedRole = "";
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// GNOME's Search panel, answered honestly.
|
|
//
|
|
// It configures which applications provide results in gnome-shell's
|
|
// overview and which folders are indexed. gnome-shell does not run here,
|
|
// so those settings would do nothing. Under Panama searching is the
|
|
// launcher's job, and Vicinae carries its own preferences -- reimplementing
|
|
// them here would give two places to change one thing.
|
|
SettingsCard {
|
|
title: "Search"
|
|
subtitle: "Applications, files, the calculator, clipboard history, emoji, and open windows are all searched from the launcher."
|
|
|
|
TextRow {
|
|
label: "Launcher"
|
|
detail: SystemSettings.vicinaeActive
|
|
? "Running as a user service"
|
|
: "Not running — Super+Shift+R opens the fallback launcher"
|
|
value: "Vicinae"
|
|
}
|
|
|
|
TextRow {
|
|
label: "Open search"
|
|
detail: "Three keys open it, because Super+A and Super+R were GNOME's app grid and run dialog"
|
|
value: "Super+Space"
|
|
}
|
|
|
|
ActionRow {
|
|
label: "Change these shortcuts"
|
|
detail: "Every launcher chord is rebindable, including clipboard history and emoji"
|
|
action: "Open keyboard"
|
|
divider: false
|
|
onTriggered: ShellState.openSettings("shortcuts")
|
|
}
|
|
}
|
|
|
|
SettingsCard {
|
|
title: "User autostart"
|
|
subtitle: "These desktop entries live in your user configuration. Select a row to toggle it."
|
|
|
|
TextRow {
|
|
visible: !DefaultApps.busy && DefaultApps.autostartEntries.length === 0
|
|
label: "No user autostart entries"
|
|
detail: "Applications can add entries to ~/.config/autostart."
|
|
value: ""
|
|
divider: false
|
|
}
|
|
|
|
Repeater {
|
|
model: DefaultApps.autostartEntries
|
|
|
|
delegate: SettingRow {
|
|
id: autostartRow
|
|
|
|
required property var modelData
|
|
required property int index
|
|
|
|
label: autostartRow.modelData.name
|
|
detail: autostartRow.modelData.id
|
|
value: autostartRow.modelData.enabled ? "Enabled" : "Disabled"
|
|
activatable: !DefaultApps.busy
|
|
divider: autostartRow.index < DefaultApps.autostartEntries.length - 1
|
|
onActivated: DefaultApps.setAutostart(autostartRow.modelData.id, !autostartRow.modelData.enabled)
|
|
}
|
|
}
|
|
}
|
|
|
|
SettingsCard {
|
|
title: "Compositor autostart"
|
|
subtitle: "These are started from the Hyprland configuration. They are read-only here."
|
|
|
|
TextRow {
|
|
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: ""
|
|
divider: false
|
|
}
|
|
|
|
Repeater {
|
|
model: DefaultApps.luaAutostartEntries
|
|
|
|
delegate: TextRow {
|
|
id: luaRow
|
|
|
|
required property var modelData
|
|
required property int index
|
|
|
|
label: luaRow.modelData.name
|
|
detail: luaRow.modelData.command
|
|
value: "Hyprland"
|
|
divider: luaRow.index < DefaultApps.luaAutostartEntries.length - 1
|
|
}
|
|
}
|
|
}
|
|
|
|
SettingsCard {
|
|
title: "Refresh"
|
|
|
|
ActionRow {
|
|
label: "Reload application settings"
|
|
detail: "Re-read desktop entries, defaults, and user autostart files"
|
|
action: DefaultApps.busy ? "Refreshing…" : "Refresh"
|
|
enabled: !DefaultApps.busy
|
|
divider: false
|
|
onTriggered: DefaultApps.refresh()
|
|
}
|
|
}
|
|
}
|