diff --git a/bindings.lua b/bindings.lua index 0228cc3..fa499dd 100644 --- a/bindings.lua +++ b/bindings.lua @@ -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") diff --git a/hyprland.lua b/hyprland.lua index 8051648..a748f42 100644 --- a/hyprland.lua +++ b/hyprland.lua @@ -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. diff --git a/input.lua b/input.lua index e7a17dc..10a83a2 100644 --- a/input.lua +++ b/input.lua @@ -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 }, }) diff --git a/monitors.lua b/monitors.lua index cbbf512..708f602 100644 --- a/monitors.lua +++ b/monitors.lua @@ -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:. +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