Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions default/hypr/apple-gestures.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
-- Macs swipe between workspaces with three fingers, as they do in macOS.
-- Loaded after the user's files, so a three-finger sideways gesture the user
-- set, or omarchy_workspace_gesture = false, keeps this one out instead of
-- Hyprland rejecting it as a duplicate. Lines added to hyprland.lua after the
-- toggles come too late for that, which is why the manual points at input.lua.

if _G.omarchy_workspace_gesture == false or not (o and o.apple_silicon and o.apple_silicon()) then
return
end

local sideways = {
horizontal = true,
horiz = true,
left = true,
l = true,
right = true,
r = true,
swipe = true,
}

for _, gesture in ipairs(o.registered_gestures or {}) do
if gesture.fingers == 3 and not gesture.modified and sideways[gesture.direction] then
return
end
end

hl.gesture({ fingers = 3, direction = "horizontal", action = "workspace" })
53 changes: 48 additions & 5 deletions default/hypr/helpers.lua
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,14 @@ local function opens_overlay(command)
return false
end

function o.apple_silicon()
if apple_silicon == nil then
apple_silicon = o.shell_succeeds("omarchy-hw-apple-silicon")
end

return apple_silicon
end

function o.focus_builtin_screen()
for _, monitor in ipairs(hl.get_monitors()) do
if monitor.name:match("^eDP%-") then
Expand All @@ -134,11 +142,7 @@ end
-- built-in keyboard, moves focus before the menu bind runs. Other keyboards only
-- match the menu bind.
local function bind_builtin_screen_focus(keys)
if apple_silicon == nil then
apple_silicon = o.shell_succeeds("omarchy-hw-apple-silicon")
end

if apple_silicon then
if o.apple_silicon() then
hl.bind(keys, o.focus_builtin_screen, { device = { inclusive = true, list = builtin_keyboards } })
end
end
Expand Down Expand Up @@ -215,3 +219,42 @@ function o.window(match, rules)

hl.window_rule(rules)
end

local modifier_names = { "SHIFT", "CAPS", "CTRL", "CONTROL", "ALT", "MOD1", "MOD2", "MOD3", "SUPER", "WIN", "LOGO", "MOD4", "META", "MOD5" }

-- Hyprland reads a modifier out of any string that contains one's name, so
-- "NONE" or "" is no modifier at all.
local function holds_modifier(mods)
if type(mods) ~= "string" then
return mods ~= nil
end

mods = mods:upper()
for _, name in ipairs(modifier_names) do
if mods:find(name, 1, true) then
return true
end
end

return false
end

-- Hyprland rejects a gesture another one already covers, and gives Lua no way
-- to list what's registered. On a Mac, record each one so the default workspace
-- swipe, added after the user's files, can step aside for the user's own.
if hl and hl.gesture and not o.registered_gestures and o.apple_silicon() then
local register_gesture = hl.gesture
o.registered_gestures = {}

hl.gesture = function(gesture, ...)
if type(gesture) == "table" then
table.insert(o.registered_gestures, {
fingers = tonumber(gesture.fingers),
direction = type(gesture.direction) == "string" and gesture.direction:lower() or "",
modified = holds_modifier(gesture.mods),
})
end

return register_gesture(gesture, ...)
end
end
1 change: 1 addition & 0 deletions default/hypr/toggles.lua
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@ disabled_input_device("touchpad")
disabled_input_device("touchscreen")

require("default.hypr.workspace-layouts")
require("default.hypr.apple-gestures")
6 changes: 6 additions & 0 deletions mac-manual/content/06-keyboard.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,9 @@ On Macs with an ambient light sensor, the keyboard backlight follows the room: l
## Trackpad

The built-in trackpad scrolls naturally, as in macOS, and tap-to-click is off, because a palm brushing the trackpad while typing would otherwise click. Click by pressing the trackpad. Change either in `~/.config/hypr/input.lua`, as [Keyboard, mouse, trackpad](https://omarchy.org/manual/keyboard-mouse-trackpad/) describes; a value you set yourself is kept.

Swipe sideways with three fingers to move between workspaces, as in macOS. A three-finger sideways gesture of your own in `~/.config/hypr/input.lua` takes its place. To turn the swipe off, add this line to `~/.config/hypr/input.lua`:

```lua
omarchy_workspace_gesture = false
```
145 changes: 145 additions & 0 deletions test/shell.d/hyprland-apple-workspace-gesture-test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
#!/bin/bash
set -euo pipefail
source "$(dirname "${BASH_SOURCE[0]}")/base-test.sh"

require_command lua

tmpdir=$(mktemp -d)
trap 'rm -rf "$tmpdir"' EXIT

mkdir -p "$tmpdir/apple-bin" "$tmpdir/other-bin"
printf '#!/bin/sh\nexit 0\n' >"$tmpdir/apple-bin/omarchy-hw-apple-silicon"
printf '#!/bin/sh\nexit 1\n' >"$tmpdir/other-bin/omarchy-hw-apple-silicon"
chmod +x "$tmpdir"/*-bin/omarchy-hw-apple-silicon

# Loads the shipped hyprland.lua against a user's ~/.config/hypr and prints one
# line per gesture Hyprland accepted, then "error" for each one it rejected.
# The stub applies Hyprland 0.56's rule (CTrackpadGestures::addGesture): a
# gesture on the same fingers and mods is refused once an earlier one covers
# its direction or axis, and the refusal is a config error.
load_config() {
local platform="$1" edit="${2:-}"
local home
home=$(mktemp -d "$tmpdir/home.XXXXXX")

mkdir -p "$home/.config"
cp -R "$ROOT/config/hypr" "$home/.config/hypr"
[[ -z $edit ]] || printf '%s\n' "$edit" >>"$home/.config/hypr/input.lua"

HOME="$home" XDG_CONFIG_HOME="$home/.config" XDG_STATE_HOME="$home/.local/state" OMARCHY_PATH="$ROOT" \
PATH="$tmpdir/$platform-bin:$PATH" lua <<'LUA'
local function proxy()
return setmetatable({}, {
__index = function(self, key)
local value = proxy()
rawset(self, key, value)
return value
end,
__call = function()
return {}
end,
})
end

local aliases = { l = "left", r = "right", u = "up", d = "down", horiz = "horizontal", vert = "vertical" }
local axes = {
left = "horizontal", right = "horizontal", horizontal = "horizontal",
up = "vertical", down = "vertical", vertical = "vertical",
swipe = "swipe",
}
local accepted = {}

-- KeybindManager::stringToModMask: any string naming no modifier is mask zero.
local function mod_mask(mods)
local mask = {}
mods = (mods or ""):upper()
for name, bit in pairs({ SHIFT = "S", CAPS = "C", CTRL = "T", CONTROL = "T", ALT = "A", SUPER = "M", WIN = "M", META = "M" }) do
if mods:find(name, 1, true) then
mask[bit] = true
end
end
local bits = {}
for bit in pairs(mask) do
table.insert(bits, bit)
end
table.sort(bits)
return table.concat(bits)
end

hl = setmetatable({
dsp = proxy(),
gesture = function(gesture)
local direction = gesture.direction:lower()
direction = aliases[direction] or direction
local axis = axes[direction]
local mods = mod_mask(gesture.mods)

for _, g in ipairs(accepted) do
if g.fingers == gesture.fingers and g.mods == mods and
(g.direction == axis or g.direction == direction or
((axis == "horizontal" or axis == "vertical") and g.direction == "swipe")) then
print("error")
return
end
end

table.insert(accepted, { fingers = gesture.fingers, direction = direction, mods = mods })
local action = type(gesture.action) == "string" and gesture.action or "function"
print(gesture.fingers .. " " .. direction .. " " .. (mods ~= "" and "+" .. mods .. " " or "") .. action)
end,
get_config = function() return nil end,
get_active_window = function() return nil end,
get_monitors = function() return {} end,
}, {
__index = function()
return function()
return {}
end
end,
})

dofile(os.getenv("HOME") .. "/.config/hypr/hyprland.lua")
LUA
}

swipe="3 horizontal workspace"

[[ $(load_config apple) == "$swipe" ]] || fail "a Mac swipes between workspaces with three fingers" "$(load_config apple)"
[[ -z $(load_config other) ]] || fail "x86 and Snapdragon keep no default gesture" "$(load_config other)"
pass "three-finger workspace swipe is on by default on Macs only"

shipped_line=$(sed -nE 's/^-- (hl\.gesture\(\{ fingers = 3, direction = "horizontal".*)$/\1/p' "$ROOT/config/hypr/input.lua")
[[ -n $shipped_line ]] || fail "input.lua still ships the workspace gesture example"
output=$(load_config apple "$shipped_line")
[[ $output == "$swipe" ]] || fail "uncommenting the shipped example keeps one gesture and no config error" "$output"
pass "uncommenting the shipped example does not duplicate the gesture"

focus_lines=$(sed -nE 's/^-- (hl\.gesture\(\{ fingers = 3, direction = "(left|right)".*)$/\1/p' "$ROOT/config/hypr/input.lua")
(( $(wc -l <<<"$focus_lines") == 2 )) || fail "input.lua still ships the focus gesture examples" "$focus_lines"
output=$(load_config apple "$focus_lines")
[[ $output == $'3 left function\n3 right function' ]] || fail "three-finger focus gestures replace the workspace swipe" "$output"
pass "a user's own three-finger sideways gestures replace the default"

output=$(load_config apple 'hl.gesture({ fingers = 3, direction = "swipe", action = "move" })')
[[ $output == "3 swipe move" ]] || fail "a three-finger swipe gesture replaces the workspace swipe" "$output"
output=$(load_config apple 'hl.gesture({ fingers = 4, direction = "horizontal", action = "workspace" })')
[[ $output == $'4 horizontal workspace\n'"$swipe" ]] || fail "a four-finger gesture keeps the three-finger default" "$output"
output=$(load_config apple 'hl.gesture({ fingers = 3, direction = "up", action = "fullscreen" })')
[[ $output == $'3 up fullscreen\n'"$swipe" ]] || fail "a vertical gesture keeps the sideways default" "$output"
output=$(load_config apple 'hl.gesture({ fingers = 3, direction = "horizontal", mods = "SUPER", action = "move" })')
[[ $output == $'3 horizontal +M move\n'"$swipe" ]] || fail "a gesture held with a modifier keeps the default" "$output"
pass "gestures on other fingers, axes or modifiers keep the default"

for mods in '"NONE"' '""' '" "'; do
output=$(load_config apple 'hl.gesture({ fingers = 3, direction = "horizontal", mods = '"$mods"', action = "workspace" })')
[[ $output == "$swipe" ]] || fail "mods = $mods counts as no modifier and replaces the default" "$output"
done
pass "a modifier string naming no modifier still replaces the default"

output=$(load_config apple 'omarchy_workspace_gesture = false')
[[ -z $output ]] || fail "omarchy_workspace_gesture = false turns the default off" "$output"
pass "omarchy_workspace_gesture = false turns the default off"

grep -Fq 'omarchy_workspace_gesture = false' "$ROOT/mac-manual/content/06-keyboard.md" ||
fail "the manual documents how to turn the Mac gesture off"
pass "the manual documents how to turn the Mac gesture off"
Loading