Skip to content

Commit 42dad75

Browse files
committed
feat(hammerspoon): add khanelimac automation
1 parent 557202a commit 42dad75

2 files changed

Lines changed: 174 additions & 0 deletions

File tree

  • homes/aarch64-darwin/khaneliman@khanelimac
  • modules/home/programs/graphical/apps/hammerspoon

homes/aarch64-darwin/khaneliman@khanelimac/default.nix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ in
2121
programs = {
2222
graphical = {
2323
apps = {
24+
hammerspoon = enabled;
2425
raycast = enabled;
2526

2627
thunderbird =
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
{
2+
config,
3+
lib,
4+
pkgs,
5+
6+
...
7+
}:
8+
let
9+
cfg = config.khanelinix.programs.graphical.apps.hammerspoon;
10+
in
11+
{
12+
options.khanelinix.programs.graphical.apps.hammerspoon.enable =
13+
lib.mkEnableOption "Hammerspoon automation";
14+
15+
config = lib.mkIf cfg.enable {
16+
assertions = [
17+
{
18+
assertion = pkgs.stdenv.hostPlatform.isDarwin;
19+
message = "Hammerspoon automation is only supported on Darwin.";
20+
}
21+
];
22+
23+
home = {
24+
file = {
25+
"Library/Application Support/Hammerspoon/init.lua".text =
26+
# lua
27+
''
28+
hs.window.animationDuration = 0
29+
30+
local hyper = { "cmd", "alt", "ctrl" }
31+
local meetingMode = false
32+
33+
local function notify(title, text)
34+
hs.notify.new({ title = title, informativeText = text }):send()
35+
end
36+
37+
local function focusedWindow()
38+
local window = hs.window.focusedWindow()
39+
if window == nil then
40+
notify("Hammerspoon", "No focused window")
41+
return nil
42+
end
43+
return window
44+
end
45+
46+
local function moveFocused(unit)
47+
local window = focusedWindow()
48+
if window ~= nil then
49+
window:moveToUnit(unit)
50+
end
51+
end
52+
53+
local function moveFocusedToNextScreen()
54+
local window = focusedWindow()
55+
if window ~= nil then
56+
window:moveToScreen(window:screen():next(), false, true)
57+
end
58+
end
59+
60+
local function rescueWindows()
61+
local rescued = 0
62+
for _, window in ipairs(hs.window.visibleWindows()) do
63+
if window:isStandard() then
64+
local frame = window:frame()
65+
local screenFrame = window:screen():frame()
66+
local offscreen = frame.x < screenFrame.x
67+
or frame.y < screenFrame.y
68+
or frame.x + frame.w > screenFrame.x + screenFrame.w
69+
or frame.y + frame.h > screenFrame.y + screenFrame.h
70+
71+
if offscreen then
72+
window:centerOnScreen()
73+
rescued = rescued + 1
74+
end
75+
end
76+
end
77+
notify("Window Rescue", tostring(rescued) .. " windows moved")
78+
end
79+
80+
local function toggleInputMute()
81+
local device = hs.audiodevice.defaultInputDevice()
82+
if device == nil then
83+
notify("Mic", "No input device")
84+
return
85+
end
86+
87+
local muted = not device:inputMuted()
88+
device:setInputMuted(muted)
89+
notify("Mic", muted and "muted" or "unmuted")
90+
end
91+
92+
local function cycleOutputDevice()
93+
local devices = hs.audiodevice.allOutputDevices()
94+
if #devices == 0 then
95+
notify("Audio Output", "No output devices")
96+
return
97+
end
98+
99+
table.sort(devices, function(left, right)
100+
return left:name() < right:name()
101+
end)
102+
103+
local current = hs.audiodevice.defaultOutputDevice()
104+
local nextIndex = 1
105+
106+
if current ~= nil then
107+
for index, device in ipairs(devices) do
108+
if device:uid() == current:uid() then
109+
nextIndex = (index % #devices) + 1
110+
break
111+
end
112+
end
113+
end
114+
115+
local nextDevice = devices[nextIndex]
116+
nextDevice:setDefaultOutputDevice()
117+
notify("Audio Output", nextDevice:name())
118+
end
119+
120+
local function toggleMeetingMode()
121+
meetingMode = not meetingMode
122+
hs.caffeinate.set("systemIdle", meetingMode, true)
123+
hs.caffeinate.set("displayIdle", meetingMode, true)
124+
125+
hs.application.launchOrFocus("Fantastical")
126+
hs.application.launchOrFocus("Microsoft Teams")
127+
notify("Meeting Mode", meetingMode and "on" or "off")
128+
end
129+
130+
hs.hotkey.bind(hyper, "Left", function()
131+
moveFocused(hs.layout.left50)
132+
end)
133+
hs.hotkey.bind(hyper, "Right", function()
134+
moveFocused(hs.layout.right50)
135+
end)
136+
hs.hotkey.bind(hyper, "Up", function()
137+
moveFocused(hs.layout.maximized)
138+
end)
139+
hs.hotkey.bind(hyper, "Down", function()
140+
moveFocused({ x = 0.16, y = 0.10, w = 0.68, h = 0.80 })
141+
end)
142+
hs.hotkey.bind(hyper, "Return", moveFocusedToNextScreen)
143+
hs.hotkey.bind(hyper, "0", rescueWindows)
144+
hs.hotkey.bind(hyper, "M", toggleInputMute)
145+
hs.hotkey.bind(hyper, "A", cycleOutputDevice)
146+
hs.hotkey.bind(hyper, "C", toggleMeetingMode)
147+
hs.hotkey.bind(hyper, "R", hs.reload)
148+
149+
notify("Hammerspoon", "config loaded")
150+
'';
151+
152+
"Library/Logs/hammerspoon/.keep".text = "";
153+
};
154+
155+
shellAliases = {
156+
open-hammerspoon = "open -a Hammerspoon";
157+
reload-hammerspoon = "osascript -e 'tell application \"Hammerspoon\" to execute lua code \"hs.reload()\"'";
158+
};
159+
};
160+
161+
launchd.agents.hammerspoon.config = {
162+
ProgramArguments = [
163+
"/usr/bin/open"
164+
"-g"
165+
"-a"
166+
"Hammerspoon"
167+
];
168+
RunAtLoad = true;
169+
StandardErrorPath = "${config.home.homeDirectory}/Library/Logs/hammerspoon/hammerspoon.err.log";
170+
StandardOutPath = "${config.home.homeDirectory}/Library/Logs/hammerspoon/hammerspoon.out.log";
171+
};
172+
};
173+
}

0 commit comments

Comments
 (0)