-- ~/.config/hypr/functions.lua local M = {} -- focusOrLaunch: checks if a window with the given class exists. -- If it does, focuses it (and sends specific shortcuts if it's qutebrowser). -- If not, optionally switches to a workspace and launches the app. function M.focusOrLaunch(app_cmd, class_name, workspace_name) local clients = hl.get_windows() local found = false for _, client in ipairs(clients) do if client.class == class_name then hl.dispatch(hl.dsp.focus({ window = "class:" .. class_name })) -- Specific qutebrowser logic ported from your zsh script if app_cmd:match("qutebrowser") then hl.dispatch(hl.dsp.send_shortcut({ mods = "", key = "Escape", window = "class:" .. class_name })) hl.dispatch(hl.dsp.send_shortcut({ mods = "", key = "0", window = "class:" .. class_name })) hl.dispatch(hl.dsp.send_shortcut({ mods = "", key = "1", window = "class:" .. class_name })) hl.dispatch(hl.dsp.send_shortcut({ mods = "ALT", key = "J", window = "class:" .. class_name })) hl.dispatch(hl.dsp.send_shortcut({ mods = "", key = "Escape", window = "class:" .. class_name })) end found = true break end end if not found then if workspace_name and #workspace_name > 0 then hl.dispatch(hl.dsp.focus({ workspace = workspace_name })) end hl.dispatch(hl.dsp.exec_cmd(app_cmd)) end end function M.getActiveWorkspaceName() local f = io.popen("hyprctl -j activeworkspace | jq -r '.name'") local active_ws_name = f:read("*a"):gsub("%s+", "") f:close() return active_ws_name end function M.mv2workspace() local cmd = [[ bash -c ' WORKSPACES=$(hyprctl -j workspaces | jq -r ".[] | .name") ACTIVE="$(hyprctl -j activewindow)" CLASS="$(jq -r ".class" <<< "$ACTIVE")" TITLE="$(jq -r ".title" <<< "$ACTIVE")" ADDR="$(jq -r ".address" <<< "$ACTIVE")" PROMPT="Move window of ${CLASS} with title '\''${TITLE}'\'' to which workspace? " NEW_WS="$(echo "$WORKSPACES" | rofi -dmenu -p "$PROMPT")" if [ -n "$NEW_WS" ]; then hyprctl dispatch "hl.dsp.window.move({ workspace = \"$NEW_WS\", window = \"address:$ADDR\", follow = false })" fi ' ]] hl.dispatch(hl.dsp.exec_cmd(cmd)) end function M.createWorkspace(after_name) local after_str = after_name or "" local cmd = string.format([[ bash -c ' NEW_NAME=$(rofi -dmenu -p "new workspace name" -l 0 < /dev/null | tr -d "[:space:]") if [ -z "$NEW_NAME" ]; then exit 0; fi ACTIVE_MON=$(hyprctl -j monitors | jq -r ".[] | select(.focused) | .name") NEW_LINE=" { name = \"$NEW_NAME\", monitor = \"$ACTIVE_MON\" }," FILE="$HOME/.config/hypr/workspace_config.lua" AFTER="%s" if [ -n "$AFTER" ] && grep -q "name *= *\"$AFTER\"" "$FILE"; then sed -i "/name *= *\"$AFTER\"/a \\$NEW_LINE" "$FILE" else sed -i "/^}/i \\$NEW_LINE" "$FILE" fi hyprctl reload ' ]], after_str) hl.dispatch(hl.dsp.exec_cmd(cmd)) end function M.deleteWorkspace() local cmd = [[ bash -c ' ACTIVE_WS=$(hyprctl -j activeworkspace | jq -r ".name") if [ -z "$ACTIVE_WS" ]; then exit 0; fi FILE="$HOME/.config/hypr/workspace_config.lua" sed -i "/name *= *\"$ACTIVE_WS\"/d" "$FILE" hyprctl dispatch "hl.dsp.focus({ workspace = \"m-1\" })" hyprctl reload ' ]] hl.dispatch(hl.dsp.exec_cmd(cmd)) end function M.moveWorkspace(direction, target_monitor) -- If no target_monitor is provided and direction is nil, prompt interactively via async bash to avoid deadlocks if (not target_monitor or target_monitor == "") and not direction then local cmd = [[ bash -c ' ACTIVE_WS=$(hyprctl -j activeworkspace | jq -r ".name") if [ -z "$ACTIVE_WS" ]; then exit 0; fi MONITORS=$(hyprctl -j monitors | jq -r ".[].name") PROMPT="Move workspace '\''$ACTIVE_WS'\'' to which monitor? " NEW_MON=$(echo "$MONITORS" | rofi -dmenu -p "$PROMPT") if [ -n "$NEW_MON" ]; then FILE="$HOME/.config/hypr/workspace_config.lua" sed -i -E "/name *= *\"$ACTIVE_WS\"/ s/monitor *= *\"[^\"]+\"/monitor = \"$NEW_MON\"/" "$FILE" hyprctl reload fi ' ]] hl.dispatch(hl.dsp.exec_cmd(cmd)) return end -- NATIVE LUA PATH for directional shifts or explicit monitor arguments passed from keybinds local active_ws_name = M.getActiveWorkspaceName() if not active_ws_name or active_ws_name == "" then return end local path = os.getenv("HOME") .. "/.config/hypr/workspace_config.lua" local f_in = io.open(path, "r") if not f_in then return end local lines = {} local ws_idx = nil for line in f_in:lines() do table.insert(lines, line) if line:match('name%s*=%s*"' .. active_ws_name .. '"') then ws_idx = #lines end end f_in:close() if not ws_idx then return end if target_monitor and target_monitor ~= "" then lines[ws_idx] = lines[ws_idx]:gsub('monitor%s*=%s*"[^"]+"', 'monitor = "' .. target_monitor .. '"') end if direction then local first_idx = 2 local last_idx = #lines - 1 while first_idx < #lines and not lines[first_idx]:match("name%s*=") do first_idx = first_idx + 1 end while last_idx > 1 and not lines[last_idx]:match("name%s*=") do last_idx = last_idx - 1 end if first_idx <= last_idx then local current_line = table.remove(lines, ws_idx) local new_idx = ws_idx if direction > 0 then new_idx = ws_idx + 1 if new_idx > last_idx then new_idx = first_idx end elseif direction < 0 then new_idx = ws_idx - 1 if new_idx < first_idx then new_idx = last_idx end end table.insert(lines, new_idx, current_line) end end local f_out = io.open(path, "w") for _, line in ipairs(lines) do f_out:write(line .. "\n") end f_out:close() hl.dispatch(hl.dsp.exec_cmd("hyprctl reload")) end function M.pasteMacro(script_path) local cmd = string.format([[ bash -c ' exec > /tmp/paste.log 2>&1 out="$(%s)" echo -n "$out" | wl-copy # 1. Wait a moment for physical keys to be released sleep 0.3 # 2. Release modifiers logically, then execute CTRL+V wtype -m logo -m ctrl -m shift -m alt -M ctrl -k v -m ctrl # 3. Wait a full second to ensure Wayland apps finish reading the clipboard asynchronously sleep 1.0 # 4. Scrub the password from cliphist securely PREV=$(cliphist list | awk "NR==2") cliphist list | awk "NR==1" | cliphist delete if [ -n "$PREV" ]; then echo "$PREV" | cliphist decode | wl-copy else wl-copy -c fi ' ]], script_path) hl.dispatch(hl.dsp.exec_cmd(cmd)) end function M.pasteText(text) local cmd = string.format([[ bash -c ' exec &> /tmp/$(date +%%FT%%H%%M%%S)_paste.log echo -n "%s" | wl-copy # Wait a moment for physical keys to be released, then execute CTRL+V sleep 0.3 wtype -m logo -m ctrl -m shift -m alt -M ctrl -k v -m ctrl # Note: We do not scrub cliphist here because dates are not sensitive passwords, # which completely eliminates the paste race condition for dates! ' ]], text) hl.dispatch(hl.dsp.exec_cmd(cmd)) end function M.timetracker() local cmd = [[ bash -c ' CHOICE=$(echo -e "Begin...\nEnd..." | rofi -dmenu -theme-str "inputbar {enabled: false;}" -i -matching prefix -auto-select -l 2) if [ "$CHOICE" = "Begin..." ]; then DATE_STR=$(date "+%F %T") wtype -m logo -m ctrl -m shift -m alt -k Escape wtype "I${DATE_STR}: Begin " elif [ "$CHOICE" = "End..." ]; then DATE_STR=$(date "+%F %T") wtype -m logo -m ctrl -m shift -m alt -k Escape wtype "I${DATE_STR}: End " wtype -k Escape wtype "kly\$jpo" wtype "${DATE_STR}: Begin " fi ' ]] hl.dispatch(hl.dsp.exec_cmd(cmd)) end function M.randomizeWallpaper() local monitors = hl.get_monitors() local home = os.getenv("HOME") local april_dir = home .. "/images/April" local sbm_dir = home .. "/images/Seven Beats Music" local black_png = home .. "/images/black.png" -- Determine if we are on the home WiFi ("keep") local is_home_wifi = false local f = io.popen("iwgetid -r 2>/dev/null") if f then local ssid = f:read("*a") f:close() if ssid and ssid:match("keep") then is_home_wifi = true end end -- Select image source based on location local img_dir = april_dir if #monitors > 1 then img_dir = sbm_dir elseif is_home_wifi then img_dir = april_dir end local images = {} local handle = io.popen('find "' .. img_dir .. '" -type f \\( -iname "*.jpg" -o -iname "*.png" -o -iname "*.jpeg" \\) 2>/dev/null') if handle then for line in handle:lines() do table.insert(images, line) end handle:close() end if #images == 0 then table.insert(images, black_png) end local conf_lines = { "ipc = on", "splash = false", "" } -- Pick a unique random image for each monitor math.randomseed(os.time()) local used = {} local matugen_cmds = {} local primary_logical_name = "eDP-1" for _, mon in ipairs(monitors) do if mon.description and mon.description:match("E3LMTF071391") then primary_logical_name = "DP-8" end end for _, mon in ipairs(monitors) do local img = nil local attempts = 0 while attempts < #images do local idx = math.random(1, #images) if not used[idx] then img = images[idx] used[idx] = true break end attempts = attempts + 1 end if not img then img = images[1] end -- fallback if we run out of unique images if img then table.insert(conf_lines, "wallpaper {") table.insert(conf_lines, " monitor = " .. mon.name) table.insert(conf_lines, " path = " .. img) table.insert(conf_lines, " fit_mode = cover") table.insert(conf_lines, "}") table.insert(conf_lines, "") local logical_name = mon.name if mon.description then if mon.description:match("E3LMTF071438") then logical_name = "DP-7" end if mon.description:match("E3LMTF071391") then logical_name = "DP-8" end end local matugen_cmd = "matugen image '" .. img .. "' -c /home/trey/src/dotfiles/matugen/matugen-" .. logical_name .. ".toml --source-color-index 0" -- Push primary monitor to the end of the array so its global configs (like tmux) overwrite the others if logical_name == primary_logical_name then table.insert(matugen_cmds, matugen_cmd) else table.insert(matugen_cmds, 1, matugen_cmd) end end end if #matugen_cmds > 0 then local all_cmds = table.concat(matugen_cmds, " ; ") local reload_cmd = M.reloadAppsCmd() hl.dispatch(hl.dsp.exec_cmd("bash -c \"" .. all_cmds .. " ; " .. reload_cmd .. "\"")) end local c = io.open(home .. "/.config/hypr/hyprpaper.conf", "w") if c then c:write(table.concat(conf_lines, "\n")) c:close() end -- Restart hyprpaper natively to avoid uwsm systemd rate limits on rapid monitor hotplugs hl.dispatch(hl.dsp.exec_cmd("killall -q hyprpaper; sleep 0.2; hyprpaper &")) end function M.reloadAppsCmd() -- Return a shell command to reload apps that don't auto-reload (executed sequentially after matugen) local cmds = { "tmux source ~/.config/tmux/tmux.conf", "for pane in $(tmux list-panes -a -F '#{pane_id} #{pane_current_command}' | awk '$2 == \"vim\" {print $1}'); do tmux send-keys -t $pane Escape ':colorscheme matugen' Enter; done", "killall -q -USR1 zsh" } return table.concat(cmds, " ; ") end function M.setupMonitors() -- Automatically handle the wallpaper update natively M.randomizeWallpaper() local monitors = hl.get_monitors() local logical_to_physical = { ["eDP-1"] = "eDP-1" } for _, mon in ipairs(monitors) do if mon.description then if mon.description:match("E3LMTF071438") then logical_to_physical["DP-7"] = mon.name end if mon.description:match("E3LMTF071391") then logical_to_physical["DP-8"] = mon.name end end end local home = os.getenv("HOME") for logical, physical in pairs(logical_to_physical) do local config_path = home .. "/.config/waybar/config-" .. logical .. ".jsonc" local f = io.open(config_path, "r") if f then local content = f:read("*a") f:close() -- Replace any existing output value with the actual physical monitor name content = content:gsub('"output":%s*"[^"]+"', '"output": "' .. physical .. '"') local w = io.open(config_path, "w") if w then w:write(content) w:close() end end end -- Dynamically spawn waybar only for connected monitors, and use a robust kill sequence -- inside the background script to prevent race conditions during rapid hotplug events local waybar_cmds = { "killall -q waybar", "sleep 0.5", "killall -q waybar" } for logical, _ in pairs(logical_to_physical) do table.insert(waybar_cmds, string.format("nohup waybar -c ~/.config/waybar/config-%s.jsonc -s ~/.config/waybar/style-%s.css >/dev/null 2>&1 &", logical, logical)) end hl.dispatch(hl.dsp.exec_cmd("bash -c '" .. table.concat(waybar_cmds, "\n") .. "'")) end return M