-- Hyprland 0.57 (Lua). Clean dark work setup; shell = DankMaterialShell. pcall(require, "dms.colors") -- DMS theme colours; DMS rewrites that file on every theme change and Hyprland reloads when it does -- Theme colours as plain hex, read from the file DMS regenerates (fallbacks = the pink theme). local function dms_hex(key, fallback) local f = io.open(os.getenv("HOME") .. "/.config/hypr/dms/colors.lua") local t = f and f:read("*a") or "" if f then f:close() end return t:match("%f[%w_]" .. key .. '%s*=%s*"rgb%((%x%x%x%x%x%x)%)"') or fallback end local function lighten(hex, k) -- mix a colour with white local function m(c) return math.floor(c + (255 - c) * k + 0.5) end return string.format("%02x%02x%02x", m(tonumber(hex:sub(1, 2), 16)), m(tonumber(hex:sub(3, 4), 16)), m(tonumber(hex:sub(5, 6), 16))) end local primary = dms_hex("active_border", "e91e63") local outline = dms_hex("inactive_border", "9f8c8e") local light = lighten(primary, 0.55) -- Fallback login session: GNOME (via GDM). local terminal = "kitty" local mod = "SUPER" ---------------- ---- OUTPUT ---- ---------------- -- eDP-1 at 1.25 scale is 1536 logical px wide, so HDMI sits right of it. hl.monitor({ output = "eDP-1", mode = "preferred", position = "0x0", scale = 1.25 }) hl.monitor({ output = "HDMI-A-1", mode = "preferred", position = "1536x0", scale = 1 }) hl.monitor({ output = "", mode = "preferred", position = "auto", scale = "auto" }) ----------------- ---- STARTUP ---- ----------------- hl.env("XCURSOR_SIZE", "24") hl.env("HYPRCURSOR_SIZE", "24") hl.env("XDG_CURRENT_DESKTOP", "Hyprland") hl.env("XDG_SESSION_TYPE", "wayland") hl.env("QT_QPA_PLATFORM", "wayland;xcb") hl.env("ELECTRON_OZONE_PLATFORM_HINT", "auto") hl.on("hyprland.start", function() hl.exec_cmd("dbus-update-activation-environment --systemd WAYLAND_DISPLAY DISPLAY HYPRLAND_INSTANCE_SIGNATURE XDG_CURRENT_DESKTOP XDG_SESSION_TYPE") hl.exec_cmd("systemctl --user import-environment WAYLAND_DISPLAY DISPLAY HYPRLAND_INSTANCE_SIGNATURE XDG_CURRENT_DESKTOP XDG_SESSION_TYPE") -- portals (file pickers, screen sharing, Nautilus) need graphical-session.target, which is not started outside uwsm hl.exec_cmd("systemctl --user start hyprland-session.target") hl.exec_cmd("sh -c 'pgrep -x dms >/dev/null || exec dms run'") hl.exec_cmd("systemctl --user start hyprpolkitagent") hl.exec_cmd("hypridle") hl.exec_cmd("nm-applet --indicator") hl.exec_cmd("wl-paste --type text --watch cliphist store") hl.exec_cmd("wl-paste --type image --watch cliphist store") hl.exec_cmd(terminal .. " --class scratch") hl.exec_cmd("sh -c 'sleep 5; flatpak info com.nextcloud.desktopclient.nextcloud >/dev/null 2>&1 && ! pgrep -f \"[n]extcloud.*--background\" >/dev/null && flatpak run com.nextcloud.desktopclient.nextcloud --background'") hl.exec_cmd("sh -c 'sleep 5; flatpak info org.remmina.Remmina >/dev/null 2>&1 && ! pgrep -f \"[R]emmina.* -i\" >/dev/null && flatpak run org.remmina.Remmina -i'") hl.exec_cmd("sh -c 'sleep 8; command -v geary >/dev/null && ! pgrep -x geary >/dev/null && geary --gapplication-service'") end) ----------------------- ---- LOOK AND FEEL ---- ----------------------- hl.config({ general = { gaps_in = 4, gaps_out = 10, border_size = 2, col = { active_border = { colors = { "rgba(" .. primary .. "ff)", "rgba(" .. light .. "ff)", "rgba(" .. primary .. "ff)" }, angle = 45 }, inactive_border = "rgba(" .. outline .. "40)", }, resize_on_border = true, allow_tearing = false, layout = "dwindle", }, decoration = { rounding = 16, rounding_power = 4, -- squircle corners active_opacity = 1.0, inactive_opacity = 0.96, dim_inactive = true, dim_strength = 0.06, shadow = { enabled = true, range = 20, render_power = 3, color = 0x55000000, color_inactive = 0x44000000, }, glow = { -- light around the focused window, colour follows the DMS theme enabled = true, range = 14, render_power = 3, color = "rgba(" .. primary .. "55)", color_inactive = "rgba(00000000)", }, blur = { enabled = true, size = 10, passes = 4, noise = 0.02, vibrancy = 0.3, brightness = 1.05, popups = true, }, }, group = { auto_group = true, -- a new window opened while a group is focused becomes another tab insert_after_current = true, focus_removed_window = true, col = { border_active = "rgba(" .. primary .. "ff)", border_inactive = "rgba(" .. outline .. "40)" }, groupbar = { enabled = true, height = 16, font_size = 10, font_family = "JetBrains Mono", render_titles = true, gradients = true, rounding = 6, gaps_in = 2, gaps_out = 2, indicator_height = 0, scrolling = true, middle_click_close = true, text_color = "rgba(ffffffff)", text_color_inactive = "rgba(b8a4a9ff)", col = { active = "rgba(" .. primary .. "dd)", inactive = "rgba(2a2024cc)" }, }, }, animations = { enabled = true }, binds = { workspace_back_and_forth = true }, dwindle = { preserve_split = true, force_split = 2 }, -- new window always opens to the right/below xwayland = { force_zero_scaling = true }, -- crisp X11 apps on the 1.25x laptop panel cursor = { hide_on_key_press = true, inactive_timeout = 5 }, misc = { force_default_wallpaper = 0, disable_hyprland_logo = true, focus_on_activate = true, key_press_enables_dpms = true, -- wake the screen on any key / mouse move (hypridle turns it off) mouse_move_enables_dpms = true, animate_manual_resizes = true, animate_mouse_windowdragging = 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("quick", { type = "bezier", points = { {0.15, 0}, {0.1, 1} } }) hl.curve("linear", { type = "bezier", points = { {0, 0}, {1, 1} } }) hl.animation({ leaf = "global", enabled = true, speed = 10, bezier = "default" }) hl.animation({ leaf = "border", enabled = true, speed = 4, bezier = "easeOutQuint" }) hl.animation({ leaf = "borderangle", enabled = true, speed = 60, bezier = "linear", style = "loop" }) -- slowly rotating gradient border hl.animation({ leaf = "windows", enabled = true, speed = 3.0, bezier = "easeOutQuint" }) hl.animation({ leaf = "windowsIn", enabled = true, speed = 2.6, bezier = "easeOutQuint", style = "popin 94%" }) hl.animation({ leaf = "windowsOut", enabled = true, speed = 1.2, bezier = "quick", style = "popin 88%" }) hl.animation({ leaf = "fade", enabled = true, speed = 2, bezier = "quick" }) hl.animation({ leaf = "fadeDim", enabled = true, speed = 2.5, bezier = "quick" }) hl.animation({ leaf = "layers", enabled = true, speed = 2.6, bezier = "easeOutQuint" }) hl.animation({ leaf = "layersIn", enabled = true, speed = 2.6, bezier = "easeOutQuint", style = "slide" }) hl.animation({ leaf = "layersOut", enabled = true, speed = 1.0, bezier = "quick", style = "fade" }) hl.animation({ leaf = "workspaces", enabled = true, speed = 3.0, bezier = "easeInOutCubic", style = "slide" }) hl.animation({ leaf = "specialWorkspace", enabled = true, speed = 2.8, bezier = "easeOutQuint", style = "slidevert" }) -- No gaps/border/rounding for a single tiled window. hl.workspace_rule({ workspace = "w[tv1]", gaps_out = 0, gaps_in = 0 }) hl.workspace_rule({ workspace = "f[1]", gaps_out = 0, gaps_in = 0 }) hl.window_rule({ name = "smart-gaps-1", match = { float = false, workspace = "w[tv1]" }, border_size = 0, rounding = 0 }) hl.window_rule({ name = "smart-gaps-2", match = { float = false, workspace = "f[1]" }, border_size = 0, rounding = 0 }) --------------- ---- INPUT ---- --------------- hl.config({ input = { kb_layout = "us,ru", kb_options = "grp:alt_shift_toggle,grp_led:scroll", follow_mouse = 2, -- click to focus; the cursor can still cross other windows sensitivity = -0.5, touchpad = { natural_scroll = true, tap_to_click = true, tap_button_map = "lrm", disable_while_typing = true, middle_button_emulation = true, }, }, }) hl.device({ name = "type:touchpad", sensitivity = 0.3 }) hl.gesture({ fingers = 3, direction = "horizontal", action = "workspace" }) --------------------- ---- KEYBINDINGS ---- --------------------- local function b(keys, dsp, opts) hl.bind(keys, dsp, opts) end local function dms(args) return hl.dsp.exec_cmd("dms ipc call " .. args) end b(mod .. " + Return", hl.dsp.exec_cmd(terminal)) b(mod .. " + SHIFT + Q", hl.dsp.window.close()) b(mod .. " + D", dms("spotlight toggle")) b(mod .. " + SHIFT + E", dms("powermenu toggle")) b(mod .. " + SHIFT + P", dms("powermenu toggle")) b(mod .. " + SHIFT + C", hl.dsp.reload_config()) b(mod .. " + N", dms("control-center toggle")) b(mod .. " + SHIFT + N", dms("notifications toggle")) b(mod .. " + SHIFT + V", dms("clipboard toggle")) b(mod .. " + SHIFT + X", hl.dsp.exec_cmd("~/.config/hypr/scripts/lock.sh")) b(mod .. " + O", hl.dsp.exec_cmd("nautilus --new-window")) -- focus / move (vim keys + arrows) local dirs = { h = "left", j = "down", k = "up", l = "right", left = "left", down = "down", up = "up", right = "right" } for key, d in pairs(dirs) do b(mod .. " + " .. key, hl.dsp.focus({ direction = d })) b(mod .. " + SHIFT + " .. key, hl.dsp.window.move({ direction = d })) end -- layout -- Mod+F maximizes (the DMS bar stays visible); Mod+Shift+F is real fullscreen without the bar. b(mod .. " + F", hl.dsp.window.fullscreen({ mode = "maximized" })) b(mod .. " + SHIFT + F", function() local w = hl.get_active_window() if not w then return end if w.fullscreen ~= 2 then HL_FS_REAL = true end -- let the handler below leave this one alone hl.dispatch(hl.dsp.window.fullscreen({ mode = "fullscreen" })) end) b(mod .. " + SHIFT + SPACE", hl.dsp.window.float({ action = "toggle" })) b(mod .. " + C", hl.dsp.window.center()) b(mod .. " + E", hl.dsp.layout("togglesplit")) b(mod .. " + P", hl.dsp.window.pseudo()) -- Tabs: Mod+W puts every tiled window of this workspace into one tabbed group; press it again to split them back. -- While a group is focused, new windows join it as tabs. Mod+[ / Mod+] or the mouse wheel on the tab bar switch tabs. function HL_toggle_tabs() local a = hl.get_active_window() if not a then return end if a.group and a.group.size > 1 then hl.dispatch(hl.dsp.group.toggle()) return end local wins = hl.get_windows({ workspace = a.workspace.id, floating = false, mapped = true }) if #wins < 2 then hl.dispatch(hl.dsp.group.toggle()) -- single window: just start a group, the next windows become its tabs return end if not a.group then hl.dispatch(hl.dsp.group.toggle()) end local g = hl.get_window("address:" .. a.address).group for _, w in ipairs(wins) do if w.address ~= a.address and not w.group then g:add(w) end end hl.dispatch(hl.dsp.focus({ window = "address:" .. a.address })) end b(mod .. " + W", HL_toggle_tabs) b("ALT + TAB", hl.dsp.exec_cmd("~/.config/hypr/scripts/prev-window.sh")) -- previous window (press again to go back) b(mod .. " + ALT + TAB", hl.dsp.window.cycle_next()) -- next window on this workspace b(mod .. " + TAB", hl.dsp.focus({ workspace = "m+1" })) -- next open workspace on THIS monitor b(mod .. " + SHIFT + TAB", hl.dsp.focus({ workspace = "m-1" })) b(mod .. " + mouse_down", hl.dsp.focus({ workspace = "m+1" })) b(mod .. " + mouse_up", hl.dsp.focus({ workspace = "m-1" })) -- workspaces 1..10 for i = 1, 10 do local key = i % 10 b(mod .. " + " .. key, hl.dsp.focus({ workspace = i })) b(mod .. " + SHIFT + " .. key, hl.dsp.window.move({ workspace = i })) end -- scratchpad b(mod .. " + grave", hl.dsp.workspace.toggle_special("scratch")) b(mod .. " + SHIFT + minus", hl.dsp.window.move({ workspace = "special:scratch" })) b(mod .. " + minus", hl.dsp.workspace.toggle_special("scratch")) -- mouse b(mod .. " + mouse:272", hl.dsp.window.drag(), { mouse = true }) b(mod .. " + mouse:273", hl.dsp.window.resize(), { mouse = true }) -- resize the focused window without a mode: Mod+Ctrl + h/j/k/l or arrows (hold to repeat) local rs = { h = {-40, 0}, l = {40, 0}, k = {0, -40}, j = {0, 40}, left = {-40, 0}, right = {40, 0}, up = {0, -40}, down = {0, 40} } for key, d in pairs(rs) do b(mod .. " + CTRL + " .. key, hl.dsp.window.resize({ x = d[1], y = d[2], relative = true }), { repeating = true }) end -- sysadmin toolbox b(mod .. " + S", hl.dsp.exec_cmd("kitty --class ssh-menu -o font_size=12 -o env=SSH_MENU=1 --title 'ssh menu' ~/.config/hypr/scripts/ssh-menu.sh")) -- SSH hub (fzf) local raise = "~/.config/hypr/scripts/raise.sh " b(mod .. " + T", hl.dsp.exec_cmd(raise .. "'^org.telegram.desktop$' flatpak run org.telegram.desktop")) -- focus if open, else launch b(mod .. " + B", hl.dsp.exec_cmd(raise .. "'^google-chrome$' flatpak run com.google.Chrome")) -- again = next Chrome window b(mod .. " + SHIFT + B", hl.dsp.exec_cmd("flatpak run com.google.Chrome --new-window")) -- always a new window b(mod .. " + F1", hl.dsp.exec_cmd("~/.config/hypr/scripts/cheatsheet.sh")) -- hotkey reference b(mod .. " + K", hl.dsp.exec_cmd(raise .. "'^org.keepassxc.KeePassXC$' keepassxc")) b(mod .. " + SHIFT + T", hl.dsp.window.pin()) -- keep a floating window on top b(mod .. " + bracketright", hl.dsp.group.next()) b(mod .. " + bracketleft", hl.dsp.group.prev()) -- two monitors: laptop <-> external (focus only) b(mod .. " + comma", hl.dsp.focus({ monitor = "-1" })) b(mod .. " + period", hl.dsp.focus({ monitor = "+1" })) -- screenshots (grim + slurp; saved to ~/Pictures/Screenshots and copied) local shot = "mkdir -p ~/Pictures/Screenshots; f=~/Pictures/Screenshots/$(date +%F_%H-%M-%S).png; " b("Print", hl.dsp.exec_cmd(shot .. "grim -g \"$(slurp)\" - | tee $f | wl-copy")) b(mod .. " + SHIFT + S", hl.dsp.exec_cmd(shot .. "grim -g \"$(slurp)\" - | tee $f | wl-copy")) b("SHIFT + Print", hl.dsp.exec_cmd(shot .. "grim - | tee $f | wl-copy")) b("CTRL + Print", hl.dsp.exec_cmd(shot .. "grim -g \"$(hyprctl activewindow -j | jq -r '\"\\(.at[0]),\\(.at[1]) \\(.size[0])x\\(.size[1])\"')\" - | tee $f | wl-copy")) b(mod .. " + CTRL + C", hl.dsp.exec_cmd("hyprpicker -a")) -- color picker b(mod .. " + SHIFT + O", hl.dsp.exec_cmd("~/.config/hypr/scripts/ocr.sh")) -- screen region -> text in clipboard -- media / brightness / volume via DMS (shows OSD) b("XF86AudioRaiseVolume", dms("audio increment 5"), { locked = true, repeating = true }) b("XF86AudioLowerVolume", dms("audio decrement 5"), { locked = true, repeating = true }) b("XF86AudioMute", dms("audio mute"), { locked = true }) b("XF86AudioMicMute", dms("audio micmute"), { locked = true }) b("XF86MonBrightnessUp", dms("brightness increment 5 \"\""), { locked = true, repeating = true }) b("XF86MonBrightnessDown", dms("brightness decrement 5 \"\""), { locked = true, repeating = true }) b("XF86AudioPlay", dms("mpris playPause"), { locked = true }) b("XF86AudioNext", dms("mpris next"), { locked = true }) b("XF86AudioPrev", dms("mpris previous"), { locked = true }) -- Programs that go fullscreen on their own (F11, RDP, video) keep the DMS bar visible: Hyprland shows them -- maximized (1) while the program still believes it is fullscreen (2). HL_FS_REAL = false hl.on("window.fullscreen", function(w) if not w or w.fullscreen ~= 2 then return end if HL_FS_REAL then HL_FS_REAL = false; return end -- requested with Mod+Shift+F: keep it real hl.dispatch(hl.dsp.window.fullscreen_state({ internal = 1, client = w.fullscreen_client, window = "address:" .. w.address })) end) ----------------- ---- RULES ------ ----------------- hl.window_rule({ name = "suppress-maximize", match = { class = ".*" }, suppress_event = "maximize" }) 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 = "float-utils", match = { class = "^(pavucontrol|nm-connection-editor|blueman-manager|org.pulseaudio.pavucontrol)$" }, float = true, size = "700 500", center = true, }) hl.window_rule({ name = "float-mpv", match = { class = "^mpv$" }, float = true }) hl.window_rule({ name = "idle-inhibit-fs", match = { class = "^(firefox|mpv|google-chrome|org.telegram.desktop)$" }, idle_inhibit = "fullscreen" }) -- no auto-lock in the middle of an RDP/VNC session hl.window_rule({ name = "idle-inhibit-rdp", match = { class = "^(org.remmina.Remmina|com.freerdp.FreeRDP|xfreerdp)$" }, idle_inhibit = "focus" }) -- file dialogs from the portal and Chrome picture-in-picture hl.window_rule({ name = "float-portal-dialogs", match = { class = "^xdg-desktop-portal-gtk$" }, float = true, size = "900 600", center = true }) hl.window_rule({ name = "pip", match = { title = "^Picture in picture$" }, float = true, pin = true, size = "480 270", move = "monitor_w-500 monitor_h-290" }) hl.window_rule({ name = "scratch-term", match = { class = "^scratch$" }, workspace = "special:scratch silent", float = true, size = "1000 560", center = true, }) hl.window_rule({ name = "float-ssh-menu", match = { class = "^ssh-menu$" }, float = true, size = "1400 780", center = true }) hl.window_rule({ name = "float-keepass", match = { class = "^org.keepassxc.KeePassXC$" }, float = true, size = "1300 860", center = true }) -- Blur the DMS surfaces (bar, launcher, popups, notifications). hl.layer_rule({ name = "dms-blur", match = { namespace = "^dms:.*" }, blur = true, blur_popups = true, ignore_alpha = 0.3 }) hl.layer_rule({ name = "noanim-slurp", match = { namespace = "^selection$" }, no_anim = true })