From 9288cf4dee9ca3ae0f84308c9ff55c8aea660839 Mon Sep 17 00:00:00 2001 From: RGBCube Date: Sat, 22 Mar 2025 13:53:29 +0300 Subject: [PATCH] feat: hammerspoon with PaperWM and Swipe --- .gitignore | 4 + modules/darwin/hammerspoon/default.nix | 26 ++++ modules/darwin/hammerspoon/init.lua | 171 +++++++++++++++++++++++++ 3 files changed, 201 insertions(+) create mode 100644 modules/darwin/hammerspoon/default.nix create mode 100644 modules/darwin/hammerspoon/init.lua diff --git a/.gitignore b/.gitignore index d6b1677..2b20459 100644 --- a/.gitignore +++ b/.gitignore @@ -29,7 +29,10 @@ !modules/common/ !modules/common/nushell/ !modules/common/ssh/ + !modules/darwin/ +!modules/darwin/hammerspoon/ + !modules/linux/ !modules/linux/hyprland/ !modules/linux/restic/ @@ -40,6 +43,7 @@ !flake.lock !*.age +!*.lua !*.md !*.nix !*.nu diff --git a/modules/darwin/hammerspoon/default.nix b/modules/darwin/hammerspoon/default.nix new file mode 100644 index 0000000..e6174c0 --- /dev/null +++ b/modules/darwin/hammerspoon/default.nix @@ -0,0 +1,26 @@ +{ pkgs, ... }: { + home-manager.sharedModules = [{ + home.file.".hammerspoon/Spoons/PaperWM.spoon" = { + recursive = true; + + source = pkgs.fetchFromGitHub { + owner = "mogenson"; + repo = "PaperWM.spoon"; + rev = "41389206e739e6f48ea59ddcfc07254226f4c93f"; + hash = "sha256-O1Pis5udvh3PUYJmO+R2Aw11/udxk3v5hf2U9SzbeqI="; + }; + }; + home.file.".hammerspoon/Spoons/Swipe.spoon" = { + recursive = true; + + source = pkgs.fetchFromGitHub { + owner = "mogenson"; + repo = "Swipe.spoon"; + rev = "c56520507d98e663ae0e1228e41cac690557d4aa"; + hash = "sha256-G0kuCrG6lz4R+LdAqNWiMXneF09pLI+xKCiagryBb5k="; + }; + }; + + home.file.".hammerspoon/init.lua".source = ./init.lua; + }]; +} diff --git a/modules/darwin/hammerspoon/init.lua b/modules/darwin/hammerspoon/init.lua new file mode 100644 index 0000000..e52fa3b --- /dev/null +++ b/modules/darwin/hammerspoon/init.lua @@ -0,0 +1,171 @@ +---@type table +_G.hs = _G.hs + +PaperWM = hs.loadSpoon("PaperWM") +Swipe = hs.loadSpoon("Swipe") + +local windowResize = function(offsetWidth, offsetHeight) + local window = hs.window.focusedWindow() + if not window then return end + + local window_frame = window:frame() + local screen_frame = window:screen():frame() + + -- Adjust width + window_frame.w = window_frame.w + offsetWidth + window_frame.w = math.max(100, math.min(window_frame.w, screen_frame.w - window_frame.x)) + + -- Adjust height + window_frame.h = window_frame.h + offsetHeight + window_frame.h = math.max(100, math.min(window_frame.h, screen_frame.h - window_frame.y)) + + window:setFrame(window_frame) +end + +local windowClose = function() + local window = hs.window.focusedWindow() + + if not window then return end + + window:close() +end + +local spaceChange = function(offset) + local current_space = hs.spaces.activeSpaceOnScreen() + local spaces = hs.spaces.allSpaces()[hs.screen.mainScreen():getUUID()] + + local current_index = nil + for space_index, space in ipairs(spaces) do + if space == current_space then + current_index = space_index + break + end + end + + local next_index = current_index + offset + if next_index > #spaces then + next_index = 1 + elseif next_index <= 0 then + next_index = #spaces + end + + local next_space = spaces[next_index] + + hs.spaces.gotoSpace(next_space) +end + +do -- HOTKEYS + local super = { "cmd", "alt" } + local super_ctrl = { "cmd", "alt", "ctrl" } + local super_alt = { "cmd", "alt", "shift" } + + hs.hotkey.bind(super, "left", PaperWM.actions.focus_left) + hs.hotkey.bind(super, "down", PaperWM.actions.focus_down) + hs.hotkey.bind(super, "up", PaperWM.actions.focus_up) + hs.hotkey.bind(super, "right", PaperWM.actions.focus_right) + + hs.hotkey.bind(super, "h", PaperWM.actions.focus_left) + hs.hotkey.bind(super, "j", PaperWM.actions.focus_down) + hs.hotkey.bind(super, "k", PaperWM.actions.focus_up) + hs.hotkey.bind(super, "l", PaperWM.actions.focus_right) + + hs.hotkey.bind(super_ctrl, "left", function() windowResize(-100, 0) end) + hs.hotkey.bind(super_ctrl, "down", function() windowResize(0, 100) end) + hs.hotkey.bind(super_ctrl, "up", function() windowResize(0, -100) end) + hs.hotkey.bind(super_ctrl, "right", function() windowResize(100, 0) end) + + hs.hotkey.bind(super_ctrl, "h", function() windowResize(-100, 0) end) + hs.hotkey.bind(super_ctrl, "j", function() windowResize(0, 100) end) + hs.hotkey.bind(super_ctrl, "k", function() windowResize(0, -100) end) + hs.hotkey.bind(super_ctrl, "l", function() windowResize(100, 0) end) + + hs.hotkey.bind(super, "tab", function() spaceChange(1) end) + hs.hotkey.bind(super_alt, "tab", function() spaceChange(-1) end) + + for index = 1, 9 do + hs.hotkey.bind(super, tostring(index), PaperWM.actions["switch_space_" .. index]) + hs.hotkey.bind(super_alt, tostring(index), PaperWM.actions["move_window_" .. index]) + end + + hs.hotkey.bind(super_alt, "left", PaperWM.actions.swap_left) + hs.hotkey.bind(super_alt, "down", PaperWM.actions.swap_down) + hs.hotkey.bind(super_alt, "up", PaperWM.actions.swap_up) + hs.hotkey.bind(super_alt, "right", PaperWM.actions.swap_right) + + hs.hotkey.bind(super_alt, "h", PaperWM.actions.swap_left) + hs.hotkey.bind(super_alt, "j", PaperWM.actions.swap_down) + hs.hotkey.bind(super_alt, "k", PaperWM.actions.swap_up) + hs.hotkey.bind(super_alt, "l", PaperWM.actions.swap_right) + + hs.hotkey.bind(super, "q", windowClose) + hs.hotkey.bind(super, "c", PaperWM.actions.center_window) + hs.hotkey.bind(super, "f", PaperWM.actions.full_width) + hs.hotkey.bind(super_alt, "f", PaperWM.actions.toggle_floating) + + hs.hotkey.bind(super, "w", function() hs.application.launchOrFocus("Firefox") end) + hs.hotkey.bind(super, "return", function() hs.application.launchOrFocus("Ghostty") end) + hs.hotkey.bind(super, "t", function() hs.application.launchOrFocus("Finder") end) + + PaperWM.swipe_fingers = 3 + PaperWM.swipe_gain = 1.7 + + PaperWM:start() +end + +do -- 3 FINGER VERTICAL SWIPE TO CHANGE SPACES + local current_id, threshold + + Swipe:start(3, function(direction, distance, id) + if id ~= current_id then + current_id = id + threshold = 0.2 -- 20% of trackpad + return + end + + if distance > threshold then + threshold = math.huge -- only trigger once per swipe + + if direction == "up" then + spaceChange(1) + elseif direction == "down" then + spaceChange(-1) + end + end + end) +end + +do -- SPACE BUTTONS + local space_buttons = {} + + local updateSpaceButtons = function() + for _, button in pairs(space_buttons) do + button:delete() + end + space_buttons = {} + + local current_space = hs.spaces.activeSpaceOnScreen() + local spaces = hs.spaces.allSpaces()[hs.screen.mainScreen():getUUID()] + + for index = #spaces, 1, -1 do + local space = spaces[index] + + local title = tostring(index) + + local attributes = space == current_space and { + color = { red = 1 } + } or {} + + local button = hs.menubar.new() + button:setTitle(hs.styledtext.new(title, attributes)) + button:setClickCallback(function() + hs.spaces.gotoSpace(space) + end) + + table.insert(space_buttons, button) + end + end + + hs.spaces.watcher.new(updateSpaceButtons):start() + + updateSpaceButtons() +end