Lua code snippets
Code snippets
Code snippets provided on this page may prove useful as inspiration or straight-up copy-paste.
Per-layout binds
local function layout_bind(bind_table)
return function ()
local workspace = hl.get_active_special_workspace() or
hl.get_active_workspace()
if not workspace then
return
end
local layout = workspace.tiled_layout
if bind_table[layout] then
hl.dispatch(bind_table[layout])
end
end
endUsage:
hl.bind("SUPER + A", layout_bind({
scrolling = hl.dsp.layout("swapcol l"), -- Scrolling: swap column with left one
dwindle = hl.dsp.layout("swapsplit"), -- Dwindle: swap window split
monocle = hl.dsp.layout("cycleprev"), -- Monocle and master: cycle prev window
master = hl.dsp.layout("cycleprev"),
}))
hl.bind("SUPER + D", layout_bind({
scrolling = hl.dsp.layout("swapcol r"), -- Scrolling: swap column with right one
dwindle = hl.dsp.layout("togglesplit"), -- Dwindle: toggle window split
monocle = hl.dsp.layout("cyclenext"), -- Monocle and master: cycle next window
master = hl.dsp.layout("cyclenext"),
}))Minimize windows using special workspaces
This approach uses special workspaces to mimic the “minimize window” function, by using a single keybind to toggle the minimized state. Note that one keybind can only handle one window.
hl.bind("SUPER + X", function ()
if hl.get_workspace("special:minimized") then
hl.dispatch(hl.dsp.window.move({ workspace = hl.get_active_workspace(), window = "tag:minimized" }))
hl.dispatch(hl.dsp.window.clear_tags({ window = "tag:minimized" }))
else
hl.dispatch(hl.dsp.window.tag({ tag = "minimized", window = hl.get_active_window() }))
hl.dispatch(hl.dsp.window.move({ workspace = "special:minimized", follow = false }))
end
end)Smart gaps
To replicate “smart gaps”/“no gaps when only” from other WMs/compositors:
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({ match = { float = false, workspace = "w[tv1]" }, border_size = 0 })
hl.window_rule({ match = { float = false, workspace = "w[tv1]" }, rounding = 0 })
hl.window_rule({ match = { float = false, workspace = "f[1]" }, border_size = 0 })
hl.window_rule({ match = { float = false, workspace = "f[1]" }, rounding = 0 })Smart gaps (ignoring special workspaces)
You can combine workspace selectors for more fine-grained control, for example, to ignore special workspaces:
hl.workspace_rule({ workspace = "w[tv1]s[false]", gaps_out = 0, gaps_in = 0 })
hl.workspace_rule({ workspace = "f[1]s[false]", gaps_out = 0, gaps_in = 0 })
hl.window_rule({ match = { float = false, workspace = "w[tv1]s[false]" }, border_size = 0 })
hl.window_rule({ match = { float = false, workspace = "w[tv1]s[false]" }, rounding = 0 })
hl.window_rule({ match = { float = false, workspace = "f[1]s[false]" }, border_size = 0 })
hl.window_rule({ match = { float = false, workspace = "f[1]s[false]" }, rounding = 0 })Float browsers’ extension windows
All known browsers have the annoying “feature” of setting extension window titles to the browser’s own name. This function listens to opened windows and floats the matches:
hl.on("window.open", function(w)
if w.class ~= "firefox" then return end
if w.initial_title ~= "Mozilla Firefox" then return end
local ff_windows = hl.get_windows({ class = "firefox" })
if #ff_windows <= 1 then return end
hl.dispatch(hl.dsp.window.float({ action = "set", window = w }))
local sub
sub = hl.on("window.title", function(tw)
if tw.address ~= w.address then return end
if tw.title == ""
or tw.title == "Mozilla Firefox"
or tw.title == "about:blank"
or tw.title:match("^about:.*Mozilla Firefox$") then return end
sub:remove()
if tw.title:match("^Extension:") then
hl.dispatch(hl.dsp.window.resize({ x = 800, y = 600, window = tw }))
hl.dispatch(hl.dsp.window.center({ window = tw }))
hl.dispatch(hl.dsp.focus({ window = tw }))
else
hl.dispatch(hl.dsp.window.float({ action = "unset", window = tw }))
end
end)
end)Open Tor Browser and Mullvad Browser at their default window size
Tor Browser and Mullvad Browser open at a window size shared by all their users, so nobody stands out by screen dimensions. A tiled, maximized, or fullscreen window has a size of its own, and the browser letterboxes it to compensate. See the Tor Browser 9.0 release notes and TZP.
Float them so they keep their default size:
hl.window_rule({
name = "privacy-browsers-float",
match = { class = "^(Tor Browser|Mullvad Browser)$" },
float = true,
center = true,
fullscreen_state = "0 0",
suppress_event = "maximize",
})Do not add a size rule for these windows.
Config versioning
Some updates add breaking changes, which can be anticipated by looking at the Hyprland version.
You can make your configs conditional using hl.version(), e.g.:
if hl.version() == "0.55.2" then
hl.config({
general = {
changed_property = "value"
}
})
else
hl.notification.create({
text = "You're using: ".. hl.version(),
timeout = 10000
})
end“Starts with” helper function
local function starts_with(string, prefix)
return string:find(prefix, 1, true) == 1
endToggle animations/blur/etc hotkey
Easily toggling animations and other effects with a bind could be useful for temporarily having less distractions, or for saving battery on a laptop:
hl.bind("SUPER + F1", function ()
local game_mode = (hl.get_config("animations.enabled") == false)
if game_mode then
hl.exec_cmd("hyprctl reload")
return
end
hl.config({
general = {
gaps_in = 0, gaps_out = 0, -- Disable gaps
border_size = 0,
},
animations = {
enabled = false, -- Disable animations
},
-- Disable blur, shadow and window rounding
decoration = {
shadow = { enabled = false },
blur = { enabled = false },
rounding = 0,
}
})
end)Cycle layout for current workspace
To change the layout of the current workspace, you can use this bind:
hl.bind("SUPER + tab", function ()
local layouts = { "scrolling", "dwindle", "master", "monocle" }
local workspace = hl.get_active_workspace()
if hl.get_active_special_workspace() then
workspace = hl.get_active_special_workspace()
end
local next_layout = "dwindle"
if not workspace then
return
end
for i = 1, #layouts do
if layouts[i] == workspace.tiled_layout then
local next_layout_idx = (i % #layouts) + 1
next_layout = layouts[next_layout_idx]
break
end
end
if workspace.special then
hl.workspace_rule({ workspace = tostring(workspace.name), layout = next_layout })
else
hl.workspace_rule({ workspace = "name:" .. tostring(workspace.name), layout = next_layout })
end
end)Windows Magnifier-like cursor zoom
These binds adjust the zoom factor, which zooms into your entire screen and moves to keep the cursor in view. This acts similarly to Windows Magnifier, in its full-screen mode.
local MAX_ZOOM = 3
local MIN_ZOOM = 1
local ZOOM_TOGGLE_FACTOR = 1.5
---@param offset number
---@return nil
local function zoom(offset)
local current = hl.get_config("cursor.zoom_factor")
if offset ~= nil then
current = current + offset
elseif current ~= MIN_ZOOM then
current = MIN_ZOOM
else
current = ZOOM_TOGGLE_FACTOR
end
current = math.max(MIN_ZOOM, math.min(MAX_ZOOM, current))
hl.config({ cursor = { zoom_factor = current } })
end
hl.bind("SUPER + Z", zoom)
hl.bind("SUPER + KP_ADD", function()
zoom(0.5)
end)
hl.bind("SUPER + minus", function()
zoom(-0.5)
end)Move focus between tiled and floating windows
This functions similarly to the i3 focus mode_toggle bind.
hl.bind("SUPER + space", function()
local window = hl.get_active_window()
if not window then return end
hl.dispatch(hl.dsp.window.cycle_next({
floating = not window.floating,
tiled = window.floating,
}))
end, { description = "Switch focus between tiled and floating windows" })Pin floating windows with no_focus, and restore them
This function lets floating windows such as lyrics stay visible on every workspace, without stealing focus from other windows. It also restores them to normal floating windows when you need to move them around. Note that calling the function again while a window handled this way is focused restores all windows that were set to this state.
local function pin_nofocus_toggle()
local tag = "pin_nofocus"
local function has_tag(window)
for _, t in ipairs(window.tags or {}) do
if t == tag then
return true
end
end
return false
end
return function()
local window = hl.get_active_window()
if not window then return end
if window.floating and window.fullscreen == 0 and not window.pinned and not has_tag(window) then
-- Tag the window and mark it as pinned with no_focus
hl.dispatch(hl.dsp.window.tag({ tag = "+" .. tag, window = window }))
hl.dispatch(hl.dsp.window.pin({ window = window }))
hl.dispatch(hl.dsp.window.set_prop({ prop = "no_focus", value = "1", window = window }))
else
-- Restore all windows that were marked this way
for _, marked_window in ipairs(hl.get_windows({ tag = tag })) do
hl.dispatch(hl.dsp.window.set_prop({ prop = "no_focus", value = "unset", window = marked_window }))
if window.pinned then
hl.dispatch(hl.dsp.window.pin({ window = marked_window }))
end
hl.dispatch(hl.dsp.window.tag({ tag = "-" .. tag, window = marked_window }))
end
end
end
end
hl.bind("SUPER + P", pin_nofocus_toggle())Vim-like keymaps
Hyprland has so many features that you might run out of keys on your keyboard if you want to bind them all. Rest assured, you can utilize submaps to create keymaps if you want more, and they’re also easier to press than a single bind with many modifiers.
Here’s an example of managing window groups this way:
hl.bind("SUPER + G", hl.dsp.submap("group_management"), { description = "Enter a group management submap" })
local map = function(key, action, description)
hl.bind(key, function()
hl.dispatch(action)
hl.dispatch(hl.dsp.submap("reset"))
end, { description = description })
end
hl.define_submap("group_management", function()
map("g", hl.dsp.group.toggle(), "Toggle window group")
map("h", hl.dsp.window.move({ into_group = "l" }), "Move window into a group on the left")
map("j", hl.dsp.window.move({ into_group = "d" }), "Move window into a group on the bottom")
map("k", hl.dsp.window.move({ into_group = "u" }), "Move window into a group on the top")
map("l", hl.dsp.window.move({ into_group = "r" }), "Move window into a group on the right")
map("e", hl.dsp.window.move({ out_of_group = true }), "Move window out of group")
map("n", hl.dsp.group.next(), "Next window in group")
map("p", hl.dsp.group.prev(), "Previous window in group")
map("f", hl.dsp.group.move_window(), "Move window forward in the group order")
map("b", hl.dsp.group.move_window({ forward = false }), "Move window backward in the group order")
map("t", hl.dsp.group.lock_active(), "Toggle group lock")
for i = 1, 10 do
map(tostring(i % 10), hl.dsp.group.active({ index = i }), "Focus window " .. i .. " in a group")
end
hl.bind("escape", hl.dsp.submap("reset"), { description = "Quit submap" })
end)Close All Windows At Once
local function closeAllWindows()
local windows = hl.get_windows()
for _,w in pairs(windows) do
hl.dispatch(hl.dsp.window.close({ window = w }))
end
end
local function closeAllWindowsCurrentWorkspace()
local cws = hl.get_active_workspace()
local windows = hl.get_windows({ workspace = cws })
for _,w in pairs(windows) do
hl.dispatch(hl.dsp.window.close({ window = w }))
end
end
hl.bind("SUPER+BackSpace", closeAllWindowsCurrentWorkspace )
hl.bind("SUPER+Delete", closeAllWindows )Move All Windows In Current Workspace To Another
local function moveWindowsCurrentWorkspace(ws, f)
local cws = hl.get_active_workspace()
local windows = hl.get_windows({ workspace = cws })
for _,w in pairs(windows) do
hl.dispatch(
hl.dsp.window.move({
window = w ,
workspace = ws ,
follow = f
})
)
end
end
for i = 1, 10 do
local key = i % 10
hl.bind("SUPER+SHIFT+ALT+" .. key, function()
moveWindowsCurrentWorkspace(i, true)
end)
end
for i = 1, 10 do
local key = i % 10
hl.bind("SUPER+CTRL+ALT+" .. key, function()
moveWindowsCurrentWorkspace(i, false)
end)
endSwitch To Workspaces 01-99
hl.bind("SUPER+P", hl.dsp.submap("ws_tens"), { description = "Workspace picker (01-99)" })
hl.define_submap("ws_tens", function()
for tens = 0, 9 do
hl.bind(tostring(tens), hl.dsp.submap("ws_units_" .. tens))
end
local letters = "abcdefghijklmnopqrstuvwxyz"
for i = 1, #letters do
hl.bind(letters:sub(i, i), hl.dsp.submap("reset"))
end
hl.bind("escape", hl.dsp.submap("reset"))
end)
for tens = 0, 9 do
hl.define_submap("ws_units_" .. tens, "reset", function()
for units = 0, 9 do
local target = tens * 10 + units
if target > 0 then
hl.bind(tostring(units), hl.dsp.focus({ workspace = target }))
end
end
hl.bind("escape", hl.dsp.submap("reset"))
hl.bind("catchall", hl.dsp.submap("reset"))
end)
end