Default monitor isolation on and make it self-healing

Fresh installs previously started with isolation off until someone
noticed and flipped the switch by hand, since the toggle read raw
file-existence with no persisted intent behind it. Now a small state
file (colocated in Omarchy's toggles dir, which is guaranteed to exist,
avoiding an mkdir-p race a plugin-private directory would hit) records
the user's actual choice, defaulting to isolated-on the first time the
widget ever loads. Every poll reconciles the toggle-flag file against
that intent and against the current isolated-workspace setting, so
external drift (or a workspace-number change) gets corrected within
one poll interval instead of silently sticking.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MLfSWFEMyY85ZDWEYicdaF
This commit is contained in:
2026-09-08 16:19:00 +01:00
parent 0819c2e3ba
commit a90363eeee
3 changed files with 107 additions and 27 deletions

View File

@@ -28,8 +28,13 @@ Item {
readonly property bool active: _desired === -1 ? running : (_desired === 1)
property bool isolationFileExists: false
property int _isolationDesired: -1
readonly property bool isolated: _isolationDesired === -1 ? isolationFileExists : (_isolationDesired === 1)
// Persisted intent (see isolation-state.json below), not raw file
// existence — so a fresh install defaults to isolated without anyone
// clicking anything, and the plugin self-heals if the toggle file is
// ever removed out from under it.
property bool desiredIsolation: true
property bool _isolationStateLoaded: false
readonly property bool isolated: desiredIsolation
readonly property bool busy: startStopProc.running || installUnitProc.running
|| isolationRemoveProc.running || reloadProc.running
@@ -79,6 +84,7 @@ Item {
if (!whichProc.running) whichProc.running = true
if (!statusProc.running) statusProc.running = true
if (root.running && !clientsProc.running) clientsProc.running = true
root.reconcileIsolation()
}
Timer {
@@ -201,18 +207,64 @@ Item {
}
// ---------------------------------------------------------- isolation toggle
//
// Two files, two different jobs, both in toggleDir on purpose: it's
// Omarchy's own toggles directory, guaranteed to already exist on every
// install (default/hypr/toggles.lua ships a placeholder file there), so
// neither FileView below races an async `mkdir -p` the way a
// plugin-private directory would — that race is exactly what silently
// broke first-run bootstrap during testing.
// - .dotjuice.hypr-rdp-isolation-state.json records the user's
// *intent*: on by default so a fresh install behaves the same way as
// one where someone flipped the switch, off only once someone
// actually turns it off. The require_all loader only picks up
// `*.lua` files, so a `.json` file here is invisible to Hyprland.
// - hypr-rdp-isolation.lua is what Hyprland actually reads.
// reconcileIsolation() keeps it in sync with intent on every poll, so
// if anything external deletes or restores it, the plugin puts it
// back the way it's supposed to be within one poll interval.
readonly property string isolationStatePath: toggleDir + "/.dotjuice.hypr-rdp-isolation-state.json"
property FileView isolationStateFile: FileView {
path: root.isolationStatePath
watchChanges: true
printErrors: false
onLoaded: {
root.desiredIsolation = Model.parseIsolationState(text()).enabled
root._isolationStateLoaded = true
root.reconcileIsolation()
}
onLoadFailed: {
// Never configured before (fresh install, or the state file was
// removed): bootstrap to isolated-on, matching this plugin's
// out-of-the-box behavior, and persist that choice.
root._isolationStateLoaded = true
root.persistIsolationState(true)
}
onFileChanged: reload()
}
function persistIsolationState(enabled) {
root.desiredIsolation = enabled
root.isolationStateFile.setText(JSON.stringify({ enabled: enabled }))
root.reconcileIsolation()
}
property string isolationFileContent: ""
property FileView isolationFile: FileView {
path: root.isolationTogglePath
watchChanges: true
printErrors: false
onLoaded: {
root.isolationFileContent = text()
root.isolationFileExists = true
if (root._isolationDesired === 1) root._isolationDesired = -1
root.reconcileIsolation()
}
onLoadFailed: {
root.isolationFileContent = ""
root.isolationFileExists = false
if (root._isolationDesired === 0) root._isolationDesired = -1
}
onFileChanged: reload()
}
@@ -237,21 +289,32 @@ Item {
function applyReload() { reloadProc.running = true }
function toggleIsolation() {
if (isolationRemoveProc.running || reloadProc.running) return
if (root.isolated) {
root._isolationDesired = 0
// Brings the toggle-flag file into line with persisted intent. Called on
// every poll tick as well as after any explicit change, so drift from
// outside this plugin (a stray `omarchy refresh`, someone hand-editing
// the toggles directory, etc.) gets corrected automatically.
function reconcileIsolation() {
if (!root._isolationStateLoaded || isolationRemoveProc.running) return
if (root.desiredIsolation) {
var desiredContent = Model.isolationLuaContent(root.isolatedWorkspace, root.outputName)
if (!root.isolationFileExists || root.isolationFileContent !== desiredContent) {
root.isolationFile.setText(desiredContent)
// FileView does not re-emit onLoaded for its own write.
root.isolationFileContent = desiredContent
root.isolationFileExists = true
root.applyReload()
}
} else if (root.isolationFileExists) {
root.isolationFileExists = false
isolationRemoveProc.running = true
} else {
root._isolationDesired = 1
root.isolationFile.setText(Model.isolationLuaContent(root.isolatedWorkspace, root.outputName))
// FileView does not re-emit onLoaded for its own write.
root.isolationFileExists = true
root.applyReload()
}
}
function toggleIsolation() {
if (isolationRemoveProc.running || reloadProc.running) return
root.persistIsolationState(!root.desiredIsolation)
}
// ---------------------------------------------------------- credentials
property FileView configFile: FileView {