Files
dotfiles/user/dot_config/hypr/hyprland-monitors.lua
T
2026-07-30 05:00:35 +02:00

268 lines
7.8 KiB
Lua

-- Monitor configuration and automatic and manual switching--
------------------
---- PROFILES ----
------------------
-- Profiles:
-- Key of the profile is the name of the profile
-- Profiles are ordered by priority, higher number takes precedence over smaller number
-- First (by priority) profile that matches will be applied, all others will be ignored
--
-- A profile matches the current setup if _ALL_ of its match criteria are fulfilled
-- If there are no match criteria, it will match always
local PROFILES = {
-- The most basic fallback if nothing else ever works
fallback = {
priority = -999,
matches = {},
monitors = {
{
output = "",
mode = "preferred",
position = "auto",
scale = 1,
disabled = false,
},
},
},
laptop = {
priority = 0,
matches = {{ output = "eDP-1" }},
monitors = {
{
output = "eDP-1",
mode = "1920x1200@120",
position = "0x0",
scale = 1,
disabled = false,
},
},
},
home_docked = {
priority = 10,
matches = {
{ output = "desc:GIGA-BYTE TECHNOLOGY CO. LTD. M34WQ 0x00000B52" },
{ output = "desc:Dell Inc. DELL U2414H 292K475V26JL" },
{ output = "desc:Dell Inc. DELL U2724D DL7R834" },
},
monitors = {
{
output = "eDP-1",
mode = "preferred",
position = "0x0",
scale = 1,
disabled = true,
},
{
output = "desc:GIGA-BYTE TECHNOLOGY CO. LTD. M34WQ 0x00000B52",
mode = "3440x1440@60",
position = "0x0",
scale = 1,
disabled = false,
},
{
output = "desc:Dell Inc. DELL U2414H 292K475V26JL",
mode = "1920x1080@60",
position = "760x-1080",
scale = 1,
disabled = false,
},
{
output = "desc:Dell Inc. DELL U2724D DL7R834",
mode = "2560x1440@60",
position = "3440x0",
scale = 1,
disabled = false,
},
},
postprocess = function()
debug_notify("now applying workspace rules")
for i = 1,5 do
hl.workspace_rule({ workspace = string.format("%d", i), monitor = "desc:GIGA-BYTE TECHNOLOGY CO. LTD. M34WQ 0x00000B52"})
end
hl.workspace_rule({ workspace = "1", default = true, persistent = true})
for i = 6,9 do
hl.workspace_rule({ workspace = string.format("%d", i), monitor = "desc:Dell Inc. DELL U2724D DL7R834"})
end
hl.workspace_rule({ workspace = "9", default = true, persistent = true})
hl.workspace_rule({ workspace = "10", monitor = "desc:Dell Inc. DELL U2414H 292K475V26JL", default = true, persistent = true})
hl.dispatch(hl.dsp.focus({workspace = 10}))
hl.dispatch(hl.dsp.focus({workspace = 9}))
hl.dispatch(hl.dsp.focus({workspace = 1}))
end
},
}
local function refresh_monitors()
hl.exec_cmd("hyprctl monitors all -j | jq -r '.[] | [.name, .description] | @tsv' > /tmp/hypr_monitors.tsv")
end
local function get_monitors()
-- FIXME: Use hl.get_monitors({all=true}) as soon as it is released
local handle = io.open("/tmp/hypr_monitors.tsv", "r")
if handle ~= nil then
local out = handle:read("*a")
handle:close()
local result = {}
for line in out:gmatch("[^\n]+") do
local name, desc = line:match("([^\t]+)\t(.+)")
table.insert(result, { name = name, description = desc })
end
return result
end
return {}
end
local function get_matching_profile()
local monitors = get_monitors()
local profiles = {} or {}
local monitor_debug = map(monitors, function(monitor)
return monitor.name .. ": "..monitor.description
end)
debug_notify("Currently " .. #monitors .. " connected: " .. table.concat(monitor_debug, "; "))
for profile_name, profile in pairs(PROFILES) do
profile.name = profile_name or "unknown"
table.insert(profiles, profile)
end
table.sort(profiles, function(a, b) return (a.priority or 0) > (b.priority or 0) end)
for _, profile in ipairs(profiles) do
local matches = profile.matches or {}
local has_matched = {}
for _, match in ipairs(matches) do
if starts_with(match.output, "desc:") then
table.insert(has_matched, any(monitors, function(m) return "desc:" .. m.description == match.output end))
else
table.insert(has_matched, any(monitors, function(m) return m.name == match.output end))
end
end
if #has_matched == 0 then
debug_notify("Monitor profile " .. profile.name .. " is active because it does not require any matches (fallback)")
return profile
elseif all(has_matched, function(v) return v end) then
return profile
else
local failed = map(has_matched, function(v, i)
if not v then
return matches[i].output
else
return ""
end
end)
local failedmsg = table.concat(failed, " ")
debug_notify("Monitor profile " .. profile.name .. " does not match, could not match " .. failedmsg)
end
end
return nil
end
local PROFILE_CHANGE_ONGOING = false
local PROFILE_CHANGE_RESET_TIMER = nil
local PROFILE_CHANGE_START_TIMER = nil
local POSTPROCESS_TIMER = nil
local function apply_profile(profile)
if PROFILE_CHANGE_ONGOING then
debug_notify("Monitor profile change is inprogress (debounce)")
return
end
if profile == nil then
debug_notify("No monitor profile available")
return
end
debug_notify("Applying monitor profile " .. profile.name)
PROFILE_CHANGE_ONGOING = true
-- delay application of the profile for some time, as other monitor change events might still be arriving
PROFILE_CHANGE_START_TIMER = hl.timer(function()
-- after we have started applying our profile, we will generate monitor change events that we do not
-- want to process. Assume 5 seconds of non-interesting monitor events that we do not want to process
-- afterwards, reset the PROFILE_CHANGE_ONGOING flag for the next time we actually want to change
-- monitor profiles
PROFILE_CHANGE_RESET_TIMER = hl.timer(function()
PROFILE_CHANGE_ONGOING = false
debug_notify("Monitor Profile Change Debounce Finished")
PROFILE_CHANGE_RESET_TIMER = nil
end, { timeout = 5000, type = "oneshot" })
local monitors = profile.monitors
for _, monitor in ipairs(monitors) do
hl.monitor(monitor)
end
debug_notify("Monitor Profile " .. profile.name .. " successfully applied")
-- start any post processing
if profile.postprocess ~= nil then
POSTPROCESS_TIMER = hl.timer(function()
if profile.postprocess ~= nil then
profile.postprocess()
end
PROFILE_CHANGE_START_TIMER = nil
POSTPROCESS_TIMER = nil
end, { timeout = 200, type = "oneshot" })
else
PROFILE_CHANGE_START_TIMER = nil
end
end, { timeout = 500, type = "oneshot" })
end
local function apply_current_profile()
apply_profile(get_matching_profile())
end
local function force_refresh_profile()
if PROFILE_CHANGE_RESET_TIMER ~= nil then PROFILE_CHANGE_RESET_TIMER:set_enabled(false) end
if POSTPROCESS_TIMER ~= nil then POSTPROCESS_TIMER:set_enabled(false) end
if PROFILE_CHANGE_START_TIMER ~= nil then PROFILE_CHANGE_START_TIMER:set_enabled(false) end
PROFILE_CHANGE_RESET_TIMER = nil
POSTPROCESS_TIMER = nil
PROFILE_CHANGE_START_TIMER = nil
PROFILE_CHANGE_ONGOING = false
debug_notify("Force applying monitor profile")
refresh_monitors()
apply_current_profile()
end
local function on_lid_close()
debug_notify("Lid was closed")
refresh_monitors()
hl.monitor({ output = "eDP-1", disabled = true })
end
local function on_lid_open()
debug_notify("Lid was opened")
refresh_monitors()
apply_current_profile()
end
local function on_monitor_added(_event)
debug_notify("A monitor was added")
refresh_monitors()
apply_current_profile()
end
local function on_monitor_removed(_event)
debug_notify("A monitor was removed")
refresh_monitors()
apply_current_profile()
end
hl.bind("switch:on:Lid Switch", on_lid_close, { locked = true })
hl.bind("switch:off:Lid Switch", on_lid_open, { locked = true })
hl.bind("SUPER + P", function() force_refresh_profile() end, { locked = true, description = "Force Refresh Monitor Profile" })
hl.on("monitor.added", on_monitor_added)
hl.on("monitor.removed", on_monitor_removed)
hl.on("hyprland.start", function() refresh_monitors() end)