-- Phase 1 Hyprland Lua migration for Blake. -- Target: Hyprland v0.56.x -- Intent: preserve the current daily-use workflow while moving off hyprlang. local HOME = os.getenv("HOME") or "/home/blake" local XDG_CONFIG_HOME = os.getenv("XDG_CONFIG_HOME") or (HOME .. "/.config") local mainMod = "SUPER" local terminal = "kitty" local fileManager = "dolphin" local browser = "qutebrowser" local email = "thunderbird" local menu = "noctalia-shell ipc call launcher toggle" local ipc = "noctalia-shell ipc" local legacyMonitorFile = XDG_CONFIG_HOME .. "/hypr/monitors.conf" local function load_monitor_rules(path) local handle = io.open(path, "r") if not handle then hl.monitor({ output = "", mode = "preferred", position = "auto", scale = "auto", }) return end local loaded = 0 for line in handle:lines() do local rule = line:match("^monitor=(.+)$") if rule then local output, mode, position, scale = rule:match("^([^,]+),([^,]+),([^,]+),([^,]+)$") if output and mode and position and scale then hl.monitor({ output = output, mode = mode, position = position, scale = scale, }) loaded = loaded + 1 else local mon, keyword, value = rule:match("^([^,]+),([^,]+),(.+)$") if mon and keyword == "transform" and tonumber(value) then hl.monitor({ output = mon, transform = tonumber(value), }) loaded = loaded + 1 end end end end handle:close() if loaded == 0 then hl.monitor({ output = "", mode = "preferred", position = "auto", scale = "auto", }) end end local function bind_exec(keys, command, opts) hl.bind(keys, hl.dsp.exec_cmd(command), opts) end hl.env("XCURSOR_SIZE", "24") hl.env("HYPRCURSOR_SIZE", "24") hl.env("XCURSOR_THEME", "breeze_cursors") hl.env("HYPRCURSOR_THEME", "breeze_cursors") -- Prefer Wayland automatically for Electron applications while retaining -- XWayland as a fallback for applications that need it. hl.env("ELECTRON_OZONE_PLATFORM_HINT", "auto") load_monitor_rules(legacyMonitorFile) hl.config({ general = { gaps_in = 3, gaps_out = 1, border_size = 1, col = { active_border = "rgb(ffb3b2)", inactive_border = "rgb(181212)", }, resize_on_border = true, allow_tearing = false, layout = "dwindle", }, decoration = { rounding = 3, active_opacity = 1.0, inactive_opacity = 0.9, shadow = { enabled = true, range = 4, render_power = 3, color = "rgba(1a1a1aee)", }, blur = { enabled = true, size = 3, passes = 1, vibrancy = 0.1696, }, }, animations = { enabled = true, }, dwindle = { preserve_split = true, }, master = { new_status = "master", }, misc = { force_default_wallpaper = 0, disable_hyprland_logo = false, }, input = { kb_layout = "us", kb_options = "caps:escape", follow_mouse = 1, sensitivity = 0, accel_profile = "flat", scroll_factor = 0.35, touchpad = { natural_scroll = false, scroll_factor = 0.15, }, }, xwayland = { force_zero_scaling = true, }, }) hl.curve("easeOutQuint", { type = "bezier", points = { { 0.23, 1 }, { 0.32, 1 } } }) hl.curve("easeInOutCubic", { type = "bezier", points = { { 0.65, 0.05 }, { 0.36, 1 } } }) hl.curve("linear", { type = "bezier", points = { { 0, 0 }, { 1, 1 } } }) hl.curve("almostLinear", { type = "bezier", points = { { 0.5, 0.5 }, { 0.75, 1 } } }) hl.curve("quick", { type = "bezier", points = { { 0.15, 0 }, { 0.1, 1 } } }) hl.animation({ leaf = "global", enabled = true, speed = 10, bezier = "default" }) hl.animation({ leaf = "border", enabled = true, speed = 5.39, bezier = "easeOutQuint" }) hl.animation({ leaf = "windows", enabled = true, speed = 4.79, bezier = "easeOutQuint" }) hl.animation({ leaf = "windowsIn", enabled = true, speed = 4.1, bezier = "easeOutQuint", style = "popin 87%" }) hl.animation({ leaf = "windowsOut", enabled = true, speed = 1.49, bezier = "linear", style = "popin 87%" }) hl.animation({ leaf = "fadeIn", enabled = true, speed = 1.73, bezier = "almostLinear" }) hl.animation({ leaf = "fadeOut", enabled = true, speed = 1.46, bezier = "almostLinear" }) hl.animation({ leaf = "fade", enabled = true, speed = 3.03, bezier = "quick" }) hl.animation({ leaf = "layers", enabled = true, speed = 3.81, bezier = "easeOutQuint" }) hl.animation({ leaf = "layersIn", enabled = true, speed = 4, bezier = "easeOutQuint", style = "fade" }) hl.animation({ leaf = "layersOut", enabled = true, speed = 1.5, bezier = "linear", style = "fade" }) hl.animation({ leaf = "fadeLayersIn", enabled = true, speed = 1.79, bezier = "almostLinear" }) hl.animation({ leaf = "fadeLayersOut", enabled = true, speed = 1.39, bezier = "almostLinear" }) hl.animation({ leaf = "workspaces", enabled = true, speed = 1.94, bezier = "almostLinear", style = "fade" }) hl.animation({ leaf = "workspacesIn", enabled = true, speed = 1.21, bezier = "almostLinear", style = "fade" }) hl.animation({ leaf = "workspacesOut", enabled = true, speed = 1.94, bezier = "almostLinear", style = "fade" }) hl.animation({ leaf = "zoomFactor", enabled = true, speed = 7, bezier = "quick" }) hl.gesture({ fingers = 3, direction = "horizontal", action = "workspace", }) hl.window_rule({ name = "special-discord", match = { class = "^(vesktop)$" }, workspace = "special:discord", }) hl.window_rule({ name = "special-obsidian", match = { class = "^(obsidian)$" }, workspace = "special:obsidian", }) hl.window_rule({ name = "special-spotify", -- Spotify has used both lowercase and title-case X11 window classes. match = { class = "^([Ss]potify)$" }, workspace = "special:spotify", }) hl.window_rule({ name = "special-email", match = { class = "^([Tt]hunderbird)$" }, workspace = "special:email", }) hl.window_rule({ name = "suppress-maximize", match = { class = ".*" }, suppress_event = "maximize", }) hl.window_rule({ name = "hyprland-run", match = { class = "^(hyprland-run)$" }, float = true, move = "20 monitor_h-120", }) hl.window_rule({ name = "calc-term", match = { class = "^(calc-term)$" }, float = true, pseudo = true, center = true, size = "40% 40%", }) hl.window_rule({ name = "btop-popup", match = { class = "^(btop)$" }, float = true, size = "90% 80%", dim_around = true, }) hl.window_rule({ name = "popup-term", match = { class = "^(popup-term)$" }, float = true, size = "90% 80%", dim_around = true, opacity = "0.9 0.8", }) hl.window_rule({ name = "kitty-opacity", match = { class = "^(kitty)$" }, opacity = "1.0 1.0", }) hl.window_rule({ name = "xwayland-drag-fix", match = { class = "^$", title = "^$", xwayland = true, float = true, fullscreen = false, pin = false, }, no_focus = true, }) hl.window_rule({ name = "picture-in-picture", match = { title = "^(Picture in picture)$" }, float = true, pin = true, center = true, keep_aspect_ratio = true, persistent_size = true, }) hl.window_rule({ name = "privacy-browsers-float", match = { class = "^(Tor Browser|Mullvad Browser)$" }, float = true, center = true, fullscreen_state = "0 0", suppress_event = "maximize", }) hl.window_rule({ name = "noctalia-settings", match = { class = "^dev.noctalia.Noctalia$", initial_title = "^Noctalia Settings$", }, float = true, center = true, size = "50% 80%", }) hl.on("hyprland.start", function() -- Noctalia owns the wallpaper layer and restores its saved wallpaper. -- Starting Hyprpaper too creates a competing background with a different -- fit mode after each login. hl.exec_cmd("systemctl --user start hyprpolkitagent") hl.exec_cmd("udiskie") hl.exec_cmd("hyprpm reload -n") hl.exec_cmd("kdeconnectd") hl.exec_cmd("hyprshell run &") hl.exec_cmd("vesktop -m --user-agent-os windows") hl.exec_cmd("noctalia-shell") hl.exec_cmd("hyprctl setcursor breeze_cursors 24") end) hl.bind(mainMod .. " + Q", hl.dsp.exec_cmd(terminal)) -- Tap the left Super key to open Hyprshell's overview. Triggering on release -- leaves Super free to act as a modifier for bindings such as Super+Q. hl.bind("SUPER + SUPER_L", hl.dsp.exec_cmd("hyprshell socat '\"OpenOverview\"'"), { release = true }) hl.bind(mainMod .. " + CTRL + S", hl.dsp.exec_cmd("grim -g \"$(slurp)\" - | wl-copy")) hl.bind("ALT + Q", hl.dsp.exec_cmd(terminal .. " --class popup-term")) hl.bind(mainMod .. " + W", hl.dsp.exec_cmd(terminal .. " --class popup-term")) hl.bind(mainMod .. " + C", hl.dsp.window.close()) hl.bind(mainMod .. " + SHIFT + CTRL + M", hl.dsp.exec_cmd("command -v hyprshutdown >/dev/null 2>&1 && hyprshutdown || hyprctl dispatch exit")) hl.bind(mainMod .. " + P", hl.dsp.window.pin()) hl.bind(mainMod .. " + SHIFT + P", hl.dsp.window.float({ action = "toggle" })) hl.bind(mainMod .. " + SHIFT + BACKSLASH", hl.dsp.window.float({ action = "toggle" })) hl.bind(mainMod .. " + BACKSLASH", hl.dsp.layout("togglesplit")) hl.bind(mainMod .. " + CTRL + BACKSLASH", hl.dsp.window.pseudo()) hl.bind(mainMod .. " + Return", hl.dsp.window.fullscreen({ mode = "maximized", action = "toggle" })) hl.bind(mainMod .. " + SHIFT + Return", hl.dsp.window.fullscreen({ mode = "fullscreen", action = "toggle" })) hl.bind(mainMod .. " + H", hl.dsp.focus({ direction = "left" })) hl.bind(mainMod .. " + L", hl.dsp.focus({ direction = "right" })) hl.bind(mainMod .. " + K", hl.dsp.focus({ direction = "up" })) hl.bind(mainMod .. " + J", hl.dsp.focus({ direction = "down" })) hl.bind(mainMod .. " + SHIFT + H", hl.dsp.window.move({ direction = "left" })) hl.bind(mainMod .. " + SHIFT + L", hl.dsp.window.move({ direction = "right" })) hl.bind(mainMod .. " + SHIFT + K", hl.dsp.window.move({ direction = "up" })) hl.bind(mainMod .. " + SHIFT + J", hl.dsp.window.move({ direction = "down" })) for i = 1, 10 do local key = tostring(i % 10) hl.bind(mainMod .. " + " .. key, hl.dsp.focus({ workspace = i })) hl.bind(mainMod .. " + SHIFT + " .. key, hl.dsp.window.move({ workspace = i })) end hl.bind(mainMod .. " + SHIFT + S", hl.dsp.window.move({ workspace = "special:magic" })) bind_exec(mainMod .. " + D", "hyprctl clients | grep -iq \"vesktop\" && hyprctl dispatch 'hl.dsp.workspace.toggle_special(\"discord\")' || vesktop --user-agent-os windows") bind_exec(mainMod .. " + S", "hyprctl clients | grep -iq \"spotify\" && hyprctl dispatch 'hl.dsp.workspace.toggle_special(\"spotify\")' || spotify") bind_exec(mainMod .. " + E", "hyprctl clients | grep -iq \"thunderbird\" && hyprctl dispatch 'hl.dsp.workspace.toggle_special(\"email\")' || " .. email) hl.bind(mainMod .. " + SHIFT + O", hl.dsp.workspace.toggle_special("obsidian")) bind_exec(mainMod .. " + N", "obsidian") bind_exec("ALT + E", email) bind_exec(mainMod .. " + B", "brave") bind_exec(mainMod .. " + SHIFT + F", browser) bind_exec(mainMod .. " + SHIFT + Q", browser) bind_exec(mainMod .. " + SPACE", "playerctl play-pause") bind_exec("ALT_L + C", ipc .. " call clipper openPanel") bind_exec("SUPER + escape", "kitty --class btop -o \"confirm_os_window_close 0\" btop") bind_exec("CTRL + SHIFT + ESCAPE", "btop") bind_exec(mainMod .. " + comma", ipc .. " call controlCenter toggle") bind_exec(mainMod .. " + period", ipc .. " call settings toggle") bind_exec("PRINT", "hyprshot -m active -m window --output-folder ~/screenshot") bind_exec("SHIFT + PRINT", "hyprshot -m region --output-folder ~/screenshots") bind_exec("ALT + PRINT", "hyprshot -m active -m output --output-folder ~/screenshots") hl.bind(mainMod .. " + M", hl.dsp.submap("quick-settings")) hl.bind(mainMod .. " + O", hl.dsp.submap("open-apps")) bind_exec(mainMod .. " + CTRL + L", ipc .. " lockScreen lock") hl.bind(mainMod .. " + right", hl.dsp.window.resize({ x = 20, y = 0, relative = true }), { repeating = true }) hl.bind(mainMod .. " + left", hl.dsp.window.resize({ x = -20, y = 0, relative = true }), { repeating = true }) hl.bind(mainMod .. " + up", hl.dsp.window.resize({ x = 0, y = -20, relative = true }), { repeating = true }) hl.bind(mainMod .. " + down", hl.dsp.window.resize({ x = 0, y = 20, relative = true }), { repeating = true }) hl.bind(mainMod .. " + mouse:272", hl.dsp.window.drag()) hl.bind(mainMod .. " + mouse:273", hl.dsp.window.resize()) bind_exec("XF86AudioRaiseVolume", "wpctl set-volume -l 1 @DEFAULT_AUDIO_SINK@ 5%+", { locked = true, repeating = true }) bind_exec("XF86AudioLowerVolume", "wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-", { locked = true, repeating = true }) bind_exec("XF86AudioMute", "wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle", { locked = true, repeating = true }) bind_exec("XF86AudioMicMute", "wpctl set-mute @DEFAULT_AUDIO_SOURCE@ toggle", { locked = true, repeating = true }) bind_exec("XF86MonBrightnessUp", "brightnessctl -e4 -n2 set 5%+", { locked = true, repeating = true }) bind_exec("XF86MonBrightnessDown", "brightnessctl -e4 -n2 set 5%-", { locked = true, repeating = true }) bind_exec("XF86AudioNext", "playerctl next", { locked = true }) bind_exec("XF86AudioPause", "playerctl play-pause", { locked = true }) bind_exec("XF86AudioPlay", "playerctl play-pause", { locked = true }) bind_exec("XF86AudioPrev", "playerctl previous", { locked = true }) bind_exec("ALT + mouse:274", "playerctl play-pause") hl.define_submap("quick-settings", function() bind_exec("V", "protonvpn-app") bind_exec("S", ipc .. " settings toggle") bind_exec("C", ipc .. " plugin:clipper togglePanel") bind_exec("P", ipc .. " sessionMenu toggle") bind_exec(mainMod .. " + L", ipc .. " sessionMenu toggle") bind_exec("T", ipc .. " systemMonitor toggle") bind_exec("M", ipc .. " controlCenter toggle") bind_exec("D", ipc .. " notifications toggleDND") bind_exec("A", ipc .. " notifications dismissAll") bind_exec("N", ipc .. " notifications toggleHistory") hl.bind("L", hl.dsp.submap("layouts")) bind_exec("SHIFT + B", ipc .. " bar toggle") bind_exec("B", ipc .. " bluetooth togglePanel") bind_exec("W", ipc .. " network togglePanel") bind_exec("SHIFT + W", ipc .. " wifi toggle") hl.bind("catchall", hl.dsp.submap("reset")) end) hl.define_submap("open-apps", function() bind_exec("S", "spotify") bind_exec("D", "vesktop --user-agent-os windows") bind_exec("F", "firefox") bind_exec("Q", browser) bind_exec("M", "feishin") bind_exec("O", "obsidian") bind_exec("T", email) bind_exec("R", "calibre") bind_exec("E", "darktable") hl.bind("catchall", hl.dsp.submap("reset")) end) hl.define_submap("layouts", function() hl.bind("D", hl.dsp.exec_cmd("hyprctl keyword general:layout dwindle")) hl.bind("M", hl.dsp.exec_cmd("hyprctl keyword general:layout master")) hl.bind("catchall", hl.dsp.submap("reset")) end)