diff --git a/quickshell/Services/PluginService.qml b/quickshell/Services/PluginService.qml index f07a3e3d0..7b074334a 100644 --- a/quickshell/Services/PluginService.qml +++ b/quickshell/Services/PluginService.qml @@ -1108,16 +1108,11 @@ Singleton { } const path = getPluginStatePath(pluginId); try { - const fv = stateSaveFvComp.createObject(root, { + const fv = stateLoadFvComp.createObject(root, { path: path }); _stateWriters[pluginId] = fv; - fv.loaded.connect(function () { - fv.setText(content); - }); - fv.loadFailed.connect(function () { - fv.setText(content); - }); + fv.setText(content); } catch (e) { log.warn("Failed to write state for", pluginId, e.message); } @@ -1132,14 +1127,6 @@ Singleton { } } - Component { - id: stateSaveFvComp - FileView { - blockWrites: true - atomicWrites: true - } - } - function _flushDirtyStates() { const dirty = _stateDirtyPlugins; _stateDirtyPlugins = {}; diff --git a/quickshell/tests/plugin-state.test.mjs b/quickshell/tests/plugin-state.test.mjs new file mode 100644 index 000000000..1a8822d91 --- /dev/null +++ b/quickshell/tests/plugin-state.test.mjs @@ -0,0 +1,58 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import vm from "node:vm"; +import test from "node:test"; + +const source = readFileSync(new URL("../Services/PluginService.qml", import.meta.url), "utf8"); + +// Mirrors Quickshell's FileView, where `loaded` is a bool property and not a connectable signal. +function fileView(disk, blockLoading) { + return { + createObject(_, props) { + return { + path: props.path, + loaded: !!blockLoading, + text() { return disk.get(this.path) ?? ""; }, + setText(content) { disk.set(this.path, content); }, + destroy() {} + }; + } + }; +} + +function service() { + const disk = new Map(); + const context = vm.createContext({ + JSON, Object, + _stateCache: {}, _stateLoaded: {}, _stateWriters: {}, _stateDirtyPlugins: {}, _stateDirCreated: true, + _stateWriteTimer: { restart() {} }, + stateLoadFvComp: fileView(disk, true), + stateSaveFvComp: fileView(disk, false), + Paths: { state: "/state", strip: path => path, mkdir() {} }, + log: { warn() {} }, + pluginStateChanged() {} + }); + context.root = context; + for (const match of source.matchAll(/^ function (\w+)\([^\n]*\)(?:: \w+)? \{\n[\s\S]*?^ \}/gm)) + vm.runInContext(match[0], context); + return { context, disk }; +} + +test("the first state save after a plugin reload reaches disk", () => { + const { context, disk } = service(); + const path = context.getPluginStatePath("fixture"); + for (const existing of [false, true]) { + disk.clear(); + context._stateCache = {}; + context._stateLoaded = {}; + context._stateWriters = {}; + if (existing) + disk.set(path, JSON.stringify({ count: 0 })); + context.savePluginState("fixture", "count", 1); + context._flushDirtyStates(); + context._cleanupPluginStateWriter("fixture"); + context.savePluginState("fixture", "count", 2); + context._flushDirtyStates(); + assert.equal(JSON.parse(disk.get(path)).count, 2); + } +});