feat(monitors): main external display, Samsung ultrawide support

monitors.lua picks the main external (Studio Display, else the Samsung
S34CG50 ultrawide, matched on EDID) and reloads the config when it changes.
Workspaces 1-4, the Wacom mapping, and its active-area aspect follow it.
eDP-1 uses auto-right so it sits beside whichever external is connected.

Also unbinds SUPER+RETURN / SUPER+SHIFT+RETURN (terminal and browser live
on SUPER+CTRL+ALT+J/K) and pins the Fastmail/Beeper scratch workspaces'
comments to eDP-1.
This commit is contained in:
Prad Nukala committed 2026-10-15 14:28:18 -04:00
1 parent de202fb5f8
commit 95c8d15814
4 files changed
+85 -27

No files matched your search

+10 -8
View File
@@ -7,8 +7,9 @@
-- ─── Window management ────────────────────────────────────────────────────── -- ─── Window management ──────────────────────────────────────────────────────
-- Spawn Terminal -- Terminal and browser live on SUPER + CTRL + ALT + J / K (Applications, below).
-- hl.unbind("SUPER + RETURN") -- was: toggle window transparency hl.unbind("SUPER + RETURN") -- was: launch terminal
hl.unbind("SUPER + SHIFT + RETURN") -- was: launch browser
-- Mod+Q -> close. Omarchy already binds this, so it is only made explicit. -- Mod+Q -> close. Omarchy already binds this, so it is only made explicit.
hl.unbind("SUPER + Q") hl.unbind("SUPER + Q")
@@ -141,7 +142,7 @@ o.bind("SUPER + A", "Toggle scratch workspace", "omarchy-toggle-scratch scratchp
o.bind("SUPER + CTRL + ALT + H", "Toggle Tern scratch workspace", "omarchy-toggle-scratch scratchpad") o.bind("SUPER + CTRL + ALT + H", "Toggle Tern scratch workspace", "omarchy-toggle-scratch scratchpad")
o.bind("SUPER + CTRL + ALT + L", "Toggle Claude scratch workspace", "omarchy-toggle-scratch claude") o.bind("SUPER + CTRL + ALT + L", "Toggle Claude scratch workspace", "omarchy-toggle-scratch claude")
-- Workspaces 1 to 4 (Studio Display) on Q W E R and 5 to 8 (laptop panel) on -- Workspaces 1 to 4 (main external display) on Q W E R and 5 to 8 (laptop panel) on
-- Y U I O: ALT to focus, ALT + SHIFT to send the window there. The pinning -- Y U I O: ALT to focus, ALT + SHIFT to send the window there. The pinning
-- lives in hypr/hyprland.lua, so focusing a workspace also moves to its -- lives in hypr/hyprland.lua, so focusing a workspace also moves to its
-- monitor. No Omarchy default uses ALT with a letter, so nothing is unbound. -- monitor. No Omarchy default uses ALT with a letter, so nothing is unbound.
@@ -242,9 +243,10 @@ o.bind("SUPER + CTRL + ALT + G", "Ghostty", "omarchy-cycle ghostty")
o.bind("SUPER + CTRL + ALT + O", "Obsidian", "omarchy-cycle obsidian") o.bind("SUPER + CTRL + ALT + O", "Obsidian", "omarchy-cycle obsidian")
o.bind("SUPER + CTRL + ALT + F", "File manager", "omarchy-launch-or-focus nautilus \"gtk-launch org.gnome.Nautilus\"") o.bind("SUPER + CTRL + ALT + F", "File manager", "omarchy-launch-or-focus nautilus \"gtk-launch org.gnome.Nautilus\"")
-- Fastmail, Beeper, Google Home and LM Studio each live on their own scratch -- Fastmail, Beeper, Google Home and LM Studio each live on their own scratch
-- workspace (window rules in hypr/hyprland.lua); the toggle reveals it on the -- workspace (window rules in hypr/hyprland.lua); the toggle reveals it (on
-- focused monitor and starts the app only when no window of it exists -- the -- eDP-1 for Fastmail and Beeper, else the focused monitor) and starts the app
-- web-app launcher would otherwise open a second window every time. -- only when no window of it exists -- the web-app launcher would otherwise
-- open a second window every time.
o.bind("SUPER + CTRL + ALT + E", "Fastmail", "omarchy-toggle-scratch fastmail") o.bind("SUPER + CTRL + ALT + E", "Fastmail", "omarchy-toggle-scratch fastmail")
o.bind("SUPER + CTRL + ALT + M", "Beeper", "omarchy-toggle-scratch beeper") o.bind("SUPER + CTRL + ALT + M", "Beeper", "omarchy-toggle-scratch beeper")
o.bind("SUPER + CTRL + H", "Google Home", "omarchy-toggle-scratch home") o.bind("SUPER + CTRL + H", "Google Home", "omarchy-toggle-scratch home")
@@ -306,8 +308,8 @@ hl.unbind("SUPER + SPACE") -- was: pseudo window
-- o.bind("ALT + SPACE", "Toggle Launcher", "omarchy-menu toggle launcher") -- o.bind("ALT + SPACE", "Toggle Launcher", "omarchy-menu toggle launcher")
-- Mod+Shift+Return was the vicinae window switcher. ScrollOverview on -- Mod+Shift+Return was the vicinae window switcher. ScrollOverview on
-- ALT + TAB below takes that job, so SUPER + SHIFT + RETURN keeps its default -- ALT + TAB below takes that job; SUPER + SHIFT + RETURN is unbound
-- job of opening the browser. -- (Window management, above).
-- --
-- flea --default: begin. Written by `flea --default`; `flea --default off` removes the block whole. -- flea --default: begin. Written by `flea --default`; `flea --default off` removes the block whole.
hl.unbind("SUPER + SHIFT + F") hl.unbind("SUPER + SHIFT + F")
+16 -7
View File
@@ -28,19 +28,28 @@ require("default.hypr.toggles")
-- Add any other personal Hyprland configuration below. -- Add any other personal Hyprland configuration below.
-- o.window("qemu", { workspace = "5" }) -- o.window("qemu", { workspace = "5" })
-- Pin workspaces to monitors: 1-4 on the Studio Display, 5-8 on the laptop -- Pin workspaces to monitors: 1-4 on the main external display (Studio
-- Display or Samsung ultrawide, see hypr/monitors.lua), 5-8 on the laptop
-- panel; 1 and 5 are what each shows on connect. Persistent so empty ones -- panel; 1 and 5 are what each shows on connect. Persistent so empty ones
-- stay on their monitor (the bar's workspace dots list them per monitor). -- stay on their monitor (the bar's workspace dots list them per monitor).
-- Matched on EDID description for the external display so it survives -- The main external is picked at config load, and hypr/monitors.lua reloads
-- landing on a different DP connector after re-docking. -- the config when it changes. Workspaces 1-4 that already exist are moved
-- over, since rules only place workspaces when they are created.
local monitors = require("hypr.monitors")
local external = monitors.external()
for id = 1, 8 do for id = 1, 8 do
hl.workspace_rule({ hl.workspace_rule({
workspace = tostring(id), workspace = tostring(id),
monitor = id <= 4 and "desc:Apple Computer Inc StudioDisplay 0x984262A7" or "eDP-1", monitor = id <= 4 and (external or monitors.externals[1]) or "eDP-1",
default = id == 1 or id == 5, default = id == 1 or id == 5,
persistent = true, persistent = true,
}) })
end end
if external then
for id = 1, 4 do
hl.dispatch(hl.dsp.workspace.move({ workspace = tostring(id), monitor = external }))
end
end
-- Always open these apps on a fixed workspace and follow them there. Classes -- Always open these apps on a fixed workspace and follow them there. Classes
-- come from `hyprctl clients` -- the desktop files' StartupWMClass hints -- come from `hyprctl clients` -- the desktop files' StartupWMClass hints
@@ -59,12 +68,12 @@ o.window([[^tldraw-offline$]], { workspace = "5", fullscreen = true })
-- each resident gets its own column, in the order they appear. -- each resident gets its own column, in the order they appear.
-- --
-- SUPER + A / SUPER + CTRL + S: Tern; SUPER + SHIFT + A: Claude Desktop. -- SUPER + A / SUPER + CTRL + S: Tern; SUPER + SHIFT + A: Claude Desktop.
-- Each alone on its own workspace, Studio Display. Tern has no per-window -- Each alone on its own workspace, main external display. Tern has no per-window
-- class flag, so every Tern window lands on the scratchpad. -- class flag, so every Tern window lands on the scratchpad.
o.window([[^so\.stencil\.tern$]], { workspace = "special:scratchpad silent" }) o.window([[^so\.stencil\.tern$]], { workspace = "special:scratchpad silent" })
o.window([[^com\.anthropic\.Claude$]], { workspace = "special:claude silent" }) o.window([[^com\.anthropic\.Claude$]], { workspace = "special:claude silent" })
-- SUPER + CTRL + ALT + E / M: Fastmail and Beeper, each alone on the -- SUPER + CTRL + ALT + E / M: Fastmail and Beeper, each alone on its own
-- focused monitor. -- workspace, laptop panel (eDP-1).
o.window([[^com\.fastmail\.Fastmail$]], { workspace = "special:fastmail silent" }) o.window([[^com\.fastmail\.Fastmail$]], { workspace = "special:fastmail silent" })
o.window([[^Beeper$]], { workspace = "special:beeper silent" }) o.window([[^Beeper$]], { workspace = "special:beeper silent" })
-- SUPER + CTRL + H: Google Home (Brave web app), on the focused monitor. -- SUPER + CTRL + H: Google Home (Brave web app), on the focused monitor.
+14 -9
View File
@@ -111,18 +111,23 @@ hl.device({ name = "elan9008:00-04f3:43c7", output = "eDP-1" })
-- flips arbitration to ignore-all. Cost: no tilt in drawing apps. The 90ms -- flips arbitration to ignore-all. Cost: no tilt in drawing apps. The 90ms
-- re-enable after the pen leaves proximity is hard-coded in libinput. -- re-enable after the pen leaves proximity is hard-coded in libinput.
-- Wacom Intuos BT S: pinned to the Studio Display, matched on EDID description -- Wacom Intuos BT S: pinned to the main external display (Studio Display or
-- so it survives a different DP connector after re-docking. With that display -- Samsung ultrawide, see hypr/monitors.lua, which reloads the config when it
-- absent, Hyprland maps across the whole layout, which undocked is just eDP-1. -- changes). With neither connected it keeps the Studio Display selector,
-- which matches nothing, so Hyprland maps across the whole layout
-- (undocked: just eDP-1).
-- --
-- The tablet is 152x95mm (16:10) and the display is 16:9, so the active area is -- The tablet is 152x95mm (16:10). The active area is cropped to a full-width
-- cropped to a 152x85.5mm (16:9) band to keep strokes undistorted while still -- band with the display's aspect ratio (152x85.5mm for the 16:9 Studio
-- reaching every pixel. The band starts at the top edge, leaving the bottom -- Display, 152x63.6mm for the 3440x1440 ultrawide) to keep strokes
-- 9.5mm of the tablet as dead space (the pen there parks at the screen bottom). -- undistorted while still reaching every pixel. The band starts at the top
-- edge; below it is dead space (the pen there parks at the screen bottom).
local monitors = require("hypr.monitors")
local external, monitor = monitors.external()
hl.device({ hl.device({
name = "wacom-intuos-bt-s-pen", name = "wacom-intuos-bt-s-pen",
output = "desc:Apple Computer Inc StudioDisplay 0x984262A7", output = external or monitors.externals[1],
active_area_size = { 152, 85.5 }, active_area_size = { 152, monitor and 152 * monitor.height / monitor.width or 85.5 },
active_area_position = { 0, 0 }, active_area_position = { 0, 0 },
}) })
+45 -3
View File
@@ -7,13 +7,55 @@ local omarchy_monitor_scale = 3.2
hl.env("GDK_SCALE", tostring(omarchy_gdk_scale)) hl.env("GDK_SCALE", tostring(omarchy_gdk_scale))
hl.monitor({ output = "", mode = "preferred", position = "auto", scale = omarchy_monitor_scale }) hl.monitor({ output = "", mode = "preferred", position = "auto", scale = omarchy_monitor_scale })
-- Apple Studio Display (DP-4) on the left, laptop panel (eDP-1) on the right. -- The main external display sits on the left, the laptop panel (eDP-1) to its
-- Logical widths at scale 2: 5120/2 = 2560, so eDP-1 starts at x=2560. -- right; auto-right puts eDP-1 at the external's logical width (2560 for the
hl.monitor({ output = "eDP-1", mode = "2560x1600@180", position = "2560x0", scale = 2 }) -- Studio Display at scale 2, 3440 for the ultrawide at scale 1), or at 0x0
-- when undocked. Apple Studio Display over Thunderbolt, Samsung ultrawide on
-- a USB-C to HDMI cable from the iGPU (DP alt mode, so it shows up as DP-n;
-- matched on EDID, so the connector name doesn't matter).
hl.monitor({ output = "eDP-1", mode = "2560x1600@180", position = "auto-right", scale = 2 })
hl.monitor({ output = "DP-4", mode = "5120x2880@60", position = "0x0", scale = 2 }) hl.monitor({ output = "DP-4", mode = "5120x2880@60", position = "0x0", scale = 2 })
hl.monitor({ output = "desc:Samsung Electric Company S34CG50 HNTY501103", mode = "3440x1440@100", position = "0x0", scale = 1 })
-- Configure a specific monitor. -- Configure a specific monitor.
-- hl.monitor({ output = "DP-2", mode = "2560x1440@144", position = "0x0", scale = 1 }) -- hl.monitor({ output = "DP-2", mode = "2560x1440@144", position = "0x0", scale = 1 })
-- Portrait/rotated secondary monitor (transform: 1 = 90°, 3 = 270°). -- Portrait/rotated secondary monitor (transform: 1 = 90°, 3 = 270°).
-- hl.monitor({ output = "DP-2", mode = "preferred", position = "auto", scale = 1, transform = 1 }) -- hl.monitor({ output = "DP-2", mode = "preferred", position = "auto", scale = 1, transform = 1 })
-- Main external display: the first of these that is connected. Workspaces
-- 1-4 (hypr/hyprland.lua) and the Wacom tablet (hypr/input.lua) follow it.
-- Entries are Hyprland monitor selectors: a connector name or
-- desc:<EDID description>.
local M = {
externals = {
"desc:Apple Computer Inc StudioDisplay 0x984262A7",
"desc:Samsung Electric Company S34CG50 HNTY501103",
},
}
-- The connected main external as (selector, monitor), or nil.
function M.external()
for _, selector in ipairs(M.externals) do
for _, monitor in ipairs(hl.get_monitors()) do
if selector == monitor.name or selector == "desc:" .. monitor.description then
return selector, monitor
end
end
end
end
-- Workspace rules only create persistent workspaces at config load, so a
-- change of main external reloads the config rather than patching rules in
-- place; the reload re-runs every user of M.external() against the new
-- monitor. Omarchy's reload guard skips it mid package update.
local loaded = M.external()
local function reload_on_change()
if M.external() ~= loaded then
hl.exec_cmd("omarchy-hyprland-reload-guard paused || hyprctl reload")
end
end
hl.on("monitor.added", reload_on_change)
hl.on("monitor.removed", reload_on_change)
return M