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:
17
Model.js
17
Model.js
@@ -69,7 +69,7 @@ function serializeConfig(originalText, patch) {
|
|||||||
// hypr-rdp headless output's workspace so Hyprland's normal
|
// hypr-rdp headless output's workspace so Hyprland's normal
|
||||||
// auto-assignment never reuses it; deleting the file removes the pin.
|
// auto-assignment never reuses it; deleting the file removes the pin.
|
||||||
function isolationLuaContent(workspaceId, outputName) {
|
function isolationLuaContent(workspaceId, outputName) {
|
||||||
return "-- Managed by the jandieman.hypr-rdp bar widget's isolation\n" +
|
return "-- Managed by the dotjuice.hypr-rdp bar widget's isolation\n" +
|
||||||
"-- toggle. Do not hand-edit -- this file is overwritten or removed\n" +
|
"-- toggle. Do not hand-edit -- this file is overwritten or removed\n" +
|
||||||
"-- automatically when the toggle is flipped.\n" +
|
"-- automatically when the toggle is flipped.\n" +
|
||||||
"--\n" +
|
"--\n" +
|
||||||
@@ -111,6 +111,18 @@ function parseActiveWorkspaceId(raw) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Isolation intent is on unless the state file explicitly says otherwise —
|
||||||
|
// covers a fresh install (no file yet, handled by the caller) and a
|
||||||
|
// corrupt/unexpected file the same way: default to the safer "isolated" side.
|
||||||
|
function parseIsolationState(raw) {
|
||||||
|
try {
|
||||||
|
var obj = JSON.parse(String(raw || ""))
|
||||||
|
return { enabled: obj.enabled !== false }
|
||||||
|
} catch (e) {
|
||||||
|
return { enabled: true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof module !== "undefined") {
|
if (typeof module !== "undefined") {
|
||||||
module.exports = {
|
module.exports = {
|
||||||
escapeTomlString: escapeTomlString,
|
escapeTomlString: escapeTomlString,
|
||||||
@@ -119,6 +131,7 @@ if (typeof module !== "undefined") {
|
|||||||
serializeConfig: serializeConfig,
|
serializeConfig: serializeConfig,
|
||||||
isolationLuaContent: isolationLuaContent,
|
isolationLuaContent: isolationLuaContent,
|
||||||
parseClients: parseClients,
|
parseClients: parseClients,
|
||||||
parseActiveWorkspaceId: parseActiveWorkspaceId
|
parseActiveWorkspaceId: parseActiveWorkspaceId,
|
||||||
|
parseIsolationState: parseIsolationState
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
26
README.md
26
README.md
@@ -48,17 +48,21 @@ omarchy pkg drop hypr-rdp-git # or hypr-rdp
|
|||||||
- **Service**: start/stop via a `systemd --user` unit
|
- **Service**: start/stop via a `systemd --user` unit
|
||||||
(`~/.config/systemd/user/hypr-rdp.service`). Created automatically on
|
(`~/.config/systemd/user/hypr-rdp.service`). Created automatically on
|
||||||
first start if it doesn't already exist; never overwritten after that.
|
first start if it doesn't already exist; never overwritten after that.
|
||||||
- **Monitor isolation**: toggling this creates or removes
|
- **Monitor isolation**: **on by default**, no setup needed. It works by
|
||||||
`~/.local/state/omarchy/toggles/hypr/hypr-rdp-isolation.lua` (Omarchy's
|
creating `~/.local/state/omarchy/toggles/hypr/hypr-rdp-isolation.lua`
|
||||||
standard toggle-flag convention — the same one `omarchy toggle
|
(Omarchy's standard toggle-flag convention — the same one `omarchy
|
||||||
window-gaps` uses), which pins an isolated workspace (default 11) to the
|
toggle window-gaps` uses), which pins an isolated workspace (default 11)
|
||||||
`hypr-rdp` headless output via `hl.workspace_rule`, then runs `hyprctl
|
to the `hypr-rdp` headless output via `hl.workspace_rule`, then runs
|
||||||
reload` + `hyprctl configerrors`. On: Hyprland's normal workspace
|
`hyprctl reload` + `hyprctl configerrors`. On: Hyprland's normal
|
||||||
auto-assignment never reuses that workspace, so starting an RDP session
|
workspace auto-assignment never reuses that workspace, so starting an
|
||||||
can't silently steal one of your visible workspaces. Off: the headless
|
RDP session can't silently steal one of your visible workspaces. Off:
|
||||||
output behaves like a normal display. Workspace-to-monitor binding is
|
the headless output behaves like a normal display. Workspace-to-monitor
|
||||||
config-file-only in Hyprland (no live `hyprctl keyword` for it), so a
|
binding is config-file-only in Hyprland (no live `hyprctl keyword` for
|
||||||
flip takes effect the next time hypr-rdp (re)starts, not mid-session.
|
it), so a flip takes effect the next time hypr-rdp (re)starts, not
|
||||||
|
mid-session. Your choice is remembered in a small state file next to the
|
||||||
|
toggle flag, and every poll cycle re-applies it — so if the toggle file
|
||||||
|
is ever deleted or edited by something else, the widget puts it back
|
||||||
|
within a few seconds rather than silently losing the setting.
|
||||||
**If you already have your own `hl.workspace_rule` pinning a workspace to
|
**If you already have your own `hl.workspace_rule` pinning a workspace to
|
||||||
the `hypr-rdp` output in `monitors.lua` or elsewhere, remove it first** —
|
the `hypr-rdp` output in `monitors.lua` or elsewhere, remove it first** —
|
||||||
a leftover static rule will keep isolation on regardless of what the
|
a leftover static rule will keep isolation on regardless of what the
|
||||||
|
|||||||
91
Service.qml
91
Service.qml
@@ -28,8 +28,13 @@ Item {
|
|||||||
readonly property bool active: _desired === -1 ? running : (_desired === 1)
|
readonly property bool active: _desired === -1 ? running : (_desired === 1)
|
||||||
|
|
||||||
property bool isolationFileExists: false
|
property bool isolationFileExists: false
|
||||||
property int _isolationDesired: -1
|
// Persisted intent (see isolation-state.json below), not raw file
|
||||||
readonly property bool isolated: _isolationDesired === -1 ? isolationFileExists : (_isolationDesired === 1)
|
// 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
|
readonly property bool busy: startStopProc.running || installUnitProc.running
|
||||||
|| isolationRemoveProc.running || reloadProc.running
|
|| isolationRemoveProc.running || reloadProc.running
|
||||||
@@ -79,6 +84,7 @@ Item {
|
|||||||
if (!whichProc.running) whichProc.running = true
|
if (!whichProc.running) whichProc.running = true
|
||||||
if (!statusProc.running) statusProc.running = true
|
if (!statusProc.running) statusProc.running = true
|
||||||
if (root.running && !clientsProc.running) clientsProc.running = true
|
if (root.running && !clientsProc.running) clientsProc.running = true
|
||||||
|
root.reconcileIsolation()
|
||||||
}
|
}
|
||||||
|
|
||||||
Timer {
|
Timer {
|
||||||
@@ -201,18 +207,64 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------- isolation toggle
|
// ---------------------------------------------------------- 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 {
|
property FileView isolationFile: FileView {
|
||||||
path: root.isolationTogglePath
|
path: root.isolationTogglePath
|
||||||
watchChanges: true
|
watchChanges: true
|
||||||
printErrors: false
|
printErrors: false
|
||||||
onLoaded: {
|
onLoaded: {
|
||||||
|
root.isolationFileContent = text()
|
||||||
root.isolationFileExists = true
|
root.isolationFileExists = true
|
||||||
if (root._isolationDesired === 1) root._isolationDesired = -1
|
root.reconcileIsolation()
|
||||||
}
|
}
|
||||||
onLoadFailed: {
|
onLoadFailed: {
|
||||||
|
root.isolationFileContent = ""
|
||||||
root.isolationFileExists = false
|
root.isolationFileExists = false
|
||||||
if (root._isolationDesired === 0) root._isolationDesired = -1
|
|
||||||
}
|
}
|
||||||
onFileChanged: reload()
|
onFileChanged: reload()
|
||||||
}
|
}
|
||||||
@@ -237,21 +289,32 @@ Item {
|
|||||||
|
|
||||||
function applyReload() { reloadProc.running = true }
|
function applyReload() { reloadProc.running = true }
|
||||||
|
|
||||||
function toggleIsolation() {
|
// Brings the toggle-flag file into line with persisted intent. Called on
|
||||||
if (isolationRemoveProc.running || reloadProc.running) return
|
// every poll tick as well as after any explicit change, so drift from
|
||||||
if (root.isolated) {
|
// outside this plugin (a stray `omarchy refresh`, someone hand-editing
|
||||||
root._isolationDesired = 0
|
// 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
|
root.isolationFileExists = false
|
||||||
isolationRemoveProc.running = true
|
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
|
// ---------------------------------------------------------- credentials
|
||||||
|
|
||||||
property FileView configFile: FileView {
|
property FileView configFile: FileView {
|
||||||
|
|||||||
Reference in New Issue
Block a user