-- Laptop lid handling. -- -- One rule, applied from several triggers: -- -- panel off <=> lid is shut AND something else is plugged in -- -- Both halves are re-checked every time, so the session can never end up with -- zero outputs. Undocking with the lid shut brings the panel back; booting -- alone after a docked shutdown just works, because nothing about the previous -- session is remembered. -- -- That last part is the fix for the old design: scripts/lid_handler.sh wrote a -- lid_state.conf that got sourced on the next start, so a laptop that was shut -- while docked came up believing it still had an external display and blanked -- its own panel. Lid state now comes from the kernel instead, and the external -- check is a live one. local monitors = require("lua/monitors") -- ------------------------------------------------------------- reading state -- The kernel's own view of the hinge. Present on this machine as -- /proc/acpi/button/lid/LID/state; the device name differs per vendor, hence -- the glob. Returns nil if the file is missing or unparseable. local function acpi_lid_closed() local pipe = io.popen("cat /proc/acpi/button/lid/*/state 2>/dev/null") if not pipe then return nil end local out = pipe:read("a") or "" pipe:close() if out:find("closed", 1, true) then return true elseif out:find("open", 1, true) then return false end return nil end -- Whatever the last switch event said, used only when ACPI has nothing for us. local last_switch_closed = nil local function lid_closed() local acpi = acpi_lid_closed() if acpi ~= nil then return acpi end return last_switch_closed == true end -- ---------------------------------------------------------------- reconciling -- nil until the first decision, so the first pass always applies. local panel_off = nil -- `ignore` is the output being unplugged: during monitor.removed it can still -- be listed, and counting it would keep the panel switched off. local function reconcile(ignore) -- Nothing enumerated yet means we are still parsing the config on a cold -- start. Leave the defaults from lua/monitors.lua alone and wait for the -- start event. local all = hl.get_monitors() if #all == 0 then return end local externals = 0 for _, mon in ipairs(monitors.externals_present()) do if mon.name ~= ignore then externals = externals + 1 end end local want_off = lid_closed() and externals > 0 if want_off == panel_off then return end -- First decision of the session, and it agrees with what lua/monitors.lua -- already applied: nothing actually moved, so no daemon needs poking. local settled = panel_off == nil and not want_off panel_off = want_off if want_off then monitors.disable_builtin() else monitors.enable_builtin() end monitors.apply_external() -- Quickshell follows monitor hotplug by itself; only the wallpaper daemon -- needs a kick. if not settled then hl.exec_cmd("systemctl restart --user hyprpaper") end end -- ------------------------------------------------------------------ triggers hl.bind("switch:on:Lid Switch", function() last_switch_closed = true reconcile() end, { locked = true }) hl.bind("switch:off:Lid Switch", function() last_switch_closed = false reconcile() end, { locked = true }) -- Docking and undocking. Removal passes the output that is going away. hl.on("monitor.added", function() reconcile() end) hl.on("monitor.removed", function(mon) reconcile(mon and mon.name) end) -- Cold start: outputs are not up yet at this point, so this settles once they -- are. On `hyprctl reload` the start event does not fire again, but the -- monitor list is already populated, so run it inline too. hl.on("hyprland.start", function() reconcile() end) reconcile()