* feat: Add Hyprland special workspaces plugin * fix: Nest translation keys under settings object
195 lines
5.7 KiB
Luau
195 lines
5.7 KiB
Luau
-- Publishes populated or active Hyprland special workspaces to the plugin state
|
|
-- channel.
|
|
-- Hyprland's event socket triggers snapshots after relevant changes.
|
|
|
|
local STATE_KEY = "special_workspaces"
|
|
local SEPARATOR = "__NOCTALIA_SPECIAL_WORKSPACES_MONITORS__"
|
|
local STREAM_REFRESH_MARKER = "__NOCTALIA_SPECIAL_WORKSPACES_STREAM_REFRESH__"
|
|
local SNAPSHOT_ATTEMPTS = 3
|
|
|
|
local loggedSnapshotError = false
|
|
local refreshInFlight = false
|
|
local refreshPending = false
|
|
|
|
local function logSnapshotError(message)
|
|
if not loggedSnapshotError then
|
|
noctalia.log("Special workspaces: " .. message .. "; retaining last state")
|
|
loggedSnapshotError = true
|
|
end
|
|
end
|
|
|
|
local function shellQuote(value)
|
|
return "'" .. string.gsub(value, "'", "'\"'\"'") .. "'"
|
|
end
|
|
|
|
local function specialName(value)
|
|
if type(value) ~= "string" then
|
|
return nil
|
|
end
|
|
|
|
local name = string.match(value, "^special:(.+)$")
|
|
if name and name ~= "" then
|
|
return name
|
|
end
|
|
return nil
|
|
end
|
|
|
|
local function snapshotFromJson(clientsJson, monitorsJson)
|
|
local clients, clientErr = noctalia.json.decode(clientsJson)
|
|
local monitors, monitorErr = noctalia.json.decode(monitorsJson)
|
|
if type(clients) ~= "table" or type(monitors) ~= "table" then
|
|
return nil, "invalid Hyprland JSON" .. (clientErr and (": " .. clientErr) or (monitorErr and (": " .. monitorErr) or ""))
|
|
end
|
|
|
|
local workspaces = {}
|
|
for _, client in ipairs(clients) do
|
|
local workspace = client.workspace
|
|
local name = workspace and specialName(workspace.name)
|
|
if name then
|
|
local item = workspaces[name]
|
|
if not item then
|
|
item = { name = name, windowCount = 0, active = false }
|
|
workspaces[name] = item
|
|
end
|
|
item.windowCount = item.windowCount + 1
|
|
end
|
|
end
|
|
|
|
for _, monitor in ipairs(monitors) do
|
|
local special = monitor.specialWorkspace
|
|
local name = special and specialName(special.name)
|
|
if name then
|
|
local item = workspaces[name]
|
|
if not item then
|
|
item = { name = name, windowCount = 0, active = false }
|
|
workspaces[name] = item
|
|
end
|
|
item.active = true
|
|
end
|
|
end
|
|
|
|
local result = {}
|
|
for _, item in pairs(workspaces) do
|
|
table.insert(result, item)
|
|
end
|
|
table.sort(result, function(a, b) return a.name < b.name end)
|
|
return result
|
|
end
|
|
|
|
local function publishSnapshot(clientsJson, monitorsJson)
|
|
local state, err = snapshotFromJson(clientsJson, monitorsJson)
|
|
if not state then
|
|
-- Keep the last valid state during temporary compositor command failures.
|
|
logSnapshotError("snapshot failed (" .. err .. ")")
|
|
return false
|
|
end
|
|
loggedSnapshotError = false
|
|
noctalia.state.set(STATE_KEY, state)
|
|
return true
|
|
end
|
|
|
|
local function refresh()
|
|
if refreshInFlight then
|
|
refreshPending = true
|
|
return
|
|
end
|
|
refreshInFlight = true
|
|
local command = "attempt=1; while [ \"$attempt\" -le " .. SNAPSHOT_ATTEMPTS
|
|
.. " ]; do clients=$(hyprctl -j clients) && monitors=$(hyprctl -j monitors)"
|
|
.. " && { printf '%s\\n" .. SEPARATOR .. "\\n%s\\n' \"$clients\" \"$monitors\"; exit 0; };"
|
|
.. " attempt=$((attempt + 1)); [ \"$attempt\" -le " .. SNAPSHOT_ATTEMPTS
|
|
.. " ] && sleep 1; done; exit 1"
|
|
local started = noctalia.runAsync(command, function(result)
|
|
if result.exitCode ~= 0 then
|
|
logSnapshotError("hyprctl failed")
|
|
else
|
|
local markerStart, markerEnd = string.find(result.stdout, "\n" .. SEPARATOR .. "\n", 1, true)
|
|
if not markerStart then
|
|
logSnapshotError("incomplete hyprctl snapshot")
|
|
else
|
|
publishSnapshot(string.sub(result.stdout, 1, markerStart - 1), string.sub(result.stdout, markerEnd + 1))
|
|
end
|
|
end
|
|
|
|
refreshInFlight = false
|
|
if refreshPending then
|
|
refreshPending = false
|
|
refresh()
|
|
end
|
|
end)
|
|
if not started then
|
|
refreshInFlight = false
|
|
logSnapshotError("could not start hyprctl snapshot")
|
|
end
|
|
end
|
|
|
|
local refreshEvents = {
|
|
activespecial = true,
|
|
activespecialv2 = true,
|
|
openwindow = true,
|
|
closewindow = true,
|
|
movewindow = true,
|
|
movewindowv2 = true,
|
|
workspace = true,
|
|
workspacev2 = true,
|
|
focusedmon = true,
|
|
focusedmonv2 = true,
|
|
createworkspace = true,
|
|
createworkspacev2 = true,
|
|
destroyworkspace = true,
|
|
destroyworkspacev2 = true,
|
|
moveworkspace = true,
|
|
moveworkspacev2 = true,
|
|
renameworkspace = true,
|
|
monitoradded = true,
|
|
monitoraddedv2 = true,
|
|
monitorremoved = true,
|
|
monitorremovedv2 = true,
|
|
}
|
|
|
|
local function startEventStream()
|
|
if not noctalia.commandExists("socat") then
|
|
noctalia.log("Special workspaces: socat is unavailable; event updates disabled")
|
|
return
|
|
end
|
|
|
|
local runtimeDir = noctalia.getenv("XDG_RUNTIME_DIR")
|
|
local signature = noctalia.getenv("HYPRLAND_INSTANCE_SIGNATURE")
|
|
if not runtimeDir or not signature then
|
|
noctalia.log("Special workspaces: Hyprland environment is unavailable; event updates disabled")
|
|
return
|
|
end
|
|
|
|
local socket = runtimeDir .. "/hypr/" .. signature .. "/.socket2.sock"
|
|
-- Keep a single helper alive across temporary socket failures. Noctalia stops
|
|
-- the complete stream command automatically when this service exits.
|
|
local command = "while true; do while [ ! -S " .. shellQuote(socket)
|
|
.. " ]; do sleep 5; done; printf '" .. STREAM_REFRESH_MARKER
|
|
.. "\\n'; socat -u UNIX-CONNECT:" .. shellQuote(socket)
|
|
.. " - 2>&1; sleep 5; done"
|
|
local started = noctalia.runStream(command, function(line)
|
|
if line == STREAM_REFRESH_MARKER then
|
|
refresh()
|
|
return
|
|
end
|
|
|
|
local event = string.match(line, "^([%a%d_]+)>>")
|
|
if event then
|
|
if refreshEvents[event] then
|
|
refresh()
|
|
end
|
|
end
|
|
end)
|
|
|
|
if not started then
|
|
noctalia.log("Special workspaces: could not start socat event stream")
|
|
end
|
|
end
|
|
|
|
refresh()
|
|
startEventStream()
|
|
|
|
function onOutputsChanged()
|
|
refresh()
|
|
end
|