-- Hyprland 0.57 (Lua). Clean dark work setup; shell = DankMaterialShell. -- 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 = 6, gaps_out = 14, border_size = 2, col = { active_border = { colors = { "rgba(e91e63ff)", "rgba(ff6398ff)", "rgba(f8bbd9ff)", "rgba(e91e63ff)" }, angle = 45 }, inactive_border = "rgba(3a2a2eaa)", }, 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 = 30, render_power = 3, color = 0x66e91e63, -- soft pink glow on the focused window color_inactive = 0x66000000, }, blur = { enabled = true, size = 10, passes = 4, noise = 0.02, vibrancy = 0.3, brightness = 1.05, popups = true, }, }, 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.curve("snappy", { type = "spring", mass = 1, stiffness = 340, damping = 24 }) hl.curve("bouncy", { type = "spring", mass = 1, stiffness = 280, damping = 17 }) 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.2, spring = "snappy" }) hl.animation({ leaf = "windowsIn", enabled = true, speed = 3.0, spring = "bouncy", style = "popin 88%" }) 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()) b(mod .. " + W", hl.dsp.group.toggle()) 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 = "e+1" })) b(mod .. " + SHIFT + TAB", hl.dsp.focus({ workspace = "e-1" })) b(mod .. " + mouse_down", hl.dsp.focus({ workspace = "e+1" })) b(mod .. " + mouse_up", hl.dsp.focus({ workspace = "e-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 })