The panel's 4500x3000 mode, 1.5 scale, and 10-bit request were a rule for connector DP-2 outright, which handed them to whatever monitor a stranger's machine had on its most common DisplayPort connector. The rule is now matched by description, the per-output prefs loop covers every connector including DP-2, and the displays contract pins the policy to the description rather than the port.
215 lines
8.2 KiB
Lua
215 lines
8.2 KiB
Lua
-- ─────────────────────────────────────────────────────────────────────────────
|
|
-- 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 cm = "hdr" below (or override it in
|
|
-- overrides.lua) and read the notes in that file 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,
|
|
-- } }
|
|
--
|
|
-- Only mode, scale, and transform are read. Color management and bit depth
|
|
-- stay here, because those are the settings with a documented reason attached
|
|
-- (see the header) rather than preferences, and a settings page has no way to
|
|
-- explain the screencopy tradeoff at the moment you would be changing it.
|
|
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
|
|
|
|
-- 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({
|
|
output = output,
|
|
mode = entry.mode,
|
|
position = display_position(entry, "auto"),
|
|
scale = entry.scale,
|
|
transform = entry.transform,
|
|
})
|
|
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
|
|
local kuycon = display_entry("DP-2")
|
|
hl.monitor({
|
|
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,
|
|
|
|
-- 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 line first.
|
|
bitdepth = 10,
|
|
|
|
-- "auto" = sRGB at 8bpc, wide gamut at 10bpc. Not HDR; see header.
|
|
cm = "auto",
|
|
})
|
|
|
|
-- 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
|