Files
hypr/functions.lua
T

413 lines
14 KiB
Lua

-- ~/.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