204 lines
7.0 KiB
QML
204 lines
7.0 KiB
QML
pragma Singleton
|
|
|
|
// Snapshots of Panama's durable settings stores.
|
|
//
|
|
// DesktopPreferences and HomePreferences use separate files. The helper owns
|
|
// the transactional filesystem boundary; this service owns settling the live
|
|
// desktop after those files have changed underneath it.
|
|
//
|
|
// HomePreferences intentionally keeps its FileView in Quickshell's private
|
|
// state directory while snapshots use Panama's canonical state directory. This
|
|
// service bridges them through HomePreferences' public mutation API, then soft
|
|
// reloads once external consumers have settled.
|
|
|
|
import Quickshell
|
|
import Quickshell.Io
|
|
import QtQuick
|
|
import qs.config
|
|
|
|
Singleton {
|
|
id: root
|
|
|
|
readonly property string helperPath: Quickshell.shellDir + "/scripts/panama-settings-backup"
|
|
|
|
property var snapshots: []
|
|
property string lastError: ""
|
|
property string lastAction: ""
|
|
|
|
readonly property bool busy: listQuery.running || actionRun.running
|
|
|| applyRestoredState.running || settleReload.running
|
|
|
|
Process {
|
|
id: listQuery
|
|
command: [root.helperPath, "list"]
|
|
stdout: StdioCollector {
|
|
onStreamFinished: {
|
|
try {
|
|
const parsed = JSON.parse(this.text);
|
|
root.snapshots = Array.isArray(parsed) ? parsed : [];
|
|
if (root.lastError === "Could not read the list of snapshots.")
|
|
root.lastError = "";
|
|
} catch (error) {
|
|
root.lastError = "Could not read the list of snapshots.";
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Process {
|
|
id: actionRun
|
|
property bool restoring: false
|
|
property string outputText: ""
|
|
stdout: StdioCollector {
|
|
onStreamFinished: actionRun.outputText = this.text
|
|
}
|
|
onStarted: actionRun.outputText = ""
|
|
onExited: (exitCode, exitStatus) => {
|
|
if (exitCode !== 0) {
|
|
root.lastError = actionRun.restoring
|
|
? "That snapshot could not be restored."
|
|
: "The settings could not be backed up.";
|
|
return;
|
|
}
|
|
root.lastAction = actionRun.restoring ? "restored" : "saved";
|
|
if (actionRun.restoring) {
|
|
const homeReloaded = root.reloadHomeState(actionRun.outputText);
|
|
root.lastError = homeReloaded
|
|
? ""
|
|
: "Desktop settings were restored, but Home favourites could not be reloaded.";
|
|
DesktopPreferences.reload();
|
|
applyRestoredState.restart();
|
|
} else
|
|
root.lastError = "";
|
|
root.refresh();
|
|
}
|
|
}
|
|
|
|
Timer {
|
|
id: applyRestoredState
|
|
interval: 80
|
|
repeat: false
|
|
onTriggered: {
|
|
// DesktopPreferences.reload() invalidates reactive shell bindings.
|
|
// These services also own state outside QML and need an explicit
|
|
// replay: compositor options, Lua-generated binds, and hyprpaper.
|
|
SystemSettings.applyPersistedDisplayPolicy();
|
|
Keybinds.applyReload();
|
|
Wallpaper.set(String(DesktopPreferences.get("wallpaperPath") ?? ""));
|
|
|
|
settleReload.attempts = 0;
|
|
settleReload.restart();
|
|
}
|
|
}
|
|
|
|
Timer {
|
|
id: settleReload
|
|
property int attempts: 0
|
|
interval: 100
|
|
repeat: true
|
|
onTriggered: {
|
|
attempts++;
|
|
// Let the current instances finish their external writes before a
|
|
// soft reload replaces them. The cap keeps a failed external tool
|
|
// from leaving restored Home state stale indefinitely.
|
|
if ((!Keybinds.reloading && !SystemSettings.busy) || attempts >= 30) {
|
|
stop();
|
|
Quickshell.reload(false);
|
|
}
|
|
}
|
|
}
|
|
|
|
Component.onCompleted: root.refresh()
|
|
|
|
function refresh(): void {
|
|
if (!listQuery.running)
|
|
listQuery.running = true;
|
|
}
|
|
|
|
function save(): void {
|
|
if (actionRun.running)
|
|
return;
|
|
actionRun.restoring = false;
|
|
actionRun.exec([root.helperPath, "save", root.serialiseHomeState()]);
|
|
}
|
|
|
|
function serialiseHomeState(): string {
|
|
const favorites = [];
|
|
for (const favorite of HomePreferences.favorites ?? []) {
|
|
favorites.push({
|
|
id: String(favorite.id ?? ""),
|
|
alias: String(favorite.alias ?? "")
|
|
});
|
|
}
|
|
return JSON.stringify({
|
|
initialized: HomePreferences.initialized,
|
|
favorites: favorites
|
|
});
|
|
}
|
|
|
|
// Restore output carries the canonical Home state. Reconstructing through
|
|
// these methods keeps validation and persistence inside HomePreferences;
|
|
// this service never mutates its aliases or private FileView directly.
|
|
function reloadHomeState(text: string): bool {
|
|
try {
|
|
const result = JSON.parse(text);
|
|
const restored = result?.home;
|
|
if (!restored || restored.preserve === true)
|
|
return true;
|
|
|
|
if (restored.present !== true)
|
|
return restored.present === false
|
|
? root.resetHomeState()
|
|
: false;
|
|
|
|
const data = restored.data;
|
|
if (!data || typeof data.initialized !== "boolean" || !Array.isArray(data.favorites))
|
|
return false;
|
|
const ids = [];
|
|
const aliases = [];
|
|
const seen = {};
|
|
for (const favorite of data.favorites) {
|
|
const id = favorite?.id;
|
|
const alias = favorite?.alias;
|
|
if (typeof id !== "string" || !/^light\.[a-z0-9_]+$/.test(id)
|
|
|| typeof alias !== "string" || seen[id])
|
|
return false;
|
|
seen[id] = true;
|
|
ids.push(id);
|
|
aliases.push(alias);
|
|
}
|
|
if (!data.initialized && ids.length > 0)
|
|
return false;
|
|
|
|
HomePreferences.resetHomeDefaults();
|
|
if (!data.initialized)
|
|
return true;
|
|
HomePreferences.initialize(ids);
|
|
for (let index = 0; index < ids.length; index++)
|
|
HomePreferences.setAlias(ids[index], aliases[index]);
|
|
return true;
|
|
} catch (error) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function resetHomeState(): bool {
|
|
HomePreferences.resetHomeDefaults();
|
|
return true;
|
|
}
|
|
|
|
// The name is matched against the snapshot list rather than trusted, so no
|
|
// caller-supplied path reaches the helper even though it validates as well.
|
|
function restore(name: string): bool {
|
|
if (actionRun.running)
|
|
return false;
|
|
if (!root.snapshots.some(snapshot => snapshot.name === name)) {
|
|
root.lastError = "That snapshot is not in the list.";
|
|
return false;
|
|
}
|
|
actionRun.restoring = true;
|
|
actionRun.exec([root.helperPath, "restore", name]);
|
|
return true;
|
|
}
|
|
}
|