multi-monitor setup, pinned workspaces, weather widget

- Switch from special workspaces to named pinned workspaces (pin-*)
  - Add dual-monitor layout with portrait secondary display
  - Add weather widget (wttr.in) with location management
  - Add workspace drag-and-drop improvements and debug overlay
  - Add SwayNC notification center config
  - Add mouse-capture-reset utility script
  - Fix DPMS resume by re-applying monitor settings after wake
This commit is contained in:
David Aizenberg
2026-02-27 02:10:48 +01:00
parent fafbf6626b
commit 270b2edb61
11 changed files with 1595 additions and 149 deletions

View File

@@ -45,10 +45,10 @@
# Device-specific configuration
{{- if eq $deviceProfile "desktop" }}
primaryMonitor = "DP-2"
secondaryMonitor = ""
primaryResolution = "2560x1440@144"
secondaryResolution = ""
hasMultipleMonitors = false
secondaryMonitor = "DP-1"
primaryResolution = "3440x1440@100"
secondaryResolution = "2560x1440@60"
hasMultipleMonitors = true
hasTouchpad = false
hasBattery = false
idleTimeout = 300

View File

@@ -0,0 +1,16 @@
#!/usr/bin/env bash
set -euo pipefail
# Force Hyprland to re-evaluate pointer focus/capture state.
# "soft" avoids workspace churn; "hard" does a quick workspace flip-flop too.
mode="${1:-soft}"
hyprctl keyword input:follow_mouse 0 >/dev/null 2>&1 || true
sleep 0.04
hyprctl keyword input:follow_mouse 1 >/dev/null 2>&1 || true
if [ "$mode" = "hard" ]; then
hyprctl dispatch workspace previous >/dev/null 2>&1 || true
sleep 0.06
hyprctl dispatch workspace previous >/dev/null 2>&1 || true
fi

View File

@@ -24,13 +24,13 @@ exec-once = copyq --start-server
# User apps
exec-once = zen-browser
exec-once = ~/.local/bin/launch-on-workspace 2 chromium-work chromium --profile-directory=Default --class=chromium-work
exec-once = ~/.local/bin/launch-on-workspace "special:llm" chromium-llm chromium --user-data-dir=$HOME/.config/chromium-llm --class=chromium-llm
exec-once = ~/.local/bin/launch-on-workspace "special:llm" Heynote heynote
exec-once = ~/.local/bin/launch-on-workspace "special:tg" org.telegram.desktop Telegram
exec-once = ~/.local/bin/launch-on-workspace "special:tg" Slack slack
exec-once = ~/.local/bin/launch-on-workspace "special:tg" org.mozilla.Thunderbird thunderbird
exec-once = ~/.local/bin/launch-on-workspace "special:media" spotify spotify
exec-once = ~/.local/bin/launch-on-workspace "special:org" ticktick ticktick
exec-once = ~/.local/bin/launch-on-workspace "name:pin-llm" chromium-llm chromium --user-data-dir=$HOME/.config/chromium-llm --class=chromium-llm
exec-once = ~/.local/bin/launch-on-workspace "name:pin-llm" Heynote heynote
exec-once = ~/.local/bin/launch-on-workspace "name:pin-tg" org.telegram.desktop Telegram
exec-once = ~/.local/bin/launch-on-workspace "name:pin-tg" Slack slack
exec-once = ~/.local/bin/launch-on-workspace "name:pin-tg" org.mozilla.Thunderbird thunderbird
exec-once = ~/.local/bin/launch-on-workspace "name:pin-media" spotify spotify
exec-once = ~/.local/bin/launch-on-workspace "name:pin-org" ticktick ticktick
{{- if eq .deviceProfile "desktop" }}
# Desktop: Hyprland plugins

View File

@@ -5,13 +5,13 @@
general {
lock_cmd = hyprctl switchxkblayout all 0; pidof hyprlock || hyprlock
before_sleep_cmd = loginctl lock-session
after_sleep_cmd = hyprctl dispatch dpms on
after_sleep_cmd = hyprctl dispatch dpms on; sleep 1; hyprctl keyword monitor {{ .primaryMonitor }},{{ .primaryResolution }},0x0,1
}
listener {
timeout = {{ .idleTimeout }}
on-timeout = hyprctl dispatch dpms off
on-resume = hyprctl dispatch dpms on
on-resume = hyprctl dispatch dpms on; sleep 1; hyprctl keyword monitor {{ .primaryMonitor }},{{ .primaryResolution }},0x0,1
}
{{- if .hasBattery }}

View File

@@ -225,6 +225,7 @@ bind = $mainMod, t, togglegroup
# VPN switcher
bind = , F6, exec, ~/.local/bin/vpn-switcher
bind = $mainMod CTRL, Escape, exec, ~/.local/bin/mouse-capture-reset hard
# Move focus with mainMod + arrow keys
bind = $mainMod, left, movefocus, l
@@ -243,22 +244,22 @@ bind = $mainMod, 7, workspace, 7
bind = $mainMod, 8, workspace, 8
bind = $mainMod, 9, workspace, 9
# Special workspaces
# Pinned utility workspaces
bind = SUPER, F12, exec, ~/.local/bin/workspace-pin 1337
bind = SUPER, A, togglespecialworkspace, org
bind = SUPER SHIFT, A, movetoworkspace, special:org
bind = SUPER, A, workspace, name:pin-org
bind = SUPER SHIFT, A, movetoworkspace, name:pin-org
# Quick Memo (QuickShell)
bind = , F12, exec, touch /tmp/qs-memo-input
bind = SHIFT, F12, exec, touch /tmp/qs-memo-clip
bind = SUPER, X, togglespecialworkspace, media
bind = SUPER SHIFT, X, movetoworkspacesilent, special:media
bind = SUPER, C, togglespecialworkspace, tg
bind = SUPER SHIFT, C, movetoworkspacesilent, special:tg
bind = $mainMod, S, togglespecialworkspace, termius
bind = $mainMod SHIFT, S, movetoworkspacesilent, special:termius
bind = $mainMod, G, togglespecialworkspace, llm
bind = $mainMod SHIFT, G, movetoworkspacesilent, special:llm
bind = SUPER, X, workspace, name:pin-media
bind = SUPER SHIFT, X, movetoworkspacesilent, name:pin-media
bind = SUPER, C, workspace, name:pin-tg
bind = SUPER SHIFT, C, movetoworkspacesilent, name:pin-tg
bind = $mainMod, S, workspace, name:pin-termius
bind = $mainMod SHIFT, S, movetoworkspacesilent, name:pin-termius
bind = $mainMod, G, workspace, name:pin-llm
bind = $mainMod SHIFT, G, movetoworkspacesilent, name:pin-llm
# Move active window to workspace
bind = $mainMod SHIFT, 1, movetoworkspacesilent, 1
@@ -279,8 +280,8 @@ bind = , Print, exec, ~/.local/bin/screenshot
bind = $mainMod, mouse_down, workspace, e+1
bind = $mainMod, mouse_up, workspace, e-1
# Toggle llm workspace
bind = , mouse:279, exec, ~/.local/bin/hypr-debounce 125 hyprctl dispatch togglespecialworkspace llm
# Jump to llm pinned workspace
bind = , mouse:279, exec, ~/.local/bin/hypr-debounce 125 hyprctl dispatch workspace name:pin-llm
bind = , mouse:278, hyprexpo:expo, toggle
bind = , mouse:277, exec, ~/.local/bin/hypr-debounce 125 hyprctl dispatch workspace previous
@@ -310,17 +311,17 @@ bindl = , XF86AudioPrev, exec, playerctl previous
windowrule = match:class ^zen$, workspace 1
windowrule = match:class ^chromium-work$, workspace 2
windowrule = match:initial_class ^chromium-work$, workspace 2
windowrule = match:class ^chromium-llm$, workspace special:llm
windowrule = match:initial_class ^chromium-llm$, workspace special:llm
windowrule = match:class ^(heynote|Heynote)$, workspace special:llm
windowrule = match:class ^(org.telegram.desktop)$, workspace special:tg
windowrule = match:class ^(discord)$, workspace special:tg
windowrule = match:class ^Slack$, workspace special:tg
windowrule = match:class ^org.mozilla.Thunderbird$, workspace special:tg
windowrule = match:class ^spotify$, workspace special:media
windowrule = match:initial_class ^spotify$, workspace special:media
windowrule = match:class ^ticktick$, workspace special:org
windowrule = match:initial_class ^ticktick$, workspace special:org
windowrule = match:class ^chromium-llm$, workspace name:pin-llm
windowrule = match:initial_class ^chromium-llm$, workspace name:pin-llm
windowrule = match:class ^(heynote|Heynote)$, workspace name:pin-llm
windowrule = match:class ^(org.telegram.desktop)$, workspace name:pin-tg
windowrule = match:class ^(discord)$, workspace name:pin-tg
windowrule = match:class ^Slack$, workspace name:pin-tg
windowrule = match:class ^org.mozilla.Thunderbird$, workspace name:pin-tg
windowrule = match:class ^spotify$, workspace name:pin-media
windowrule = match:initial_class ^spotify$, workspace name:pin-media
windowrule = match:class ^ticktick$, workspace name:pin-org
windowrule = match:initial_class ^ticktick$, workspace name:pin-org
windowrule = match:class .*, suppress_event maximize
windowrule = match:class ^$, match:title ^$, match:xwayland true, match:float true, match:fullscreen false, match:pin false, no_focus on
@@ -330,7 +331,7 @@ windowrule = match:class ^$, match:title ^$, match:xwayland true, match:float tr
plugin {
hyprexpo {
columns = 4
columns = 3
gap_size = 0
bg_col = rgb(0f0f0f)
workspace_method = first 1

View File

@@ -3,15 +3,20 @@
{{- if eq .deviceProfile "desktop" }}
# Desktop dual-monitor setup
# Primary: {{ .primaryMonitor }} at {{ .primaryResolution }}
monitor = {{ .primaryMonitor }},{{ .primaryResolution }},0x0,1
{{- if .secondaryMonitor }}
# Secondary: {{ .secondaryMonitor }} at {{ .secondaryResolution }}
# Positioned to the right of primary
monitor = {{ .secondaryMonitor }},{{ .secondaryResolution }},2560x0,1
# Secondary (portrait, left): {{ .secondaryMonitor }} at {{ .secondaryResolution }}, rotated 90°
# Bottom-aligned with primary, Dell 5% lower (128px overhang)
monitor = {{ .secondaryMonitor }},{{ .secondaryResolution }},0x-992,1,transform,1
# Pin LLM and Chat workspaces to the portrait monitor
workspace = name:pin-llm, monitor:{{ .secondaryMonitor }}
workspace = name:pin-tg, monitor:{{ .secondaryMonitor }}
{{- end }}
# Primary (main): {{ .primaryMonitor }} at {{ .primaryResolution }}
# Positioned after the portrait monitor (1440px wide after rotation)
monitor = {{ .primaryMonitor }},{{ .primaryResolution }},1440x0,1
{{- else }}
# Laptop single monitor
monitor = {{ .primaryMonitor }},{{ .primaryResolution }},auto,1

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,46 @@
{
"$schema": "/etc/xdg/swaync/configSchema.json",
"positionX": "right",
"positionY": "top",
"layer": "overlay",
"control-center-layer": "top",
"layer-shell": true,
"cssPriority": "user",
"notification-2fa-action": true,
"notification-inline-replies": false,
"notification-body-image-height": 100,
"notification-body-image-width": 200,
"timeout": 10,
"timeout-low": 5,
"timeout-critical": 0,
"fit-to-screen": true,
"relative-timestamps": true,
"control-center-width": 420,
"control-center-height": 600,
"notification-window-width": 400,
"keyboard-shortcuts": true,
"notification-grouping": true,
"image-visibility": "when-available",
"transition-time": 150,
"hide-on-clear": false,
"hide-on-action": true,
"text-empty": "No Notifications",
"widgets": [
"title",
"dnd",
"notifications"
],
"widget-config": {
"notifications": {
"vexpand": true
},
"title": {
"text": "Notifications",
"clear-all-button": true,
"button-text": "Clear"
},
"dnd": {
"text": "Do Not Disturb"
}
}
}

View File

@@ -0,0 +1,386 @@
:root {
--bg: #0f0f0f;
--bg-secondary: #1a1a1a;
--bg-tertiary: #242424;
--fg: #e0e0e0;
--fg-secondary: #b0b0b0;
--fg-dim: #888888;
--accent: #e67e22;
--ok: #2ecc71;
--warn: #f1c40f;
--crit: #e74c3c;
--border-color: #333333;
--border-subtle: #2a2a2a;
--font: "Terminus", "IBM Plex Mono", "JetBrainsMono Nerd Font", monospace;
--notification-icon-size: 48px;
--notification-app-icon-size: 16px;
--notification-group-icon-size: 24px;
}
* {
font-family: var(--font);
font-size: 12px;
}
notificationwindow,
blankwindow {
background: transparent;
}
/* --- Notification popups --- */
.notification-row {
background: none;
outline: none;
margin: 0;
padding: 0;
}
.notification-row:focus {
background: none;
}
.notification-row .notification-background {
padding: 4px 8px;
}
.notification-row .notification-background .notification {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 0;
padding: 0;
box-shadow: none;
}
.notification-row .notification-background .notification.critical {
border-color: var(--crit);
}
.notification-row .notification-background .notification .notification-default-action {
padding: 8px;
margin: 0;
box-shadow: none;
background: transparent;
border: none;
border-radius: 0;
color: var(--fg);
}
.notification-row .notification-background .notification .notification-default-action:hover {
background: var(--bg-tertiary);
}
.notification-row .notification-background .notification .notification-default-action:not(:only-child) {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
.notification-row .notification-background .notification .notification-default-action .notification-content {
background: transparent;
border-radius: 0;
padding: 0;
}
.notification-row .notification-background .notification .notification-default-action .notification-content .image {
-gtk-icon-filter: none;
-gtk-icon-size: var(--notification-icon-size);
border-radius: 0;
margin: 4px 8px 4px 0;
}
.notification-row .notification-background .notification .notification-default-action .notification-content .app-icon {
-gtk-icon-filter: none;
-gtk-icon-size: var(--notification-app-icon-size);
margin: 4px;
}
.notification-row .notification-background .notification .notification-default-action .notification-content .text-box label {
filter: none;
}
.notification-row .notification-background .notification .notification-default-action .notification-content .text-box .summary {
font-size: 12px;
font-weight: bold;
background: transparent;
color: var(--fg);
text-shadow: none;
}
.notification-row .notification-background .notification .notification-default-action .notification-content .text-box .time {
font-size: 11px;
font-weight: normal;
background: transparent;
color: var(--fg-dim);
text-shadow: none;
margin-right: 24px;
}
.notification-row .notification-background .notification .notification-default-action .notification-content .text-box .body {
font-size: 12px;
font-weight: normal;
background: transparent;
color: var(--fg-secondary);
text-shadow: none;
}
.notification-row .notification-background .notification .notification-default-action .notification-content progressbar {
margin-top: 4px;
}
.notification-row .notification-background .notification .notification-default-action .notification-content progressbar trough {
background: var(--bg);
border-radius: 0;
min-height: 4px;
}
.notification-row .notification-background .notification .notification-default-action .notification-content progressbar progress {
background: var(--accent);
border-radius: 0;
min-height: 4px;
}
.notification-row .notification-background .notification .notification-default-action .notification-content .body-image {
margin-top: 4px;
background-color: var(--bg);
border-radius: 0;
-gtk-icon-filter: none;
}
.notification-row .notification-background .notification .notification-default-action .notification-content .inline-reply {
margin-top: 4px;
}
.notification-row .notification-background .notification .notification-default-action .notification-content .inline-reply .inline-reply-entry {
background: var(--bg);
color: var(--fg);
caret-color: var(--fg);
border: 1px solid var(--border-color);
border-radius: 0;
}
.notification-row .notification-background .notification .notification-default-action .notification-content .inline-reply .inline-reply-button {
margin-left: 4px;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 0;
color: var(--fg);
}
.notification-row .notification-background .notification .notification-default-action .notification-content .inline-reply .inline-reply-button:hover {
background: var(--bg-tertiary);
}
.notification-row .notification-background .notification .notification-default-action .notification-content .inline-reply .inline-reply-button:disabled {
background: var(--bg);
color: var(--fg-dim);
border-color: var(--border-subtle);
}
/* Action buttons */
.notification-row .notification-background .notification .notification-alt-actions {
background: none;
border-radius: 0;
padding: 0 8px 8px 8px;
}
.notification-row .notification-background .notification .notification-action {
margin: 0 4px 0 0;
padding: 0;
}
.notification-row .notification-background .notification .notification-action:last-child {
margin-right: 0;
}
.notification-row .notification-background .notification .notification-action > button {
background: var(--bg);
border: 1px solid var(--border-color);
border-radius: 0;
color: var(--fg);
padding: 4px 8px;
}
.notification-row .notification-background .notification .notification-action > button:hover {
background: var(--bg-tertiary);
}
/* Close button */
.close-button {
background: var(--bg-tertiary);
color: var(--fg-dim);
text-shadow: none;
padding: 0;
border-radius: 0;
margin-top: 6px;
margin-right: 6px;
box-shadow: none;
border: 1px solid var(--border-color);
min-width: 20px;
min-height: 20px;
}
.close-button:hover {
box-shadow: none;
background: var(--accent);
color: var(--bg);
border-color: var(--accent);
}
/* --- Notification groups --- */
.notification-group {
transition: opacity 150ms ease-in-out;
}
.notification-group:focus {
background: none;
}
.notification-group .notification-group-close-button .close-button {
margin: 8px 16px;
}
.notification-group .notification-group-buttons,
.notification-group .notification-group-headers {
margin: 0 12px;
color: var(--fg);
}
.notification-group .notification-group-headers .notification-group-icon {
color: var(--fg-dim);
-gtk-icon-size: var(--notification-group-icon-size);
}
.notification-group .notification-group-headers .notification-group-header {
color: var(--fg);
}
.notification-group.collapsed.not-expanded {
opacity: 0.4;
}
.notification-group.collapsed .notification-row .notification {
background-color: var(--bg-secondary);
}
.notification-group.collapsed .notification-row:not(:last-child) .notification-action,
.notification-group.collapsed .notification-row:not(:last-child) .notification-default-action {
opacity: 0;
}
.notification-group.collapsed:hover .notification-row:not(:only-child) .notification {
background-color: var(--bg-tertiary);
}
/* --- Control Center --- */
.control-center {
background: var(--bg);
color: var(--fg);
border: 1px solid var(--border-color);
border-radius: 0;
margin: 0;
padding: 0;
}
.control-center .control-center-list-placeholder {
opacity: 0.5;
color: var(--fg-dim);
}
.control-center .control-center-list {
background: transparent;
}
.control-center .control-center-list .notification {
box-shadow: none;
}
.control-center .control-center-list .notification .notification-default-action:hover,
.control-center .control-center-list .notification .notification-action:hover {
background-color: var(--bg-tertiary);
}
.floating-notifications {
background: transparent;
}
.floating-notifications .notification {
box-shadow: none;
}
.blank-window {
background: transparent;
}
/* --- Widgets --- */
.widget {
margin: 4px 8px;
padding: 8px;
border-radius: 0;
}
/* Title */
.widget-title {
border-bottom: 1px solid var(--border-color);
margin-bottom: 0;
padding-bottom: 8px;
}
.widget-title > label {
margin-right: 8px;
font-size: 12px;
font-weight: bold;
color: var(--fg);
}
.widget-title > button {
margin-left: 8px;
border-radius: 0;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
color: var(--fg);
padding: 4px 8px;
}
.widget-title > button:hover {
background: var(--bg-tertiary);
}
/* DND */
.widget-dnd {
border-bottom: 1px solid var(--border-subtle);
padding-bottom: 8px;
}
.widget-dnd label {
color: var(--fg-secondary);
margin-right: 8px;
font-size: 11px;
}
.widget-dnd switch {
border-radius: 0;
background: var(--bg);
border: 1px solid var(--border-color);
}
.widget-dnd switch:checked {
background: var(--accent);
}
.widget-dnd switch slider {
border-radius: 0;
background: var(--fg);
min-width: 16px;
min-height: 16px;
}
/* Label */
.widget-label > label {
font-size: 11px;
color: var(--fg-secondary);
}

View File

@@ -27,12 +27,12 @@
"hyprland/workspaces": {
"disable-scroll": true,
"warp-on-scroll": false,
"show-special": true,
"show-special": false,
"sort-by": "id",
"format": "{name}",
"on-scroll-up": "hyprctl dispatch workspace e+1",
"on-scroll-down": "hyprctl dispatch workspace e-1",
"ignore-workspaces": ["(special:)?chrome-sharing-indicator"],
"ignore-workspaces": ["(.*:)?chrome-sharing-indicator"],
"format-icons": {
"1": "WEB",
"2": "CODE",
@@ -44,9 +44,11 @@
"8": "GAME",
"9": "MISC",
"10": "TMP",
"special:termius": "s:term",
"special:org": "s:org",
"special:llm": "s:llm",
"pin-termius": "TERMIUS",
"pin-org": "ORG",
"pin-llm": "LLM",
"pin-tg": "CHAT",
"pin-media": "MUSIC",
"default": "WS"
}
},

View File

@@ -33,6 +33,14 @@ qs-reload:
hypr-reload:
hyprctl reload
fix-res:
#!/usr/bin/env bash
set -euo pipefail
mon=$(chezmoi execute-template '{{ "{{" }} .primaryMonitor {{ "}}" }}')
res=$(chezmoi execute-template '{{ "{{" }} .primaryResolution {{ "}}" }}')
echo "Applying $res to $mon"
hyprctl keyword monitor "$mon,$res,0x0,1"
# Combined helpers
apply-niri: apply niri-validate niri-reload