8 Commits

60 changed files with 2083 additions and 2752 deletions

View File

@@ -274,9 +274,6 @@ Required behavior:
- `Super+p` opens the application launcher.
- `Super+Shift+p` opens the run menu.
- `Super+Shift+Return` opens a terminal.
- `Super+q` reloads the window manager config.
- `Super+Shift+c` closes the focused window.
- `Super+Shift+q` exits the window manager session.
- `Super+Tab` opens the overview.
- `Super+Shift+Tab` opens the overview in bring-window mode when supported.
- `Super+g` opens the go-to-window picker.

View File

@@ -3,5 +3,3 @@
!AGENTS.md
!config.toml
!skills
# Generated/local Codex state, including config.local.toml, stays ignored.

View File

@@ -2,8 +2,147 @@ model = "gpt-5.5"
model_reasoning_effort = "high"
personality = "pragmatic"
# Portable Codex defaults. Machine-local additions are appended from
# dotfiles/codex/config.local.toml by Home Manager.
notify = ["/Users/kat/dotfiles/dotfiles/codex/plugins/cache/openai-bundled/computer-use/1.0.755/Codex Computer Use.app/Contents/SharedSupport/SkyComputerUseClient.app/Contents/MacOS/SkyComputerUseClient", "turn-ended"]
[projects."/home/imalison/Projects/nixpkgs"]
trust_level = "trusted"
[projects."/home/imalison/dotfiles"]
trust_level = "trusted"
[projects."/home/imalison/Projects/railbird"]
trust_level = "trusted"
[projects."/home/imalison/Projects/subtr-actor"]
trust_level = "trusted"
[projects."/home/imalison/Projects/google-messages-api"]
trust_level = "trusted"
[projects."/home/imalison"]
trust_level = "trusted"
[projects."/home/imalison/Projects/scrobble-scrubber"]
trust_level = "trusted"
[projects."/home/imalison/temp"]
trust_level = "trusted"
[projects."/home/imalison/Projects/org-agenda-api"]
trust_level = "untrusted"
[projects."/home/imalison/org"]
trust_level = "trusted"
[projects."/home/imalison/dotfiles/.git/modules/dotfiles/config/taffybar"]
trust_level = "trusted"
[projects."/home/imalison/Projects/notifications-tray-icon"]
trust_level = "trusted"
[projects."/home/imalison/Projects/hyprland"]
trust_level = "trusted"
[projects."/home/imalison/Projects/git-sync-rs"]
trust_level = "trusted"
[projects."/home/imalison/Projects/keepbook"]
trust_level = "trusted"
[projects."/home/imalison/Projects/boxcars"]
trust_level = "trusted"
[projects."/home/imalison/Projects/rumno"]
trust_level = "trusted"
[projects."/home/imalison/Projects/git-blame-rank"]
trust_level = "trusted"
[projects."/home/imalison/Projects/hatchet"]
trust_level = "trusted"
[projects."/home/imalison/dotfiles/dotfiles/emacs.d/elpaca/sources/org-project-capture"]
trust_level = "trusted"
[projects."/home/imalison/dotfiles/dotfiles/config/taffybar/taffybar/packages"]
trust_level = "trusted"
[projects."/home/imalison/Projects/scrobble-tools"]
trust_level = "trusted"
[projects."/home/imalison/.password-store"]
trust_level = "trusted"
[projects."/home/imalison/Projects/subtr-actor-mechanics"]
trust_level = "trusted"
[projects."/home/imalison/Projects/lastfm-edit"]
trust_level = "trusted"
[projects."/home/imalison/Projects/mova"]
trust_level = "trusted"
[projects."/home/imalison/dotfiles/dotfiles/config/taffybar/taffybar"]
trust_level = "trusted"
[projects."/home/imalison/Projects"]
trust_level = "trusted"
[projects."/home/imalison/Projects/rofi-systemd"]
trust_level = "trusted"
[projects."/home/imalison/Projects/map-quiz"]
trust_level = "trusted"
[projects."/run/media/imalison/NETDEBUGUSB"]
trust_level = "trusted"
[projects."/home/imalison/Projects/coqui-tts-streamer"]
trust_level = "trusted"
[projects."/home/imalison/Downloads"]
trust_level = "trusted"
[projects."/home/imalison/keysmith_generated"]
trust_level = "trusted"
[projects."/run/media/imalison/NIXOS_SD"]
trust_level = "trusted"
[projects."/Users/kat/dotfiles"]
trust_level = "trusted"
[projects."/Users/kat"]
trust_level = "trusted"
[projects."/Users/kat/org"]
trust_level = "trusted"
[projects."/Users/kat/Documents/Codex/2026-04-25/do-you-see-the-sandisk-external"]
trust_level = "trusted"
[projects."/Volumes/Extreme SSD/Projects/keepbook"]
trust_level = "trusted"
[projects."/Users/kat/Documents/Codex/2026-04-25/it-seems-like-maybe-we-dont"]
trust_level = "trusted"
[projects."/Users/kat/Documents/Codex/2026-04-25/what-is-the-state-of-tiling"]
trust_level = "trusted"
[projects."/home/imalison/Pictures/ai/2026/celeb"]
trust_level = "trusted"
[projects."/home/imalison/.local/share/keepbook"]
trust_level = "trusted"
[notice]
hide_gpt5_1_migration_prompt = true
"hide_gpt-5.1-codex-max_migration_prompt" = true
[notice.model_migrations]
"gpt-5.2" = "gpt-5.2-codex"
[mcp_servers.chrome-devtools]
command = "npx"
@@ -21,6 +160,16 @@ unified_exec = true
apps = true
steer = true
[marketplaces.openai-bundled]
last_updated = "2026-04-21T17:43:57Z"
source_type = "local"
source = "/Users/kat/.codex/.tmp/bundled-marketplaces/openai-bundled"
[marketplaces.openai-primary-runtime]
last_updated = "2026-04-25T23:49:36Z"
source_type = "local"
source = "/Users/kat/.cache/codex-runtimes/codex-primary-runtime/plugins/openai-primary-runtime"
[plugins."google-calendar@openai-curated"]
enabled = true
@@ -47,3 +196,6 @@ enabled = true
[plugins."browser-use@openai-bundled"]
enabled = true
[tui.model_availability_nux]
"gpt-5.5" = 4

View File

@@ -1,10 +1,11 @@
general {
lock_cmd = pidof hyprlock || hyprlock
before_sleep_cmd = loginctl lock-session
after_sleep_cmd = hyprctl dispatch dpms on
}
listener {
timeout = 300
on-timeout = /home/imalison/dotfiles/dotfiles/lib/bin/hypr-screensaver start
on-resume = /home/imalison/dotfiles/dotfiles/lib/bin/hypr-screensaver stop
timeout = 900
on-timeout = hypr-screensaver stop && hyprctl dispatch dpms off
on-resume = hyprctl dispatch dpms on
}

View File

@@ -19,15 +19,15 @@ monitor=,preferred,auto,1
# =============================================================================
$terminal = ghostty --gtk-single-instance=false
$fileManager = dolphin
$shellUi = hypr_shell_ui
$menu = $shellUi launcher
$runMenu = $shellUi run
$menu = rofi -show drun -show-icons
$runMenu = rofi -show run
# =============================================================================
# ENVIRONMENT VARIABLES
# =============================================================================
env = XCURSOR_SIZE,24
env = QT_QPA_PLATFORMTHEME,qt6ct
env = QT_QPA_PLATFORMTHEME,qt5ct
# Used by ~/.config/hypr/scripts/* to keep workspace IDs bounded.
env = HYPR_MAX_WORKSPACE,9
# =============================================================================
@@ -246,8 +246,6 @@ $hyper = SUPER CTRL ALT
# -----------------------------------------------------------------------------
bind = $mainMod, P, exec, $menu
bind = $mainMod SHIFT, P, exec, $runMenu
bind = $hyper SHIFT, N, exec, $shellUi control-center
bind = $hyper CTRL, N, exec, $shellUi settings
bind = $mainMod SHIFT, Return, exec, $terminal
# -----------------------------------------------------------------------------
@@ -255,15 +253,15 @@ bind = $mainMod SHIFT, Return, exec, $terminal
# -----------------------------------------------------------------------------
bind = $mainMod, TAB, hyprexpo:expo, toggle
bind = $mainMod SHIFT, TAB, hyprexpo:expo, bring
bind = $mainMod, Q, exec, hyprctl reload
bind = $mainMod, Q, killactive,
bind = $mainMod SHIFT, C, killactive,
bind = $mainMod SHIFT, Q, exit,
# Emacs-everywhere (like XMonad's emacs-everywhere)
bind = $mainMod, E, exec, emacsclient --eval '(emacs-everywhere)'
bind = $mainMod, V, exec, wl-paste | xdotool type --file -
# Chrome/Browser
bind = $modAlt, C, exec, google-chrome-stable
# Chrome/Browser (raise or spawn like XMonad's bindBringAndRaise)
bind = $modAlt, C, exec, ~/.config/hypr/scripts/raise-or-run.sh google-chrome google-chrome-stable
# -----------------------------------------------------------------------------
# SCRATCHPADS (managed by hyprscratch daemon with auto-dismiss)
@@ -293,11 +291,11 @@ bind = $mainMod, S, hy3:movefocus, d
bind = $mainMod, A, hy3:movefocus, l
bind = $mainMod, D, hy3:movefocus, r
# Move windows (Mod + Shift + WASD)
bind = $mainMod SHIFT, W, hy3:movewindow, u
bind = $mainMod SHIFT, S, hy3:movewindow, d
bind = $mainMod SHIFT, A, hy3:movewindow, l
bind = $mainMod SHIFT, D, hy3:movewindow, r
# Move windows (Mod + Shift + WASD) - hy3:movewindow with once=true for swapping
bind = $mainMod SHIFT, W, exec, ~/.config/hypr/scripts/movewindow-follow-cursor.sh u once
bind = $mainMod SHIFT, S, exec, ~/.config/hypr/scripts/movewindow-follow-cursor.sh d once
bind = $mainMod SHIFT, A, exec, ~/.config/hypr/scripts/movewindow-follow-cursor.sh l once
bind = $mainMod SHIFT, D, exec, ~/.config/hypr/scripts/movewindow-follow-cursor.sh r once
# Resize windows (Mod + Ctrl + WASD)
binde = $mainMod CTRL, W, resizeactive, 0 -50
@@ -317,6 +315,13 @@ bind = $hyper SHIFT, S, movewindow, mon:d
bind = $hyper SHIFT, A, movewindow, mon:l
bind = $hyper SHIFT, D, movewindow, mon:r
# Shift to empty workspace on screen direction (Super + Ctrl + Shift + WASD)
# Like XMonad's shiftToEmptyOnScreen
bind = $mainMod CTRL SHIFT, W, exec, ~/.config/hypr/scripts/shift-to-empty-on-screen.sh u
bind = $mainMod CTRL SHIFT, S, exec, ~/.config/hypr/scripts/shift-to-empty-on-screen.sh d
bind = $mainMod CTRL SHIFT, A, exec, ~/.config/hypr/scripts/shift-to-empty-on-screen.sh l
bind = $mainMod CTRL SHIFT, D, exec, ~/.config/hypr/scripts/shift-to-empty-on-screen.sh r
# -----------------------------------------------------------------------------
# LAYOUT CONTROL (XMonad-like with hy3)
# -----------------------------------------------------------------------------
@@ -368,6 +373,31 @@ bind = $mainMod, N, hy3:killactive
# hy3:setswallow - set a window to swallow newly spawned windows
bind = $mainMod CTRL, M, hy3:setswallow, toggle
# Minimize/unminimize (via special workspace)
bind = $mainMod, M, exec, ~/.config/hypr/scripts/minimize-active.sh minimized
bind = $mainMod SHIFT, M, exec, ~/.config/hypr/scripts/unminimize-last.sh minimized
# Minimized "picker" mode:
# Open the minimized special workspace, focus a window, press Enter to restore it.
bind = $modAlt, Return, exec, ~/.config/hypr/scripts/minimized-mode.sh minimized
submap = minimized
bind = , Return, exec, ~/.config/hypr/scripts/unminimize-last.sh minimized; hyprctl dispatch submap reset
bind = , Escape, exec, ~/.config/hypr/scripts/minimized-cancel.sh minimized
bind = $modAlt, Return, exec, ~/.config/hypr/scripts/minimized-cancel.sh minimized
# Optional: basic focus navigation inside the picker.
bind = , H, movefocus, l
bind = , J, movefocus, d
bind = , K, movefocus, u
bind = , L, movefocus, r
bind = , left, movefocus, l
bind = , down, movefocus, d
bind = , up, movefocus, u
bind = , right, movefocus, r
submap = reset
# -----------------------------------------------------------------------------
# WORKSPACE CONTROL
# -----------------------------------------------------------------------------
@@ -418,17 +448,37 @@ bind = $mainMod CTRL, 9, focusworkspaceoncurrentmonitor, 9
# built-in per-monitor workspace history.
bind = $mainMod, backslash, workspace, previous_per_monitor
# Swap current workspace with another (like XMonad's swapWithCurrent)
bind = $hyper, 5, exec, ~/.config/hypr/scripts/swap-workspaces.sh
# Go to next empty workspace (like XMonad's moveTo Next emptyWS)
bind = $hyper, E, exec, ~/.config/hypr/scripts/workspace-goto-empty.sh
# Move to next screen (like XMonad's shiftToNextScreenX)
bind = $mainMod, Z, focusmonitor, +1
bind = $mainMod SHIFT, Z, movewindow, mon:+1
# Shift to empty workspace and view (like XMonad's shiftToEmptyAndView)
bind = $mainMod SHIFT, H, exec, ~/.config/hypr/scripts/workspace-move-to-empty.sh
# -----------------------------------------------------------------------------
# WINDOW MANAGEMENT
# -----------------------------------------------------------------------------
bind = $mainMod, G, exec, $shellUi window go
bind = $mainMod, B, exec, $shellUi window bring
bind = $mainMod SHIFT, B, exec, $shellUi window replace
# Go to window (rofi window switcher with icons)
bind = $mainMod, G, exec, ~/.config/hypr/scripts/go-to-window.sh
# Bring window (move to current workspace)
bind = $mainMod, B, exec, ~/.config/hypr/scripts/bring-window.sh
# Replace window (swap focused with selected - like XMonad's myReplaceWindow)
bind = $mainMod SHIFT, B, exec, ~/.config/hypr/scripts/replace-window.sh
# Gather windows of same class (like XMonad's gatherThisClass)
bind = $hyper, G, exec, ~/.config/hypr/scripts/gather-class.sh
# Focus next window of different class (like XMonad's focusNextClass)
bind = $mainMod, apostrophe, exec, ~/.config/hypr/scripts/focus-next-class.sh
# -----------------------------------------------------------------------------
# MEDIA KEYS
@@ -491,8 +541,8 @@ bindm = $mainMod, mouse:272, movewindow
bindm = $mainMod, mouse:273, resizewindow
# Scroll through workspaces
bind = $mainMod, mouse_down, workspace, e+1
bind = $mainMod, mouse_up, workspace, e-1
bind = $mainMod, mouse_down, exec, ~/.config/hypr/scripts/workspace-scroll.sh +1
bind = $mainMod, mouse_up, exec, ~/.config/hypr/scripts/workspace-scroll.sh -1
# =============================================================================
# AUTOSTART
@@ -502,7 +552,7 @@ bind = $mainMod, mouse_up, workspace, e-1
# `graphical-session.target` pulls in most tray/SNI applets (which in turn pull in `tray.target`).
# Keep the systemd user manager in sync with the current Hyprland session before
# starting any session-bound units. Separate `exec-once` commands race.
exec-once = sh -lc 'export IMALISON_SESSION_TYPE=wayland; dbus-update-activation-environment --systemd XDG_RUNTIME_DIR WAYLAND_DISPLAY DISPLAY XAUTHORITY HYPRLAND_INSTANCE_SIGNATURE XDG_CURRENT_DESKTOP XDG_SESSION_TYPE IMALISON_SESSION_TYPE; systemctl --user start graphical-session.target hyprland-session.target'
exec-once = sh -lc 'export IMALISON_SESSION_TYPE=wayland; dbus-update-activation-environment --systemd WAYLAND_DISPLAY DISPLAY XAUTHORITY HYPRLAND_INSTANCE_SIGNATURE XDG_CURRENT_DESKTOP XDG_SESSION_TYPE IMALISON_SESSION_TYPE; systemctl --user start graphical-session.target hyprland-session.target'
# Force a fresh daemon after compositor restarts so hyprscratch doesn't keep a stale socket.
exec-once = systemctl --user restart hyprscratch.service
exec-once = hypridle

View File

@@ -3,23 +3,17 @@ local mod_alt = "SUPER + ALT"
local hyper = "SUPER + CTRL + ALT"
local terminal = "ghostty --gtk-single-instance=false"
local shell_ui_command = "hypr_shell_ui"
local launcher_command = shell_ui_command .. " launcher"
local run_menu = shell_ui_command .. " run"
local menu = "rofi -show drun -show-icons"
local run_menu = "rofi -show run"
local max_workspace = 9
local scratchpad_top_margin = 60
local columns_layout = "nStack"
local monocle_layout = "monocle"
local minimized_workspace = "special:minimized"
local tabbed_group_staging_workspace = "special:tabbed-monocle-staging"
local current_layout = columns_layout
local enable_nstack = true
local enable_hyprexpo = true
local configure_nstack_plugin_from_lua = false
local workspace_layouts = {}
local minimized_windows = {}
local tabbed_workspace_groups = {}
local window_picker_mode = nil
local window_picker_candidates = {}
local stack_update_timer = nil
@@ -90,13 +84,6 @@ local function exec(command)
return hl.dsp.exec_cmd(command)
end
local function window_selector(window)
if not window or not window.address then
return nil
end
return "address:" .. tostring(window.address)
end
local function hyprexpo(action)
return function()
if hl.plugin and hl.plugin.hyprexpo and hl.plugin.hyprexpo.expo then
@@ -114,7 +101,7 @@ local function hyprexpo(action)
end
local function apply_nstack_config()
if verify_config or not enable_nstack or not configure_nstack_plugin_from_lua then
if verify_config then
return
end
@@ -139,7 +126,7 @@ local function apply_nstack_config()
end
local function apply_hyprexpo_config()
if verify_config or not enable_hyprexpo then
if verify_config then
return
end
@@ -274,26 +261,6 @@ local function tiled_window_count(workspace)
return #tiled_windows(workspace)
end
local function sort_windows_by_focus_history(windows)
table.sort(windows, function(left, right)
return (left.focus_history_id or 0) < (right.focus_history_id or 0)
end)
end
local function window_address_set(windows)
local addresses = {}
for _, window in ipairs(windows) do
if window and window.address then
addresses[window.address] = true
end
end
return addresses
end
local function window_address_in_set(window, addresses)
return window and window.address and addresses[window.address] or false
end
local function workspace_window_count(workspace_id)
local workspace = hl.get_workspace(tostring(workspace_id))
if not workspace then
@@ -327,7 +294,7 @@ local function find_empty_workspace(target_monitor, exclude_id)
end
local function update_nstack_count()
if not enable_nstack or current_layout ~= columns_layout then
if current_layout ~= columns_layout then
return
end
@@ -336,7 +303,6 @@ local function update_nstack_count()
if count == 0 then
return
end
count = math.max(count, 2)
hl.dsp.layout("setstackcount " .. tostring(count))()
end
@@ -420,16 +386,8 @@ local function toggle_columns_monocle()
end
end
local function active_group_size()
local window = hl.get_active_window()
return window and window.group and window.group.size or 0
end
local function monocle_next()
local window = hl.get_active_window()
if window and window.group and window.group.size and window.group.size > 1 then
hl.dsp.group.next({ window = window_selector(window) })()
elseif current_layout == monocle_layout then
if current_layout == monocle_layout then
hl.dsp.layout("cyclenext")()
update_monocle_notice()
else
@@ -438,10 +396,7 @@ local function monocle_next()
end
local function monocle_prev()
local window = hl.get_active_window()
if window and window.group and window.group.size and window.group.size > 1 then
hl.dsp.group.prev({ window = window_selector(window) })()
elseif current_layout == monocle_layout then
if current_layout == monocle_layout then
hl.dsp.layout("cycleprev")()
update_monocle_notice()
else
@@ -450,7 +405,7 @@ local function monocle_prev()
end
local function focus_direction(direction)
if active_group_size() > 1 or current_layout == monocle_layout then
if current_layout == monocle_layout then
if direction == "up" or direction == "left" then
monocle_prev()
else
@@ -462,15 +417,6 @@ local function focus_direction(direction)
hl.dsp.focus({ direction = direction })()
end
local function swap_direction(direction)
if enable_nstack and current_layout == columns_layout and active_group_size() <= 1 then
hl.dsp.layout("swapdirection " .. direction)()
return
end
hl.dsp.window.swap({ direction = direction })()
end
local function focus_workspace(workspace_id)
hl.dsp.focus({ workspace = tostring(workspace_id), on_current_monitor = true })()
end
@@ -508,165 +454,9 @@ local function focus_previous_workspace_for_monitor()
end
local function move_window_to_workspace(workspace_id, follow, window)
local target_window = window or hl.get_active_window()
local target_selector = window_selector(target_window)
hl.dsp.window.move({ workspace = tostring(workspace_id), follow = false, window = target_selector })()
hl.dsp.window.move({ workspace = tostring(workspace_id), follow = follow, window = window })()
if follow then
focus_workspace(workspace_id)
if target_selector then
hl.dsp.focus({ window = target_selector })()
end
end
end
local function notify_tabbed_group(text)
hl.notification.create({
text = text,
duration = 1800,
icon = "info",
color = "rgba(edb443ff)",
font_size = 13,
})
end
local function workspace_visible_normal_windows(workspace)
local windows = {}
if not workspace then
return windows
end
for _, window in ipairs(hl.get_workspace_windows(workspace)) do
if is_normal_window(window) and not window.hidden then
windows[#windows + 1] = window
end
end
return windows
end
local function active_workspace_tiled_group_candidates(workspace)
local candidates = tiled_windows(workspace)
sort_windows_by_focus_history(candidates)
return candidates
end
local function find_tabbed_group_anchor(state)
local active = hl.get_active_window()
if active and active.group and active.group.size and active.group.size > 1 then
return active
end
if not state then
return nil
end
for _, window in ipairs(hl.get_windows()) do
if window and window.address == state.anchor and window.group and window.group.size and window.group.size > 1 then
return window
end
end
return nil
end
local function restore_workspace_tabbed_group()
local key = workspace_key()
local anchor = find_tabbed_group_anchor(tabbed_workspace_groups[key])
local anchor_selector = window_selector(anchor)
if not anchor_selector then
tabbed_workspace_groups[key] = nil
set_layout(columns_layout)
notify_tabbed_group("No tabbed group to restore")
return
end
hl.dsp.focus({ window = anchor_selector })()
hl.dsp.group.toggle({ window = anchor_selector })()
tabbed_workspace_groups[key] = nil
set_layout(columns_layout)
schedule_nstack_count_update()
end
local function gather_workspace_into_tabbed_group()
local workspace = active_workspace()
if not is_normal_workspace(workspace) then
return
end
local key = workspace_key(workspace)
if tabbed_workspace_groups[key] or active_group_size() > 1 then
restore_workspace_tabbed_group()
return
end
local candidates = active_workspace_tiled_group_candidates(workspace)
if #candidates <= 1 then
set_layout(columns_layout)
return
end
local candidate_addresses = window_address_set(candidates)
local focused = hl.get_active_window()
local anchor = nil
if focused and not focused.floating and not focused.group and window_address_in_set(focused, candidate_addresses) then
anchor = focused
end
if not anchor then
for _, window in ipairs(candidates) do
if not window.group then
anchor = window
break
end
end
end
local anchor_selector = window_selector(anchor)
if not anchor_selector then
notify_tabbed_group("Current tiled windows are already grouped")
return
end
set_layout(columns_layout)
local staged_windows = {}
for _, window in ipairs(workspace_visible_normal_windows(workspace)) do
if window ~= anchor then
staged_windows[#staged_windows + 1] = window
move_window_to_workspace(tabbed_group_staging_workspace, false, window)
end
end
hl.dsp.focus({ window = anchor_selector })()
hl.dsp.group.toggle({ window = anchor_selector })()
hl.config({ group = { group_on_movetoworkspace = true } })
for _, window in ipairs(candidates) do
if window ~= anchor then
move_window_to_workspace(workspace.id, false, window)
end
end
hl.config({ group = { group_on_movetoworkspace = false } })
for _, window in ipairs(staged_windows) do
if not window_address_in_set(window, candidate_addresses) then
move_window_to_workspace(workspace.id, false, window)
end
end
tabbed_workspace_groups[key] = {
anchor = anchor.address,
windows = candidate_addresses,
}
hl.dsp.focus({ window = anchor_selector })()
end
local function force_columns_layout()
if active_group_size() > 1 or tabbed_workspace_groups[workspace_key()] then
restore_workspace_tabbed_group()
else
set_layout(columns_layout)
end
end
@@ -749,7 +539,7 @@ local function move_window_to_monitor(direction, follow)
end
local original_monitor = hl.get_active_monitor()
hl.dsp.window.move({ monitor = direction, follow = follow, window = window_selector(window) })()
hl.dsp.window.move({ monitor = direction, follow = follow, window = window })()
if not follow and original_monitor then
hl.dsp.focus({ monitor = original_monitor })()
@@ -913,16 +703,15 @@ local function apply_scratchpad_geometry(name, window, target_monitor)
x = monitor.x + math.floor((monitor.width - width) / 2)
y = monitor.y + scratchpad_top_margin
end
local selector = window_selector(window)
hl.dsp.window.float({ action = "enable", window = selector })()
hl.dsp.window.tag({ tag = "+scratchpad", window = selector })()
hl.dsp.window.tag({ tag = "+scratchpad-" .. name, window = selector })()
hl.dsp.window.resize({ x = width, y = height, relative = false, window = selector })()
hl.dsp.window.move({ x = x, y = y, relative = false, window = selector })()
hl.dsp.window.float({ action = "enable", window = window })()
hl.dsp.window.tag({ tag = "+scratchpad", window = window })()
hl.dsp.window.tag({ tag = "+scratchpad-" .. name, window = window })()
hl.dsp.window.resize({ x = width, y = height, relative = false, window = window })()
hl.dsp.window.move({ x = x, y = y, relative = false, window = window })()
if def.dropdown then
hl.dsp.window.set_prop({ prop = "border_size", value = "0", window = selector })()
hl.dsp.window.set_prop({ prop = "no_shadow", value = "1", window = selector })()
hl.dsp.window.set_prop({ prop = "border_size", value = "0", window = window })()
hl.dsp.window.set_prop({ prop = "no_shadow", value = "1", window = window })()
end
end
@@ -945,7 +734,7 @@ local function show_scratchpad_window(name, window, workspace, target_monitor)
remove_minimized_window(window)
move_window_to_workspace(workspace.id, false, window)
hl.dsp.focus({ window = window_selector(window) })()
hl.dsp.focus({ window = window })()
schedule_scratchpad_geometry(name, window, target_monitor or hl.get_active_monitor())
end
@@ -1031,29 +820,29 @@ local function activate_window_picker_candidate(index)
end
if mode == "go" then
hl.dsp.focus({ window = window_selector(window) })()
hl.dsp.focus({ window = window })()
return
end
local workspace = active_workspace()
if mode == "bring" and workspace then
move_window_to_workspace(workspace.id, false, window)
hl.dsp.focus({ window = window_selector(window) })()
hl.dsp.focus({ window = window })()
return
end
if mode == "minimized" and workspace then
remove_minimized_window(window)
restore_minimized_window(window, workspace)
hl.dsp.focus({ window = window_selector(window) })()
hl.dsp.focus({ window = window })()
return
end
if mode == "replace" then
local focused = hl.get_active_window()
if focused and focused ~= window then
hl.dsp.window.swap({ target = window_selector(window), window = window_selector(focused) })()
hl.dsp.focus({ window = window_selector(window) })()
hl.dsp.window.swap({ target = window, window = focused })()
hl.dsp.focus({ window = window })()
end
end
end
@@ -1150,7 +939,7 @@ local function focus_next_class()
local next_class = classes[(current_index % #classes) + 1]
local target = first_by_class[next_class]
if target then
hl.dsp.focus({ window = window_selector(target) })()
hl.dsp.focus({ window = target })()
end
end
@@ -1189,7 +978,7 @@ local function raise_or_spawn(class_fragment, command)
local fragment = string.lower(class_fragment)
for _, window in ipairs(hl.get_windows()) do
if is_normal_window(window) and window.class and string.find(string.lower(window.class), fragment, 1, true) then
hl.dsp.focus({ window = window_selector(window) })()
hl.dsp.focus({ window = window })()
return
end
end
@@ -1219,7 +1008,7 @@ local function restore_last_minimized()
local window = table.remove(minimized_windows)
if window and window.address and is_minimized_window(window) then
restore_minimized_window(window, workspace)
hl.dsp.focus({ window = window_selector(window) })()
hl.dsp.focus({ window = window })()
return
end
end
@@ -1314,10 +1103,8 @@ local function toggle_scratchpad(name)
end
end
if enable_nstack then
hl.plugin.load("/run/current-system/sw/lib/libhyprNStack.so")
end
if enable_hyprexpo and not verify_config then
if not verify_config then
hl.plugin.load("/run/current-system/sw/lib/libhyprexpo.so")
end
@@ -1371,28 +1158,19 @@ hl.config({
workspace_back_and_forth = true,
},
group = {
group_on_movetoworkspace = false,
col = {
border_active = "rgba(edb443ff)",
border_inactive = "rgba(091f2eff)",
},
groupbar = {
enabled = true,
blur = true,
font_size = 13,
gradients = true,
height = 26,
indicator_gap = 0,
indicator_height = 1,
rounding = 5,
gradient_rounding = 5,
text_padding = 8,
font_size = 12,
height = 22,
col = {
active = "rgba(edb443ff)",
inactive = "rgba(101820f2)",
inactive = "rgba(091f2eff)",
},
text_color = "rgba(091018ff)",
text_color_inactive = "rgba(f2f5f7ff)",
text_color = "rgba(091f2eff)",
},
},
misc = {
@@ -1407,50 +1185,15 @@ hl.curve("smoothOut", { type = "bezier", points = { { 0.36, 1 }, { 0.3, 1 } } })
hl.curve("smoothInOut", { type = "bezier", points = { { 0.42, 0 }, { 0.58, 1 } } })
hl.curve("linear", { type = "bezier", points = { { 0, 0 }, { 1, 1 } } })
local animations = {
{ leaf = "global", enabled = true, speed = 8, bezier = "default" },
{ leaf = "windows", enabled = true, speed = 6, bezier = "overshoot", style = "gnomed" },
{ leaf = "windowsIn", enabled = true, speed = 6, bezier = "overshoot", style = "gnomed" },
{ leaf = "windowsOut", enabled = true, speed = 5, bezier = "smoothInOut", style = "gnomed" },
{ leaf = "windowsMove", enabled = true, speed = 6, bezier = "smoothOut" },
{ leaf = "border", enabled = false },
{ leaf = "borderangle", enabled = false },
{ leaf = "fade", enabled = true, speed = 5, bezier = "smoothOut" },
{ leaf = "fadeIn", enabled = true, speed = 5, bezier = "smoothOut" },
{ leaf = "fadeOut", enabled = true, speed = 5, bezier = "smoothOut" },
{ leaf = "fadeSwitch", enabled = true, speed = 5, bezier = "smoothOut" },
{ leaf = "fadeShadow", enabled = true, speed = 5, bezier = "smoothOut" },
{ leaf = "fadeGlow", enabled = true, speed = 5, bezier = "smoothOut" },
{ leaf = "fadeDim", enabled = true, speed = 5, bezier = "smoothOut" },
{ leaf = "fadeLayers", enabled = true, speed = 5, bezier = "smoothOut" },
{ leaf = "fadeLayersIn", enabled = true, speed = 5, bezier = "smoothOut" },
{ leaf = "fadeLayersOut", enabled = true, speed = 5, bezier = "smoothOut" },
{ leaf = "fadePopups", enabled = true, speed = 5, bezier = "smoothOut" },
{ leaf = "fadePopupsIn", enabled = true, speed = 5, bezier = "smoothOut" },
{ leaf = "fadePopupsOut", enabled = true, speed = 5, bezier = "smoothOut" },
{ leaf = "fadeDpms", enabled = true, speed = 5, bezier = "smoothOut" },
{ leaf = "layers", enabled = true, speed = 5, bezier = "smoothOut", style = "fade" },
{ leaf = "layersIn", enabled = true, speed = 5, bezier = "smoothOut", style = "fade" },
{ leaf = "layersOut", enabled = true, speed = 5, bezier = "smoothOut", style = "fade" },
{ leaf = "workspaces", enabled = true, speed = 6, bezier = "smoothOut", style = "slidefade 15%" },
{ leaf = "workspacesIn", enabled = true, speed = 6, bezier = "smoothOut", style = "slidefade 15%" },
{ leaf = "workspacesOut", enabled = true, speed = 6, bezier = "smoothOut", style = "slidefade 15%" },
{ leaf = "specialWorkspace", enabled = true, speed = 6, bezier = "smoothOut", style = "slidevert" },
{ leaf = "specialWorkspaceIn", enabled = true, speed = 6, bezier = "smoothOut", style = "slidevert" },
{ leaf = "specialWorkspaceOut", enabled = true, speed = 6, bezier = "smoothOut", style = "slidevert" },
{ leaf = "zoomFactor", enabled = true, speed = 7, bezier = "smoothOut" },
{ leaf = "monitorAdded", enabled = true, speed = 5, bezier = "smoothOut" },
}
for _, animation in ipairs(animations) do
hl.animation(animation)
end
hl.animation({ leaf = "windows", enabled = true, speed = 6, bezier = "overshoot", style = "gnomed" })
hl.animation({ leaf = "windowsIn", enabled = true, speed = 6, bezier = "overshoot", style = "gnomed" })
hl.animation({ leaf = "windowsOut", enabled = true, speed = 5, bezier = "smoothInOut", style = "gnomed" })
hl.animation({ leaf = "windowsMove", enabled = true, speed = 6, bezier = "smoothOut" })
hl.animation({ leaf = "border", enabled = false })
hl.animation({ leaf = "borderangle", enabled = false })
hl.animation({ leaf = "fade", enabled = true, speed = 5, bezier = "smoothOut" })
hl.animation({ leaf = "workspaces", enabled = true, speed = 6, bezier = "smoothOut", style = "slidefade 15%" })
hl.animation({ leaf = "specialWorkspace", enabled = true, speed = 6, bezier = "smoothOut", style = "slidevert" })
local function apply_rules()
if verify_config then
@@ -1465,10 +1208,6 @@ local function apply_rules()
hl.window_rule({ match = { title = "^(Open File)$" }, float = true })
hl.window_rule({ match = { title = "^(Save File)$" }, float = true })
hl.window_rule({ match = { title = "^(Confirm)$" }, float = true })
hl.window_rule({
match = { class = "^(com\\.mitchellh\\.ghostty\\.dropdown)$" },
animation = "slide top",
})
hl.window_rule({
match = { class = "^(.*[Rr]umno.*)$" },
float = true,
@@ -1487,21 +1226,27 @@ local function apply_rules()
})
end
bind(main_mod .. " + P", exec(launcher_command))
apply_rules()
bind(main_mod .. " + P", exec(menu))
bind(main_mod .. " + SHIFT + P", exec(run_menu))
bind(hyper .. " + SHIFT + N", exec(shell_ui_command .. " control-center"))
bind(hyper .. " + CTRL + N", exec(shell_ui_command .. " settings"))
bind(main_mod .. " + SHIFT + Return", exec(terminal))
bind(main_mod .. " + Q", exec("hyprctl reload"))
bind(main_mod .. " + Q", hl.dsp.window.close())
bind(main_mod .. " + SHIFT + C", hl.dsp.window.close())
bind(main_mod .. " + SHIFT + Q", hl.dsp.exit())
bind(main_mod .. " + E", exec("emacsclient --eval '(emacs-everywhere)'"))
bind(main_mod .. " + V", exec("wl-paste | xdotool type --file -"))
bind(main_mod .. " + Tab", hyprexpo("toggle"))
bind(main_mod .. " + SHIFT + Tab", hyprexpo("bring"))
bind(main_mod .. " + G", exec(shell_ui_command .. " window go"))
bind(main_mod .. " + B", exec(shell_ui_command .. " window bring"))
bind(main_mod .. " + SHIFT + B", exec(shell_ui_command .. " window replace"))
bind(main_mod .. " + G", function()
enter_window_picker("go")
end)
bind(main_mod .. " + B", function()
enter_window_picker("bring")
end)
bind(main_mod .. " + SHIFT + B", function()
enter_window_picker("replace")
end)
bind(main_mod .. " + W", function()
focus_direction("up")
@@ -1516,18 +1261,10 @@ bind(main_mod .. " + D", function()
focus_direction("right")
end)
bind(main_mod .. " + SHIFT + W", function()
swap_direction("up")
end)
bind(main_mod .. " + SHIFT + S", function()
swap_direction("down")
end)
bind(main_mod .. " + SHIFT + A", function()
swap_direction("left")
end)
bind(main_mod .. " + SHIFT + D", function()
swap_direction("right")
end)
bind(main_mod .. " + SHIFT + W", hl.dsp.window.swap({ direction = "up" }))
bind(main_mod .. " + SHIFT + S", hl.dsp.window.swap({ direction = "down" }))
bind(main_mod .. " + SHIFT + A", hl.dsp.window.swap({ direction = "left" }))
bind(main_mod .. " + SHIFT + D", hl.dsp.window.swap({ direction = "right" }))
bind(main_mod .. " + CTRL + W", function()
move_window_to_monitor("u", false)
@@ -1601,9 +1338,13 @@ bind(hyper .. " + SHIFT + D", function()
move_window_to_monitor("r", true)
end)
bind(main_mod .. " + Space", gather_workspace_into_tabbed_group)
bind(main_mod .. " + SHIFT + Space", force_columns_layout)
bind(main_mod .. " + CTRL + Space", gather_workspace_into_tabbed_group)
bind(main_mod .. " + Space", toggle_columns_monocle)
bind(main_mod .. " + SHIFT + Space", function()
set_layout(columns_layout)
end)
bind(main_mod .. " + CTRL + Space", function()
set_layout(monocle_layout)
end)
bind(main_mod .. " + bracketright", monocle_next)
bind(main_mod .. " + bracketleft", monocle_prev)
bind(main_mod .. " + F", hl.dsp.window.fullscreen({ mode = "fullscreen" }))
@@ -1724,8 +1465,7 @@ bind(main_mod .. " + mouse:273", hl.dsp.window.resize())
hl.on("hyprland.start", function()
apply_nstack_config()
apply_hyprexpo_config()
apply_rules()
hl.exec_cmd("sh -lc 'export IMALISON_SESSION_TYPE=wayland; dbus-update-activation-environment --systemd XDG_RUNTIME_DIR WAYLAND_DISPLAY DISPLAY XAUTHORITY HYPRLAND_INSTANCE_SIGNATURE XDG_CURRENT_DESKTOP XDG_SESSION_TYPE IMALISON_SESSION_TYPE; systemctl --user start graphical-session.target hyprland-session.target'")
hl.exec_cmd("sh -lc 'export IMALISON_SESSION_TYPE=wayland; dbus-update-activation-environment --systemd WAYLAND_DISPLAY DISPLAY XAUTHORITY HYPRLAND_INSTANCE_SIGNATURE XDG_CURRENT_DESKTOP XDG_SESSION_TYPE IMALISON_SESSION_TYPE; systemctl --user start graphical-session.target hyprland-session.target'")
hl.exec_cmd("hypridle")
hl.exec_cmd("wl-paste --type text --watch cliphist store")
hl.exec_cmd("wl-paste --type image --watch cliphist store")
@@ -1736,7 +1476,6 @@ end)
hl.on("config.reloaded", apply_nstack_config)
hl.on("config.reloaded", apply_hyprexpo_config)
hl.on("config.reloaded", apply_rules)
hl.on("window.open", schedule_nstack_count_update)
hl.on("window.destroy", schedule_nstack_count_update)

View File

@@ -0,0 +1,40 @@
#!/usr/bin/env bash
# Bring window to current workspace (like XMonad's bringWindow)
# Uses rofi with icons to select a window, then moves it here.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
source "$SCRIPT_DIR/window-icon-map.sh"
CURRENT_WS=$(hyprctl activeworkspace -j | jq -r '.id')
# Get windows on OTHER workspaces as TSV
WINDOW_DATA=$(hyprctl clients -j | jq -r --argjson cws "$CURRENT_WS" '
.[] | select(.workspace.id >= 0 and .workspace.id != $cws)
| [.address, .class, (.title | gsub("\t"; " ")), (.workspace.id | tostring)]
| @tsv')
if [ -z "$WINDOW_DATA" ]; then
notify-send "Bring Window" "No windows on other workspaces"
exit 0
fi
addresses=()
TMPFILE=$(mktemp)
trap 'rm -f "$TMPFILE"' EXIT
while IFS=$'\t' read -r address class title ws_id; do
icon=$(icon_for_class "$class")
addresses+=("$address")
printf '%-24s %s WS:%s\0icon\x1f%s\n' \
"$class" "$title" "$ws_id" "$icon"
done <<< "$WINDOW_DATA" > "$TMPFILE"
INDEX=$(rofi -dmenu -i -show-icons -p "Bring window" -format i < "$TMPFILE") || exit 0
if [ -n "$INDEX" ] && [ -n "${addresses[$INDEX]:-}" ]; then
ADDRESS="${addresses[$INDEX]}"
hyprctl dispatch movetoworkspace "$CURRENT_WS,address:$ADDRESS"
hyprctl dispatch focuswindow "address:$ADDRESS"
fi

View File

@@ -0,0 +1,15 @@
#!/usr/bin/env bash
# Cycle between master and dwindle layouts
# Like XMonad's NextLayout
set -euo pipefail
CURRENT=$(hyprctl getoption general:layout -j | jq -r '.str')
if [ "$CURRENT" = "master" ]; then
hyprctl keyword general:layout dwindle
notify-send "Layout" "Switched to Dwindle (binary tree)"
else
hyprctl keyword general:layout master
notify-send "Layout" "Switched to Master (XMonad-like)"
fi

View File

@@ -0,0 +1,72 @@
#!/usr/bin/env bash
set -euo pipefail
# Print an "empty" workspace id within 1..$HYPR_MAX_WORKSPACE (default 9).
#
# Preference order (lowest id wins within each tier):
# 1. Workspace exists on the target monitor and has 0 windows
# 2. Workspace id does not exist at all (will be created on dispatch)
# 3. Workspace exists (elsewhere) and has 0 windows
#
# Usage:
# find-empty-workspace.sh [monitor] [exclude_id]
max_ws="${HYPR_MAX_WORKSPACE:-9}"
monitor="${1:-}"
exclude_id="${2:-}"
if [[ -z "${monitor}" ]]; then
monitor="$(hyprctl activeworkspace -j | jq -r '.monitor' 2>/dev/null || true)"
fi
if [[ -z "${monitor}" || "${monitor}" == "null" ]]; then
exit 1
fi
workspaces_json="$(hyprctl workspaces -j 2>/dev/null || echo '[]')"
unused_candidate=""
elsewhere_empty_candidate=""
for i in $(seq 1 "${max_ws}"); do
if [[ -n "${exclude_id}" && "${i}" == "${exclude_id}" ]]; then
continue
fi
exists="$(jq -r --argjson id "${i}" '[.[] | select(.id == $id)] | length' <<<"${workspaces_json}")"
if [[ "${exists}" == "0" ]]; then
if [[ -z "${unused_candidate}" ]]; then
unused_candidate="${i}"
fi
continue
fi
windows="$(jq -r --argjson id "${i}" '([.[] | select(.id == $id) | .windows] | .[0]) // 0' <<<"${workspaces_json}")"
if [[ "${windows}" != "0" ]]; then
continue
fi
ws_monitor="$(jq -r --argjson id "${i}" '([.[] | select(.id == $id) | .monitor] | .[0]) // ""' <<<"${workspaces_json}")"
if [[ "${ws_monitor}" == "${monitor}" ]]; then
printf '%s\n' "${i}"
exit 0
fi
if [[ -z "${elsewhere_empty_candidate}" ]]; then
elsewhere_empty_candidate="${i}"
fi
done
if [[ -n "${unused_candidate}" ]]; then
printf '%s\n' "${unused_candidate}"
exit 0
fi
if [[ -n "${elsewhere_empty_candidate}" ]]; then
printf '%s\n' "${elsewhere_empty_candidate}"
exit 0
fi
exit 1

View File

@@ -0,0 +1,48 @@
#!/usr/bin/env bash
# Focus next window of a different class (like XMonad's focusNextClass)
set -euo pipefail
# Get focused window class
FOCUSED_CLASS=$(hyprctl activewindow -j | jq -r '.class')
FOCUSED_ADDR=$(hyprctl activewindow -j | jq -r '.address')
if [ "$FOCUSED_CLASS" = "null" ] || [ -z "$FOCUSED_CLASS" ]; then
# No focused window, just focus any window
hyprctl dispatch cyclenext
exit 0
fi
# Get all unique classes
ALL_CLASSES=$(hyprctl clients -j | jq -r '[.[] | select(.workspace.id >= 0) | .class] | unique | .[]')
# Get sorted list of classes
CLASSES_ARRAY=()
while IFS= read -r class; do
CLASSES_ARRAY+=("$class")
done <<< "$ALL_CLASSES"
# Find current class index and get next class
CURRENT_INDEX=-1
for i in "${!CLASSES_ARRAY[@]}"; do
if [ "${CLASSES_ARRAY[$i]}" = "$FOCUSED_CLASS" ]; then
CURRENT_INDEX=$i
break
fi
done
if [ $CURRENT_INDEX -eq -1 ] || [ ${#CLASSES_ARRAY[@]} -le 1 ]; then
# Only one class or class not found
exit 0
fi
# Get next class (wrapping around)
NEXT_INDEX=$(( (CURRENT_INDEX + 1) % ${#CLASSES_ARRAY[@]} ))
NEXT_CLASS="${CLASSES_ARRAY[$NEXT_INDEX]}"
# Find first window of next class
NEXT_WINDOW=$(hyprctl clients -j | jq -r ".[] | select(.class == \"$NEXT_CLASS\" and .workspace.id >= 0) | .address" | head -1)
if [ -n "$NEXT_WINDOW" ]; then
hyprctl dispatch focuswindow "address:$NEXT_WINDOW"
fi

View File

@@ -0,0 +1,30 @@
#!/usr/bin/env bash
# Gather all windows of the same class as focused window (like XMonad's gatherThisClass)
set -euo pipefail
# Get focused window class
FOCUSED_CLASS=$(hyprctl activewindow -j | jq -r '.class')
CURRENT_WS=$(hyprctl activeworkspace -j | jq -r '.id')
if [ "$FOCUSED_CLASS" = "null" ] || [ -z "$FOCUSED_CLASS" ]; then
notify-send "Gather Class" "No focused window"
exit 0
fi
# Find all windows with same class on other workspaces
WINDOWS=$(hyprctl clients -j | jq -r ".[] | select(.class == \"$FOCUSED_CLASS\" and .workspace.id != $CURRENT_WS and .workspace.id >= 0) | .address")
if [ -z "$WINDOWS" ]; then
notify-send "Gather Class" "No other windows of class '$FOCUSED_CLASS'"
exit 0
fi
# Move each window to current workspace
COUNT=0
for ADDR in $WINDOWS; do
hyprctl dispatch movetoworkspace "$CURRENT_WS,address:$ADDR"
COUNT=$((COUNT + 1))
done
notify-send "Gather Class" "Gathered $COUNT windows of class '$FOCUSED_CLASS'"

View File

@@ -0,0 +1,33 @@
#!/usr/bin/env bash
# Go to a window selected via rofi (with icons from desktop entries).
# Replaces "rofi -show window" which doesn't work well on Wayland.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
source "$SCRIPT_DIR/window-icon-map.sh"
# Get all windows on regular workspaces as TSV
WINDOW_DATA=$(hyprctl clients -j | jq -r '
.[] | select(.workspace.id >= 0)
| [.address, .class, (.title | gsub("\t"; " ")), (.workspace.id | tostring)]
| @tsv')
[ -n "$WINDOW_DATA" ] || exit 0
addresses=()
TMPFILE=$(mktemp)
trap 'rm -f "$TMPFILE"' EXIT
while IFS=$'\t' read -r address class title ws_id; do
icon=$(icon_for_class "$class")
addresses+=("$address")
printf '%-24s %s WS:%s\0icon\x1f%s\n' \
"$class" "$title" "$ws_id" "$icon"
done <<< "$WINDOW_DATA" > "$TMPFILE"
INDEX=$(rofi -dmenu -i -show-icons -p "Go to window" -format i < "$TMPFILE") || exit 0
if [ -n "$INDEX" ] && [ -n "${addresses[$INDEX]:-}" ]; then
hyprctl dispatch focuswindow "address:${addresses[$INDEX]}"
fi

View File

@@ -0,0 +1,49 @@
#!/usr/bin/env bash
# Minimize the active window by moving it to a special workspace without
# toggling that special workspace open.
#
# Usage: minimize-active.sh <name>
# Example: minimize-active.sh minimized
set -euo pipefail
NAME="${1:-minimized}"
NAME="${NAME#special:}"
if ! command -v hyprctl >/dev/null 2>&1; then
exit 0
fi
if ! command -v jq >/dev/null 2>&1; then
# We could parse plain output, but jq should exist in this setup; if it
# doesn't, fail soft.
exit 0
fi
ACTIVE_JSON="$(hyprctl -j activewindow 2>/dev/null || true)"
ADDR="$(printf '%s' "$ACTIVE_JSON" | jq -r '.address // empty')"
if [ -z "$ADDR" ] || [ "$ADDR" = "null" ]; then
exit 0
fi
# If the minimized special workspace is currently visible, closing it after the
# move keeps the window hidden (what "minimize" usually means).
MONITOR_ID="$(printf '%s' "$ACTIVE_JSON" | jq -r '.monitor // empty')"
SPECIAL_OPEN="$(
hyprctl -j monitors 2>/dev/null \
| jq -r --arg n "special:$NAME" --argjson mid "${MONITOR_ID:-0}" '
.[]
| select(.id == $mid)
| (.specialWorkspace.name // "")
| select(. == $n)
' \
| head -n 1 \
|| true
)"
hyprctl dispatch movetoworkspacesilent "special:${NAME},address:${ADDR}" >/dev/null 2>&1 || true
if [ -n "$SPECIAL_OPEN" ]; then
hyprctl dispatch togglespecialworkspace "$NAME" >/dev/null 2>&1 || true
fi
exit 0

View File

@@ -0,0 +1,39 @@
#!/usr/bin/env bash
# Exit minimized picker mode:
# - Hide the minimized special workspace on the active monitor (if visible)
# - Reset the submap
#
# Usage: minimized-cancel.sh <name>
set -euo pipefail
NAME="${1:-minimized}"
NAME="${NAME#special:}"
SPECIAL_WS="special:${NAME}"
if ! command -v hyprctl >/dev/null 2>&1; then
exit 0
fi
if ! command -v jq >/dev/null 2>&1; then
exit 0
fi
MONITOR_ID="$(hyprctl -j activeworkspace 2>/dev/null | jq -r '.monitorID // empty' || true)"
if [ -z "$MONITOR_ID" ] || [ "$MONITOR_ID" = "null" ]; then
MONITOR_ID=0
fi
OPEN="$(
hyprctl -j monitors 2>/dev/null \
| jq -r --argjson mid "$MONITOR_ID" '.[] | select(.id == $mid) | (.specialWorkspace.name // "")' \
| head -n 1 \
|| true
)"
if [ "$OPEN" = "$SPECIAL_WS" ]; then
hyprctl dispatch togglespecialworkspace "$NAME" >/dev/null 2>&1 || true
fi
hyprctl dispatch submap reset >/dev/null 2>&1 || true
exit 0

View File

@@ -0,0 +1,40 @@
#!/usr/bin/env bash
# Enter a "picker" mode for minimized windows:
# - Ensure the minimized special workspace is visible on the active monitor
# - Switch Hyprland into a submap so Enter restores and Escape cancels
#
# Usage: minimized-mode.sh <name>
set -euo pipefail
NAME="${1:-minimized}"
NAME="${NAME#special:}"
SPECIAL_WS="special:${NAME}"
if ! command -v hyprctl >/dev/null 2>&1; then
exit 0
fi
if ! command -v jq >/dev/null 2>&1; then
exit 0
fi
MONITOR_ID="$(hyprctl -j activeworkspace 2>/dev/null | jq -r '.monitorID // empty' || true)"
if [ -z "$MONITOR_ID" ] || [ "$MONITOR_ID" = "null" ]; then
MONITOR_ID=0
fi
OPEN="$(
hyprctl -j monitors 2>/dev/null \
| jq -r --argjson mid "$MONITOR_ID" '.[] | select(.id == $mid) | (.specialWorkspace.name // "")' \
| head -n 1 \
|| true
)"
# Ensure it's visible (but don't toggle it off if already open).
if [ "$OPEN" != "$SPECIAL_WS" ]; then
hyprctl dispatch togglespecialworkspace "$NAME" >/dev/null 2>&1 || true
fi
hyprctl dispatch submap minimized >/dev/null 2>&1 || true
exit 0

View File

@@ -0,0 +1,83 @@
#!/usr/bin/env bash
# Move the active window in a direction and warp the cursor to keep its
# relative position inside the moved window.
set -euo pipefail
export PATH="/run/current-system/sw/bin:${PATH}"
if [[ $# -lt 1 ]]; then
echo "usage: $0 <dir> [mode]" >&2
exit 1
fi
dir="$1"
mode="${2:-}"
if ! command -v hyprctl >/dev/null; then
exit 0
fi
move_window() {
if [[ -n "$mode" ]]; then
hyprctl dispatch hy3:movewindow "$dir, $mode" >/dev/null 2>&1 || true
else
hyprctl dispatch hy3:movewindow "$dir" >/dev/null 2>&1 || true
fi
}
win_json="$(hyprctl -j activewindow 2>/dev/null || true)"
cur_json="$(hyprctl -j cursorpos 2>/dev/null || true)"
if [[ -z "$win_json" || "$win_json" == "null" || -z "$cur_json" || "$cur_json" == "null" ]]; then
move_window
exit 0
fi
win_x="$(jq -er '.at[0]' <<<"$win_json" 2>/dev/null || true)"
win_y="$(jq -er '.at[1]' <<<"$win_json" 2>/dev/null || true)"
win_w="$(jq -er '.size[0]' <<<"$win_json" 2>/dev/null || true)"
win_h="$(jq -er '.size[1]' <<<"$win_json" 2>/dev/null || true)"
cur_x="$(jq -er '.x' <<<"$cur_json" 2>/dev/null || true)"
cur_y="$(jq -er '.y' <<<"$cur_json" 2>/dev/null || true)"
if [[ ! "$win_x" =~ ^-?[0-9]+$ || ! "$win_y" =~ ^-?[0-9]+$ || ! "$win_w" =~ ^-?[0-9]+$ || ! "$win_h" =~ ^-?[0-9]+$ || ! "$cur_x" =~ ^-?[0-9]+$ || ! "$cur_y" =~ ^-?[0-9]+$ ]]; then
move_window
exit 0
fi
rel_x=$((cur_x - win_x))
rel_y=$((cur_y - win_y))
move_window
win_json="$(hyprctl -j activewindow 2>/dev/null || true)"
if [[ -z "$win_json" || "$win_json" == "null" ]]; then
exit 0
fi
win_x="$(jq -er '.at[0]' <<<"$win_json" 2>/dev/null || true)"
win_y="$(jq -er '.at[1]' <<<"$win_json" 2>/dev/null || true)"
win_w="$(jq -er '.size[0]' <<<"$win_json" 2>/dev/null || true)"
win_h="$(jq -er '.size[1]' <<<"$win_json" 2>/dev/null || true)"
if [[ ! "$win_x" =~ ^-?[0-9]+$ || ! "$win_y" =~ ^-?[0-9]+$ || ! "$win_w" =~ ^-?[0-9]+$ || ! "$win_h" =~ ^-?[0-9]+$ ]]; then
exit 0
fi
if ((rel_x < 0)); then
rel_x=0
elif ((rel_x > win_w)); then
rel_x=$win_w
fi
if ((rel_y < 0)); then
rel_y=0
elif ((rel_y > win_h)); then
rel_y=$win_h
fi
new_x=$((win_x + rel_x))
new_y=$((win_y + rel_y))
hyprctl dispatch movecursor "$new_x" "$new_y" >/dev/null 2>&1 || true

View File

@@ -0,0 +1,19 @@
#!/usr/bin/env bash
# Raise existing window or run command (like XMonad's raiseNextMaybe)
# Usage: raise-or-run.sh <class-pattern> <command>
set -euo pipefail
CLASS_PATTERN="$1"
COMMAND="$2"
# Find windows matching the class pattern
MATCHING=$(hyprctl clients -j | jq -r ".[] | select(.class | test(\"$CLASS_PATTERN\"; \"i\")) | .address" | head -1)
if [ -n "$MATCHING" ]; then
# Window exists, focus it
hyprctl dispatch focuswindow "address:$MATCHING"
else
# No matching window, run the command
exec $COMMAND
fi

View File

@@ -0,0 +1,43 @@
#!/usr/bin/env bash
# Replace focused window with selected window (like XMonad's myReplaceWindow)
# Swaps the positions of focused window and selected window
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
source "$SCRIPT_DIR/window-icon-map.sh"
FOCUSED=$(hyprctl activewindow -j | jq -r '.address')
if [ "$FOCUSED" = "null" ] || [ -z "$FOCUSED" ]; then
notify-send "Replace Window" "No focused window"
exit 0
fi
# Get all windows except focused as TSV
WINDOW_DATA=$(hyprctl clients -j | jq -r --arg focused "$FOCUSED" '
.[] | select(.workspace.id >= 0 and .address != $focused)
| [.address, .class, (.title | gsub("\t"; " ")), (.workspace.id | tostring)]
| @tsv')
if [ -z "$WINDOW_DATA" ]; then
notify-send "Replace Window" "No other windows available"
exit 0
fi
addresses=()
TMPFILE=$(mktemp)
trap 'rm -f "$TMPFILE"' EXIT
while IFS=$'\t' read -r address class title ws_id; do
icon=$(icon_for_class "$class")
addresses+=("$address")
printf '%-24s %s WS:%s\0icon\x1f%s\n' \
"$class" "$title" "$ws_id" "$icon"
done <<< "$WINDOW_DATA" > "$TMPFILE"
INDEX=$(rofi -dmenu -i -show-icons -p "Replace with" -format i < "$TMPFILE") || exit 0
if [ -n "$INDEX" ] && [ -n "${addresses[$INDEX]:-}" ]; then
hyprctl dispatch hy3:movewindow "address:${addresses[$INDEX]}"
fi

View File

@@ -0,0 +1,43 @@
#!/usr/bin/env bash
# Shift window to empty workspace on screen in given direction
# Like XMonad's shiftToEmptyOnScreen
# Usage: shift-to-empty-on-screen.sh <direction: u|d|l|r>
set -euo pipefail
DIRECTION="$1"
max_ws="${HYPR_MAX_WORKSPACE:-9}"
# Track the current monitor so we can return
ORIG_MONITOR=$(hyprctl activeworkspace -j | jq -r '.monitor')
# Move focus to the screen in that direction
hyprctl dispatch focusmonitor "$DIRECTION"
# Get the monitor we're now on (target monitor)
MONITOR=$(hyprctl activeworkspace -j | jq -r '.monitor')
# If there is no monitor in that direction, bail
if [ "$MONITOR" = "$ORIG_MONITOR" ]; then
exit 0
fi
# Find an empty workspace within 1..$HYPR_MAX_WORKSPACE.
EMPTY_WS="$(~/.config/hypr/scripts/find-empty-workspace.sh "${MONITOR}" 2>/dev/null || true)"
if [[ -z "${EMPTY_WS}" ]]; then
# No empty workspace available within the cap; restore focus and bail.
hyprctl dispatch focusmonitor "$ORIG_MONITOR"
exit 0
fi
if (( EMPTY_WS < 1 || EMPTY_WS > max_ws )); then
hyprctl dispatch focusmonitor "$ORIG_MONITOR"
exit 0
fi
# Ensure the workspace exists on the target monitor
hyprctl dispatch workspace "$EMPTY_WS"
# Go back to original monitor and move the window (without following)
hyprctl dispatch focusmonitor "$ORIG_MONITOR"
hyprctl dispatch movetoworkspacesilent "$EMPTY_WS"

View File

@@ -0,0 +1,52 @@
#!/usr/bin/env bash
# Swap the contents of the current workspace with another workspace.
# Intended to mirror XMonad's swapWithCurrent behavior.
set -euo pipefail
max_ws="${HYPR_MAX_WORKSPACE:-9}"
CURRENT_WS="$(hyprctl activeworkspace -j | jq -r '.id')"
if [[ -z "${CURRENT_WS}" || "${CURRENT_WS}" == "null" ]]; then
exit 0
fi
TARGET_WS="${1:-}"
if [[ -z "${TARGET_WS}" ]]; then
WS_LIST="$({
seq 1 "${max_ws}"
hyprctl workspaces -j | jq -r '.[].id' 2>/dev/null || true
} | awk 'NF {print $1}' | awk '!seen[$0]++' | sort -n)"
TARGET_WS="$(printf "%s\n" "${WS_LIST}" | rofi -dmenu -p "Swap with workspace")"
fi
if [[ -z "${TARGET_WS}" || "${TARGET_WS}" == "null" ]]; then
exit 0
fi
if [[ "${TARGET_WS}" == "${CURRENT_WS}" ]]; then
exit 0
fi
if ! [[ "${TARGET_WS}" =~ ^-?[0-9]+$ ]]; then
notify-send "Swap Workspace" "Invalid workspace: ${TARGET_WS}"
exit 1
fi
if (( TARGET_WS < 1 || TARGET_WS > max_ws )); then
notify-send "Swap Workspace" "Workspace out of range (1-${max_ws}): ${TARGET_WS}"
exit 1
fi
WINDOWS_CURRENT="$(hyprctl clients -j | jq -r --arg ws "${CURRENT_WS}" '.[] | select((.workspace.id|tostring) == $ws) | .address')"
WINDOWS_TARGET="$(hyprctl clients -j | jq -r --arg ws "${TARGET_WS}" '.[] | select((.workspace.id|tostring) == $ws) | .address')"
for ADDR in ${WINDOWS_CURRENT}; do
hyprctl dispatch movetoworkspace "${TARGET_WS},address:${ADDR}"
done
for ADDR in ${WINDOWS_TARGET}; do
hyprctl dispatch movetoworkspace "${CURRENT_WS},address:${ADDR}"
done

View File

@@ -0,0 +1,51 @@
#!/usr/bin/env bash
# Toggle a named Hyprland scratchpad, spawning it if needed.
# Usage: toggle-scratchpad.sh <name> <class_regex|-> <title_regex|-> <command...>
set -euo pipefail
if [ "$#" -lt 4 ]; then
echo "usage: $0 <name> <class_regex|-> <title_regex|-> <command...>" >&2
exit 1
fi
NAME="$1"
shift
CLASS_REGEX="$1"
shift
TITLE_REGEX="$1"
shift
COMMAND=("$@")
if [ "$CLASS_REGEX" = "-" ]; then
CLASS_REGEX=""
fi
if [ "$TITLE_REGEX" = "-" ]; then
TITLE_REGEX=""
fi
if [ -z "$CLASS_REGEX" ] && [ -z "$TITLE_REGEX" ]; then
echo "toggle-scratchpad: provide a class or title regex" >&2
exit 1
fi
MATCHING=$(hyprctl clients -j | jq -r --arg cre "$CLASS_REGEX" --arg tre "$TITLE_REGEX" '
.[]
| select(
(($cre == "") or (.class | test($cre; "i")))
and
(($tre == "") or (.title | test($tre; "i")))
)
| .address
')
if [ -z "$MATCHING" ]; then
"${COMMAND[@]}" &
else
while IFS= read -r ADDR; do
[ -n "$ADDR" ] || continue
hyprctl dispatch movetoworkspacesilent "special:$NAME,address:$ADDR"
done <<< "$MATCHING"
fi
hyprctl dispatch togglespecialworkspace "$NAME"

View File

@@ -0,0 +1,86 @@
#!/usr/bin/env bash
# Restore a minimized window by moving it out of a special workspace.
#
# Usage: unminimize-last.sh <name>
# Example: unminimize-last.sh minimized
set -euo pipefail
NAME="${1:-minimized}"
NAME="${NAME#special:}"
SPECIAL_WS="special:${NAME}"
if ! command -v hyprctl >/dev/null 2>&1; then
exit 0
fi
if ! command -v jq >/dev/null 2>&1; then
exit 0
fi
ACTIVE_JSON="$(hyprctl -j activewindow 2>/dev/null || true)"
ACTIVE_ADDR="$(printf '%s' "$ACTIVE_JSON" | jq -r '.address // empty')"
ACTIVE_WS="$(printf '%s' "$ACTIVE_JSON" | jq -r '.workspace.name // empty')"
MONITOR_ID="$(printf '%s' "$ACTIVE_JSON" | jq -r '.monitor // empty')"
# Destination is the normal active workspace for the active monitor.
DEST_WS="$(
hyprctl -j monitors 2>/dev/null \
| jq -r --argjson mid "${MONITOR_ID:-0}" '.[] | select(.id == $mid) | .activeWorkspace.name' \
| head -n 1 \
|| true
)"
if [ -z "$DEST_WS" ] || [ "$DEST_WS" = "null" ]; then
DEST_WS="$(hyprctl -j activeworkspace 2>/dev/null | jq -r '.name // empty' || true)"
fi
if [ -z "$DEST_WS" ] || [ "$DEST_WS" = "null" ]; then
exit 0
fi
# If we're focused on a minimized window already, restore that one.
ADDR=""
if [ "$ACTIVE_WS" = "$SPECIAL_WS" ] && [ -n "$ACTIVE_ADDR" ] && [ "$ACTIVE_ADDR" != "null" ]; then
ADDR="$ACTIVE_ADDR"
else
# Otherwise, restore the "most recent" minimized window we can find.
# focusHistoryID tends to have 0 as most recent; pick the smallest value.
ADDR="$(
hyprctl -j clients 2>/dev/null \
| jq -r --arg sw "$SPECIAL_WS" '
[ .[]
| select(.workspace.name == $sw)
| { addr: .address, fh: (.focusHistoryID // 999999999) }
]
| sort_by(.fh)
| (.[0].addr // empty)
' \
| head -n 1 \
|| true
)"
fi
if [ -z "$ADDR" ] || [ "$ADDR" = "null" ]; then
exit 0
fi
hyprctl dispatch movetoworkspacesilent "${DEST_WS},address:${ADDR}" >/dev/null 2>&1 || true
hyprctl dispatch focuswindow "address:${ADDR}" >/dev/null 2>&1 || true
# If the minimized special workspace is currently visible, close it so we don't
# leave things in a special state after a restore.
SPECIAL_OPEN="$(
hyprctl -j monitors 2>/dev/null \
| jq -r --arg n "$SPECIAL_WS" --argjson mid "${MONITOR_ID:-0}" '
.[]
| select(.id == $mid)
| (.specialWorkspace.name // "")
| select(. == $n)
' \
| head -n 1 \
|| true
)"
if [ -n "$SPECIAL_OPEN" ]; then
hyprctl dispatch togglespecialworkspace "$NAME" >/dev/null 2>&1 || true
fi
exit 0

View File

@@ -0,0 +1,66 @@
#!/usr/bin/env bash
# Source this file to get icon_for_class function.
# Builds a mapping from window class → freedesktop icon name
# by scanning .desktop files for StartupWMClass and Icon fields.
#
# Usage:
# source "$(dirname "$0")/window-icon-map.sh"
# icon=$(icon_for_class "google-chrome")
declare -A _WINDOW_ICON_MAP
_build_window_icon_map() {
local IFS=':'
local -a search_dirs=()
local dir
for dir in ${XDG_DATA_DIRS:-/run/current-system/sw/share:/usr/share:/usr/local/share}; do
[ -d "$dir/applications" ] && search_dirs+=("$dir/applications")
done
[ -d "$HOME/.local/share/applications" ] && search_dirs+=("$HOME/.local/share/applications")
[ ${#search_dirs[@]} -eq 0 ] && return
# Expand globs per-directory so the pattern works correctly
local -a desktop_files=()
for dir in "${search_dirs[@]}"; do
desktop_files+=("$dir"/*.desktop)
done
[ ${#desktop_files[@]} -eq 0 ] && return
# Single grep pass across all desktop files
local -A file_icons file_wmclass
local filepath line
while IFS=: read -r filepath line; do
case "$line" in
Icon=*)
[ -z "${file_icons[$filepath]:-}" ] && file_icons["$filepath"]="${line#Icon=}"
;;
StartupWMClass=*)
[ -z "${file_wmclass[$filepath]:-}" ] && file_wmclass["$filepath"]="${line#StartupWMClass=}"
;;
esac
done < <(grep -H '^Icon=\|^StartupWMClass=' "${desktop_files[@]}" 2>/dev/null)
# Build class → icon map
local icon wm_class bn name
for filepath in "${!file_icons[@]}"; do
icon="${file_icons[$filepath]}"
[ -n "$icon" ] || continue
wm_class="${file_wmclass[$filepath]:-}"
if [ -n "$wm_class" ]; then
_WINDOW_ICON_MAP["${wm_class,,}"]="$icon"
fi
bn="${filepath##*/}"
name="${bn%.desktop}"
_WINDOW_ICON_MAP["${name,,}"]="$icon"
done
}
_build_window_icon_map
icon_for_class() {
local class_lower="${1,,}"
echo "${_WINDOW_ICON_MAP[$class_lower]:-$class_lower}"
}

View File

@@ -0,0 +1,16 @@
#!/usr/bin/env bash
set -euo pipefail
cur_ws="$(hyprctl activeworkspace -j | jq -r '.id' 2>/dev/null || true)"
monitor="$(hyprctl activeworkspace -j | jq -r '.monitor' 2>/dev/null || true)"
ws="$(
~/.config/hypr/scripts/find-empty-workspace.sh "${monitor}" "${cur_ws}" 2>/dev/null || true
)"
if [[ -z "${ws}" ]]; then
exit 0
fi
hyprctl dispatch workspace "${ws}" >/dev/null 2>&1 || true

View File

@@ -0,0 +1,16 @@
#!/usr/bin/env bash
set -euo pipefail
cur_ws="$(hyprctl activeworkspace -j | jq -r '.id' 2>/dev/null || true)"
monitor="$(hyprctl activeworkspace -j | jq -r '.monitor' 2>/dev/null || true)"
ws="$(
~/.config/hypr/scripts/find-empty-workspace.sh "${monitor}" "${cur_ws}" 2>/dev/null || true
)"
if [[ -z "${ws}" ]]; then
exit 0
fi
hyprctl dispatch movetoworkspace "${ws}" >/dev/null 2>&1 || true

View File

@@ -0,0 +1,42 @@
#!/usr/bin/env bash
set -euo pipefail
max_ws="${HYPR_MAX_WORKSPACE:-9}"
delta="${1:-}"
case "${delta}" in
+1|-1) ;;
next) delta="+1" ;;
prev) delta="-1" ;;
*)
exit 2
;;
esac
cur="$(hyprctl activeworkspace -j | jq -r '.id' 2>/dev/null || true)"
if ! [[ "${cur}" =~ ^[0-9]+$ ]]; then
exit 0
fi
if (( cur < 1 )); then
cur=1
elif (( cur > max_ws )); then
cur="${max_ws}"
fi
if [[ "${delta}" == "+1" ]]; then
if (( cur >= max_ws )); then
nxt=1
else
nxt=$((cur + 1))
fi
else
if (( cur <= 1 )); then
nxt="${max_ws}"
else
nxt=$((cur - 1))
fi
fi
hyprctl dispatch workspace "${nxt}" >/dev/null 2>&1 || true

View File

@@ -136,11 +136,11 @@
"xmonad-contrib": "xmonad-contrib"
},
"locked": {
"lastModified": 1777452249,
"narHash": "sha256-Emhn9sIFRVyIlUULDuYjeFcYJld6EAD31TGasYwQsWg=",
"ref": "refs/heads/master",
"rev": "9a6463e68c7bc0a712e49d9ba6c6d1b764260cd7",
"revCount": 2295,
"lastModified": 1777319252,
"narHash": "sha256-mPft6i8ReJAvW2LdylFI6FF6NFGa1HMa3RNbisfAsbc=",
"ref": "refs/heads/codex/fix-gdk-backend-strut-detection",
"rev": "c2cee23fc57384cd322d589944129e6c31d4f0fd",
"revCount": 2288,
"type": "git",
"url": "file:///home/imalison/dotfiles/dotfiles/config/taffybar/taffybar"
},

View File

@@ -121,6 +121,13 @@
(oa: {
doHaddock = false;
doCheck = false;
# Legacy fix for older GHC (harmless on newer)
postPatch = (oa.postPatch or "") + ''
substituteInPlace src/System/Taffybar/DBus/Client/Util.hs \
--replace-fail "import Control.Monad (forM)" \
"import Control.Monad (forM)
import Control.Applicative (liftA2)"
'';
# Needed for gi-gtk-layer-shell (introspection data).
librarySystemDepends = (oa.librarySystemDepends or []) ++ [ pkgs.gtk-layer-shell ];
});

View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 174 148.18">
<path
fill="#d97757"
d="m 105.01,322.07 29.14,-16.35 0.49,-1.42 -0.49,-0.79 h -1.42 l -4.87,-0.3 -16.65,-0.45 -14.44,-0.6 -13.99,-0.75 -3.52,-0.75 -3.3,-4.35 0.34,-2.17 2.96,-1.99 4.24,0.37 9.37,0.64 14.06,0.97 10.2,0.6 15.11,1.57 h 2.4 l 0.34,-0.97 -0.82,-0.6 -0.64,-0.6 -14.55,-9.86 -15.75,-10.42 -8.25,-6 -4.46,-3.04 -2.25,-2.85 -0.97,-6.22 4.05,-4.46 5.44,0.37 1.39,0.37 5.51,4.24 11.77,9.11 15.37,11.32 2.25,1.87 0.9,-0.64 0.11,-0.45 -1.01,-1.69 -8.36,-15.11 -8.92,-15.37 -3.97,-6.37 -1.05,-3.82 c -0.37,-1.57 -0.64,-2.89 -0.64,-4.5 l 4.61,-6.26 2.55,-0.82 6.15,0.82 2.59,2.25 3.82,8.74 6.19,13.76 9.6,18.71 2.81,5.55 1.5,5.14 0.56,1.57 h 0.97 v -0.9 l 0.79,-10.54 1.46,-12.94 1.42,-16.65 0.49,-4.69 2.32,-5.62 4.61,-3.04 3.6,1.72 2.96,4.24 -0.41,2.74 -1.76,11.44 -3.45,17.92 -2.25,12 h 1.31 l 1.5,-1.5 6.07,-8.06 10.2,-12.75 4.5,-5.06 5.25,-5.59 3.37,-2.66 h 6.37 l 4.69,6.97 -2.1,7.2 -6.56,8.32 -5.44,7.05 -7.8,10.5 -4.87,8.4 0.45,0.67 1.16,-0.11 17.62,-3.75 9.52,-1.72 11.36,-1.95 5.14,2.4 0.56,2.44 -2.02,4.99 -12.15,3 -14.25,2.85 -21.22,5.02 -0.26,0.19 0.3,0.37 9.56,0.9 4.09,0.22 h 10.01 l 18.64,1.39 4.87,3.22 2.92,3.94 -0.49,3 -7.5,3.82 -10.12,-2.4 -23.62,-5.62 -8.1,-2.02 h -1.12 v 0.67 l 6.75,6.6 12.37,11.17 15.49,14.4 0.79,3.56 -1.99,2.81 -2.1,-0.3 -13.61,-10.24 -5.25,-4.61 -11.89,-10.01 h -0.79 v 1.05 l 2.74,4.01 14.47,21.75 0.75,6.67 -1.05,2.17 -3.75,1.31 -4.12,-0.75 -8.47,-11.89 -8.74,-13.39 -7.05,-12 -0.86,0.49 -4.16,44.81 -1.95,2.29 -4.5,1.72 -3.75,-2.85 -1.99,-4.61 1.99,-9.11 2.4,-11.89 1.95,-9.45 1.76,-11.74 1.05,-3.9 -0.07,-0.26 -0.86,0.11 -8.85,12.15 -13.46,18.19 -10.65,11.4 -2.55,1.01 -4.42,-2.29 0.41,-4.09 2.47,-3.64 14.74,-18.75 8.89,-11.62 5.74,-6.71 -0.04,-0.97 h -0.34 l -39.15,25.42 -6.97,0.9 -3,-2.81 0.37,-4.61 1.42,-1.5 11.77,-8.1 -0.04,0.04 z"
transform="translate(-75.96,-223.53)"
shape-rendering="optimizeQuality" />
</svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 158.7128 157.296">
<!-- Generator: Adobe Illustrator 29.2.1, SVG Export Plug-In . SVG Version: 2.1.0 Build 116) -->
<path fill="#e7e4ee" d="M60.8734,57.2556v-14.9432c0-1.2586.4722-2.2029,1.5728-2.8314l30.0443-17.3023c4.0899-2.3593,8.9662-3.4599,13.9988-3.4599,18.8759,0,30.8307,14.6289,30.8307,30.2006,0,1.1007,0,2.3593-.158,3.6178l-31.1446-18.2467c-1.8872-1.1006-3.7754-1.1006-5.6629,0l-39.4812,22.9651ZM131.0276,115.4561v-35.7074c0-2.2028-.9446-3.7756-2.8318-4.8763l-39.481-22.9651,12.8982-7.3934c1.1007-.6285,2.0453-.6285,3.1458,0l30.0441,17.3024c8.6523,5.0341,14.4708,15.7296,14.4708,26.1107,0,11.9539-7.0769,22.965-18.2461,27.527v.0021ZM51.593,83.9964l-12.8982-7.5497c-1.1007-.6285-1.5728-1.5728-1.5728-2.8314v-34.6048c0-16.8303,12.8982-29.5722,30.3585-29.5722,6.607,0,12.7403,2.2029,17.9324,6.1349l-30.987,17.9324c-1.8871,1.1007-2.8314,2.6735-2.8314,4.8764v45.6159l-.0014-.0015ZM79.3562,100.0403l-18.4829-10.3811v-22.0209l18.4829-10.3811,18.4812,10.3811v22.0209l-18.4812,10.3811ZM91.2319,147.8591c-6.607,0-12.7403-2.2031-17.9324-6.1344l30.9866-17.9333c1.8872-1.1005,2.8318-2.6728,2.8318-4.8759v-45.616l13.0564,7.5498c1.1005.6285,1.5723,1.5728,1.5723,2.8314v34.6051c0,16.8297-13.0564,29.5723-30.5147,29.5723v.001ZM53.9522,112.7822l-30.0443-17.3024c-8.652-5.0343-14.471-15.7296-14.471-26.1107,0-12.1119,7.2356-22.9652,18.403-27.5272v35.8634c0,2.2028.9443,3.7756,2.8314,4.8763l39.3248,22.8068-12.8982,7.3938c-1.1007.6287-2.045.6287-3.1456,0ZM52.2229,138.5791c-17.7745,0-30.8306-13.3713-30.8306-29.8871,0-1.2585.1578-2.5169.3143-3.7754l30.987,17.9323c1.8871,1.1005,3.7757,1.1005,5.6628,0l39.4811-22.807v14.9435c0,1.2585-.4721,2.2021-1.5728,2.8308l-30.0443,17.3025c-4.0898,2.359-8.9662,3.4605-13.9989,3.4605h.0014ZM91.2319,157.296c19.0327,0,34.9188-13.5272,38.5383-31.4594,17.6164-4.562,28.9425-21.0779,28.9425-37.908,0-11.0112-4.719-21.7066-13.2133-29.4143.7867-3.3035,1.2595-6.607,1.2595-9.909,0-22.4929-18.2471-39.3247-39.3251-39.3247-4.2461,0-8.3363.6285-12.4262,2.045-7.0792-6.9213-16.8318-11.3254-27.5271-11.3254-19.0331,0-34.9191,13.5268-38.5384,31.4591C11.3255,36.0212,0,52.5373,0,69.3675c0,11.0112,4.7184,21.7065,13.2125,29.4142-.7865,3.3035-1.2586,6.6067-1.2586,9.9092,0,22.4923,18.2466,39.3241,39.3248,39.3241,4.2462,0,8.3362-.6277,12.426-2.0441,7.0776,6.921,16.8302,11.3251,27.5271,11.3251Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -40,16 +40,6 @@
-GtkLabel-justify: left;
}
/* Compact logo column for stacked AI usage sections. */
.usage-section.icon-label > .icon {
min-width: 22px;
padding-right: 8px;
}
.usage-section.icon-label > .label {
padding-right: 0px;
}
/* Compact two-line RAM/SWAP widget: reduce icon padding a bit. */
.ram-swap .icon-label > .icon {
/* Different glyphs have different visual widths; fix the icon column width

View File

@@ -1,5 +1,4 @@
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
module Main (main) where
@@ -47,21 +46,11 @@ import System.Taffybar.SimpleConfig
import System.Taffybar.Util (getPixbufFromFilePath, maybeTCombine, postGUIASync, (<|||>))
import System.Taffybar.Widget
import qualified System.Taffybar.Widget.ASUS as ASUS
import System.Taffybar.Widget.AnthropicUsage
( AnthropicUsageDisplayMode (AnthropicUsageDisplayRemaining),
AnthropicUsageStackConfig (..),
anthropicUsageStackNewWith,
defaultAnthropicUsageStackConfig,
)
import System.Taffybar.Widget.AnthropicUsage (anthropicUsageStackNew)
import System.Taffybar.Widget.CPUMonitor (cpuMonitorNew)
import System.Taffybar.Widget.Generic.Graph (GraphConfig (..), GraphDirection (..), GraphStyle (..), defaultGraphConfig)
import qualified System.Taffybar.Widget.NetworkManager as NetworkManager
import System.Taffybar.Widget.OpenAIUsage
( OpenAIUsageDisplayMode (OpenAIUsageDisplayRemaining),
OpenAIUsageStackConfig (..),
defaultOpenAIUsageStackConfig,
openAIUsageStackNewWith,
)
import System.Taffybar.Widget.OpenAIUsage (openAIUsageStackNew)
import qualified System.Taffybar.Widget.PulseAudio as PulseAudio
import System.Taffybar.Widget.SNIMenu (withNmAppletMenu)
import System.Taffybar.Widget.SNITray
@@ -76,7 +65,7 @@ import System.Taffybar.Widget.SNITray.PrioritizedCollapsible
sniTrayPrioritizedCollapsibleNewFromParams,
)
import qualified System.Taffybar.Widget.ScreenLock as ScreenLock
import System.Taffybar.Widget.Util (backgroundLoop, buildContentsBox, buildIconLabelBox, loadPixbufByName, pixbufNewFromFileAtScaleByHeight, widgetSetClassGI)
import System.Taffybar.Widget.Util (backgroundLoop, buildContentsBox, buildIconLabelBox, loadPixbufByName, widgetSetClassGI)
import qualified System.Taffybar.Widget.Wlsunset as Wlsunset
import qualified System.Taffybar.Widget.Workspaces as Workspaces
import System.Taffybar.WindowIcon (pixBufFromColor)
@@ -518,10 +507,10 @@ simplifiedScreensaverWidget =
then return False
else case button of
1 -> do
void $ spawnCommand "/home/imalison/dotfiles/dotfiles/lib/bin/hypr-screensaver toggle >/dev/null 2>&1"
void $ spawnCommand "hypr-screensaver toggle >/dev/null 2>&1"
return True
3 -> do
void $ spawnCommand "/home/imalison/dotfiles/dotfiles/lib/bin/hypr-screensaver stop >/dev/null 2>&1"
void $ spawnCommand "hypr-screensaver stop >/dev/null 2>&1"
return True
_ -> return False
Gtk.widgetShowAll ebox
@@ -562,40 +551,13 @@ wakeupDebugWidget :: TaffyIO Gtk.Widget
wakeupDebugWidget =
decorateWithClassAndBoxM "wakeup-debug" wakeupDebugWidgetNew
usageLogoWidget :: FilePath -> Text -> IO Gtk.Widget
usageLogoWidget iconFile tooltip = do
iconPath <- getUserConfigFile "taffybar" ("icons/" <> iconFile)
iconWidget <-
pixbufNewFromFileAtScaleByHeight 18 iconPath >>= \case
Right pixbuf -> Gtk.toWidget =<< Gtk.imageNewFromPixbuf (Just pixbuf)
Left _ -> Gtk.toWidget =<< Gtk.labelNew (Just "?")
Gtk.widgetSetTooltipText iconWidget (Just tooltip)
widgetSetClassGI iconWidget "usage-logo"
usageSectionWidget :: Text -> FilePath -> Text -> TaffyIO Gtk.Widget -> TaffyIO Gtk.Widget
usageSectionWidget klass iconFile tooltip stackBuilder =
decorateWithClassAndBoxM klass $ do
stack <- stackBuilder
liftIO $ do
iconWidget <- usageLogoWidget iconFile tooltip
section <- buildIconLabelBox iconWidget stack
widgetSetClassGI section "usage-section"
openAIUsageWidget :: TaffyIO Gtk.Widget
openAIUsageWidget =
usageSectionWidget "openai-usage" "openai-symbol.svg" "OpenAI usage" $
openAIUsageStackNewWith
defaultOpenAIUsageStackConfig
{ openAIUsageStackDefaultDisplayMode = OpenAIUsageDisplayRemaining
}
decorateWithClassAndBoxM "openai-usage" openAIUsageStackNew
anthropicUsageWidget :: TaffyIO Gtk.Widget
anthropicUsageWidget =
usageSectionWidget "anthropic-usage" "claude-symbol.svg" "Anthropic usage" $
anthropicUsageStackNewWith
defaultAnthropicUsageStackConfig
{ anthropicUsageStackDefaultDisplayMode = AnthropicUsageDisplayRemaining
}
decorateWithClassAndBoxM "anthropic-usage" anthropicUsageStackNew
sniPriorityVisibilityThresholdDefault :: Int
sniPriorityVisibilityThresholdDefault = 0

View File

@@ -1,93 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
state_file="${IM_DESKTOP_SHELL_UI_STATE:-${XDG_STATE_HOME:-$HOME/.local/state}/imalison/desktop-shell-ui}"
default_shell_ui="${IM_HYPRLAND_SHELL_UI:-taffybar}"
normalize_shell_ui() {
case "${1:-}" in
noctalia)
printf '%s\n' "noctalia"
;;
taffybar|rofi)
printf '%s\n' "taffybar"
;;
*)
return 1
;;
esac
}
current_shell_ui() {
local configured=""
if [[ -r "$state_file" ]]; then
IFS= read -r configured < "$state_file" || true
fi
normalize_shell_ui "$configured" 2>/dev/null \
|| normalize_shell_ui "$default_shell_ui" 2>/dev/null \
|| printf '%s\n' "taffybar"
}
write_shell_ui() {
local shell_ui="$1"
mkdir -p "$(dirname "$state_file")"
printf '%s\n' "$shell_ui" > "$state_file"
}
apply_shell_ui() {
local shell_ui="$1"
export IM_HYPRLAND_SHELL_UI="$shell_ui"
systemctl --user import-environment IM_HYPRLAND_SHELL_UI 2>/dev/null || true
case "$shell_ui" in
noctalia)
systemctl --user stop taffybar.service 2>/dev/null || true
systemctl --user reset-failed taffybar.service 2>/dev/null || true
systemctl --user start noctalia-shell.service
;;
taffybar)
systemctl --user stop noctalia-shell.service 2>/dev/null || true
systemctl --user reset-failed noctalia-shell.service 2>/dev/null || true
systemctl --user start taffybar.service
;;
esac
}
set_shell_ui() {
local shell_ui
shell_ui="$(normalize_shell_ui "${1:-}")" || {
echo "usage: desktop_shell_ui set {taffybar|noctalia}" >&2
exit 2
}
write_shell_ui "$shell_ui"
apply_shell_ui "$shell_ui"
}
case "${1:-current}" in
current)
current_shell_ui
;;
set)
set_shell_ui "${2:-}"
;;
toggle)
case "$(current_shell_ui)" in
noctalia) set_shell_ui taffybar ;;
*) set_shell_ui noctalia ;;
esac
;;
apply)
apply_shell_ui "$(current_shell_ui)"
;;
exec-condition)
[[ "$(current_shell_ui)" == "$(normalize_shell_ui "${2:-}")" ]]
;;
*)
echo "usage: desktop_shell_ui {current|set|toggle|apply|exec-condition}" >&2
exit 2
;;
esac

View File

@@ -2,48 +2,27 @@
set -euo pipefail
script_path="$(readlink -f "${BASH_SOURCE[0]}")"
state_dir="${XDG_RUNTIME_DIR:-/tmp}/hypr-screensaver"
pid_file="$state_dir/mpvpaper.pid"
event_log="$state_dir/events.log"
mkdir -p "$state_dir"
title_prefix="hypr-screensaver:"
screensaver_dir="${HYPR_SCREENSAVER_DIR:-/var/lib/syncthing/sync/Screensaver}"
screensaver_use_dir="${HYPR_SCREENSAVER_USE_DIR:-$screensaver_dir/use}"
usage() {
cat <<'EOF'
Usage: hypr-screensaver <start|stop|toggle|status|session>
Commands:
start Launch the screensaver as a Wayland layer-shell overlay.
stop Stop any running screensaver overlay.
start Launch the screensaver on every Hyprland monitor.
stop Stop any running screensaver windows.
toggle Start if stopped, otherwise stop.
status Exit 0 if the screensaver overlay is running, otherwise exit 1.
session Compatibility alias for start.
status Exit 0 if any screensaver window is running, otherwise exit 1.
session Run the configured screensaver payload for one monitor.
By default, start chooses a random media file from:
/var/lib/syncthing/sync/Screensaver/use
Populate that directory with symlinks to generated screensaver loops you want
in rotation. You can override the source with HYPR_SCREENSAVER_SOURCE, for
example:
The default payload is an mpv-rendered lavfi animation. You can override the
source with HYPR_SCREENSAVER_SOURCE, for example:
HYPR_SCREENSAVER_SOURCE='/path/to/video.mp4'
HYPR_SCREENSAVER_SOURCE='av://lavfi:mandelbrot=s=2560x1440:r=60'
You can also override the rotation directory:
HYPR_SCREENSAVER_USE_DIR='/path/to/use'
Layer-shell/output overrides:
HYPR_SCREENSAVER_OUTPUT='ALL'
HYPR_SCREENSAVER_LAYER='overlay'
HDR handling defaults to matching Hyprland's monitor color-management preset.
Only monitors with preset "hdr" or "hdredid" get HDR colorspace hints. Override
with:
HYPR_SCREENSAVER_HDR_MODE=auto
HYPR_SCREENSAVER_HDR_MODE=sdr
HYPR_SCREENSAVER_HDR_MODE=hdr
EOF
}
@@ -51,11 +30,19 @@ monitors_json() {
hyprctl -j monitors
}
log_event() {
printf '%s %s\n' "$(date --iso-8601=seconds)" "$*" >>"$event_log"
monitor_names() {
monitors_json | jq -r '.[].name'
}
legacy_screensaver_window_pids() {
monitor_specs() {
monitors_json | jq -c '.[] | { name, width, height }'
}
focused_monitor() {
monitors_json | jq -r '.[] | select(.focused) | .name'
}
screensaver_window_pids() {
hyprctl -j clients 2>/dev/null | jq -r --arg prefix "$title_prefix" '
.[]
| select((.title // "") | startswith($prefix))
@@ -65,170 +52,106 @@ legacy_screensaver_window_pids() {
is_running() {
local pid
if [ -f "$pid_file" ]; then
for pid in $(screensaver_window_pids); do
if kill -0 "$pid" 2>/dev/null; then
return 0
fi
done
shopt -s nullglob
local pid_file
for pid_file in "$state_dir"/*.pid; do
pid="$(<"$pid_file")"
if kill -0 "$pid" 2>/dev/null; then
return 0
fi
rm -f "$pid_file"
fi
done
return 1
}
default_source() {
local size width height
size="$(
monitors_json 2>/dev/null \
| jq -r 'max_by((.width // 0) * (.height // 0)) | "\(.width // 1920)x\(.height // 1080)"' 2>/dev/null \
|| true
)"
size="${size:-1920x1080}"
width="${size%x*}"
height="${size#*x}"
local width="$1"
local height="$2"
printf 'av://lavfi:life=s=%sx%s:r=60:mold=10:ratio=0.065:death_color=#101414:life_color=#7dd3fc:mold_color=#1e3a5f,format=yuv420p' \
"$width" "$height"
}
random_source() {
[ -d "$screensaver_use_dir" ] || return 1
local -a candidates=()
local candidate
while IFS= read -r -d '' candidate; do
candidates+=("$candidate")
done < <(
find -L "$screensaver_use_dir" -maxdepth 1 -type f \
\( \
-iname '*.mp4' -o \
-iname '*.mkv' -o \
-iname '*.mov' -o \
-iname '*.webm' -o \
-iname '*.gif' -o \
-iname '*.png' -o \
-iname '*.jpg' -o \
-iname '*.jpeg' \
\) \
-print0
)
[ "${#candidates[@]}" -gt 0 ] || return 1
printf '%s\n' "${candidates[$((RANDOM % ${#candidates[@]}))]}"
}
screensaver_uses_hdr() {
local mode="${HYPR_SCREENSAVER_HDR_MODE:-auto}"
case "$mode" in
hdr)
return 0
;;
sdr)
return 1
;;
auto)
monitors_json 2>/dev/null \
| jq -e 'any(.[]; (.colorManagementPreset // "srgb") == "hdr" or (.colorManagementPreset // "srgb") == "hdredid")' >/dev/null 2>&1
;;
*)
printf 'Invalid HYPR_SCREENSAVER_HDR_MODE=%s; expected auto, sdr, or hdr\n' "$mode" >&2
return 1
;;
esac
}
mpv_color_options() {
if screensaver_uses_hdr; then
printf '%s ' \
target-colorspace-hint=yes \
target-colorspace-hint-mode=source
return
fi
printf '%s ' \
target-colorspace-hint=no \
target-prim=bt.709 \
target-trc=srgb \
target-gamut=bt.709 \
target-peak=80 \
inverse-tone-mapping=no
}
mpv_options() {
if [ -n "${HYPR_SCREENSAVER_MPV_OPTIONS:-}" ]; then
printf '%s\n' "$HYPR_SCREENSAVER_MPV_OPTIONS"
return
fi
printf '%s %s\n' \
"no-audio loop-file=inf osc=no osd-level=0 input-default-bindings=no terminal=no image-display-duration=inf keep-open=yes" \
"$(mpv_color_options)"
}
run_mpvpaper() {
if command -v mpvpaper >/dev/null 2>&1; then
exec mpvpaper "$@"
fi
exec nix shell nixpkgs#mpvpaper --command mpvpaper "$@"
}
start() {
local source output layer options pid
local current_monitor spec monitor width height pid
if is_running; then
log_event "start ignored: already running pid=$(<"$pid_file")"
exit 0
fi
stop
current_monitor="$(focused_monitor || true)"
source="${HYPR_SCREENSAVER_SOURCE:-}"
if [ -z "$source" ]; then
source="$(random_source || true)"
fi
if [ -z "$source" ]; then
source="$(default_source)"
fi
output="${HYPR_SCREENSAVER_OUTPUT:-ALL}"
layer="${HYPR_SCREENSAVER_LAYER:-overlay}"
options="$(mpv_options)"
log_event "start output=$output layer=$layer source=$source"
(
exec </dev/null
run_mpvpaper --layer "$layer" --mpv-options "$options" "$output" "$source"
) >>"$state_dir/mpvpaper.log" 2>&1 &
while IFS= read -r spec; do
monitor="$(jq -r '.name' <<<"$spec")"
width="$(jq -r '.width' <<<"$spec")"
height="$(jq -r '.height' <<<"$spec")"
[ -n "$monitor" ] || continue
HYPR_SCREENSAVER_MONITOR="$monitor" \
HYPR_SCREENSAVER_WIDTH="$width" \
HYPR_SCREENSAVER_HEIGHT="$height" \
"$script_path" session >/dev/null 2>&1 &
pid=$!
printf '%s\n' "$pid" > "$pid_file"
sleep 0.2
if ! kill -0 "$pid" 2>/dev/null; then
rm -f "$pid_file"
log_event "start failed: process exited early pid=$pid"
return 1
printf '%s\n' "$pid" > "$state_dir/${monitor}.pid"
sleep 0.15
done < <(monitor_specs)
if [ -n "$current_monitor" ]; then
hyprctl dispatch focusmonitor "$current_monitor" >/dev/null 2>&1 || true
fi
log_event "start ok pid=$pid"
}
stop() {
local pid legacy_pid
local pid pid_file
if [ -f "$pid_file" ]; then
pid="$(<"$pid_file")"
log_event "stop pid=$pid"
for pid in $(screensaver_window_pids); do
kill "$pid" >/dev/null 2>&1 || true
done
shopt -s nullglob
for pid_file in "$state_dir"/*.pid; do
pid="$(<"$pid_file")"
kill "$pid" >/dev/null 2>&1 || true
pkill -TERM -P "$pid" >/dev/null 2>&1 || true
rm -f "$pid_file"
else
log_event "stop with no pid file"
done
}
session() {
local monitor="${HYPR_SCREENSAVER_MONITOR:?missing HYPR_SCREENSAVER_MONITOR}"
local width="${HYPR_SCREENSAVER_WIDTH:-1920}"
local height="${HYPR_SCREENSAVER_HEIGHT:-1080}"
local source="${HYPR_SCREENSAVER_SOURCE:-$(default_source "$width" "$height")}"
local -a mpv_args=(
--no-config
--really-quiet
--fullscreen
--fs-screen-name="$monitor"
--screen-name="$monitor"
--force-window=immediate
--border=no
--title-bar=no
--ontop
--keep-open=yes
--loop-file=inf
--audio=no
--osc=no
--osd-level=0
--input-default-bindings=no
--wayland-app-id=hypr-screensaver
--title="${title_prefix}${monitor}"
--image-display-duration=inf
"$source"
)
if command -v mpv >/dev/null 2>&1; then
exec mpv "${mpv_args[@]}"
fi
for legacy_pid in $(legacy_screensaver_window_pids); do
log_event "stop legacy pid=$legacy_pid"
kill "$legacy_pid" >/dev/null 2>&1 || true
done
exec nix shell nixpkgs#mpv --command mpv "${mpv_args[@]}"
}
status() {
@@ -253,7 +176,7 @@ case "${1:-}" in
status
;;
session)
start
session
;;
""|-h|--help|help)
usage

View File

@@ -1,254 +0,0 @@
#!/usr/bin/env python3
import argparse
import json
import os
import subprocess
import sys
from pathlib import Path
PROMPTS = {
"go": "Go to window",
"bring": "Bring window",
"replace": "Replace with",
}
def ensure_hyprland_instance():
if os.environ.get("HYPRLAND_INSTANCE_SIGNATURE"):
return
runtime = Path(os.environ.get("XDG_RUNTIME_DIR", f"/run/user/{os.getuid()}"))
hypr_dir = runtime / "hypr"
if not hypr_dir.is_dir():
return
sockets = []
for socket in hypr_dir.glob("*/.socket.sock"):
try:
sockets.append((socket.stat().st_mtime, socket.parent.name))
except OSError:
pass
if sockets:
os.environ["HYPRLAND_INSTANCE_SIGNATURE"] = max(sockets)[1]
def run_json(*args):
result = subprocess.run(args, check=False, text=True, capture_output=True)
if result.returncode != 0 or not result.stdout.strip():
return None
return json.loads(result.stdout)
def dispatch_lua(expression):
return subprocess.run(["hyprctl", "dispatch", expression], check=False).returncode == 0
def dispatch_legacy(*args):
return subprocess.run(["hyprctl", "dispatch", *args], check=False).returncode == 0
def normal_workspace(window):
workspace = window.get("workspace") or {}
workspace_id = workspace.get("id")
return isinstance(workspace_id, int) and workspace_id >= 0
def window_address(window):
address = window.get("address") or ""
return address if isinstance(address, str) and address else None
def clean_text(value):
return str(value or "").replace("\t", " ").replace("\n", " ").strip()
def app_dirs():
seen = set()
for base in os.environ.get("XDG_DATA_DIRS", "/run/current-system/sw/share:/usr/share:/usr/local/share").split(":"):
path = Path(base) / "applications"
if path.is_dir() and path not in seen:
seen.add(path)
yield path
local = Path.home() / ".local/share/applications"
if local.is_dir() and local not in seen:
yield local
def parse_desktop_file(path):
icon = None
wm_class = None
try:
with path.open(errors="ignore") as handle:
for raw_line in handle:
line = raw_line.strip()
if not icon and line.startswith("Icon="):
icon = line.removeprefix("Icon=").strip()
elif not wm_class and line.startswith("StartupWMClass="):
wm_class = line.removeprefix("StartupWMClass=").strip()
if icon and wm_class:
break
except OSError:
return None, None
return icon, wm_class
def icon_map():
mapping = {}
for directory in app_dirs():
for desktop_file in directory.glob("*.desktop"):
icon, wm_class = parse_desktop_file(desktop_file)
if not icon:
continue
mapping.setdefault(desktop_file.stem.lower(), icon)
if wm_class:
mapping.setdefault(wm_class.lower(), icon)
return mapping
def icon_for(mapping, class_name):
class_key = clean_text(class_name).lower()
return mapping.get(class_key, class_key or "application-x-executable")
def rofi_index(entries, prompt):
menu = b"".join(entry.encode("utf-8", errors="replace") for entry in entries)
result = subprocess.run(
["rofi", "-dmenu", "-i", "-show-icons", "-p", prompt, "-format", "i"],
input=menu,
check=False,
capture_output=True,
)
if result.returncode != 0:
return None
try:
return int(result.stdout.decode("utf-8").strip())
except ValueError:
return None
def candidates(mode, clients, active_workspace, active_window):
current_ws = (active_workspace or {}).get("id")
focused = window_address(active_window or {}) if active_window else None
filtered = []
for window in clients:
if not normal_workspace(window):
continue
address = window_address(window)
if not address:
continue
workspace_id = (window.get("workspace") or {}).get("id")
if mode == "bring" and workspace_id == current_ws:
continue
if mode == "replace" and address == focused:
continue
filtered.append(window)
filtered.sort(
key=lambda window: (
(window.get("workspace") or {}).get("id", 9999),
window.get("focusHistoryID", 999999),
clean_text(window.get("class")).lower(),
clean_text(window.get("title")).lower(),
)
)
return filtered
def menu_entry(window, icons):
class_name = clean_text(window.get("class"))
title = clean_text(window.get("title"))
workspace_id = (window.get("workspace") or {}).get("id", "?")
label = f"{class_name[:24]:<24} {title} WS:{workspace_id}"
icon = icon_for(icons, class_name)
return f"{label}\0icon\x1f{icon}\n"
def focus_window(address):
if dispatch_lua(f'hl.dsp.focus({{ window = "address:{address}" }})'):
return
dispatch_legacy("focuswindow", f"address:{address}")
def move_to_workspace(address, workspace_id):
workspace = str(workspace_id)
if dispatch_lua(f'hl.dsp.window.move({{ workspace = "{workspace}", window = "address:{address}" }})'):
return
dispatch_legacy("movetoworkspace", f"{workspace},address:{address}")
def swap_with_focused(target_address, focused_address):
if dispatch_lua(
f'hl.dsp.window.swap({{ target = "address:{target_address}", window = "address:{focused_address}" }})'
):
return
dispatch_legacy("hy3:movewindow", f"address:{target_address}")
def activate(mode, window, active_workspace, active_window):
address = window_address(window)
if not address:
return
if mode == "go":
focus_window(address)
return
if mode == "bring":
current_ws = (active_workspace or {}).get("id")
if current_ws is not None:
move_to_workspace(address, current_ws)
focus_window(address)
return
if mode == "replace":
focused = window_address(active_window or {})
if focused and focused != address:
swap_with_focused(address, focused)
focus_window(address)
def main():
parser = argparse.ArgumentParser(description="Rofi window go/bring/replace for Hyprland.")
parser.add_argument("mode", choices=sorted(PROMPTS))
parser.add_argument("--print-candidates", action="store_true")
parser.add_argument("--select-index", type=int)
args = parser.parse_args()
ensure_hyprland_instance()
clients = run_json("hyprctl", "clients", "-j") or []
active_workspace = run_json("hyprctl", "activeworkspace", "-j") or {}
active_window = run_json("hyprctl", "activewindow", "-j") or {}
windows = candidates(args.mode, clients, active_workspace, active_window)
if args.print_candidates:
print(json.dumps(windows, indent=2, sort_keys=True))
return 0
if not windows:
return 0
if args.select_index is None:
icons = icon_map()
entries = [menu_entry(window, icons) for window in windows]
index = rofi_index(entries, PROMPTS[args.mode])
else:
index = args.select_index
if index is None or index < 0 or index >= len(windows):
return 0
activate(args.mode, windows[index], active_workspace, active_window)
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -1,78 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
if command -v desktop_shell_ui >/dev/null 2>&1; then
shell_ui="$(desktop_shell_ui current 2>/dev/null || true)"
else
shell_ui=""
fi
shell_ui="${shell_ui:-${IM_HYPRLAND_SHELL_UI:-taffybar}}"
run_noctalia() {
noctalia-shell ipc --any-display call "$@"
}
run_launcher() {
case "$shell_ui" in
noctalia)
run_noctalia launcher toggle
;;
taffybar|rofi)
exec rofi -show drun -show-icons
;;
*)
echo "unknown IM_HYPRLAND_SHELL_UI: $shell_ui" >&2
exit 2
;;
esac
}
run_window_picker() {
local mode="${1:-}"
case "$mode" in
go|bring|replace) ;;
*)
echo "usage: hypr_shell_ui window {go|bring|replace}" >&2
exit 2
;;
esac
if [[ "$shell_ui" == "noctalia" && "${IM_HYPRLAND_NOCTALIA_WINDOW_PICKER:-0}" == "1" ]]; then
# Future Noctalia launcher-provider hook. Until that plugin exists or if it
# fails to load, keep the existing rofi picker as the working path.
run_noctalia "plugin:hypr-window-picker" "$mode" 2>/dev/null && exit 0
fi
exec hypr_rofi_window "$mode"
}
case "${1:-}" in
launcher)
run_launcher
;;
run)
exec rofi -show run
;;
control-center)
if [[ "$shell_ui" == "noctalia" ]]; then
run_noctalia controlCenter toggle || true
fi
exit 0
;;
settings)
if [[ "$shell_ui" == "noctalia" ]]; then
run_noctalia settings toggle || true
fi
exit 0
;;
window)
shift
run_window_picker "$@"
;;
*)
echo "usage: hypr_shell_ui {launcher|run|control-center|settings|window}" >&2
exit 2
;;
esac

View File

@@ -1,275 +0,0 @@
#!/usr/bin/env python3
import argparse
import json
import os
import subprocess
import sys
from pathlib import Path
CONFIG_PATH = Path.home() / ".config" / "roborock-control" / "config.json"
PYTHON_ENV_EXPR = (
"with import <nixpkgs> {}; "
"python313.withPackages (ps: [ ps.python-roborock ps.pyyaml ps.pyshark ])"
)
COMMON_COMMANDS = {
"start": ("app_start", None),
"pause": ("app_pause", None),
"stop": ("app_stop", None),
"dock": ("app_charge", None),
"find": ("find_me", None),
"dust": ("app_start_collect_dust", None),
"stop-dust": ("app_stop_collect_dust", None),
"wash": ("app_start_wash", None),
"stop-wash": ("app_stop_wash", None),
}
def load_config():
if not CONFIG_PATH.exists():
return {}
return json.loads(CONFIG_PATH.read_text())
def save_config(config):
CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
CONFIG_PATH.write_text(json.dumps(config, indent=2, sort_keys=True) + "\n")
def roborock_args(*args):
if os.getenv("ROBOROCK_CONTROL_RUNNER") == "direct":
return ["roborock", *args]
return [
"nix",
"shell",
"--impure",
"--expr",
PYTHON_ENV_EXPR,
"--command",
"roborock",
*args,
]
def run_roborock(*args, capture=False, check=True):
kwargs = {
"text": True,
"check": check,
}
if capture:
kwargs["stdout"] = subprocess.PIPE
kwargs["stderr"] = subprocess.PIPE
return subprocess.run(roborock_args(*args), **kwargs)
def configured_device_id(args, config):
return args.device_id or os.getenv("ROBOROCK_DEVICE_ID") or config.get("device_id")
def infer_single_device_id():
result = run_roborock("list-devices", capture=True)
devices = json.loads(result.stdout)
if len(devices) != 1:
names = ", ".join(sorted(devices)) or "none"
raise SystemExit(
"Could not infer a default device. "
f"Found {len(devices)} devices: {names}. "
"Pass --device-id or run 'roborock-control config --device-id <id>'."
)
return next(iter(devices.values()))
def get_device_id(args, config):
return configured_device_id(args, config) or infer_single_device_id()
def handle_config(args):
config = load_config()
changed = False
if args.clear:
config = {}
changed = True
if args.device_id is not None:
config["device_id"] = args.device_id
changed = True
if args.email is not None:
config["email"] = args.email
changed = True
if changed:
save_config(config)
print(json.dumps(config, indent=2, sort_keys=True))
def handle_login(args):
config = load_config()
email = args.email or os.getenv("ROBOROCK_EMAIL") or config.get("email")
if not email:
raise SystemExit(
"Email is required. Pass --email or run "
"'roborock-control config --email <address>'."
)
cli_args = ["login", "--email", email]
if args.reauth:
cli_args.append("--reauth")
if args.password_command:
password = subprocess.check_output(args.password_command, shell=True, text=True).strip()
cli_args.extend(["--password", password])
run_roborock(*cli_args)
def handle_devices(args):
if args.refresh:
run_roborock("discover")
run_roborock("list-devices")
def command_with_device(args, upstream_command):
config = load_config()
return [upstream_command, "--device_id", get_device_id(args, config)]
def handle_home(args):
cli_args = command_with_device(args, "home")
if args.refresh:
cli_args.append("--refresh")
run_roborock(*cli_args)
def handle_maps(args):
run_roborock(*command_with_device(args, "maps"))
def handle_rooms(args):
run_roborock(*command_with_device(args, "rooms"))
def handle_map_data(args):
cli_args = command_with_device(args, "map-data")
if args.include_path:
cli_args.append("--include_path")
run_roborock(*cli_args)
def handle_map_image(args):
run_roborock(
*command_with_device(args, "map-image"),
"--output-file",
args.output_file,
)
def handle_command(args):
config = load_config()
params = args.params
cli_args = [
"command",
"--device_id",
get_device_id(args, config),
"--cmd",
args.command_name,
]
if params is not None:
cli_args.extend(["--params", params])
run_roborock(*cli_args)
def handle_common_command(args):
command_name, params = COMMON_COMMANDS[args.action]
config = load_config()
cli_args = [
"command",
"--device_id",
get_device_id(args, config),
"--cmd",
command_name,
]
if params is not None:
cli_args.extend(["--params", params])
run_roborock(*cli_args)
def handle_status(args):
config = load_config()
run_roborock("status", "--device_id", get_device_id(args, config))
def handle_upstream(args):
run_roborock(*args.args)
def build_parser():
parser = argparse.ArgumentParser(
description="Control Roborock vacuums through python-roborock."
)
parser.add_argument(
"--device-id",
help="Roborock device id. Defaults to ROBOROCK_DEVICE_ID, saved config, or the only cached device.",
)
subparsers = parser.add_subparsers(dest="subcommand", required=True)
config_parser = subparsers.add_parser("config", help="Show or update saved defaults")
config_parser.add_argument("--device-id")
config_parser.add_argument("--email")
config_parser.add_argument("--clear", action="store_true")
config_parser.set_defaults(func=handle_config)
login_parser = subparsers.add_parser("login", help="Login with email code or password")
login_parser.add_argument("--email")
login_parser.add_argument("--reauth", action="store_true")
login_parser.add_argument(
"--password-command",
help="Shell command that prints the Roborock password, e.g. 'pass show path/to/login'.",
)
login_parser.set_defaults(func=handle_login)
devices_parser = subparsers.add_parser("devices", help="List cached devices")
devices_parser.add_argument("--refresh", action="store_true", help="Refresh discovery first")
devices_parser.set_defaults(func=handle_devices)
subparsers.add_parser("status", help="Show vacuum status").set_defaults(func=handle_status)
home_parser = subparsers.add_parser("home", help="Discover and cache home layout")
home_parser.add_argument("--refresh", action="store_true")
home_parser.set_defaults(func=handle_home)
subparsers.add_parser("maps", help="Show map metadata").set_defaults(func=handle_maps)
subparsers.add_parser("rooms", help="Show room metadata").set_defaults(func=handle_rooms)
map_data_parser = subparsers.add_parser("map-data", help="Show parsed map data as JSON")
map_data_parser.add_argument("--include-path", action="store_true")
map_data_parser.set_defaults(func=handle_map_data)
map_image_parser = subparsers.add_parser("map-image", help="Save the map image")
map_image_parser.add_argument("output_file")
map_image_parser.set_defaults(func=handle_map_image)
raw_parser = subparsers.add_parser("raw", help="Send a raw python-roborock command")
raw_parser.add_argument("command_name")
raw_parser.add_argument("params", nargs="?")
raw_parser.set_defaults(func=handle_command)
upstream_parser = subparsers.add_parser("cli", help="Pass arguments to the upstream CLI")
upstream_parser.add_argument("args", nargs=argparse.REMAINDER)
upstream_parser.set_defaults(func=handle_upstream)
for action in sorted(COMMON_COMMANDS):
subparsers.add_parser(action).set_defaults(func=handle_common_command, action=action)
return parser
def main():
parser = build_parser()
args = parser.parse_args()
try:
args.func(args)
except subprocess.CalledProcessError as error:
if error.stdout:
print(error.stdout, end="")
if error.stderr:
print(error.stderr, end="", file=sys.stderr)
raise SystemExit(error.returncode)
if __name__ == "__main__":
main()

View File

@@ -69,7 +69,6 @@
multiplexerAliases = import ../../shared/multiplexer-aliases.nix;
excludedTopLevelEntries = [
"codex"
"config"
];

View File

@@ -5,10 +5,9 @@
...
}: let
cfg = config.myModules.codexGeneratedSkills;
oos = config.lib.file.mkOutOfStoreSymlink;
in {
options.myModules.codexGeneratedSkills = {
enable = lib.mkEnableOption "Codex home setup";
enable = lib.mkEnableOption "generated Codex skill setup";
codexHome = lib.mkOption {
type = lib.types.str;
@@ -16,12 +15,6 @@ in {
description = "Codex home directory.";
};
worktreeCodexDir = lib.mkOption {
type = lib.types.str;
default = "${config.home.homeDirectory}/dotfiles/dotfiles/codex";
description = "Codex dotfiles directory in the live worktree.";
};
skillsDir = lib.mkOption {
type = lib.types.str;
default = "${cfg.codexHome}/skills";
@@ -36,67 +29,6 @@ in {
};
config = lib.mkIf cfg.enable {
home.file = {
".codex/.gitignore" = {
force = true;
source = oos "${cfg.worktreeCodexDir}/.gitignore";
};
".codex/AGENTS.md" = {
force = true;
source = oos "${cfg.worktreeCodexDir}/AGENTS.md";
};
".codex/skills" = {
force = true;
source = oos "${cfg.worktreeCodexDir}/skills";
};
};
home.activation.prepareCodexDirectory = lib.hm.dag.entryBefore ["checkLinkTargets"] ''
codex_home=${lib.escapeShellArg cfg.codexHome}
worktree_codex=${lib.escapeShellArg cfg.worktreeCodexDir}
if [ -L "$codex_home" ] && [ "$(readlink "$codex_home")" = "$worktree_codex" ]; then
rm -f "$codex_home"
mkdir -p "$codex_home"
elif [ ! -e "$codex_home" ]; then
mkdir -p "$codex_home"
elif [ ! -d "$codex_home" ]; then
echo "Skipping Codex setup because $codex_home is not a directory" >&2
exit 1
fi
'';
home.activation.generateCodexConfig = lib.hm.dag.entryAfter ["writeBoundary"] ''
codex_home=${lib.escapeShellArg cfg.codexHome}
base=${lib.escapeShellArg "${cfg.worktreeCodexDir}/config.toml"}
local_config=${lib.escapeShellArg "${cfg.worktreeCodexDir}/config.local.toml"}
target="$codex_home/config.toml"
if [ ! -r "$base" ]; then
echo "Missing shared Codex config at $base" >&2
exit 1
fi
mkdir -p "$codex_home"
tmp="$(mktemp "$codex_home/config.toml.XXXXXX")"
trap 'rm -f "$tmp"' EXIT
chmod 600 "$tmp"
cat "$base" > "$tmp"
if [ -r "$local_config" ]; then
printf '\n' >> "$tmp"
cat "$local_config" >> "$tmp"
fi
if [ -e "$target" ] && cmp -s "$tmp" "$target"; then
rm -f "$tmp"
else
mv -f "$tmp" "$target"
fi
'';
home.activation.setupCodexGeneratedSkills = lib.hm.dag.entryAfter ["writeBoundary"] ''
codex_home=${lib.escapeShellArg cfg.codexHome}
skills_dir=${lib.escapeShellArg cfg.skillsDir}

View File

@@ -29,14 +29,12 @@
./laptop.nix
./nix.nix
./notifications-tray-icon.nix
./noctalia.nix
./nvidia.nix
./options.nix
./plasma.nix
./postgres.nix
./rabbitmq.nix
./quickshell.nix
./remote-hyprland.nix
./secrets.nix
./ssh.nix
./sni.nix

View File

@@ -6,20 +6,7 @@
makeEnable,
...
}:
let
cfg = config.myModules.desktop;
desktopShellUi = pkgs.writeShellApplication {
name = "desktop_shell_ui";
runtimeInputs = [
pkgs.bash
pkgs.coreutils
pkgs.systemd
];
text = ''
exec ${../dotfiles/lib/bin/desktop_shell_ui} "$@"
'';
};
enabledModule = makeEnable config "myModules.desktop" true {
makeEnable config "myModules.desktop" true {
services.greenclip.enable = true;
imports = [
./fonts.nix
@@ -53,11 +40,8 @@ let
enable = true;
};
environment.sessionVariables = {
# This is for the benefit of VSCODE running natively in wayland
NIXOS_OZONE_WL = "1";
IM_HYPRLAND_SHELL_UI = cfg.shellUi;
};
environment.sessionVariables.NIXOS_OZONE_WL = "1";
system.activationScripts.playwrightChromeCompat.text = lib.optionalString (pkgs.stdenv.hostPlatform.system == "x86_64-linux") ''
# Playwright's Chrome channel lookup expects the FHS path below.
@@ -103,8 +87,6 @@ let
environment.systemPackages = with pkgs;
[
desktopShellUi
# Appearance
adwaita-icon-theme
hicolor-icon-theme
@@ -187,18 +169,4 @@ let
]
else []
);
};
in
enabledModule
// {
options = lib.recursiveUpdate enabledModule.options {
myModules.desktop.shellUi = lib.mkOption {
type = lib.types.enum [ "noctalia" "taffybar" ];
default = "taffybar";
description = ''
Desktop shell UI used by Hyprland-oriented bindings. This controls
the active shell service and Hyprland launcher/window picker dispatch.
'';
};
};
}

View File

@@ -1,8 +1,5 @@
{
config,
lib,
...
}: let
{ config, lib, ... }:
let
# Replicate the useful part of rcm/rcup:
# - dotfiles live in ~/dotfiles/dotfiles (no leading dots in the repo)
# - links in $HOME add a leading '.' to the first path component
@@ -19,9 +16,6 @@
srcDotfiles = ../dotfiles;
excludedTop = [
# Managed by nix-shared/home-manager/codex-generated-skills.nix so
# config.toml can be generated from shared and machine-local fragments.
"codex"
# Managed by Nix directly (PATH/fpath), not meant to appear as ~/.lib.
"lib"
# Avoid colliding with HM-generated xdg.configFile entries for now.
@@ -30,23 +24,25 @@
"emacs.d"
];
firstComponent = rel: let
parts = lib.splitString "/" rel;
in
lib.elemAt parts 0;
firstComponent = rel:
let parts = lib.splitString "/" rel;
in lib.elemAt parts 0;
isExcluded = rel: lib.elem (firstComponent rel) excludedTop;
listFilesRec = dir: let
listFilesRec = dir:
let
entries = builtins.readDir dir;
names = builtins.attrNames entries;
go = name: let
go = name:
let
ty = entries.${name};
path = dir + "/${name}";
in
if ty == "directory"
then map (p: "${name}/${p}") (listFilesRec path)
else [name];
if ty == "directory" then
map (p: "${name}/${p}") (listFilesRec path)
else
[ name ];
in
lib.concatLists (map go names);
@@ -57,10 +53,9 @@
lib.nameValuePair ".${rel}" {
source = oos "${worktreeDotfiles}/${rel}";
};
in {
imports = [
../nix-shared/home-manager/codex-generated-skills.nix
];
in
{
imports = [ ../nix-shared/home-manager/codex-generated-skills.nix ];
home.file =
builtins.listToAttrs (map mkManaged managedRelFiles);
@@ -79,4 +74,5 @@ in {
echo "Skipping ~/.emacs.d relink because it is not a symlink" >&2
fi
'';
}

View File

@@ -10,8 +10,6 @@ makeEnable config "myModules.extra" false {
signal-desktop
gource
gimp
kef
roborock-control
texlive.combined.scheme-full
tor
yt-dlp

306
nixos/flake.lock generated
View File

@@ -335,15 +335,15 @@
"flake-compat_3": {
"flake": false,
"locked": {
"lastModified": 1696426674,
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
"owner": "edolstra",
"lastModified": 1767039857,
"narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=",
"owner": "NixOS",
"repo": "flake-compat",
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
"rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab",
"type": "github"
},
"original": {
"owner": "edolstra",
"owner": "NixOS",
"repo": "flake-compat",
"type": "github"
}
@@ -353,13 +353,13 @@
"locked": {
"lastModified": 1767039857,
"narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=",
"owner": "NixOS",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab",
"type": "github"
},
"original": {
"owner": "NixOS",
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
@@ -367,11 +367,11 @@
"flake-compat_5": {
"flake": false,
"locked": {
"lastModified": 1767039857,
"narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=",
"lastModified": 1696426674,
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab",
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
"type": "github"
},
"original": {
@@ -502,24 +502,6 @@
"type": "github"
}
},
"flake-utils_2": {
"inputs": {
"systems": "systems_3"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"git-blame-rank": {
"inputs": {
"fenix": "fenix",
@@ -685,7 +667,6 @@
"gitignore_3": {
"inputs": {
"nixpkgs": [
"imalison-taffybar",
"taffybar",
"weeder-nix",
"pre-commit-hooks",
@@ -731,7 +712,7 @@
"hercules-ci-effects_2": {
"inputs": {
"flake-parts": "flake-parts_5",
"nixpkgs": "nixpkgs_7"
"nixpkgs": "nixpkgs_6"
},
"locked": {
"lastModified": 1701009247,
@@ -799,16 +780,15 @@
]
},
"locked": {
"lastModified": 1777475074,
"narHash": "sha256-shgepEMtMB532/df5QfociNzTiqimuoBbJssw0WPVH4=",
"lastModified": 1777317717,
"narHash": "sha256-Rj4vx0RvEWtnpnizggWRtrGe092bXiGLLt0WijwYWtI=",
"owner": "colonelpanic8",
"repo": "hyprNStack",
"rev": "d43c8a506b32dc5057fdce0569c38f401c01eb60",
"rev": "94607cd53f2ddac88f6b26261393275e7dd590ef",
"type": "github"
},
"original": {
"owner": "colonelpanic8",
"ref": "hyprland-lua-integration",
"repo": "hyprNStack",
"type": "github"
}
@@ -1104,16 +1084,16 @@
]
},
"locked": {
"lastModified": 1777471981,
"narHash": "sha256-cd3pQg+vKv6vht4xzButsi/Kaw9P4d3itm46jYXyiDM=",
"owner": "colonelpanic8",
"lastModified": 1767020608,
"narHash": "sha256-BSRT1Uu1ot4WfMfZc6KW0nwpmt2xl9wpUqmH/JoMTfk=",
"owner": "hyprwm",
"repo": "hyprland-plugins",
"rev": "725e354bbee982566068b5b90fec4fcd787c1036",
"rev": "d7b67e8f4ba8ebeee4ce899348fcee6291512169",
"type": "github"
},
"original": {
"owner": "colonelpanic8",
"ref": "hyprexpo-v0.53.0-custom",
"owner": "hyprwm",
"ref": "v0.53.0",
"repo": "hyprland-plugins",
"type": "github"
}
@@ -1135,16 +1115,16 @@
]
},
"locked": {
"lastModified": 1777492340,
"narHash": "sha256-9uRI/opXD+zOK2BLlzQ2NluRJL1SRSdUO2tlvbUJ7Ys=",
"lastModified": 1777413654,
"narHash": "sha256-lVGYGUWf9ynV5lR8QAygAfmYRkko1btIe26UcQ1bGXw=",
"owner": "colonelpanic8",
"repo": "hyprland-plugins",
"rev": "ca1992973b5fb8ab95e88e8ffba16792c41568ae",
"rev": "ff36c04b270c26fcd53e623fc688e4eb41672897",
"type": "github"
},
"original": {
"owner": "colonelpanic8",
"ref": "codex/fix-main-ci",
"ref": "hyprexpo-lua-hyprland",
"repo": "hyprland-plugins",
"type": "github"
}
@@ -1548,7 +1528,9 @@
"nixpkgs": [
"nixpkgs"
],
"taffybar": "taffybar",
"taffybar": [
"taffybar"
],
"xmonad": [
"xmonad"
]
@@ -1598,11 +1580,11 @@
]
},
"locked": {
"lastModified": 1777434099,
"narHash": "sha256-GutKXyfGI7o89Dge4bP0yt0CQn1rqA6LyYDOH4GemdE=",
"lastModified": 1777261868,
"narHash": "sha256-30E1RBr0FGrf1IdXi2OKua+vQ4sUvjwUq6lfC1qcBug=",
"owner": "colonelpanic8",
"repo": "keepbook",
"rev": "240fe454c26e7dbdd25a2fa4f0b436bf1c4f937b",
"rev": "2e178e7ba864af0a880456dca241615dd75afd24",
"type": "github"
},
"original": {
@@ -1629,10 +1611,10 @@
},
"nix": {
"inputs": {
"flake-compat": "flake-compat_4",
"flake-compat": "flake-compat_3",
"flake-parts": "flake-parts",
"git-hooks-nix": "git-hooks-nix",
"nixpkgs": "nixpkgs_5",
"nixpkgs": "nixpkgs_4",
"nixpkgs-23-11": "nixpkgs-23-11",
"nixpkgs-regression": "nixpkgs-regression"
},
@@ -1689,7 +1671,7 @@
},
"nixos-wsl": {
"inputs": {
"flake-compat": "flake-compat_5",
"flake-compat": "flake-compat_4",
"nixpkgs": [
"nixpkgs"
]
@@ -1823,22 +1805,6 @@
}
},
"nixpkgs_4": {
"locked": {
"lastModified": 1776877367,
"narHash": "sha256-EHq1/OX139R1RvBzOJ0aMRT3xnWyqtHBRUBuO1gFzjI=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "0726a0ecb6d4e08f6adced58726b95db924cef57",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_5": {
"locked": {
"lastModified": 1771903837,
"narHash": "sha256-jEA8WggGKtMFeNeCKq3NK8cLEjJmG6/RLUElYYbBZ0E=",
@@ -1851,7 +1817,7 @@
"url": "https://channels.nixos.org/nixos-25.11/nixexprs.tar.xz"
}
},
"nixpkgs_6": {
"nixpkgs_5": {
"locked": {
"lastModified": 1776877367,
"narHash": "sha256-EHq1/OX139R1RvBzOJ0aMRT3xnWyqtHBRUBuO1gFzjI=",
@@ -1867,7 +1833,7 @@
"type": "github"
}
},
"nixpkgs_7": {
"nixpkgs_6": {
"locked": {
"lastModified": 1697723726,
"narHash": "sha256-SaTWPkI8a5xSHX/rrKzUe+/uVNy6zCGMXgoeMb7T9rg=",
@@ -1883,7 +1849,7 @@
"type": "github"
}
},
"nixpkgs_8": {
"nixpkgs_7": {
"locked": {
"lastModified": 1703255338,
"narHash": "sha256-Z6wfYJQKmDN9xciTwU3cOiOk+NElxdZwy/FiHctCzjU=",
@@ -1903,7 +1869,7 @@
"inputs": {
"flake-parts": "flake-parts_4",
"hercules-ci-effects": "hercules-ci-effects_2",
"nixpkgs": "nixpkgs_8",
"nixpkgs": "nixpkgs_7",
"osx-kvm": "osx-kvm"
},
"locked": {
@@ -1920,50 +1886,6 @@
"type": "github"
}
},
"noctalia": {
"inputs": {
"nixpkgs": [
"nixpkgs"
],
"noctalia-qs": "noctalia-qs"
},
"locked": {
"lastModified": 1777427472,
"narHash": "sha256-kqcfLdxb+CqTroMErCScvx6YQcZYJcf6X+z5I8kBJK8=",
"owner": "noctalia-dev",
"repo": "noctalia-shell",
"rev": "9f8dd48c8df5ab1f7f87ddf9842627e1e5682186",
"type": "github"
},
"original": {
"owner": "noctalia-dev",
"repo": "noctalia-shell",
"type": "github"
}
},
"noctalia-qs": {
"inputs": {
"nixpkgs": [
"noctalia",
"nixpkgs"
],
"systems": "systems_4",
"treefmt-nix": "treefmt-nix"
},
"locked": {
"lastModified": 1777380063,
"narHash": "sha256-q5mWOEICcZzr+KnjIwDHV9EXiBxOC9cnBpxZbDAViU8=",
"owner": "noctalia-dev",
"repo": "noctalia-qs",
"rev": "8742a7a748c43bf44eb6862a8ebd3591ed71502d",
"type": "github"
},
"original": {
"owner": "noctalia-dev",
"repo": "noctalia-qs",
"type": "github"
}
},
"notifications-tray-icon": {
"inputs": {
"flake-utils": [
@@ -2133,10 +2055,9 @@
},
"pre-commit-hooks_3": {
"inputs": {
"flake-compat": "flake-compat_3",
"flake-compat": "flake-compat_5",
"gitignore": "gitignore_3",
"nixpkgs": [
"imalison-taffybar",
"nixpkgs"
]
},
@@ -2227,16 +2148,16 @@
"nixified-ai": "nixified-ai",
"nixos-hardware": "nixos-hardware",
"nixos-wsl": "nixos-wsl",
"nixpkgs": "nixpkgs_6",
"nixpkgs": "nixpkgs_5",
"nixtheplanet": "nixtheplanet",
"noctalia": "noctalia",
"notifications-tray-icon": "notifications-tray-icon",
"org-agenda-api": "org-agenda-api",
"railbird-secrets": "railbird-secrets",
"systems": "systems_5",
"systems": "systems_3",
"taffybar": "taffybar",
"vscode-server": "vscode-server",
"xmonad": "xmonad_2",
"xmonad-contrib": "xmonad-contrib_2"
"xmonad": "xmonad",
"xmonad-contrib": "xmonad-contrib"
}
},
"rust-analyzer-src": {
@@ -2338,77 +2259,33 @@
"type": "github"
}
},
"systems_4": {
"locked": {
"lastModified": 1689347949,
"narHash": "sha256-12tWmuL2zgBgZkdoB6qXZsgJEH9LR3oUgpaQq2RbI80=",
"owner": "nix-systems",
"repo": "default-linux",
"rev": "31732fcf5e8fea42e59c2488ad31a0e651500f68",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default-linux",
"type": "github"
}
},
"systems_5": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"taffybar": {
"inputs": {
"flake-utils": "flake-utils_2",
"nixpkgs": "nixpkgs_4",
"weeder-nix": "weeder-nix",
"xmonad": "xmonad",
"xmonad-contrib": "xmonad-contrib"
},
"locked": {
"lastModified": 1777452249,
"narHash": "sha256-Emhn9sIFRVyIlUULDuYjeFcYJld6EAD31TGasYwQsWg=",
"ref": "refs/heads/master",
"rev": "9a6463e68c7bc0a712e49d9ba6c6d1b764260cd7",
"revCount": 2295,
"type": "git",
"url": "file:///home/imalison/dotfiles/dotfiles/config/taffybar/taffybar"
},
"original": {
"type": "git",
"url": "file:///home/imalison/dotfiles/dotfiles/config/taffybar/taffybar"
}
},
"treefmt-nix": {
"inputs": {
"flake-utils": [
"flake-utils"
],
"nixpkgs": [
"noctalia",
"noctalia-qs",
"nixpkgs"
],
"weeder-nix": "weeder-nix",
"xmonad": [
"xmonad"
],
"xmonad-contrib": [
"xmonad-contrib"
]
},
"locked": {
"lastModified": 1775636079,
"narHash": "sha256-pc20NRoMdiar8oPQceQT47UUZMBTiMdUuWrYu2obUP0=",
"owner": "numtide",
"repo": "treefmt-nix",
"rev": "790751ff7fd3801feeaf96d7dc416a8d581265ba",
"lastModified": 1777401169,
"narHash": "sha256-bciN/qFjXYm8ZIKXSc/OssUsLt9GoNs/cU9xT/pw7QY=",
"owner": "taffybar",
"repo": "taffybar",
"rev": "59e3c75990156dcd4353ad9fad5823303e751f0f",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "treefmt-nix",
"owner": "taffybar",
"repo": "taffybar",
"type": "github"
}
},
@@ -2438,7 +2315,6 @@
"weeder-nix": {
"inputs": {
"nixpkgs": [
"imalison-taffybar",
"taffybar",
"nixpkgs"
],
@@ -2541,7 +2417,20 @@
}
},
"xmonad": {
"flake": false,
"inputs": {
"flake-utils": [
"flake-utils"
],
"git-ignore-nix": [
"git-ignore-nix"
],
"nixpkgs": [
"nixpkgs"
],
"unstable": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1776502138,
"narHash": "sha256-mSOpNU1iJvfFh5uwayA6aPxneFMduNW1kG1gV2tGE+c=",
@@ -2552,29 +2441,11 @@
},
"original": {
"owner": "xmonad",
"ref": "master",
"repo": "xmonad",
"type": "github"
}
},
"xmonad-contrib": {
"flake": false,
"locked": {
"lastModified": 1769258911,
"narHash": "sha256-YGEKXs4UmS5QOIELJTdCiMzTktuue+Bd3yFoIKSHuBU=",
"owner": "xmonad",
"repo": "xmonad-contrib",
"rev": "803bc3d12bdcc512ec06856c4f119d37de1ba338",
"type": "github"
},
"original": {
"owner": "xmonad",
"ref": "master",
"repo": "xmonad-contrib",
"type": "github"
}
},
"xmonad-contrib_2": {
"inputs": {
"flake-utils": [
"flake-utils"
@@ -2603,35 +2474,6 @@
"repo": "xmonad-contrib",
"type": "github"
}
},
"xmonad_2": {
"inputs": {
"flake-utils": [
"flake-utils"
],
"git-ignore-nix": [
"git-ignore-nix"
],
"nixpkgs": [
"nixpkgs"
],
"unstable": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1776502138,
"narHash": "sha256-mSOpNU1iJvfFh5uwayA6aPxneFMduNW1kG1gV2tGE+c=",
"owner": "xmonad",
"repo": "xmonad",
"rev": "a618fb32662e44eb5d8276a3dc1925b0233e638b",
"type": "github"
},
"original": {
"owner": "xmonad",
"repo": "xmonad",
"type": "github"
}
}
},
"root": "root",

View File

@@ -102,7 +102,7 @@
};
hyprNStack = {
url = "github:colonelpanic8/hyprNStack?ref=hyprland-lua-integration";
url = "github:colonelpanic8/hyprNStack";
inputs = {
hyprland.follows = "hyprland-lua-config";
nixpkgs.follows = "nixpkgs";
@@ -115,12 +115,12 @@
};
hyprland-plugins = {
url = "github:colonelpanic8/hyprland-plugins?ref=hyprexpo-v0.53.0-custom";
url = "github:hyprwm/hyprland-plugins?ref=v0.53.0";
inputs.hyprland.follows = "hyprland";
};
hyprland-plugins-lua = {
url = "github:colonelpanic8/hyprland-plugins?ref=codex/fix-main-ci";
url = "github:colonelpanic8/hyprland-plugins?ref=hyprexpo-lua-hyprland";
inputs.hyprland.follows = "hyprland-lua-config";
};
@@ -231,11 +231,6 @@
inputs.nixpkgs.follows = "nixpkgs";
};
noctalia = {
url = "github:noctalia-dev/noctalia-shell";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs = inputs @ {
@@ -448,7 +443,6 @@
"https://colonelpanic8-dotfiles.cachix.org"
"https://codex-cli.cachix.org"
"https://claude-code.cachix.org"
"https://noctalia.cachix.org"
];
extra-trusted-substituters = [
"https://ai.cachix.org"
@@ -470,7 +464,6 @@
"colonelpanic8-dotfiles.cachix.org-1:O6GF3nptpeMFapX29okzO92eSWXR36zqW6ZF2C8P0eQ="
"codex-cli.cachix.org-1:1Br3H1hHoRYG22n//cGKJOk3cQXgYobUel6O8DgSing="
"claude-code.cachix.org-1:YeXf2aNu7UTX8Vwrze0za1WEDS+4DuI2kVeWEE4fsRk="
"noctalia.cachix.org-1:pCOR47nnMEo5thcxNDtzWpOxNFQsBRglJzxWPp3dkU4="
];
};
nixosConfigurations =

View File

@@ -6,6 +6,33 @@
...
}: let
mimeMap = desktopId: mimeTypes: lib.genAttrs mimeTypes (_: [desktopId]);
in {
# Automatic garbage collection of old home-manager generations
nix.gc = {
automatic = true;
dates = "weekly";
options = "--delete-older-than 7d";
};
xdg.configFile."greenclip.toml".text = ''
[greenclip]
history_file = "~/.cache/greenclip.history"
max_history_length = 50
max_selection_size_bytes = 0
trim_space_from_selection = true
use_primary_selection_as_input = false
blacklisted_applications = []
enable_image_support = true
image_cache_directory = "~/.cache/greenclip"
static_history = []
'';
xdg.configFile."zellij/config.kdl".source =
config.lib.file.mkOutOfStoreSymlink
"${config.home.homeDirectory}/dotfiles/dotfiles/config/zellij/config.kdl";
xdg.mimeApps = lib.mkIf nixos.config.myModules.desktop.enable (
let
browser = "google-chrome.desktop";
imageViewer = "org.kde.gwenview.desktop";
pdfViewer = "okularApplication_pdf.desktop";
@@ -20,6 +47,7 @@
wordProcessor = "writer.desktop";
spreadsheet = "calc.desktop";
presentation = "impress.desktop";
defaultApplications =
(mimeMap imageViewer [
"image/avif"
@@ -135,82 +163,12 @@
"x-scheme-handler/element" = ["element-desktop.desktop"];
"x-scheme-handler/magnet" = ["transmission-gtk.desktop"];
};
mimeAppsListText = let
formatApplications = applications:
lib.concatStringsSep "\n" (
lib.mapAttrsToList (
mimeType: desktopIds: "${mimeType}=${lib.concatStringsSep ";" desktopIds};"
)
applications
);
in ''
[Added Associations]
${formatApplications defaultApplications}
[Default Applications]
${formatApplications defaultApplications}
[Removed Associations]
'';
in {
# Automatic garbage collection of old home-manager generations
nix.gc = {
automatic = true;
dates = "weekly";
options = "--delete-older-than 7d";
};
xdg.configFile."greenclip.toml".text = ''
[greenclip]
history_file = "~/.cache/greenclip.history"
max_history_length = 50
max_selection_size_bytes = 0
trim_space_from_selection = true
use_primary_selection_as_input = false
blacklisted_applications = []
enable_image_support = true
image_cache_directory = "~/.cache/greenclip"
static_history = []
'';
xdg.configFile."zellij/config.kdl".source =
config.lib.file.mkOutOfStoreSymlink
"${config.home.homeDirectory}/dotfiles/dotfiles/config/zellij/config.kdl";
xdg.configFile."menus/applications.menu" = lib.mkIf nixos.config.myModules.desktop.enable {
source = "${pkgs.kdePackages.plasma-workspace}/etc/xdg/menus/plasma-applications.menu";
};
xdg.configFile."kde-mimeapps.list" = lib.mkIf nixos.config.myModules.desktop.enable {
text = mimeAppsListText;
};
xdg.configFile."none+xmonad-mimeapps.list" = lib.mkIf nixos.config.myModules.desktop.enable {
text = mimeAppsListText;
};
xdg.configFile."xmonad-mimeapps.list" = lib.mkIf nixos.config.myModules.desktop.enable {
text = mimeAppsListText;
};
xdg.configFile."hyprland-mimeapps.list" = lib.mkIf nixos.config.myModules.desktop.enable {
text = mimeAppsListText;
};
xdg.dataFile."mimeapps.list" = lib.mkIf nixos.config.myModules.desktop.enable {
text = mimeAppsListText;
};
xdg.dataFile."applications/kde-mimeapps.list" = lib.mkIf nixos.config.myModules.desktop.enable {
text = mimeAppsListText;
};
xdg.mimeApps = lib.mkIf nixos.config.myModules.desktop.enable {
enable = true;
associations.added = defaultApplications;
inherit defaultApplications;
};
}
);
home.activation.refreshChromeDesktopMimeCache = lib.hm.dag.entryAfter ["writeBoundary"] ''
applications_dir="$HOME/.local/share/applications"
@@ -222,7 +180,6 @@ in {
do
if [ -f "$desktop_file" ]; then
${pkgs.gnused}/bin/sed -i \
-e 's,application/pdf;,,g' \
-e 's,image/gif;,,g' \
-e 's,image/jpeg;,,g' \
-e 's,image/png;,,g' \
@@ -231,34 +188,10 @@ in {
fi
done
for desktop_file in "$applications_dir"/okular*.desktop "$applications_dir"/vlc*.desktop; do
if [ -f "$desktop_file" ]; then
${pkgs.gnused}/bin/sed -i \
-e 's,image/avif;,,g' \
-e 's,image/bmp;,,g' \
-e 's,image/gif;,,g' \
-e 's,image/heic;,,g' \
-e 's,image/heif;,,g' \
-e 's,image/jpeg;,,g' \
-e 's,image/jxl;,,g' \
-e 's,image/png;,,g' \
-e 's,image/svg+xml;,,g' \
-e 's,image/svg+xml-compressed;,,g' \
-e 's,image/tiff;,,g' \
-e 's,image/vnd.microsoft.icon;,,g' \
-e 's,image/webp;,,g' \
"$desktop_file"
fi
done
${pkgs.desktop-file-utils}/bin/update-desktop-database "$applications_dir" >/dev/null 2>&1 || true
fi
'';
home.activation.refreshKdeServiceCache = lib.hm.dag.entryAfter ["refreshChromeDesktopMimeCache"] ''
${pkgs.kdePackages.kservice}/bin/kbuildsycoca6 --noincremental >/dev/null 2>&1 || true
'';
xsession = {
enable = true;
preferStatusNotifierItems = true;

View File

@@ -1,11 +1,4 @@
{
config,
pkgs,
lib,
makeEnable,
inputs,
...
}:
{ config, pkgs, lib, makeEnable, inputs, ... }:
let
cfg = config.myModules.hyprland;
system = pkgs.stdenv.hostPlatform.system;
@@ -18,29 +11,88 @@ let
inputs.hyprNStack.packages.${system}.hyprNStack
inputs.hyprland-plugins-lua.packages.${system}.hyprexpo
];
hyprRofiWindow = pkgs.writeShellApplication {
name = "hypr_rofi_window";
runtimeInputs = [
pkgs.python3
pkgs.rofi
hyprlandInput.packages.${system}.hyprland
hyprexpoPatched = inputs.hyprland-plugins.packages.${system}.hyprexpo.overrideAttrs (old: {
patches = (old.patches or [ ]) ++ [
./patches/hyprexpo-pr-612-workspace-numbers.patch
./patches/hyprexpo-pr-616-bring-mode.patch
];
text = ''
exec python3 ${../dotfiles/lib/bin/hypr_rofi_window} "$@"
'';
});
enabledModule = makeEnable config "myModules.hyprland" true {
myModules.taffybar.enable = true;
# Needed for hyprlock authentication without PAM fallback warnings.
security.pam.services.hyprlock = {};
# DDC/CI monitor control for keyboard-driven input switching.
hardware.i2c = {
enable = true;
group = "video";
};
hyprShellUi = pkgs.writeShellApplication {
name = "hypr_shell_ui";
runtimeInputs = [
pkgs.rofi
hyprRofiWindow
inputs.noctalia.packages.${system}.default
programs.hyprland = {
enable = true;
# Keep Hyprland and plugins on a matched flake input for ABI compatibility.
package = hyprlandInput.packages.${system}.hyprland;
# Let UWSM manage the Hyprland session targets
withUWSM = true;
};
home-manager.sharedModules = [
inputs.hyprscratch.homeModules.default
({ config, ... }: {
xdg.configFile."hypr" = {
force = true;
source =
if cfg.useLuaConfigBranch
then ../dotfiles/config/hypr
else config.lib.file.mkOutOfStoreSymlink "${config.home.homeDirectory}/dotfiles/dotfiles/config/hypr";
};
services.kanshi = {
enable = true;
systemdTarget = "graphical-session.target";
settings = [
{
# USB-C connector names can move between DP-* ports across docks/reboots.
# Match the ultrawide by make/model and allow the serial field to vary.
profile.name = "ultrawide-usbc-desk";
profile.outputs = [
{
criteria = "eDP-1";
status = "enable";
mode = "2560x1600@240Hz";
position = "0,0";
scale = 1.0;
}
{
criteria = "Microstep MPG341CX OLED *";
status = "enable";
mode = "3440x1440@240Hz";
position = "2560,0";
scale = 1.0;
}
];
}
{
# When the laptop panel is unavailable (e.g. lid-closed docked use),
# still drive the ultrawide at its full refresh rate.
profile.name = "ultrawide-only";
profile.outputs = [
{
criteria = "Microstep MPG341CX OLED *";
status = "enable";
mode = "3440x1440@240Hz";
position = "0,0";
scale = 1.0;
}
];
}
];
text = ''
exec ${../dotfiles/lib/bin/hypr_shell_ui} "$@"
'';
};
hyprscratchSettings = {
programs.hyprscratch = {
enable = !cfg.useLuaConfigBranch;
settings = {
daemon_options = "clean";
global_options = "";
global_rules = "float;size monitor_w*0.95 monitor_h*0.95;center";
@@ -94,115 +146,12 @@ let
title = "Messages";
};
};
enabledModule = makeEnable config "myModules.hyprland" true {
# Install both shell service units so `desktop_shell_ui set ...` can switch
# between them at runtime without a NixOS rebuild.
myModules.noctalia.enable = lib.mkDefault true;
myModules.taffybar.enable = lib.mkDefault true;
# Needed for hyprlock authentication without PAM fallback warnings.
security.pam.services.hyprlock = { };
# DDC/CI monitor control for keyboard-driven input switching.
hardware.i2c = {
enable = true;
group = "video";
};
programs.hyprland = {
enable = true;
# Keep Hyprland and plugins on a matched flake input for ABI compatibility.
package = hyprlandInput.packages.${system}.hyprland;
# Let UWSM manage the Hyprland session targets
withUWSM = true;
};
home-manager.sharedModules = [
inputs.hyprscratch.homeModules.default
(
{ config, lib, ... }:
let
hyprConfig =
name:
config.lib.file.mkOutOfStoreSymlink "${config.home.homeDirectory}/dotfiles/dotfiles/config/hypr/${name}";
in
{
services.kanshi = {
enable = true;
systemdTarget = "graphical-session.target";
settings = [
{
# USB-C connector names can move between DP-* ports across docks/reboots.
# Match the ultrawide by make/model and allow the serial field to vary.
profile.name = "ultrawide-usbc-desk";
profile.outputs = [
{
criteria = "eDP-1";
status = "enable";
mode = "2560x1600@240Hz";
position = "0,0";
scale = 1.0;
}
{
criteria = "Microstep MPG341CX OLED *";
status = "enable";
mode = "3440x1440@240Hz";
position = "2560,0";
scale = 1.0;
}
];
}
{
# When the laptop panel is unavailable (e.g. lid-closed docked use),
# still drive the ultrawide at its full refresh rate.
profile.name = "ultrawide-only";
profile.outputs = [
{
criteria = "Microstep MPG341CX OLED *";
status = "enable";
mode = "3440x1440@240Hz";
position = "0,0";
scale = 1.0;
}
];
}
];
};
programs.hyprscratch = {
enable = !cfg.useLuaConfigBranch;
settings = { };
};
xdg.configFile."hyprscratch/config.conf" = lib.mkIf (!cfg.useLuaConfigBranch) {
text = lib.hm.generators.toHyprconf {
attrs = hyprscratchSettings;
};
};
xdg.configFile."hypr/hyprland.conf" = {
force = true;
source = hyprConfig "hyprland.conf";
};
xdg.configFile."hypr/hyprland.lua" = lib.mkIf cfg.useLuaConfigBranch {
force = true;
source = hyprConfig "hyprland.lua";
};
xdg.configFile."hypr/hypridle.conf".source = hyprConfig "hypridle.conf";
xdg.configFile."hypr/hyprlock.conf".source = hyprConfig "hyprlock.conf";
xdg.configFile."hypr/scripts".enable = false;
}
)
})
];
# Hyprland-specific packages
environment.systemPackages =
with pkgs;
[
environment.systemPackages = with pkgs; [
# Hyprland utilities
hyprpaper # Wallpaper
hypridle # Idle daemon
@@ -215,26 +164,21 @@ let
slurp # Region selection
swappy # Screenshot annotation
nwg-displays # GUI monitor arrangement
mpvpaper # Layer-shell video screensaver payload
mpv # Graphical screensaver payload
ddcutil # Monitor input switching over DDC/CI
# For scripts
hyprRofiWindow
hyprShellUi
jq
]
++ luaPluginPackages
++ lib.optionals enableExternalPluginPackages [
] ++ luaPluginPackages ++ lib.optionals enableExternalPluginPackages [
# External plugin packages are pinned to the stable 0.53 stack.
# Keep hy3 on the stable stack; the Lua branch uses hyprNStack and the
# forked Lua-compatible hyprexpo input instead.
inputs.hy3.packages.${system}.hy3
inputs.hyprland-plugins.packages.${system}.hyprexpo
hyprexpoPatched
];
};
in
enabledModule
// {
enabledModule // {
options = lib.recursiveUpdate enabledModule.options {
myModules.hyprland.useLuaConfigBranch = lib.mkOption {
type = lib.types.bool;

View File

@@ -184,11 +184,16 @@
type = "Application";
categories = [ "Network" "WebBrowser" ];
mimeType = [
"application/pdf"
"application/rdf+xml"
"application/rss+xml"
"application/xhtml+xml"
"application/xhtml_xml"
"application/xml"
"image/gif"
"image/jpeg"
"image/png"
"image/webp"
"text/html"
"text/xml"
"x-scheme-handler/http"

View File

@@ -63,7 +63,6 @@
# Disable the old multi-node railbird k3s setup
myModules.railbird-k3s.enable = false;
myModules."keepbook-sync".enable = true;
myModules.remote-hyprland.enable = true;
# Mirror the old biskcomp "Syncthing hosting" pattern: serve the synced railbird tree over HTTPS with autoindex.
services.nginx.virtualHosts."syncthing.railbird.ai" = {

View File

@@ -113,9 +113,6 @@
codex = inputs.codex-cli-nix.packages.${prev.stdenv.hostPlatform.system}.default;
claude-code = inputs.claude-code-nix.packages.${prev.stdenv.hostPlatform.system}.default;
git-sync-rs = inputs.git-sync-rs.packages.${prev.stdenv.hostPlatform.system}.default;
kef = final.callPackage ./packages/kef {};
pykefcontrol = final.python3Packages.callPackage ./packages/pykefcontrol {};
roborock-control = final.callPackage ./packages/roborock-control {};
})
]
++ (

View File

@@ -1,87 +0,0 @@
{
config,
inputs,
lib,
makeEnable,
pkgs,
...
}:
let
system = pkgs.stdenv.hostPlatform.system;
noctaliaPackage = inputs.noctalia.packages.${system}.default;
waitForWayland = pkgs.writeShellScript "noctalia-wait-for-wayland" ''
runtime_dir="''${XDG_RUNTIME_DIR:-/run/user/$(${pkgs.coreutils}/bin/id -u)}"
for _ in $(${pkgs.coreutils}/bin/seq 1 50); do
if [ -n "''${WAYLAND_DISPLAY:-}" ] && [ -S "$runtime_dir/$WAYLAND_DISPLAY" ]; then
exit 0
fi
${pkgs.coreutils}/bin/sleep 0.1
done
echo "Wayland socket not ready: WAYLAND_DISPLAY=''${WAYLAND_DISPLAY:-<unset>} XDG_RUNTIME_DIR=$runtime_dir" >&2
exit 1
'';
in
makeEnable config "myModules.noctalia" false {
environment.systemPackages = [
noctaliaPackage
];
# Noctalia's battery widget talks to UPower. Hosts that deliberately do not
# have batteries can still override this back to false.
services.upower.enable = lib.mkDefault true;
home-manager.sharedModules = [
inputs.noctalia.homeModules.default
({ lib, ... }: {
programs.noctalia-shell = {
enable = true;
# This module provides the Hyprland-scoped service below.
systemd.enable = false;
};
home.activation.noctaliaLauncherOverviewLayer =
lib.hm.dag.entryAfter [ "writeBoundary" ] ''
settings_file="$HOME/.config/noctalia/settings.json"
settings_tmp="$(${pkgs.coreutils}/bin/mktemp)"
${pkgs.coreutils}/bin/mkdir -p "$(${pkgs.coreutils}/bin/dirname "$settings_file")"
if [ -e "$settings_file" ] && ${lib.getExe pkgs.jq} -e . "$settings_file" >/dev/null 2>&1; then
${lib.getExe pkgs.jq} \
'.appLauncher = (.appLauncher // {}) | .appLauncher.overviewLayer = true' \
"$settings_file" > "$settings_tmp"
${pkgs.coreutils}/bin/mv "$settings_tmp" "$settings_file"
else
${pkgs.coreutils}/bin/printf '%s\n' \
'{' \
' "appLauncher": {' \
' "overviewLayer": true' \
' }' \
'}' > "$settings_file"
${pkgs.coreutils}/bin/rm -f "$settings_tmp"
fi
'';
systemd.user.services.noctalia-shell = {
Unit = {
Description = "Noctalia Shell";
Documentation = "https://docs.noctalia.dev";
PartOf = [ "hyprland-session.target" ];
After = [ "hyprland-session.target" ];
};
Service = {
ExecCondition = "/run/current-system/sw/bin/desktop_shell_ui exec-condition noctalia";
ExecStartPre = "${waitForWayland}";
ExecStart = "${lib.getExe noctaliaPackage} --no-duplicate";
Restart = "on-failure";
RestartSec = 1;
};
Install.WantedBy = [ "hyprland-session.target" ];
};
})
];
}

View File

@@ -1,30 +0,0 @@
{
lib,
python3,
python3Packages,
writeShellApplication,
}:
let
pykefcontrol = python3Packages.callPackage ../pykefcontrol {};
python = python3.withPackages (ps: [
pykefcontrol
ps.zeroconf
]);
in
writeShellApplication {
name = "kef";
runtimeInputs = [ python ];
text = ''
exec python ${./kef.py} "$@"
'';
meta = {
description = "Command-line controller for KEF W2 speakers";
license = lib.licenses.mit;
maintainers = with lib.maintainers; [ imalison ];
mainProgram = "kef";
};
}

View File

@@ -1,251 +0,0 @@
#!/usr/bin/env python3
import argparse
import json
import os
import sys
import threading
import time
import requests
from pykefcontrol.kef_connector import KefConnector
from zeroconf import IPVersion, ServiceBrowser, ServiceListener, Zeroconf
SOURCES = ("wifi", "bluetooth", "tv", "optic", "coaxial", "analog", "standby")
DISCOVERY_SERVICE_TYPES = (
"_kef-info._tcp.local.",
"_sues800device._tcp.local.",
"_http._tcp.local.",
)
class KefDiscoveryListener(ServiceListener):
def __init__(self):
self._lock = threading.Lock()
self.speakers = {}
def add_service(self, zeroconf, service_type, name):
self._record_service(zeroconf, service_type, name)
def update_service(self, zeroconf, service_type, name):
self._record_service(zeroconf, service_type, name)
def remove_service(self, zeroconf, service_type, name):
return None
def _record_service(self, zeroconf, service_type, name):
info = zeroconf.get_service_info(service_type, name, timeout=1000)
if info is None:
return
properties = {
key.decode("utf-8", errors="replace"): (
value.decode("utf-8", errors="replace") if value is not None else ""
)
for key, value in info.properties.items()
}
addresses = [
address
for address in info.parsed_addresses(IPVersion.V4Only)
if not address.startswith("127.")
]
if not addresses:
return
is_kef = (
service_type in ("_kef-info._tcp.local.", "_sues800device._tcp.local.")
or properties.get("manufacturer", "").lower() == "kef"
or "kef" in name.lower()
or "ls50" in name.lower()
)
if not is_kef:
return
key = addresses[0]
with self._lock:
existing = self.speakers.get(key, {})
self.speakers[key] = {
**existing,
"address": addresses[0],
"hostname": (info.server.rstrip(".") if info.server else None)
or existing.get("hostname"),
"name": properties.get("name")
or properties.get("fn")
or existing.get("name")
or strip_service_name(name),
"model": properties.get("modelName")
or properties.get("model")
or properties.get("mn")
or existing.get("model"),
"version": properties.get("version") or existing.get("version"),
"service": existing.get("service") or service_type.removesuffix(".local."),
}
def strip_service_name(name):
for suffix in DISCOVERY_SERVICE_TYPES:
if name.endswith("." + suffix):
return name[: -(len(suffix) + 1)]
return name.rstrip(".")
def discover_speakers(timeout):
listener = KefDiscoveryListener()
zeroconf = Zeroconf(ip_version=IPVersion.V4Only)
try:
browsers = [
ServiceBrowser(zeroconf, service_type, listener)
for service_type in DISCOVERY_SERVICE_TYPES
]
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
if listener.speakers:
time.sleep(0.25)
break
time.sleep(0.05)
return sorted(
listener.speakers.values(),
key=lambda speaker: (
speaker.get("name") or "",
speaker.get("address") or "",
),
)
finally:
for browser in locals().get("browsers", []):
browser.cancel()
zeroconf.close()
def discover_host(timeout):
speakers = discover_speakers(timeout)
if not speakers:
raise SystemExit(
"Could not discover a KEF speaker. Pass --host <speaker-ip> or set KEF_HOST."
)
if len(speakers) > 1:
choices = ", ".join(
f"{speaker.get('name') or 'KEF'} at {speaker['address']}" for speaker in speakers
)
raise SystemExit(f"Multiple KEF speakers discovered: {choices}. Pass --host explicitly.")
return speakers[0]["address"]
def connector(args):
host = (
args.host
or os.environ.get("KEF_HOST")
or os.environ.get("KEF_IP")
or discover_host(args.discovery_timeout)
)
return KefConnector(host, model=args.model)
def print_status(speaker):
info = {
"name": speaker.speaker_name,
"model": speaker.speaker_model,
"firmware": speaker.firmware_version,
"status": speaker.status,
"source": speaker.source,
"volume": speaker.volume,
"playing": speaker.is_playing,
}
try:
song = speaker.get_song_information()
if any(song.values()):
info["song"] = song
except (KeyError, IndexError, TypeError, requests.RequestException):
pass
print(json.dumps(info, indent=2, sort_keys=True))
def bounded_volume(value):
return max(0, min(100, value))
def main():
parser = argparse.ArgumentParser(prog="kef", description="Control KEF W2 speakers.")
parser.add_argument(
"--host",
help="Speaker IP address. Defaults to KEF_HOST/KEF_IP, then mDNS discovery.",
)
parser.add_argument(
"--model",
default="LS50W2",
help="Speaker model passed to pykefcontrol. Default: LS50W2.",
)
parser.add_argument(
"--discovery-timeout",
default=2.0,
type=float,
help="Seconds to wait for mDNS discovery when --host/KEF_HOST is unset.",
)
subparsers = parser.add_subparsers(dest="command", required=True)
subparsers.add_parser("discover", help="List discovered KEF speakers as JSON.")
subparsers.add_parser("status", help="Print speaker status as JSON.")
subparsers.add_parser("on", help="Power on.")
subparsers.add_parser("standby", help="Put the speaker in standby.")
subparsers.add_parser("mute", help="Mute by setting volume to 0.")
volume = subparsers.add_parser("volume", help="Get or set volume.")
volume.add_argument("level", nargs="?", type=int, help="Volume level, 0-100.")
up = subparsers.add_parser("up", help="Increase volume.")
up.add_argument("step", nargs="?", default=3, type=int)
down = subparsers.add_parser("down", help="Decrease volume.")
down.add_argument("step", nargs="?", default=3, type=int)
source = subparsers.add_parser("source", help="Get or set source.")
source.add_argument("name", nargs="?", choices=SOURCES)
subparsers.add_parser("play-pause", help="Toggle play/pause.")
subparsers.add_parser("next", help="Next track.")
subparsers.add_parser("previous", help="Previous track.")
args = parser.parse_args()
if args.command == "discover":
print(json.dumps(discover_speakers(args.discovery_timeout), indent=2, sort_keys=True))
return 0
speaker = connector(args)
try:
if args.command == "status":
print_status(speaker)
elif args.command == "on":
speaker.power_on()
elif args.command == "standby":
speaker.shutdown()
elif args.command == "mute":
speaker.volume = 0
elif args.command == "volume":
if args.level is None:
print(speaker.volume)
else:
speaker.volume = bounded_volume(args.level)
elif args.command == "up":
speaker.volume = bounded_volume(speaker.volume + args.step)
elif args.command == "down":
speaker.volume = bounded_volume(speaker.volume - args.step)
elif args.command == "source":
if args.name is None:
print(speaker.source)
else:
speaker.source = args.name
elif args.command == "play-pause":
speaker.toggle_play_pause()
elif args.command == "next":
speaker.next_track()
elif args.command == "previous":
speaker.previous_track()
except requests.RequestException as error:
print(f"kef: failed to reach speaker: {error}", file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -1,37 +0,0 @@
{
lib,
buildPythonPackage,
fetchPypi,
hatchling,
aiohttp,
requests,
}:
buildPythonPackage rec {
pname = "pykefcontrol";
version = "0.9.2";
pyproject = true;
src = fetchPypi {
inherit pname version;
hash = "sha256-3kGhN+E7driiE6ePyF0EZOEnUhTm07sxHCKdzrn/MxM=";
};
build-system = [
hatchling
];
dependencies = [
aiohttp
requests
];
pythonImportsCheck = [ "pykefcontrol" ];
meta = {
description = "Python library for controlling KEF LS50WII, LSX II, and LS60 speakers";
homepage = "https://github.com/N0ciple/pykefcontrol";
license = lib.licenses.mit;
maintainers = with lib.maintainers; [ imalison ];
};
}

View File

@@ -1,30 +0,0 @@
{
lib,
python3,
writeShellApplication,
}:
let
python = python3.withPackages (ps: [
ps.python-roborock
ps.pyshark
ps.pyyaml
]);
in
writeShellApplication {
name = "roborock-control";
runtimeInputs = [ python ];
text = ''
export ROBOROCK_CONTROL_RUNNER=direct
exec python ${../../../dotfiles/lib/bin/roborock-control} "$@"
'';
meta = {
description = "Command-line controller for Roborock vacuums";
license = lib.licenses.mit;
maintainers = with lib.maintainers; [ imalison ];
mainProgram = "roborock-control";
};
}

View File

@@ -0,0 +1,228 @@
From aaefc0ff0bc4348de04f311ad0101da44c62ae94 Mon Sep 17 00:00:00 2001
From: Ivan Malison <IvanMalison@gmail.com>
Date: Wed, 4 Feb 2026 00:54:52 -0800
Subject: [PATCH 1/2] hyprexpo: optionally render workspace numbers
---
hyprexpo/README.md | 3 +-
hyprexpo/main.cpp | 2 +
hyprexpo/overview.cpp | 109 ++++++++++++++++++++++++++++++++++++++++++
hyprexpo/overview.hpp | 4 ++
4 files changed, 117 insertions(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 97bd1d4..aac2e97 100644
--- a/README.md
+++ b/README.md
@@ -28,6 +28,8 @@ gap_size | number | gap between desktops | `5`
bg_col | color | color in gaps (between desktops) | `rgb(000000)`
workspace_method | [center/first] [workspace] | position of the desktops | `center current`
skip_empty | boolean | whether the grid displays workspaces sequentially by id using selector "r" (`false`) or skips empty workspaces using selector "m" (`true`) | `false`
+show_workspace_numbers | boolean | show numeric labels for workspaces | `false`
+workspace_number_color | color | color of workspace number labels | `rgb(ffffff)`
gesture_distance | number | how far is the max for the gesture | `300`
### Keywords
@@ -57,4 +59,3 @@ off | hides the overview
disable | same as `off`
on | displays the overview
enable | same as `on`
-
diff --git a/main.cpp b/main.cpp
index 883fd82..ff9f380 100644
--- a/main.cpp
+++ b/main.cpp
@@ -239,6 +239,8 @@ APICALL EXPORT PLUGIN_DESCRIPTION_INFO PLUGIN_INIT(HANDLE handle) {
HyprlandAPI::addConfigValue(PHANDLE, "plugin:hyprexpo:bg_col", Hyprlang::INT{0xFF111111});
HyprlandAPI::addConfigValue(PHANDLE, "plugin:hyprexpo:workspace_method", Hyprlang::STRING{"center current"});
HyprlandAPI::addConfigValue(PHANDLE, "plugin:hyprexpo:skip_empty", Hyprlang::INT{0});
+ HyprlandAPI::addConfigValue(PHANDLE, "plugin:hyprexpo:show_workspace_numbers", Hyprlang::INT{0});
+ HyprlandAPI::addConfigValue(PHANDLE, "plugin:hyprexpo:workspace_number_color", Hyprlang::INT{0xFFFFFFFF});
HyprlandAPI::addConfigValue(PHANDLE, "plugin:hyprexpo:gesture_distance", Hyprlang::INT{200});
diff --git a/overview.cpp b/overview.cpp
index 5721948..926a9f8 100644
--- a/overview.cpp
+++ b/overview.cpp
@@ -1,5 +1,8 @@
#include "overview.hpp"
#include <any>
+#include <algorithm>
+#include <cmath>
+#include <pango/pangocairo.h>
#define private public
#include <hyprland/src/render/Renderer.hpp>
#include <hyprland/src/Compositor.hpp>
@@ -15,6 +18,86 @@
#undef private
#include "OverviewPassElement.hpp"
+static Vector2D renderLabelTexture(SP<CTexture> out, const std::string& text, const CHyprColor& color, int fontSizePx) {
+ if (!out || text.empty() || fontSizePx <= 0)
+ return {};
+
+ auto measureSurface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, 1, 1);
+ auto measureCairo = cairo_create(measureSurface);
+
+ PangoLayout* measureLayout = pango_cairo_create_layout(measureCairo);
+ pango_layout_set_text(measureLayout, text.c_str(), -1);
+ auto* fontDesc = pango_font_description_from_string("Sans Bold");
+ pango_font_description_set_size(fontDesc, fontSizePx * PANGO_SCALE);
+ pango_layout_set_font_description(measureLayout, fontDesc);
+ pango_font_description_free(fontDesc);
+
+ PangoRectangle inkRect, logicalRect;
+ pango_layout_get_extents(measureLayout, &inkRect, &logicalRect);
+
+ const int textW = std::max(1, (int)std::ceil(logicalRect.width / (double)PANGO_SCALE));
+ const int textH = std::max(1, (int)std::ceil(logicalRect.height / (double)PANGO_SCALE));
+
+ g_object_unref(measureLayout);
+ cairo_destroy(measureCairo);
+ cairo_surface_destroy(measureSurface);
+
+ const int pad = std::max(4, (int)std::round(fontSizePx * 0.35));
+ const int width = textW + pad * 2;
+ const int height = textH + pad * 2;
+
+ auto surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height);
+ auto cairo = cairo_create(surface);
+
+ // Clear the pixmap
+ cairo_save(cairo);
+ cairo_set_operator(cairo, CAIRO_OPERATOR_CLEAR);
+ cairo_paint(cairo);
+ cairo_restore(cairo);
+
+ // Background for legibility
+ cairo_set_source_rgba(cairo, 0.0, 0.0, 0.0, 0.55);
+ cairo_rectangle(cairo, 0, 0, width, height);
+ cairo_fill(cairo);
+
+ PangoLayout* layout = pango_cairo_create_layout(cairo);
+ pango_layout_set_text(layout, text.c_str(), -1);
+ fontDesc = pango_font_description_from_string("Sans Bold");
+ pango_font_description_set_size(fontDesc, fontSizePx * PANGO_SCALE);
+ pango_layout_set_font_description(layout, fontDesc);
+ pango_font_description_free(fontDesc);
+
+ pango_layout_get_extents(layout, &inkRect, &logicalRect);
+ const double xOffset = (width - logicalRect.width / (double)PANGO_SCALE) / 2.0;
+ const double yOffset = (height - logicalRect.height / (double)PANGO_SCALE) / 2.0;
+
+ cairo_set_source_rgba(cairo, color.r, color.g, color.b, color.a);
+ cairo_move_to(cairo, xOffset, yOffset);
+ pango_cairo_show_layout(cairo, layout);
+
+ g_object_unref(layout);
+
+ cairo_surface_flush(surface);
+
+ const auto DATA = cairo_image_surface_get_data(surface);
+ out->allocate();
+ glBindTexture(GL_TEXTURE_2D, out->m_texID);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+
+#ifndef GLES2
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_R, GL_BLUE);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_B, GL_RED);
+#endif
+
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, DATA);
+
+ cairo_destroy(cairo);
+ cairo_surface_destroy(surface);
+
+ return {width, height};
+}
+
static void damageMonitor(WP<Hyprutils::Animation::CBaseAnimatedVariable> thisptr) {
g_pOverview->damage();
}
@@ -34,11 +117,14 @@ COverview::COverview(PHLWORKSPACE startedOn_, bool swipe_) : startedOn(startedOn
static auto* const* PGAPS = (Hyprlang::INT* const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:hyprexpo:gap_size")->getDataStaticPtr();
static auto* const* PCOL = (Hyprlang::INT* const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:hyprexpo:bg_col")->getDataStaticPtr();
static auto* const* PSKIP = (Hyprlang::INT* const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:hyprexpo:skip_empty")->getDataStaticPtr();
+ static auto* const* PSHOWNUM = (Hyprlang::INT* const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:hyprexpo:show_workspace_numbers")->getDataStaticPtr();
+ static auto* const* PNUMCOL = (Hyprlang::INT* const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:hyprexpo:workspace_number_color")->getDataStaticPtr();
static auto const* PMETHOD = (Hyprlang::STRING const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:hyprexpo:workspace_method")->getDataStaticPtr();
SIDE_LENGTH = **PCOLUMNS;
GAP_WIDTH = **PGAPS;
BG_COLOR = **PCOL;
+ showWorkspaceNumbers = **PSHOWNUM;
// process the method
bool methodCenter = true;
@@ -126,6 +212,17 @@ COverview::COverview(PHLWORKSPACE startedOn_, bool swipe_) : startedOn(startedOn
Vector2D tileRenderSize = (pMonitor->m_size - Vector2D{GAP_WIDTH * pMonitor->m_scale, GAP_WIDTH * pMonitor->m_scale} * (SIDE_LENGTH - 1)) / SIDE_LENGTH;
CBox monbox{0, 0, tileSize.x * 2, tileSize.y * 2};
+ if (showWorkspaceNumbers) {
+ const CHyprColor numberColor = **PNUMCOL;
+ const int fontSizePx = std::max(12, (int)std::round(tileRenderSize.y * pMonitor->m_scale * 0.22));
+ for (auto& image : images) {
+ if (image.workspaceID == WORKSPACE_INVALID)
+ continue;
+ image.labelTex = makeShared<CTexture>();
+ image.labelSizePx = renderLabelTexture(image.labelTex, std::to_string(image.workspaceID), numberColor, fontSizePx);
+ }
+ }
+
if (!ENABLE_LOWRES)
monbox = {{0, 0}, pMonitor->m_pixelSize};
@@ -452,6 +549,18 @@ void COverview::fullRender() {
texbox.round();
CRegion damage{0, 0, INT16_MAX, INT16_MAX};
g_pHyprOpenGL->renderTextureInternal(images[x + y * SIDE_LENGTH].fb.getTexture(), texbox, {.damage = &damage, .a = 1.0});
+
+ if (showWorkspaceNumbers) {
+ auto& image = images[x + y * SIDE_LENGTH];
+ if (image.workspaceID != WORKSPACE_INVALID && image.labelTex && image.labelTex->m_texID != 0 && image.labelSizePx.x > 0 && image.labelSizePx.y > 0) {
+ const Vector2D labelSize = image.labelSizePx / pMonitor->m_scale;
+ const float margin = std::max(4.0, tileRenderSize.y * 0.05);
+ CBox labelBox = {x * tileRenderSize.x + x * GAPSIZE + margin, y * tileRenderSize.y + y * GAPSIZE + margin, labelSize.x, labelSize.y};
+ labelBox.scale(pMonitor->m_scale).translate(pos->value());
+ labelBox.round();
+ g_pHyprOpenGL->renderTexture(image.labelTex, labelBox, {.a = 1.0});
+ }
+ }
}
}
}
diff --git a/overview.hpp b/overview.hpp
index 4b02400..1f6bf3c 100644
--- a/overview.hpp
+++ b/overview.hpp
@@ -8,6 +8,7 @@
#include <hyprland/src/helpers/AnimatedVariable.hpp>
#include <hyprland/src/managers/HookSystemManager.hpp>
#include <vector>
+class CTexture;
// saves on resources, but is a bit broken rn with blur.
// hyprland's fault, but cba to fix.
@@ -58,6 +59,8 @@ class COverview {
int64_t workspaceID = -1;
PHLWORKSPACE pWorkspace;
CBox box;
+ SP<CTexture> labelTex;
+ Vector2D labelSizePx;
};
Vector2D lastMousePosLocal = Vector2D{};
@@ -81,6 +84,7 @@ class COverview {
bool swipe = false;
bool swipeWasCommenced = false;
+ bool showWorkspaceNumbers = false;
friend class COverviewPassElement;
};
--
2.52.0

View File

@@ -0,0 +1,147 @@
From edc05ce88f79ceda0cdcb9aa68ec371b1af323de Mon Sep 17 00:00:00 2001
From: Ivan Malison <IvanMalison@gmail.com>
Date: Mon, 16 Feb 2026 21:50:16 -0800
Subject: [PATCH 2/2] hyprexpo: add bring selection mode
---
hyprexpo/README.md | 1 +
hyprexpo/main.cpp | 50 +++++++++++++++++++++++++++++++++++++++++++
hyprexpo/overview.cpp | 12 +++++++++--
hyprexpo/overview.hpp | 3 ++-
4 files changed, 63 insertions(+), 3 deletions(-)
diff --git a/README.md b/README.md
index aac2e97..084f02b 100644
--- a/README.md
+++ b/README.md
@@ -55,6 +55,7 @@ Here are a list of options you can use:
| --- | --- |
toggle | displays if hidden, hide if displayed
select | selects the hovered desktop
+bring | brings a window from the hovered desktop to the current desktop
off | hides the overview
disable | same as `off`
on | displays the overview
diff --git a/main.cpp b/main.cpp
index ff9f380..78bac24 100644
--- a/main.cpp
+++ b/main.cpp
@@ -65,6 +65,47 @@ static void hkAddDamageB(void* thisptr, const pixman_region32_t* rg) {
g_pOverview->onDamageReported();
}
+static PHLWINDOW windowToBringFromWorkspace(const PHLWORKSPACE& workspace) {
+ if (!workspace)
+ return nullptr;
+
+ for (auto it = g_pCompositor->m_windows.rbegin(); it != g_pCompositor->m_windows.rend(); ++it) {
+ const auto& w = *it;
+ if (!w || w->m_workspace != workspace || !w->m_isMapped || w->isHidden())
+ continue;
+
+ return w;
+ }
+
+ return nullptr;
+}
+
+static SDispatchResult bringWindowFromWorkspace(int64_t sourceWorkspaceID) {
+ if (sourceWorkspaceID == WORKSPACE_INVALID)
+ return {.success = false, .error = "selected workspace is empty"};
+
+ const auto FOCUSSTATE = Desktop::focusState();
+ const auto MONITOR = FOCUSSTATE->monitor();
+ if (!MONITOR || !MONITOR->m_activeWorkspace)
+ return {.success = false, .error = "no active monitor/workspace"};
+
+ if (sourceWorkspaceID == MONITOR->activeWorkspaceID())
+ return {};
+
+ const auto SOURCEWORKSPACE = g_pCompositor->getWorkspaceByID(sourceWorkspaceID);
+ if (!SOURCEWORKSPACE)
+ return {.success = false, .error = "selected workspace is not open"};
+
+ const auto WINDOW = windowToBringFromWorkspace(SOURCEWORKSPACE);
+ if (!WINDOW)
+ return {.success = false, .error = "selected workspace has no mapped windows"};
+
+ g_pCompositor->moveWindowToWorkspaceSafe(WINDOW, MONITOR->m_activeWorkspace);
+ FOCUSSTATE->fullWindowFocus(WINDOW);
+ g_pCompositor->warpCursorTo(WINDOW->middle());
+ return {};
+}
+
static SDispatchResult onExpoDispatcher(std::string arg) {
if (g_pOverview && g_pOverview->m_isSwiping)
@@ -77,6 +118,15 @@ static SDispatchResult onExpoDispatcher(std::string arg) {
}
return {};
}
+ if (arg == "bring") {
+ if (g_pOverview) {
+ g_pOverview->selectHoveredWorkspace();
+ const auto BRINGRESULT = bringWindowFromWorkspace(g_pOverview->selectedWorkspaceID());
+ g_pOverview->close(false);
+ return BRINGRESULT;
+ }
+ return {};
+ }
if (arg == "toggle") {
if (g_pOverview)
g_pOverview->close();
diff --git a/overview.cpp b/overview.cpp
index 926a9f8..45ee982 100644
--- a/overview.cpp
+++ b/overview.cpp
@@ -343,6 +343,14 @@ void COverview::selectHoveredWorkspace() {
closeOnID = x + y * SIDE_LENGTH;
}
+int64_t COverview::selectedWorkspaceID() const {
+ const int ID = closeOnID == -1 ? openedID : closeOnID;
+ if (ID < 0 || ID >= (int)images.size())
+ return WORKSPACE_INVALID;
+
+ return images[ID].workspaceID;
+}
+
void COverview::redrawID(int id, bool forcelowres) {
if (!pMonitor)
return;
@@ -451,7 +459,7 @@ void COverview::onDamageReported() {
g_pCompositor->scheduleFrameForMonitor(pMonitor.lock());
}
-void COverview::close() {
+void COverview::close(bool switchToSelection) {
if (closing)
return;
@@ -471,7 +479,7 @@ void COverview::close() {
redrawAll();
- if (TILE.workspaceID != pMonitor->activeWorkspaceID()) {
+ if (switchToSelection && TILE.workspaceID != pMonitor->activeWorkspaceID()) {
pMonitor->setSpecialWorkspace(0);
// If this tile's workspace was WORKSPACE_INVALID, move to the next
diff --git a/overview.hpp b/overview.hpp
index 1f6bf3c..ca59f32 100644
--- a/overview.hpp
+++ b/overview.hpp
@@ -33,8 +33,9 @@ class COverview {
void onSwipeEnd();
// close without a selection
- void close();
+ void close(bool switchToSelection = true);
void selectHoveredWorkspace();
+ int64_t selectedWorkspaceID() const;
bool blockOverviewRendering = false;
bool blockDamageReporting = false;
--
2.52.0

View File

@@ -1,200 +0,0 @@
{ config, lib, pkgs, makeEnable, ... }:
let
cfg = config.myModules.remote-hyprland;
hyprlandPackage = config.programs.hyprland.package;
geometry = "${toString cfg.width}x${toString cfg.height}@${toString cfg.refreshRate}";
monitorRule = "${cfg.output},${geometry},0x0,${toString cfg.scale}";
remoteHyprlandStartVnc = pkgs.writeShellScript "remote-hyprland-start-vnc" ''
set -euo pipefail
export WAYLAND_DISPLAY=${cfg.socket}
export XDG_CURRENT_DESKTOP=Hyprland
export XDG_SESSION_DESKTOP=Hyprland
export XDG_SESSION_TYPE=wayland
for _ in $(${pkgs.coreutils}/bin/seq 1 50); do
if ${hyprlandPackage}/bin/hyprctl -j monitors >/dev/null 2>&1; then
break
fi
${pkgs.coreutils}/bin/sleep 0.1
done
# Give wayvnc a stable output name instead of relying on Hyprland's
# fallback HEADLESS-* naming.
${hyprlandPackage}/bin/hyprctl output create headless ${cfg.output} >/dev/null 2>&1 || true
${hyprlandPackage}/bin/hyprctl keyword monitor '${monitorRule}' >/dev/null 2>&1 || true
exec ${pkgs.wayvnc}/bin/wayvnc \
--log-level=info \
--output ${cfg.output} \
${cfg.bindAddress} ${toString cfg.port}
'';
remoteHyprlandConfig = pkgs.writeText "remote-hyprland.conf" ''
monitor=${monitorRule}
monitor=,${geometry},0x0,${toString cfg.scale}
env = XDG_CURRENT_DESKTOP,Hyprland
env = XDG_SESSION_DESKTOP,Hyprland
env = XDG_SESSION_TYPE,wayland
input {
kb_layout = us
follow_mouse = 1
}
general {
gaps_in = 4
gaps_out = 8
border_size = 2
layout = dwindle
}
decoration {
rounding = 4
}
dwindle {
pseudotile = true
preserve_split = true
}
misc {
disable_hyprland_logo = true
disable_splash_rendering = true
force_default_wallpaper = 0
}
$mainMod = SUPER
bind = $mainMod, Return, exec, ${cfg.terminalCommand}
bind = $mainMod, D, exec, ${pkgs.rofi}/bin/rofi -show drun
bind = $mainMod, Q, killactive
bind = $mainMod SHIFT, M, exit
bind = $mainMod, H, movefocus, l
bind = $mainMod, J, movefocus, d
bind = $mainMod, K, movefocus, u
bind = $mainMod, L, movefocus, r
bind = $mainMod SHIFT, H, movewindow, l
bind = $mainMod SHIFT, J, movewindow, d
bind = $mainMod SHIFT, K, movewindow, u
bind = $mainMod SHIFT, L, movewindow, r
bind = $mainMod, 1, workspace, 1
bind = $mainMod, 2, workspace, 2
bind = $mainMod, 3, workspace, 3
bind = $mainMod, 4, workspace, 4
bind = $mainMod, 5, workspace, 5
bind = $mainMod, 6, workspace, 6
bind = $mainMod, 7, workspace, 7
bind = $mainMod, 8, workspace, 8
bind = $mainMod, 9, workspace, 9
bind = $mainMod SHIFT, 1, movetoworkspace, 1
bind = $mainMod SHIFT, 2, movetoworkspace, 2
bind = $mainMod SHIFT, 3, movetoworkspace, 3
bind = $mainMod SHIFT, 4, movetoworkspace, 4
bind = $mainMod SHIFT, 5, movetoworkspace, 5
bind = $mainMod SHIFT, 6, movetoworkspace, 6
bind = $mainMod SHIFT, 7, movetoworkspace, 7
bind = $mainMod SHIFT, 8, movetoworkspace, 8
bind = $mainMod SHIFT, 9, movetoworkspace, 9
exec-once = ${remoteHyprlandStartVnc}
exec-once = ${cfg.terminalCommand}
'';
enabledModule = makeEnable config "myModules.remote-hyprland" false {
myModules.hyprland.enable = true;
users.manageLingering = true;
users.users.${cfg.user}.linger = true;
environment.systemPackages = [ pkgs.wayvnc ];
home-manager.users.${cfg.user}.systemd.user.services.remote-hyprland = {
Unit = {
Description = "Headless Hyprland session for remote VNC access";
After = [ "default.target" ];
};
Service = {
ExecStart = "${hyprlandPackage}/bin/Hyprland --socket ${cfg.socket} --config ${remoteHyprlandConfig}";
Restart = "on-failure";
RestartSec = 5;
Environment = [
"XDG_CURRENT_DESKTOP=Hyprland"
"XDG_SESSION_DESKTOP=Hyprland"
"XDG_SESSION_TYPE=wayland"
"WAYLAND_DISPLAY=${cfg.socket}"
];
};
Install = {
WantedBy = [ "default.target" ];
};
};
};
in
enabledModule // {
options = lib.recursiveUpdate enabledModule.options {
myModules.remote-hyprland = {
user = lib.mkOption {
type = lib.types.str;
default = "imalison";
description = "User account that owns the remote Hyprland session.";
};
bindAddress = lib.mkOption {
type = lib.types.str;
default = "127.0.0.1";
description = "Address for wayvnc to bind. Keep localhost when using SSH or Tailscale forwarding.";
};
port = lib.mkOption {
type = lib.types.port;
default = 5900;
description = "TCP port for wayvnc.";
};
socket = lib.mkOption {
type = lib.types.str;
default = "wayland-remote-hyprland";
description = "Wayland socket name used by the remote Hyprland instance.";
};
output = lib.mkOption {
type = lib.types.str;
default = "remote";
description = "Stable Hyprland headless output name captured by wayvnc.";
};
width = lib.mkOption {
type = lib.types.ints.positive;
default = 1920;
description = "Remote output width.";
};
height = lib.mkOption {
type = lib.types.ints.positive;
default = 1080;
description = "Remote output height.";
};
refreshRate = lib.mkOption {
type = lib.types.ints.positive;
default = 60;
description = "Remote output refresh rate.";
};
scale = lib.mkOption {
type = lib.types.number;
default = 1;
description = "Remote output scale.";
};
terminalCommand = lib.mkOption {
type = lib.types.str;
default = "${pkgs.ghostty}/bin/ghostty --gtk-single-instance=false";
description = "Command launched for the default terminal binding and initial window.";
};
};
};
}

View File

@@ -2,19 +2,17 @@
makeEnable,
config,
...
}:
let
}: let
shared = import ../nix-shared/syncthing.nix;
inherit (shared) devices allDevices;
in
makeEnable config "myModules.syncthing" true {
system.activationScripts.syncthingPermissions = {
text = ''
mkdir -p /var/lib/syncthing/sync
mkdir -p /var/lib/syncthing/sync/Screensaver/use
mkdir -p /var/lib/syncthing/railbird
chown -R syncthing:syncthing /var/lib/syncthing
chmod -R 2770 /var/lib/syncthing
mkdir -p /var/lib/syncthing/sync
mkdir -p /var/lib/syncthing/railbird
'';
};
systemd.services.syncthing = {