-- All keybindings live in this module so conflicts can be audited in one -- place. Shared values come from settings; command wrapping comes from lib. local commands = require("lib.commands") local settings = require("config.settings") local main = settings.modifiers.main local hyper = settings.modifiers.hyper -- Build normalized bind strings and avoid hand-written separator mistakes such -- as doubled `+` characters. local function chord(...) return table.concat({ ... }, " + ") end -- Stage uses vertical navigation to move through its preview ordering. Keep -- the ordinary directional-focus behavior for every other tiled layout. local function focus_or_stage(direction, stage_message) local focus = hl.dsp.focus({ direction = direction }) if not stage_message then return focus end return function() local workspace = hl.get_active_special_workspace() or hl.get_active_workspace() if workspace and workspace.tiled_layout == "stage" then hl.dispatch(hl.dsp.layout(stage_message)) return end hl.dispatch(focus) end end local tiled_layouts = { "dwindle", "master", "scrolling", "monocle", "stage" } local active_workspace_layout_rules = {} local function cycle_active_workspace_layout() local workspace = hl.get_active_special_workspace() or hl.get_active_workspace() if not workspace or workspace.special then return end local next_index = 1 for index, layout in ipairs(tiled_layouts) do if layout == workspace.tiled_layout then next_index = index % #tiled_layouts + 1 break end end local selector = workspace.config_name if not selector or selector == "" then selector = tostring(workspace.id) end local previous_rule = active_workspace_layout_rules[workspace.id] if previous_rule then previous_rule:set_enabled(false) end active_workspace_layout_rules[workspace.id] = hl.workspace_rule({ workspace = selector, layout = tiled_layouts[next_index], }) end -- Generate matching focus/move bindings for numbered workspaces. Workspace 10 -- maps to the 0 key because `% 10` returns zero. local function bind_numbered_workspaces(modifier, move_modifier, count) for workspace = 1, count do local key = workspace % 10 hl.bind( chord(modifier, key), hl.dsp.focus({ workspace = workspace }), { description = "Switch to workspace " .. workspace } ) hl.bind( chord(modifier, move_modifier, key), hl.dsp.window.move({ workspace = workspace }), { description = "Send focused window to workspace " .. workspace } ) end end -- Core application and window-management shortcuts. hl.bind( chord(main, "SPACE"), hl.dsp.exec_cmd("noctalia msg panel-toggle launcher"), { description = "Toggle Noctalia launcher" } ) hl.bind( chord(main, "W"), hl.dsp.window.close(), { description = "Close focused window" } ) hl.bind( chord(main, "Q"), hl.dsp.exec_cmd(commands.uwsm(settings.programs.terminal)), { description = "Open terminal" } ) hl.bind( chord(main, "E"), hl.dsp.exec_cmd(commands.uwsm(settings.programs.file_manager)), { description = "Open file manager" } ) hl.bind( chord(main, "V"), hl.dsp.window.float({ action = "toggle" }), { description = "Toggle focused window floating" } ) hl.bind( chord(main, "R"), hl.dsp.exec_cmd(settings.programs.launcher), { description = "Open application launcher" } ) hl.bind( chord(main, "P"), hl.dsp.window.pseudo(), { description = "Toggle pseudo-tiling for focused window" } ) hl.bind( chord(main, "J"), hl.dsp.layout("togglesplit"), { description = "Toggle dwindle split direction" } ) -- One direction table drives arrow-key focus, Hyper+WASD focus/movement, and -- moving the active workspace between adjacent monitors. local directions = { { arrow = "left", key = "A", direction = "l", monitor = "left" }, { arrow = "right", key = "D", direction = "r", monitor = "right" }, { arrow = "up", key = "W", direction = "u", monitor = "above", stage_message = "cycleprev" }, { arrow = "down", key = "S", direction = "d", monitor = "below", stage_message = "cyclenext" }, } for _, direction in ipairs(directions) do local description = "Focus window " .. direction.arrow if direction.stage_message then description = description .. " / navigate Stage" end hl.bind( chord(main, direction.arrow), hl.dsp.focus({ direction = direction.arrow }), { description = "Focus window " .. direction.arrow } ) hl.bind( chord(hyper, direction.key), focus_or_stage(direction.direction, direction.stage_message), { description = description } ) hl.bind( chord(hyper, "CTRL", direction.key), hl.dsp.window.move({ direction = direction.direction }), { description = "Move focused window " .. direction.arrow } ) hl.bind( chord(hyper, main, direction.key), hl.dsp.workspace.move({ monitor = direction.direction }), { description = "Move workspace to monitor " .. direction.monitor } ) end -- Cycle only the active workspace through the configured tiled layouts. hl.bind( chord(hyper, "grave"), cycle_active_workspace_layout, { description = "Cycle active workspace layout" } ) bind_numbered_workspaces(main, "SHIFT", 10) bind_numbered_workspaces(hyper, "CTRL", 9) -- Cycle workspaces with the wheel and manipulate windows with mouse buttons. hl.bind( chord(main, "mouse_down"), hl.dsp.focus({ workspace = "e+1" }), { description = "Switch to next workspace" } ) hl.bind( chord(main, "mouse_up"), hl.dsp.focus({ workspace = "e-1" }), { description = "Switch to previous workspace" } ) hl.bind( chord(main, "mouse:272"), hl.dsp.window.drag(), { mouse = true, description = "Drag focused window" } ) hl.bind( chord(main, "mouse:273"), hl.dsp.window.resize(), { mouse = true, description = "Resize focused window" } ) -- Locked media keys also work while the session is locked. Volume and -- brightness binds repeat while held; playback controls fire once. local media_binds = { { key = "XF86AudioRaiseVolume", command = "wpctl set-volume -l 1 @DEFAULT_AUDIO_SINK@ 5%+", description = "Increase output volume", repeating = true, }, { key = "XF86AudioLowerVolume", command = "wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-", description = "Decrease output volume", repeating = true, }, { key = "XF86AudioMute", command = "wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle", description = "Toggle output mute", repeating = true, }, { key = "XF86AudioMicMute", command = "wpctl set-mute @DEFAULT_AUDIO_SOURCE@ toggle", description = "Toggle microphone mute", repeating = true, }, { key = "XF86MonBrightnessUp", command = "brightnessctl -e4 -n2 set 5%+", description = "Increase display brightness", repeating = true, }, { key = "XF86MonBrightnessDown", command = "brightnessctl -e4 -n2 set 5%-", description = "Decrease display brightness", repeating = true, }, { key = "XF86AudioNext", command = "playerctl next", description = "Play next media track", }, { key = "XF86AudioPause", command = "playerctl play-pause", description = "Toggle media playback", }, { key = "XF86AudioPlay", command = "playerctl play-pause", description = "Toggle media playback", }, { key = "XF86AudioPrev", command = "playerctl previous", description = "Play previous media track", }, } for _, bind in ipairs(media_binds) do hl.bind(bind.key, hl.dsp.exec_cmd(bind.command), { locked = true, repeating = bind.repeating, description = bind.description, }) end -- Generate focus/move shortcuts for every named workspace in settings.lua. for _, workspace in ipairs(settings.named_workspaces) do hl.bind( chord(hyper, workspace.key), hl.dsp.focus({ workspace = workspace.selector }), { description = "Switch to " .. workspace.name .. " workspace" } ) hl.bind( chord(hyper, "CTRL", workspace.key), hl.dsp.window.move({ workspace = workspace.selector }), { description = "Send focused window to " .. workspace.name .. " workspace" } ) end -- Promote the focused client when a workspace is using master layout. hl.bind( chord(hyper, "M"), hl.dsp.layout("swapwithmaster master ignoremaster"), { description = "Promote to master" } ) -- Toggle ScrollOverview with SUPER+g hl.bind( chord(hyper, "SPACE"), function() hl.plugin.scrolloverview.overview("toggle all") end) -- Toggle a focused client into an 80%-sized centered floating mode. Returning -- it to tiled mode lets the active layout restore its previous geometry. hl.bind(chord(hyper, "F"), function() local window = hl.get_active_window() local monitor = hl.get_active_monitor() if not window or not monitor then return end local was_tiled = not window.floating hl.dispatch(hl.dsp.window.float({ action = "toggle" })) if not was_tiled then return end hl.dispatch(hl.dsp.window.resize({ x = math.floor(monitor.width * 0.80), y = math.floor(monitor.height * 0.80), relative = false, })) hl.dispatch(hl.dsp.window.center()) hl.dispatch(hl.dsp.window.alter_zorder({ mode = "top" })) end, { description = "Toggle focused floating window", }) -- Open the HyprCapture plugin's interactive region/window picker. hl.bind( chord(main, "SHIFT", "S"), hl.plugin.hyprcapture.open, { description = "HyprCapture window capture" } )