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 ──────────────────────────────────────────────────────
-- Spawn Terminal
-- hl.unbind("SUPER + RETURN") -- was: toggle window transparency
-- Terminal and browser live on SUPER + CTRL + ALT + J / K (Applications, below).
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.
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 + 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
-- 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.
@@ -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 + 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
-- workspace (window rules in hypr/hyprland.lua); the toggle reveals it on the
-- focused monitor and starts the app only when no window of it exists -- the
-- web-app launcher would otherwise open a second window every time.
-- workspace (window rules in hypr/hyprland.lua); the toggle reveals it (on
-- eDP-1 for Fastmail and Beeper, else the focused monitor) and starts the app
-- 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 + M", "Beeper", "omarchy-toggle-scratch beeper")
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")
-- Mod+Shift+Return was the vicinae window switcher. ScrollOverview on
-- ALT + TAB below takes that job, so SUPER + SHIFT + RETURN keeps its default
-- job of opening the browser.
-- ALT + TAB below takes that job; SUPER + SHIFT + RETURN is unbound
-- (Window management, above).
--
-- flea --default: begin. Written by `flea --default`; `flea --default off` removes the block whole.
hl.unbind("SUPER + SHIFT + F")
+16 -7
View File
@@ -28,19 +28,28 @@ require("default.hypr.toggles")
-- Add any other personal Hyprland configuration below.
-- 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
-- stay on their monitor (the bar's workspace dots list them per monitor).
-- Matched on EDID description for the external display so it survives
-- landing on a different DP connector after re-docking.
-- The main external is picked at config load, and hypr/monitors.lua reloads
-- 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
hl.workspace_rule({
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,
persistent = true,
})
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
-- 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.
--
-- 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.
o.window([[^so\.stencil\.tern$]], { workspace = "special:scratchpad silent" })
o.window([[^com\.anthropic\.Claude$]], { workspace = "special:claude silent" })
-- SUPER + CTRL + ALT + E / M: Fastmail and Beeper, each alone on the
-- focused monitor.
-- SUPER + CTRL + ALT + E / M: Fastmail and Beeper, each alone on its own
-- workspace, laptop panel (eDP-1).
o.window([[^com\.fastmail\.Fastmail$]], { workspace = "special:fastmail silent" })
o.window([[^Beeper$]], { workspace = "special:beeper silent" })
-- 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
-- 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
-- so it survives a different DP connector after re-docking. With that display
-- absent, Hyprland maps across the whole layout, which undocked is just eDP-1.
-- Wacom Intuos BT S: pinned to the main external display (Studio Display or
-- Samsung ultrawide, see hypr/monitors.lua, which reloads the config when it
-- 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
-- cropped to a 152x85.5mm (16:9) band to keep strokes undistorted while still
-- reaching every pixel. The band starts at the top edge, leaving the bottom
-- 9.5mm of the tablet as dead space (the pen there parks at the screen bottom).
-- The tablet is 152x95mm (16:10). The active area is cropped to a full-width
-- band with the display's aspect ratio (152x85.5mm for the 16:9 Studio
-- Display, 152x63.6mm for the 3440x1440 ultrawide) to keep strokes
-- 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({
name = "wacom-intuos-bt-s-pen",
output = "desc:Apple Computer Inc StudioDisplay 0x984262A7",
active_area_size = { 152, 85.5 },
output = external or monitors.externals[1],
active_area_size = { 152, monitor and 152 * monitor.height / monitor.width or 85.5 },
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.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.
-- Logical widths at scale 2: 5120/2 = 2560, so eDP-1 starts at x=2560.
hl.monitor({ output = "eDP-1", mode = "2560x1600@180", position = "2560x0", scale = 2 })
-- The main external display sits on the left, the laptop panel (eDP-1) to its
-- right; auto-right puts eDP-1 at the external's logical width (2560 for the
-- 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 = "desc:Samsung Electric Company S34CG50 HNTY501103", mode = "3440x1440@100", position = "0x0", scale = 1 })
-- Configure a specific monitor.
-- hl.monitor({ output = "DP-2", mode = "2560x1440@144", position = "0x0", scale = 1 })
-- Portrait/rotated secondary monitor (transform: 1 = 90°, 3 = 270°).
-- 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