-- ───────────────────────────────────────────────────────────────────────────── -- Input -- -- Deliberately GNOME-like: click-to-focus, no focus-follows-mouse, no mouse -- acceleration changes. The goal is that muscle memory transfers untouched. -- ───────────────────────────────────────────────────────────────────────────── local prefs = require("prefs") local actions = require("actions") hl.config({ input = { kb_layout = prefs.get("keyboardLayout", "us"), kb_variant = prefs.get("keyboardVariant", ""), kb_model = "", kb_options = prefs.get("keyboardOptions", "caps:escape_shifted_capslock"), kb_rules = "", numlock_by_default = prefs.get("numlockByDefault", true), -- GNOME's defaults are 500ms delay / 33Hz repeat. repeat_delay = prefs.get("keyRepeatDelay", 500), repeat_rate = prefs.get("keyRepeatRate", 33), -- 1 = FOLLOW. The window under the pointer takes focus. This comment -- previously claimed 1 was "click to focus, GNOME's behavior", which -- is the opposite of what Hyprland does -- `hyprctl descriptions` gives -- map: [{"separate":3},{"detached":2},{"follow":1},{"disabled":0}], -- so click-to-focus is 0. Changing the shipped value is a behavior -- decision rather than a correction, so the value is left alone and -- only the description is fixed; Settings exposes all four. follow_mouse = prefs.getInt("followMouse", 1), -- Softens follow_mouse: with this off, focus changes only when the -- pointer crosses a window boundary, not on every movement inside one. -- Still focus-follows-pointer, just less twitchy. mouse_refocus = false, -- 0 = NEXT, the compositor's own default: focus goes to the next window -- in the layout order. `hyprctl descriptions` publishes -- map: [{"mru":2},{"cursor":1},{"next":0}], and Settings offers all -- three; 0 is repeated here so nothing changes on a machine with no -- settings file. focus_on_close = prefs.getInt("focusOnClose", 0), -- Flat pointer response by default, no acceleration. Matters for -- gaming; Settings offers adaptive for people who want it back. sensitivity = prefs.get("pointerSensitivity", 0), accel_profile = prefs.get("accelProfile", "flat"), natural_scroll = prefs.get("naturalScroll", false), scroll_factor = prefs.get("scrollFactor", 1.0), left_handed = prefs.get("leftHanded", false), -- Empty is a value, not an omission: it is what `hyprctl getoption` -- reports as "[[EMPTY]]" before anything writes the option, and it means -- "let libinput pick per device" -- two fingers on a touchpad, the wheel -- on a mouse. Writing it back explicitly is the same branch a stock -- Hyprland takes, and it keeps the setting reversible: without an empty -- choice in the schema there would be no way back from a scroll method -- once one was picked. Settings offers 2fg / edge / on_button_down / -- no_scroll alongside it, the four words the option's own description -- names (it publishes no map). scroll_method = prefs.get("scrollMethod", ""), -- Only consulted while scroll_method is on_button_down. 0 means the -- device's own middle button. scroll_button = prefs.get("scrollButton", 0), -- Clicking a floating window raises and focuses it. float_switch_override_focus = 2, -- Every touchpad preference Settings offers is read here as well as -- applied live: the live half (hyprctl eval) reaches a running -- compositor, and THIS half is what survives `hyprctl reload` and the -- gap before the shell starts. A schema `hypr:` entry without a -- prefs.get() here is a control that quietly reverts -- the whole -- Mouse & Touchpad page once had exactly that bug. Defaults must -- match PreferenceSchema's; the hypr-prefs-contract checks both. touchpad = { tap_to_click = prefs.get("touchpadTapToClick", true), natural_scroll = prefs.get("touchpadNaturalScroll", true), disable_while_typing = prefs.get("touchpadDisableWhileTyping", true), scroll_factor = prefs.get("touchpadScrollFactor", 1.0), drag_lock = prefs.getInt("touchpadDragLock", 0), middle_button_emulation = prefs.get("touchpadMiddleButtonEmulation", false), clickfinger_behavior = prefs.get("touchpadClickfinger", false), -- Underscores here, hyphens in the option name getoption answers to -- (input:touchpad:tap-and-drag) -- the same split tap_to_click has -- at the top of this table. Hyprland's own default is true; it is -- repeated rather than omitted so the schema, the Lua, and the -- compositor all state the same value. tap_and_drag = prefs.get("touchpadTapAndDrag", true), }, }, -- Pointer BEHAVIOUR, as opposed to pointer appearance: how the cursor reacts -- to typing and to workspace switches. The cursor's looks -- theme, size, -- hardware cursors, the inactivity fade -- are a separate cursor table in -- looks.lua. hl.config calls are additive per option, so the two tables -- coexist; they are split by what a person would go looking for, and these -- two appear on Settings' Mouse & Touchpad page rather than in Appearance. cursor = { hide_on_key_press = prefs.get("cursorHideWhileTyping", false), -- An integer with three states, written from a switch: disable = 0, -- enable = 1, force = 2. Settings offers the first two, so getInt is -- what bridges a stored boolean to the number Hyprland wants -- the same -- pairing render.cm_auto_hdr uses in looks.lua. warp_on_change_workspace = prefs.getInt("cursorWarpOnWorkspaceChange", 0), }, -- Tuning for the three-finger gestures registered below. gestures = { workspace_swipe_distance = prefs.getInt("swipeDistance", 300), workspace_swipe_invert = prefs.get("swipeInvert", true), }, }) -- ── Touchpad gestures ─────────────────────────────────────────────────────── -- -- GNOME's gestures, reproduced: three fingers sideways moves between -- workspaces, three fingers up opens the overview, three fingers down closes -- it. That is the same muscle memory the keybinds were built to preserve. -- -- Registered unconditionally rather than behind a preference. Hyprland 0.56 -- dropped `gestures:workspace_swipe` in favour of this `gesture` keyword, and a -- registration is read at config time -- so a toggle would need a reload to -- take effect, which is worse than the nothing these cost on a machine with no -- touchpad. What IS tunable at runtime lives in Settings: how far a swipe has -- to travel, and which way round it goes. -- -- open and close rather than toggle twice: with a toggle on both directions, -- swiping up from an already-open overview would close it, and swiping down -- would reopen it. GNOME does not do that, and neither does this. local overview = function(fn) return function() hl.exec_cmd("qs ipc call overview " .. fn) end end hl.gesture({ fingers = 3, direction = "horizontal", action = "workspace" }) hl.gesture({ fingers = 3, direction = "up", action = overview("open") }) hl.gesture({ fingers = 3, direction = "down", action = overview("close") }) -- ── Four-finger gestures ──────────────────────────────────────────────────── -- -- The three above are the desktop's, fixed. These four are the user's: each -- holds a named action from settings.json, or {} for unassigned, and the -- vocabulary is exactly the one custom shortcuts use -- actions.lua resolves -- both, through the same whitelist tables, so a gesture can no more introduce -- a command than a keybind can. -- -- Nothing is emitted for an unassigned direction. That matters more here than -- it looks: a registration is read at config time and there is no way to -- remove one afterwards, so emitting a no-op gesture for every direction would -- consume the four-finger swipes permanently, including for whatever the -- compositor might do with them later. -- -- Four rather than three because three is spoken for, and because four fingers -- is the largest number of them a touchpad this size can tell apart. local custom_gestures = { { pref = "gestureFourUp", direction = "up" }, { pref = "gestureFourDown", direction = "down" }, { pref = "gestureFourLeft", direction = "left" }, { pref = "gestureFourRight", direction = "right" }, } for _, gesture in ipairs(custom_gestures) do local action = actions.gesture(prefs.get(gesture.pref, {})) if action ~= nil then hl.gesture({ fingers = 4, direction = gesture.direction, action = action }) end end return true