Make Applications a real app manager, and clean up storage without the racket
Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
This commit is contained in:
@@ -1,22 +1,56 @@
|
||||
// Applications and session startup.
|
||||
// Applications: what is installed, what opens your files, and what starts with
|
||||
// your session.
|
||||
//
|
||||
// This page used to answer only the middle question. That made it the one place
|
||||
// in Settings you could not do the obvious thing -- see what is on the machine
|
||||
// and remove something -- so the list came first and everything else arranged
|
||||
// itself around it.
|
||||
//
|
||||
// Two rules the page keeps to:
|
||||
//
|
||||
// It never pretends to own a system package. A Flatpak can be inspected and
|
||||
// removed here because that is a per-user operation with per-user blast
|
||||
// radius. `dnf remove` is not: removing the wrong package takes the desktop
|
||||
// with it, and a settings pane should not put that one press away. The row
|
||||
// says so and prints the command, which is more useful than a button that
|
||||
// refuses.
|
||||
//
|
||||
// It never links somewhere with nothing on it. The "Elsewhere in Settings"
|
||||
// chips appear per application, and only where that application already has a
|
||||
// rule on the page being linked to.
|
||||
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import qs.config
|
||||
import qs.services
|
||||
import qs.modules.clipboard
|
||||
|
||||
SettingsPage {
|
||||
id: root
|
||||
|
||||
objectName: "applications"
|
||||
title: "Applications"
|
||||
lede: "Choose what opens your files and links, and what starts with your session."
|
||||
lede: "What is installed, what opens your files, and what starts with your session."
|
||||
|
||||
property string expandedRole: ""
|
||||
property bool addingAutostart: false
|
||||
// Which installed application is unfolded. One at a time: the body is tall
|
||||
// enough that two open rows would push the third off the screen.
|
||||
property string expandedApp: ""
|
||||
|
||||
// Uninstalling deletes an application, so it never happens on a first
|
||||
// press. Holds the Flatpak id that is one press away from being removed.
|
||||
property string pendingUninstall: ""
|
||||
|
||||
// Which entry has been asked to be removed. Removal deletes a file, so
|
||||
// it never happens on a first press.
|
||||
property string confirmingAutostartRemoval: ""
|
||||
|
||||
property bool addingAutostart: false
|
||||
property bool showingFileTypes: false
|
||||
|
||||
// Empty means "whichever category the catalog lists first", so the card is
|
||||
// never blank before anything has been chosen.
|
||||
property string catalogCategory: ""
|
||||
|
||||
readonly property var applications: DesktopEntries.applications.values
|
||||
// Each role governs a whole family of types, not one representative: setting
|
||||
// "Images" writes PNG, JPEG, WebP and the rest together, so a file manager
|
||||
@@ -90,6 +124,154 @@ SettingsPage {
|
||||
return choices.sort((left, right) => root.displayName(left).localeCompare(root.displayName(right)));
|
||||
}
|
||||
|
||||
// ── The installed list ───────────────────────────────────────────────────
|
||||
|
||||
// How many rows the card draws before it stops and says how many are left.
|
||||
// A machine with 243 applications is a scroll, not a list; the search field
|
||||
// is what actually finds one.
|
||||
readonly property int installedLimit: 10
|
||||
|
||||
readonly property var installedApps: (AppLibrary.apps ?? []).filter(
|
||||
app => AppLibrary.matches(app, installedSearch.text))
|
||||
|
||||
readonly property var expandedAppRecord:
|
||||
(AppLibrary.apps ?? []).find(app => String(app.entryId ?? "") === root.expandedApp) ?? null
|
||||
|
||||
// Read for the open row only. The cache is read directly so this binding
|
||||
// has a dependency on it -- that is what turns "Reading…" into the summary
|
||||
// when the answer lands -- and permissionsFor() is what asks for an answer
|
||||
// that is not there yet.
|
||||
readonly property var expandedPermissions: {
|
||||
const cache = AppLibrary.permissionCache;
|
||||
const app = root.expandedAppRecord;
|
||||
if (!app || String(app.kind ?? "") !== "flatpak")
|
||||
return null;
|
||||
const key = String(app.flatpakId ?? "");
|
||||
const info = cache[key] ?? AppLibrary.permissionsFor(key);
|
||||
return info && Array.isArray(info.summary) ? info : null;
|
||||
}
|
||||
|
||||
// Flatseal is the tool that edits these; offering it when it is not
|
||||
// installed would be a button that does nothing.
|
||||
readonly property bool flatsealAvailable: root.applications.some(
|
||||
entry => String(entry.id ?? "").replace(/\.desktop$/, "") === "com.github.tchx84.Flatseal")
|
||||
|
||||
function autostartId(app: var): string {
|
||||
const id = String(app?.entryId ?? "");
|
||||
return id.endsWith(".desktop") ? id : id + ".desktop";
|
||||
}
|
||||
|
||||
function autostartEntry(desktopId: string): var {
|
||||
return DefaultApps.autostartEntries.find(entry => String(entry.id) === desktopId) ?? null;
|
||||
}
|
||||
|
||||
function autostartOn(app: var): bool {
|
||||
const entry = root.autostartEntry(root.autostartId(app));
|
||||
return entry !== null && entry.enabled !== false;
|
||||
}
|
||||
|
||||
// Turning it on for an application that has never had an entry writes one;
|
||||
// there is no separate "add" step from here, because from the row's point
|
||||
// of view there is one switch and it means one thing.
|
||||
function setAppAutostart(app: var, value: bool): void {
|
||||
const desktopId = root.autostartId(app);
|
||||
if (root.autostartEntry(desktopId) === null) {
|
||||
if (value)
|
||||
DefaultApps.addAutostart(desktopId);
|
||||
return;
|
||||
}
|
||||
DefaultApps.setAutostart(desktopId, value);
|
||||
}
|
||||
|
||||
// ── Where else this application already has a rule ───────────────────────
|
||||
|
||||
function appIdentifiers(app: var): var {
|
||||
const out = [];
|
||||
const entryId = String(app?.entryId ?? "").replace(/\.desktop$/, "");
|
||||
const flatpakId = String(app?.flatpakId ?? "");
|
||||
const name = String(app?.name ?? "");
|
||||
for (const value of [entryId, flatpakId, name]) {
|
||||
if (value !== "")
|
||||
out.push(value.toLowerCase());
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function hasNotificationRule(app: var): bool {
|
||||
const identifiers = root.appIdentifiers(app);
|
||||
return (Notifs.applications ?? []).some(
|
||||
known => identifiers.indexOf(String(known.id ?? "").toLowerCase()) >= 0);
|
||||
}
|
||||
|
||||
// A stream, not a stored rule: the Sound page's per-application rows come
|
||||
// from what is playing right now, so the chip is honest only while there is
|
||||
// something there to change.
|
||||
function hasSoundRule(app: var): bool {
|
||||
const identifiers = root.appIdentifiers(app);
|
||||
return (AudioDevices.applications ?? []).some(stream =>
|
||||
identifiers.indexOf(String(stream.key ?? "").toLowerCase()) >= 0
|
||||
|| identifiers.indexOf(String(stream.label ?? "").toLowerCase()) >= 0);
|
||||
}
|
||||
|
||||
// The portal's permission store records answers under the application's own
|
||||
// id, which is the same id this list carries -- so the match is exact and
|
||||
// costs nothing.
|
||||
function hasPrivacyRule(app: var): bool {
|
||||
const identifiers = root.appIdentifiers(app);
|
||||
return (Permissions.devices ?? []).some(device =>
|
||||
(device.applications ?? []).some(
|
||||
recorded => identifiers.indexOf(String(recorded.app ?? "").toLowerCase()) >= 0));
|
||||
}
|
||||
|
||||
// ── The catalog ──────────────────────────────────────────────────────────
|
||||
|
||||
readonly property var catalogCategories: AppLibrary.categories ?? []
|
||||
|
||||
// A category arrives either as a plain name or as an object carrying its
|
||||
// entries with it, so both are read rather than one being assumed.
|
||||
function categoryId(category: var): string {
|
||||
return String(category?.id ?? category?.name ?? category ?? "");
|
||||
}
|
||||
|
||||
function categoryLabel(category: var): string {
|
||||
const label = String(category?.label ?? category?.name ?? category?.id ?? category ?? "");
|
||||
const count = Number(category?.count ?? (category?.entries ?? []).length ?? 0);
|
||||
return count > 0 ? label + " · " + count : label;
|
||||
}
|
||||
|
||||
readonly property string activeCategory: {
|
||||
if (root.catalogCategory !== "")
|
||||
return root.catalogCategory;
|
||||
const categories = root.catalogCategories;
|
||||
return categories.length > 0 ? root.categoryId(categories[0]) : "";
|
||||
}
|
||||
|
||||
readonly property var catalogEntries: {
|
||||
// Reading the category list is what makes this re-run once the catalog
|
||||
// has been parsed; entriesFor() alone is a function call and would not.
|
||||
const categories = root.catalogCategories;
|
||||
if (categories.length === 0 || root.activeCategory === "")
|
||||
return [];
|
||||
return AppLibrary.entriesFor(root.activeCategory) ?? [];
|
||||
}
|
||||
|
||||
// ── The launcher's own chord, read from the keymap ───────────────────────
|
||||
|
||||
// Hardcoding "Super+Space" survived two rebinds of the launcher and told
|
||||
// the wrong story both times. The keymap is already loaded; this reads it.
|
||||
readonly property var launcherChords: (Keybinds.binds ?? [])
|
||||
.filter(bind => String(bind.description ?? "") === "Launcher")
|
||||
.map(bind => String(bind.chord ?? ""))
|
||||
.filter(chord => chord !== "")
|
||||
|
||||
// AppLibrary loads nothing on its own: `flatpak list` and the catalog cost
|
||||
// a process each and only this page wants them, so a shell start where
|
||||
// nobody opens Applications pays for neither.
|
||||
Component.onCompleted: {
|
||||
AppLibrary.refresh();
|
||||
AppLibrary.refreshCatalog();
|
||||
}
|
||||
|
||||
TextRow {
|
||||
visible: DefaultApps.lastError !== ""
|
||||
label: "Application settings need attention"
|
||||
@@ -98,143 +280,248 @@ SettingsPage {
|
||||
divider: false
|
||||
}
|
||||
|
||||
TextRow {
|
||||
visible: String(AppLibrary.lastError ?? "") !== ""
|
||||
label: "The application list needs attention"
|
||||
detail: String(AppLibrary.lastError ?? "")
|
||||
value: ""
|
||||
divider: false
|
||||
}
|
||||
|
||||
// ── What is installed ────────────────────────────────────────────────────
|
||||
|
||||
SettingsCard {
|
||||
title: "Installed applications"
|
||||
subtitle: !AppLibrary.flatpaksLoaded
|
||||
? "Reading the applications installed on this machine…"
|
||||
: (AppLibrary.apps ?? []).length + " installed · "
|
||||
+ AppLibrary.flatpakCount + " through Flatpak"
|
||||
|
||||
SearchField {
|
||||
id: installedSearch
|
||||
|
||||
width: parent.width
|
||||
placeholder: "Search installed applications"
|
||||
}
|
||||
|
||||
Item { width: 1; height: 8 }
|
||||
|
||||
Repeater {
|
||||
model: root.installedApps.slice(0, root.installedLimit)
|
||||
|
||||
delegate: InstalledAppRow {
|
||||
id: installedRow
|
||||
|
||||
required property var modelData
|
||||
required property int index
|
||||
|
||||
readonly property bool open: root.expandedApp === String(installedRow.modelData.entryId ?? "")
|
||||
|
||||
width: parent.width
|
||||
app: installedRow.modelData
|
||||
expanded: installedRow.open
|
||||
// A write in flight, not a read: the catalog refresh this page
|
||||
// starts on open must not grey out every row for a second.
|
||||
busy: AppLibrary.busyEntryId !== "" || DefaultApps.busy
|
||||
divider: installedRow.index
|
||||
< Math.min(root.installedApps.length, root.installedLimit) - 1
|
||||
autostart: root.autostartOn(installedRow.modelData)
|
||||
flatsealAvailable: root.flatsealAvailable
|
||||
permissionsKnown: installedRow.open && root.expandedPermissions !== null
|
||||
permissionSummary: installedRow.open && root.expandedPermissions
|
||||
? root.expandedPermissions.summary
|
||||
: []
|
||||
hasNotificationRule: root.hasNotificationRule(installedRow.modelData)
|
||||
hasSoundRule: root.hasSoundRule(installedRow.modelData)
|
||||
hasPrivacyRule: root.hasPrivacyRule(installedRow.modelData)
|
||||
confirmingUninstall: root.pendingUninstall !== ""
|
||||
&& root.pendingUninstall === String(installedRow.modelData.flatpakId ?? "")
|
||||
|
||||
onActivated: {
|
||||
root.pendingUninstall = "";
|
||||
root.expandedApp = installedRow.open
|
||||
? ""
|
||||
: String(installedRow.modelData.entryId ?? "");
|
||||
}
|
||||
onAutostartToggled: value => root.setAppAutostart(installedRow.modelData, value)
|
||||
onJumped: page => ShellState.openSettings(page)
|
||||
onFlatsealRequested: Quickshell.execDetached(
|
||||
["flatpak", "run", "com.github.tchx84.Flatseal"])
|
||||
onUninstallArmed: root.pendingUninstall = String(installedRow.modelData.flatpakId ?? "")
|
||||
onUninstallCancelled: root.pendingUninstall = ""
|
||||
onUninstallConfirmed: {
|
||||
const flatpakId = String(installedRow.modelData.flatpakId ?? "");
|
||||
root.pendingUninstall = "";
|
||||
root.expandedApp = "";
|
||||
AppLibrary.uninstall(flatpakId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TextRow {
|
||||
visible: root.installedApps.length > root.installedLimit
|
||||
label: (root.installedApps.length - root.installedLimit) + " more"
|
||||
detail: "Search finds any of them by name or by application id."
|
||||
value: ""
|
||||
divider: false
|
||||
}
|
||||
|
||||
TextRow {
|
||||
visible: (AppLibrary.apps ?? []).length > 0 && root.installedApps.length === 0
|
||||
label: "Nothing matches that"
|
||||
detail: "Both the application's name and its id are searched."
|
||||
value: ""
|
||||
divider: false
|
||||
}
|
||||
}
|
||||
|
||||
// ── What could be installed ──────────────────────────────────────────────
|
||||
|
||||
SettingsCard {
|
||||
title: "Browse the catalog"
|
||||
subtitle: "Panama's curated applications — the same list `panama apps` offers, installed the same way."
|
||||
|
||||
Flow {
|
||||
width: parent.width
|
||||
spacing: 7
|
||||
|
||||
Repeater {
|
||||
model: root.catalogCategories
|
||||
|
||||
delegate: SettingsChip {
|
||||
required property var modelData
|
||||
|
||||
text: root.categoryLabel(modelData)
|
||||
active: root.categoryId(modelData) === root.activeCategory
|
||||
onClicked: root.catalogCategory = root.categoryId(modelData)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item { width: 1; height: 10 }
|
||||
|
||||
Repeater {
|
||||
model: root.catalogEntries
|
||||
|
||||
delegate: SettingRow {
|
||||
id: catalogRow
|
||||
|
||||
required property var modelData
|
||||
required property int index
|
||||
|
||||
readonly property bool installed: catalogRow.modelData.installed === true
|
||||
readonly property bool flatpak: String(catalogRow.modelData.kind ?? "") === "flatpak"
|
||||
// `id` is the catalog line verbatim, "flatpak:" prefix and all,
|
||||
// because that is what install matches. `ref` is the same thing
|
||||
// with the prefix taken off, which is what a person reads.
|
||||
readonly property string ref: String(catalogRow.modelData.ref ?? catalogRow.modelData.id ?? "")
|
||||
|
||||
width: parent.width
|
||||
label: String(catalogRow.modelData.label ?? "") !== ""
|
||||
? String(catalogRow.modelData.label)
|
||||
: catalogRow.ref
|
||||
detail: catalogRow.flatpak
|
||||
? catalogRow.ref + " · Flatpak, from Flathub"
|
||||
: catalogRow.ref + " · system package, so installing asks for your password"
|
||||
value: catalogRow.installed ? "Installed" : ""
|
||||
controlWidth: 110
|
||||
divider: catalogRow.index < root.catalogEntries.length - 1
|
||||
|
||||
SettingsButton {
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: !catalogRow.installed
|
||||
// Only the row being installed says so, rather than the
|
||||
// whole card greying out around one press.
|
||||
text: AppLibrary.busyEntryId === String(catalogRow.modelData.id ?? "")
|
||||
? "Installing…"
|
||||
: "Install"
|
||||
tone: "accent"
|
||||
enabled: !AppLibrary.busy
|
||||
onClicked: AppLibrary.install(root.activeCategory,
|
||||
String(catalogRow.modelData.id ?? ""))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TextRow {
|
||||
visible: root.catalogCategories.length === 0
|
||||
label: AppLibrary.catalogLoaded
|
||||
? "The catalog is empty"
|
||||
: "Reading the catalog…"
|
||||
detail: "It is parsed from setup/packages/extras, the same files the command line uses."
|
||||
value: ""
|
||||
divider: false
|
||||
}
|
||||
}
|
||||
|
||||
// ── What opens what ──────────────────────────────────────────────────────
|
||||
|
||||
SettingsCard {
|
||||
title: "Default applications"
|
||||
subtitle: "Open a row to choose from applications that advertise the matching role."
|
||||
subtitle: "Each role sets its whole family of types together, so one picture never opens somewhere different from the next."
|
||||
|
||||
Repeater {
|
||||
model: root.roles
|
||||
|
||||
delegate: Column {
|
||||
id: roleBlock
|
||||
delegate: OptionPickerRow {
|
||||
id: roleRow
|
||||
|
||||
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)
|
||||
|
||||
readonly property var choices: root.choicesForRole(roleRow.modelData)
|
||||
|
||||
width: parent.width
|
||||
|
||||
SettingRow {
|
||||
id: roleRow
|
||||
|
||||
readonly property bool open: root.expandedRole === roleBlock.modelData.key
|
||||
|
||||
label: roleBlock.modelData.label
|
||||
detail: roleBlock.modelData.detail
|
||||
activatable: roleBlock.choices.length > 0 && !DefaultApps.busy
|
||||
controlWidth: 210
|
||||
|
||||
// Drawn rather than left to SettingRow's plain value text, so
|
||||
// the row carries the same chevron a PickerRow does. These
|
||||
// open a chooser but looked completely inert without it.
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 9
|
||||
|
||||
Text {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: DefaultApps.busy ? "Loading…" : (
|
||||
roleBlock.selectedEntry
|
||||
? root.displayName(roleBlock.selectedEntry)
|
||||
: (root.currentHandler(roleBlock.modelData.key) || "Not set")
|
||||
)
|
||||
color: Theme.fgDim
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSize
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: roleBlock.choices.length > 0
|
||||
text: roleRow.open ? "\u25B4" : "\u25BE"
|
||||
color: Theme.fgMuted
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
}
|
||||
}
|
||||
|
||||
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 = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
label: roleRow.modelData.label
|
||||
detail: roleRow.modelData.detail
|
||||
options: roleRow.choices.map(entry => ({
|
||||
value: root.desktopId(entry),
|
||||
label: root.displayName(entry),
|
||||
detail: String(entry.genericName || entry.comment || root.desktopId(entry))
|
||||
}))
|
||||
current: root.currentHandler(roleRow.modelData.key)
|
||||
enabled: roleRow.choices.length > 0 && !DefaultApps.busy
|
||||
value: DefaultApps.busy ? "Loading…" : (
|
||||
roleRow.currentOption
|
||||
? String(roleRow.currentOption.label)
|
||||
: (root.currentHandler(roleRow.modelData.key) || "Not set")
|
||||
)
|
||||
onPicked: value => DefaultApps.setDefault(roleRow.modelData.key, String(value))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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."
|
||||
|
||||
TextEntryRow { setting: "webSearchUrl"; placeholder: "https://duckduckgo.com/?q=" }
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
// The escape hatch from the paragraph above: one type, on its own.
|
||||
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")
|
||||
label: "One file type"
|
||||
detail: root.showingFileTypes
|
||||
? "Search the type database and give a single type its own application"
|
||||
: "Override a single type when a role's family is too broad"
|
||||
action: root.showingFileTypes ? "Close" : "Choose"
|
||||
divider: root.showingFileTypes
|
||||
onTriggered: {
|
||||
root.showingFileTypes = !root.showingFileTypes;
|
||||
if (!root.showingFileTypes)
|
||||
DefaultApps.clearTypeSearch();
|
||||
}
|
||||
}
|
||||
|
||||
FileTypePicker {
|
||||
visible: root.showingFileTypes
|
||||
width: parent.width
|
||||
matches: DefaultApps.typeMatches ?? []
|
||||
busy: DefaultApps.searchingTypes
|
||||
truncated: DefaultApps.typeSearchTruncated
|
||||
onQueried: query => DefaultApps.searchTypes(query)
|
||||
onChosen: (mime, desktopId) => DefaultApps.setType(mime, desktopId)
|
||||
}
|
||||
}
|
||||
|
||||
// ── What starts with the session ─────────────────────────────────────────
|
||||
|
||||
SettingsCard {
|
||||
title: "User autostart"
|
||||
subtitle: "Choose what starts with your session. Entries live in your user configuration, not the compositor."
|
||||
title: "Autostart"
|
||||
subtitle: "Entries live in your user configuration. The switch in an application's own row above writes the same files."
|
||||
|
||||
ActionRow {
|
||||
label: "Add an application"
|
||||
@@ -262,7 +549,6 @@ SettingsPage {
|
||||
label: "No user autostart entries"
|
||||
detail: "Applications can add entries to ~/.config/autostart."
|
||||
value: ""
|
||||
divider: false
|
||||
}
|
||||
|
||||
Repeater {
|
||||
@@ -281,7 +567,6 @@ SettingsPage {
|
||||
detail: autostartRow.confirming
|
||||
? "Removing deletes this entry. Turning it off instead is reversible."
|
||||
: autostartRow.modelData.id
|
||||
divider: autostartRow.index < DefaultApps.autostartEntries.length - 1
|
||||
controlWidth: 210
|
||||
|
||||
// A switch, not the words "Enabled"/"Disabled". The row always
|
||||
@@ -320,11 +605,19 @@ SettingsPage {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
title: "Compositor autostart"
|
||||
subtitle: "These are started from the Hyprland configuration. They are read-only here."
|
||||
// The compositor's own autostart, described rather than offered. These
|
||||
// are started from config/dot/hypr/autostart.lua, which is where they
|
||||
// are changed -- a switch here would edit a file the compositor reads
|
||||
// once at launch and give no sign that nothing had happened.
|
||||
TextRow {
|
||||
label: "Compositor autostart"
|
||||
detail: "Started from the Hyprland configuration, and read-only here."
|
||||
value: DefaultApps.luaAutostartEntries.length > 0
|
||||
? DefaultApps.luaAutostartEntries.length
|
||||
+ (DefaultApps.luaAutostartEntries.length === 1 ? " entry" : " entries")
|
||||
: ""
|
||||
}
|
||||
|
||||
TextRow {
|
||||
visible: !DefaultApps.busy && DefaultApps.luaAutostartEntries.length === 0
|
||||
@@ -351,16 +644,63 @@ SettingsPage {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Searching from the launcher ──────────────────────────────────────────
|
||||
|
||||
// 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."
|
||||
|
||||
TextEntryRow { setting: "webSearchUrl"; placeholder: "https://duckduckgo.com/?q=" }
|
||||
|
||||
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: root.launcherChords.length > 1
|
||||
? root.launcherChords.join(", ") + " all open it, because Super+A and Super+R"
|
||||
+ " were GNOME's app grid and run dialog"
|
||||
: "Rebindable from the keyboard page, like every other launcher chord"
|
||||
// The keymap is the source of truth; the literal is what shows
|
||||
// while it is still being read.
|
||||
value: root.launcherChords.length > 0 ? root.launcherChords[0] : "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: "Refresh"
|
||||
|
||||
ActionRow {
|
||||
label: "Reload application settings"
|
||||
detail: "Re-read desktop entries, defaults, and user autostart files"
|
||||
action: DefaultApps.busy ? "Refreshing…" : "Refresh"
|
||||
enabled: !DefaultApps.busy
|
||||
detail: "Re-read desktop entries, defaults, user autostart files, and the installed list"
|
||||
action: DefaultApps.busy || AppLibrary.busy ? "Refreshing…" : "Refresh"
|
||||
enabled: !DefaultApps.busy && !AppLibrary.busy
|
||||
divider: false
|
||||
onTriggered: DefaultApps.refresh()
|
||||
onTriggered: {
|
||||
DefaultApps.refresh();
|
||||
AppLibrary.refresh();
|
||||
AppLibrary.refreshCatalog();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user