* feat(hypr-screen-mirror): setup plugin * feat(hypr-screen-mirror): added socat watch on hyprland socket for monitor change * feat(hypr-screen-mirror): added hyprctl commands for starting and stopping mirroring * fix(hypr-screen-mirror): check if a mirror is active from monitor output during monitor update * doc(hypr-screen-mirror): plugin readme and image * fix(hypr-screen-mirror): issues from review * fix(hypr-screen-mirror): separate observed state from mirror actions --------- Co-authored-by: Lemmy <studio@quadbyte.net>
202 lines
6.2 KiB
Luau
202 lines
6.2 KiB
Luau
function tr(key)
|
|
return noctalia.tr(key)
|
|
end
|
|
|
|
local function shellQuote(s)
|
|
return "'" .. tostring(s):gsub("'", "'\\''") .. "'"
|
|
end
|
|
|
|
-- This could be replaced by noctalia.outputs() and noctalia.focusedOutputName() but currently the focusedOutputName() function returns nil when testing so we can't use it
|
|
local function updateMonitors()
|
|
noctalia.runAsync("hyprctl monitors all -j", function(output)
|
|
if output.timedOut or output.exitCode ~= 0 then
|
|
noctalia.notify(tr("notification.monitor_list_failed"), output.stderr or tr("notification.timed_out"))
|
|
return
|
|
end
|
|
|
|
local ok, msg = pcall(noctalia.json.decode, output.stdout)
|
|
if not ok or type(msg) ~= "table" then
|
|
noctalia.notify(tr("notification.monitor_list_failed"), msg)
|
|
return
|
|
end
|
|
|
|
local monitors = {}
|
|
local focusedMonitor = nil
|
|
for i, monitor in ipairs(msg) do
|
|
monitors[i] = monitor.name
|
|
focusedMonitor = monitor.focused and monitor.name or focusedMonitor
|
|
end
|
|
|
|
if #monitors == 0 then
|
|
noctalia.notify(tr("notification.monitor_list_failed"), tr("notification.no_monitors_found"))
|
|
return
|
|
end
|
|
|
|
-- update monitor data
|
|
noctalia.state.set("monitors", monitors)
|
|
noctalia.state.set("focusedMonitor", focusedMonitor)
|
|
|
|
-- update source and target monitor indices if they are out of bounds or not set
|
|
local sourceMonitor = tonumber(noctalia.state.get("sourceMonitor")) or 0
|
|
local targetMonitor = tonumber(noctalia.state.get("targetMonitor")) or 0
|
|
local isMirroring = false
|
|
|
|
if sourceMonitor >= #monitors then
|
|
sourceMonitor = 0
|
|
end
|
|
if targetMonitor >= #monitors then
|
|
targetMonitor = 0
|
|
end
|
|
|
|
-- check if there is an active mirror and set the source and target monitors accordingly
|
|
for _, monitor in ipairs(msg) do
|
|
if monitor.mirrorOf == "none" then
|
|
continue
|
|
end
|
|
|
|
local mirrorId = tonumber(monitor.mirrorOf)
|
|
local targetIndex = table.find(monitors, monitor.name)
|
|
if mirrorId ~= nil and targetIndex ~= nil then
|
|
for _, source in ipairs(msg) do
|
|
if tonumber(source.id) == mirrorId then
|
|
local sourceIndex = table.find(monitors, source.name)
|
|
if sourceIndex ~= nil then
|
|
sourceMonitor = sourceIndex - 1
|
|
targetMonitor = targetIndex - 1
|
|
isMirroring = true
|
|
end
|
|
break
|
|
end
|
|
end
|
|
end
|
|
|
|
if isMirroring then
|
|
break
|
|
end
|
|
end
|
|
|
|
noctalia.state.set("sourceMonitor", sourceMonitor)
|
|
noctalia.state.set("targetMonitor", targetMonitor)
|
|
noctalia.state.set("isMirroring", isMirroring)
|
|
|
|
end, 5000)
|
|
end
|
|
|
|
local function hyprlandSocketPath()
|
|
local runtimeDir = noctalia.getenv("XDG_RUNTIME_DIR")
|
|
local instanceSig = noctalia.getenv("HYPRLAND_INSTANCE_SIGNATURE")
|
|
|
|
if not runtimeDir or not instanceSig then
|
|
return nil
|
|
end
|
|
|
|
return string.format("%s/hypr/%s/.socket2.sock", runtimeDir, instanceSig)
|
|
end
|
|
|
|
local function handleHyprEvent(line)
|
|
local event, data = line:match("^([^>]+)>>(.*)$")
|
|
-- we can not use the event data itself for the monitor events because hyprland treats a mirror as a monitor remove/add so this would remove a mirrored screen from the list
|
|
if event == "monitoradded" then
|
|
updateMonitors()
|
|
elseif event == "monitorremoved" then
|
|
updateMonitors()
|
|
elseif event == "focusedmon" then
|
|
-- data is MONNAME,WORKSPACENAME
|
|
local monitorName = data:match("([^,]+)")
|
|
if not monitorName then
|
|
return
|
|
end
|
|
noctalia.state.set("focusedMonitor", monitorName)
|
|
end
|
|
end
|
|
|
|
-- Watches the Hyprland socket for monitor events and updates the state accordingly
|
|
local function watchMonitors()
|
|
local socketPath = hyprlandSocketPath()
|
|
if not socketPath then
|
|
noctalia.notify(tr("notification.monitor_watch_failed"), tr("notification.hyprland_socket_not_found"))
|
|
return
|
|
end
|
|
|
|
local cmd = "socat -U - UNIX-CONNECT:" .. shellQuote(socketPath)
|
|
noctalia.runStream(cmd, handleHyprEvent)
|
|
end
|
|
|
|
-- main
|
|
updateMonitors()
|
|
watchMonitors()
|
|
|
|
noctalia.state.watch("monitor_refresh", function()
|
|
updateMonitors()
|
|
end)
|
|
|
|
local function stopMirroring(targetIndex)
|
|
targetIndex = tonumber(targetIndex)
|
|
local monitors = noctalia.state.get("monitors") or {}
|
|
|
|
if not targetIndex then
|
|
noctalia.notify(tr("notification.stop_mirroring_failed"), tr("notification.invalid_monitor_selection"))
|
|
return
|
|
end
|
|
|
|
local targetMonitor = monitors[targetIndex + 1]
|
|
|
|
if not targetMonitor then
|
|
noctalia.notify(tr("notification.stop_mirroring_failed"), tr("notification.invalid_monitor_selection"))
|
|
return
|
|
end
|
|
|
|
local expr = string.format("hl.monitor({ output = %q, mirror = \"\" })", targetMonitor)
|
|
local cmd = "hyprctl eval " .. shellQuote(expr)
|
|
|
|
noctalia.runAsync(cmd, function(output)
|
|
if output.timedOut or output.exitCode ~= 0 then
|
|
noctalia.notify(tr("notification.stop_mirroring_failed"), output.stderr or tr("notification.timed_out"))
|
|
return
|
|
end
|
|
updateMonitors()
|
|
end, 5000)
|
|
end
|
|
|
|
local function startMirroring(sourceIndex, targetIndex)
|
|
sourceIndex = tonumber(sourceIndex)
|
|
targetIndex = tonumber(targetIndex)
|
|
local monitors = noctalia.state.get("monitors") or {}
|
|
|
|
if not sourceIndex or not targetIndex or sourceIndex == targetIndex then
|
|
noctalia.notify(tr("notification.start_mirroring_failed"), tr("notification.invalid_monitor_selection"))
|
|
return
|
|
end
|
|
|
|
local sourceMonitor = monitors[sourceIndex + 1]
|
|
local targetMonitor = monitors[targetIndex + 1]
|
|
|
|
if not sourceMonitor or not targetMonitor then
|
|
noctalia.notify(tr("notification.start_mirroring_failed"), tr("notification.invalid_monitor_selection"))
|
|
return
|
|
end
|
|
|
|
local expr = string.format("hl.monitor({ output = %q, mirror = %q })", targetMonitor, sourceMonitor)
|
|
local cmd = "hyprctl eval " .. shellQuote(expr)
|
|
|
|
noctalia.runAsync(cmd, function(output)
|
|
if output.timedOut or output.exitCode ~= 0 then
|
|
noctalia.notify(tr("notification.start_mirroring_failed"), output.stderr or tr("notification.timed_out"))
|
|
return
|
|
end
|
|
updateMonitors()
|
|
end, 5000)
|
|
end
|
|
|
|
noctalia.state.watch("mirrorRequest", function(request)
|
|
if type(request) ~= "table" then
|
|
return
|
|
end
|
|
|
|
if request.enabled == true then
|
|
startMirroring(request.sourceMonitor, request.targetMonitor)
|
|
else
|
|
stopMirroring(request.targetMonitor)
|
|
end
|
|
end)
|