half-baked re-implementation of the major parts of sdorfehs in Hammerspoon
1spoonfish = {}
2
3require("spoonfish/frames")
4require("spoonfish/windows")
5require("spoonfish/events")
6require("spoonfish/utils")
7
8
9-- default configuration, can be overridden in loading init.lua before calling
10-- spoonfish.start()
11
12-- prefix key (with control)
13spoonfish.prefix_key = "a"
14
15-- set sizes to 0 to disable
16spoonfish.border_color = "#000000"
17spoonfish.border_size = 4
18spoonfish.shadow_color = "#000000"
19spoonfish.shadow_size = 8
20
21-- space to put between windows in adjoining frames
22spoonfish.gap = 22
23
24-- program to send 'new window' to for 'c' command
25spoonfish.terminal = "iTerm2"
26
27-- for per-frame messages
28spoonfish.frame_message_secs = 1
29spoonfish.frame_message_font_size = 18
30
31-- increment to resize interactively
32spoonfish.resize_unit = 10
33
34-- for these lists, anything not starting with ^ will be run through
35-- escape_pattern to escape dashes and other special characters, so be sure
36-- to escape such characters manually in ^-prefixed patterns
37spoonfish.apps_to_watch = {
38 "^" .. spoonfish.terminal,
39 "^Firefox",
40}
41-- these override apps_to_watch
42spoonfish.apps_to_ignore = {
43}
44spoonfish.windows_to_ignore = {
45 "Picture-in-Picture",
46 "^Open ",
47 "^Save ",
48 "^Export",
49}
50
51
52-- let's go
53spoonfish.start = function()
54 local s = spoonfish
55
56 s.direction = {
57 LEFT = 1,
58 RIGHT = 2,
59 DOWN = 3,
60 UP = 4,
61 }
62
63 s.position = {
64 FRONT = 1,
65 BACK = 2,
66 REMOVE = 3,
67 }
68
69 s.initialized = false
70 s.events = hs.uielement.watcher
71
72 -- windows, array by window stack order
73 s.windows = {}
74
75 -- apps, keyed by pid
76 s.apps = {}
77
78 -- debugging flags
79 s.debug_frames = false
80
81 s.log = hs.logger.new("spoonfish", "debug")
82
83 -- spaces and frame rects, keyed by frame number
84 s.spaces = {}
85 for _, space_id in
86 pairs(hs.spaces.spacesForScreen(hs.screen.mainScreen():getUUID())) do
87 s.spaces[space_id] = { rect = hs.screen.mainScreen():frame() }
88 s.spaces[space_id].frames = {}
89 s.spaces[space_id].frames[1] = {
90 rect = hs.screen.mainScreen():frame(),
91 }
92 s.spaces[space_id].frame_previous = 1
93 s.spaces[space_id].frame_current = 1
94
95 if space_id == hs.spaces.activeSpaceOnScreen() then
96 spoonfish.draw_frames(space_id)
97 end
98 end
99
100 -- watch for new apps launched
101 s.app_watcher = hs.application.watcher.new(s.app_meta_event)
102 s.app_watcher:start()
103
104 -- watch existing apps
105 local apps = hs.application.runningApplications()
106 for i = 1, #apps do
107 s.watch_app(apps[i])
108 end
109
110 -- watch when switching spaces
111 s.spaces_watcher = hs.spaces.watcher.new(spoonfish.spaces_event)
112 s.spaces_watcher:start()
113
114 s.initialized = true
115 s.in_modal = false
116 s.send_modal = false
117 s.resizing = false
118
119 s.eventtap = hs.eventtap.new({ hs.eventtap.event.types.keyDown },
120 function(event)
121 local key = hs.keycodes.map[event:getKeyCode()]
122 local flags = event:getFlags()
123 local ctrl = flags:containExactly({ "ctrl" })
124 local nomod = flags:containExactly({}) or flags:containExactly({ "shift" })
125
126 local cs = hs.spaces.activeSpaceOnScreen()
127 local space = s.spaces[cs]
128
129 -- not sure why arrow keys come through with fn down
130 if key == "up" or key == "down" or key == "left" or key == "right" then
131 if flags:containExactly({ "ctrl", "fn" }) then
132 ctrl = true
133 elseif flags:containExactly({ "fn" }) then
134 nomod = true
135 end
136 end
137
138 if event:getType() ~= hs.eventtap.event.types.keyDown then
139 return false
140 end
141
142 if s.resizing then
143 if key == "down" or key == "left" or key == "right" or key == "up" then
144 if nomod then
145 s.frame_resize(cs, space.frame_current, s.dir_from_string(key))
146 end
147 else
148 -- any other key will exit
149 s.resizing = false
150 s.frame_message(cs, space.frame_current, nil)
151 return true
152 end
153
154 -- redisplay the frame message as it probably just changed size
155 s.frame_message(cs, space.frame_current, "Resize frame", true)
156
157 return true
158 end
159
160 if not s.in_modal then
161 if ctrl and key == spoonfish.prefix_key then
162 if s.send_modal then
163 s.send_modal = false
164 return false
165 end
166
167 s.in_modal = true
168 return true
169 end
170
171 -- not in modal, let event happen as normal
172 return false
173 end
174
175 -- we're in modal, so anything after this point will reset it
176 s.in_modal = false
177
178 -- in-modal key bindings
179 if flags:containExactly({ "shift" }) then
180 key = string.upper(key)
181 end
182
183 s.ignore_events = true
184
185 -- TODO: put these in a table for dynamic reassignment
186
187 if key == "tab" then
188 if nomod or ctrl then
189 s.frame_focus(cs, space.frame_previous, true)
190 end
191 elseif key == "down" or key == "left" or key == "right" or key == "up" then
192 local touching = s.frame_find_touching(cs, space.frame_current,
193 s.dir_from_string(key))
194 if touching[1] then
195 if nomod then
196 s.frame_focus(cs, touching[1], true)
197 elseif ctrl then
198 s.frame_swap(cs, space.frame_current, touching[1])
199 end
200 end
201 elseif key == "space" then
202 if nomod or ctrl then
203 s.frame_cycle(cs, space.frame_current, true)
204 end
205 elseif key == spoonfish.prefix_key then
206 if nomod then
207 s.send_modal = true
208 hs.eventtap.keyStroke({ "ctrl" }, spoonfish.prefix_key)
209 else
210 s.frame_reverse_cycle(cs, space.frame_current, true)
211 end
212 elseif key == "c" then
213 if nomod then
214 -- create terminal window
215 spoonfish.ignore_events = false
216 local a = hs.appfinder.appFromName(spoonfish.terminal)
217 if a == nil then
218 hs.osascript.applescript("tell application \"" .. spoonfish.terminal
219 .. "\" to activate")
220 else
221 a:setFrontmost(false)
222 hs.eventtap.keyStroke({ "cmd" }, "n")
223 end
224 end
225 elseif key == "p" then
226 if nomod or ctrl then
227 s.frame_reverse_cycle(cs, space.frame_current, true)
228 end
229 elseif key == "n" then
230 if nomod or ctrl then
231 s.frame_cycle(cs, space.frame_current, true)
232 end
233 elseif key == "r" then
234 if nomod then
235 s.frame_resize_interactively(cs, space.frame_current)
236 end
237 elseif key == "R" then
238 if nomod then
239 s.frame_remove(cs, space.frame_current)
240 end
241 elseif key == "s" then
242 if nomod then
243 s.frame_horizontal_split(cs, space.frame_current)
244 end
245 elseif key == "S" then
246 if nomod then
247 s.frame_vertical_split(cs, space.frame_current)
248 end
249 end
250
251 hs.timer.doAfter(0.25, function()
252 spoonfish.ignore_events = false
253 end)
254
255 -- swallow event
256 return true
257 end):start()
258end
259
260return spoonfish