-- ───────────────────────────────────────────────────────────────────────────── -- Monitors -- -- Kuycon P20 (matched by description): 4500x3000 @ 60Hz, 1.5x fractional scale. -- 4500/1.5 = 3000 and 3000/1.5 = 2000, both integers, so this is a "clean" -- fractional scale and Hyprland will not complain. -- -- On HDR -------------------------------------------------------------------- -- GNOME runs this panel in bt2100/HDR. Hyprland CAN do that (cm = "hdr"), but -- on 0.56.x it currently breaks screencopy: grim screenshots come back empty, -- and OBS/Sunshine capture and hyprlock's blurred background go with them. -- Root cause is still open upstream. Since screen capture matters more here -- than an HDR desktop, the desktop runs SDR at 10-bit and HDR is handed to -- fullscreen games only, via render.cm_auto_hdr in looks.lua. -- -- To try full-time HDR anyway, set shipped_cm = "hdr" below (or pick HDR for -- this display in Settings, which saves it as the display's colorProfile and -- wins over the shipped value) and read the notes in overrides.lua first. -- ───────────────────────────────────────────────────────────────────────────── local prefs = require("prefs") -- Per-output overrides written by Panama Settings, keyed by output name: -- { ["DP-2"] = { -- mode = "3840x2160@60", scale = 2, transform = 0, -- x = 0, y = 0, primary = true, -- vrrMode = -1, colorProfile = "auto", bitdepth = 10, -- sdrBrightness = 1, sdrSaturation = 1, mirrorOf = "", -- } } -- -- The fields past `primary` are optional: records written before they existed -- carry none of them, and the shipped values below stand instead. That is the -- relationship in both directions -- what is written here is what a saved -- record inherits, and what Settings saves is what overrides it, so the two -- stop fighting over the same monitor rule. -- -- Geometry and colour fail differently on purpose. A half-written position is -- refused outright (below), because guessing one can strand an output where -- nothing can reach it. An unreadable colour, VRR or mirror value is dropped -- on its own and the shipped default stands: the worst it costs is a wrong -- shade, and taking the whole record down with it would cost the arrangement. local displays = prefs.get("displays", {}) if type(displays) ~= "table" then displays = {} end local function mode_dimensions(mode) if type(mode) ~= "string" then return nil, nil end local width, height, refresh = mode:match("^(%d+)x(%d+)@(%d+%.%d+)$") if width == nil then width, height, refresh = mode:match("^(%d+)x(%d+)@(%d+)$") end width, height, refresh = tonumber(width), tonumber(height), tonumber(refresh) if width == nil or height == nil or refresh == nil or width <= 0 or height <= 0 or refresh <= 0 then return nil, nil end return width, height end local function valid_mode(mode) local width = mode_dimensions(mode) return width ~= nil end local function valid_scale(mode, scale) local width, height = mode_dimensions(mode) if width == nil or type(scale) ~= "number" or scale ~= scale or scale <= 0 or scale > 4 then return false end local logical_width = width / scale local logical_height = height / scale return math.abs(logical_width - math.floor(logical_width + 0.5)) < 0.0001 and math.abs(logical_height - math.floor(logical_height + 0.5)) < 0.0001 end local function valid_transform(transform) return type(transform) == "number" and transform == math.floor(transform) and transform >= 0 and transform <= 3 end local function valid_coordinate(value) return type(value) == "number" and value == value and value == math.floor(value) and value >= -100000 and value <= 100000 end local function valid_position(entry) return valid_coordinate(entry.x) and valid_coordinate(entry.y) end local function valid_primary(entry) return type(entry.primary) == "boolean" end local function has_layout_fields(entry) return entry.x ~= nil or entry.y ~= nil or entry.primary ~= nil end local function display_entry(output) if type(output) ~= "string" or output == "" or output:match("^[%w_.-]+$") == nil then return nil end local entry = displays[output] if type(entry) ~= "table" then return nil end if not valid_mode(entry.mode) or not valid_scale(entry.mode, entry.scale) or not valid_transform(entry.transform) then return nil end -- Legacy records have none of the layout fields and keep automatic -- placement. A partially written extended record is unsafe: accepting its -- mode but guessing its position could overlap or strand another output. if has_layout_fields(entry) and (not valid_position(entry) or not valid_primary(entry)) then return nil end return entry end local function display_position(entry, fallback) if entry ~= nil and has_layout_fields(entry) then return string.format("%dx%d", entry.x, entry.y) end return fallback end local color_profiles = { auto = true, srgb = true, wide = true, hdr = true } local function color_profile(entry, fallback) local value = entry ~= nil and entry.colorProfile or nil if type(value) == "string" and color_profiles[value] then return value end return fallback end local function bitdepth_value(entry, fallback) local value = entry ~= nil and entry.bitdepth or nil if value == 8 or value == 10 then return value end return fallback end -- -1 means the display follows the global misc.vrr policy, which is said by -- leaving the key out. 3 is the global policy's own value and not a per-display -- choice, so it is not accepted here either. local function vrr_value(entry) local value = entry ~= nil and entry.vrrMode or nil if type(value) ~= "number" or value ~= math.floor(value) or value < 0 or value > 2 then return nil end return value end -- Neutral is 1.0, and the neutral value is left out rather than written: a rule -- that names it pins the display to it, which is not the same as leaving the -- trim alone. local function sdr_value(value, minimum, maximum) if type(value) ~= "number" or value ~= value or value < minimum or value > maximum or math.abs(value - 1) < 0.001 then return nil end return value end -- A mirror needs a target that is not itself and not another mirror -- Hyprland -- has no chain to follow -- and the primary may not mirror at all, since the -- arrangement is anchored on it. local function mirror_value(entry, output) local value = entry ~= nil and entry.mirrorOf or nil if type(value) ~= "string" or value == "" or value == output or value:match("^[%w_.-]+$") == nil or entry.primary == true then return nil end local target = displays[value] if type(target) == "table" and type(target.mirrorOf) == "string" and target.mirrorOf ~= "" then return nil end return value end -- Colour, VRR and mirroring layered onto a rule that already carries -- mode/position/scale/transform. A monitor rule replaces the previous rule for -- that output whole, so the shipped defaults are passed in here rather than -- written in a rule of their own. `connector` is the output name the record was -- saved under, which is not always the rule's own output: the Kuycon rule -- matches by description. local function with_display_fields(rule, entry, connector, default_bitdepth, default_cm) rule.bitdepth = bitdepth_value(entry, default_bitdepth) rule.cm = color_profile(entry, default_cm) rule.vrr = vrr_value(entry) rule.sdrbrightness = entry ~= nil and sdr_value(entry.sdrBrightness, 0.8, 2.0) or nil rule.sdrsaturation = entry ~= nil and sdr_value(entry.sdrSaturation, 0.8, 1.2) or nil -- A mirror shows its target's picture in its target's place, so the saved -- position is not the compositor's to honour or ours to ask for. local mirror = mirror_value(entry, connector) if mirror ~= nil then rule.mirror = mirror rule.position = "auto" end return rule end -- Every connected output uses the same validated per-output store. Automatic -- placement and the compositor's normal color policy unless the entry says -- otherwise. for output, _ in pairs(displays) do local entry = display_entry(output) if entry ~= nil then hl.monitor(with_display_fields({ output = output, mode = entry.mode, position = display_position(entry, "auto"), scale = entry.scale, transform = entry.transform, }, entry, output, nil, nil)) end end -- The Kuycon P20, matched by what it is rather than where it is plugged in. -- This used to be a rule for connector DP-2 outright, which handed the panel's -- 4500x3000 mode and 1.5 scale to whatever monitor a stranger's machine had on -- its most common DisplayPort connector. Emitted after the prefs loop so a -- saved entry for its connector still carries the mode/scale/position, while -- this rule holds the shipped defaults and the panel-specific color policy. local shipped_mode = "4500x3000@60" local shipped_scale = 1.5 local shipped_transform = 0 -- 10-bit output. 4500x3000@60 at 10bpc is ~24 Gbps, right at the edge of DP 1.4 -- HBR3, so this relies on DSC. If the display fails to light up or falls back to -- a lower mode, drop this to 8 first. local shipped_bitdepth = 10 -- "auto" = sRGB at 8bpc, wide gamut at 10bpc. Not HDR; see header. local shipped_cm = "auto" local kuycon = display_entry("DP-2") hl.monitor(with_display_fields({ output = "desc:GVT Kuycon P20", mode = kuycon and kuycon.mode or shipped_mode, position = display_position(kuycon, "0x0"), scale = kuycon and kuycon.scale or shipped_scale, transform = kuycon and kuycon.transform or shipped_transform, }, kuycon, "DP-2", shipped_bitdepth, shipped_cm)) -- Any monitor not named above: sane defaults rather than nothing. hl.monitor({ output = "", mode = "preferred", position = "auto", scale = "auto", }) -- ── Workspaces on the primary display only ────────────────────────────────── -- -- GNOME offered one workspace choice worth reproducing: whether the other -- screens join in. Off, every monitor has its own workspaces and switching -- affects whichever one has focus -- Hyprland's own behaviour, so it needs no -- rules at all. On, workspaces 1-10 are pinned to the primary display and a -- second screen keeps a workspace of its own that stays put. -- -- Ten because that is how many the keybinds reach: ALT+1 through ALT+0 in -- keybinds.lua. Binding more would pin workspaces nothing can navigate to, and -- binding fewer would leave the last few behaving differently from the rest for -- no reason a person could see. -- -- The rules are emitted here rather than written live because Hyprland reads -- them at config time and offers no way to remove one afterwards: writing an -- empty monitor leaves the previous binding in place. So the config is the only -- honest source, and applying a change is a reload. if prefs.get("workspacesOnPrimaryOnly", false) == true then local primary = nil for output, entry in pairs(displays) do if type(entry) == "table" and entry.primary == true and type(output) == "string" and output:match("^[%w_.-]+$") ~= nil then primary = output break end end -- Without a primary there is nothing to pin to, and guessing one would move -- every workspace onto whichever screen happened to sort first. if primary ~= nil then for i = 1, 10 do hl.workspace_rule({ workspace = tostring(i), monitor = primary }) end end end return true