130 lines
3.9 KiB
Lua
130 lines
3.9 KiB
Lua
-- 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()
|