half-baked re-implementation of the major parts of sdorfehs in Hammerspoon
1-- take control of a new hs.window and resize it to a particular frame
2-- return a window table object
3spoonfish.frame_capture = function(space_id, frame_id, hswin)
4 local win = {
5 ["win"] = hswin,
6 ["frame"] = frame_id,
7 ["space"] = space_id,
8 ["app_pid"] = hswin:application():pid(),
9 }
10 table.insert(spoonfish.windows, win)
11 spoonfish.window_reframe(win)
12 spoonfish.window_restack(win, 1)
13 return win
14end
15
16-- move the top window of a frame to the bottom of the stack, raise the next
17-- available or last window to the top
18spoonfish._frame_cycle = function(space_id, frame_id, reverse, complain)
19 local wnf = spoonfish.windows_not_visible(space_id)
20
21 if table.count(wnf) == 0 then
22 if complain then
23 spoonfish.frame_message(space_id, spoonfish.spaces[space_id].frame_current,
24 "No more windows")
25 end
26 return
27 end
28
29 local fwin = spoonfish.frame_top_window(space_id, frame_id)
30 if fwin then
31 -- move this top window to the bottom of the stack
32 spoonfish.window_restack(fwin, spoonfish.position.BACK)
33 end
34
35 -- find the first window that is not in a frame and bring it forth
36 local cwin = wnf[1]
37 if reverse then
38 cwin = wnf[table.count(wnf)]
39 end
40 if cwin ~= nil then
41 cwin["frame"] = frame_id
42 spoonfish.frame_raise_window(space_id, frame_id, cwin)
43 end
44end
45spoonfish.frame_cycle = function(space_id, frame_id, complain)
46 spoonfish._frame_cycle(space_id, frame_id, false, complain)
47end
48spoonfish.frame_reverse_cycle = function(space_id, frame_id, complain)
49 spoonfish._frame_cycle(space_id, frame_id, true, complain)
50end
51
52-- return a table of frame ids touching frame_id on side dir
53spoonfish.frame_find_touching = function(space_id, frame_id, dir)
54 local cur = spoonfish.spaces[space_id].frames[frame_id].rect
55 local found = {}
56
57 if dir == spoonfish.direction.LEFT then
58 -- x+w touching the x of the current frame, with same y
59 for i, f in pairs(spoonfish.spaces[space_id].frames) do
60 if f.rect.x + f.rect.w == cur.x and f.rect.y == cur.y then
61 found[i] = true
62 end
63 end
64 -- or just x+w touching the x of the current frame
65 for i, f in pairs(spoonfish.spaces[space_id].frames) do
66 if f.rect.x + f.rect.w == cur.x then
67 found[i] = true
68 end
69 end
70 elseif dir == spoonfish.direction.RIGHT then
71 -- x touching the x+w of the current frame, with same y
72 for i, f in pairs(spoonfish.spaces[space_id].frames) do
73 if f.rect.x == cur.x + cur.w and f.rect.y == cur.y then
74 found[i] = true
75 end
76 end
77 -- or just x touching the x+w of the current frame
78 for i, f in pairs(spoonfish.spaces[space_id].frames) do
79 if f.rect.x == cur.x + cur.w then
80 found[i] = true
81 end
82 end
83 elseif dir == spoonfish.direction.DOWN then
84 -- y touching the y+h of the current frame, with same x
85 for i, f in pairs(spoonfish.spaces[space_id].frames) do
86 if f.rect.y == cur.y + cur.h and f.rect.x == cur.x then
87 found[i] = true
88 end
89 end
90 -- or just y touching the y+h of the current frame
91 for i, f in pairs(spoonfish.spaces[space_id].frames) do
92 if f.rect.y == cur.y + cur.h then
93 found[i] = true
94 end
95 end
96 elseif dir == spoonfish.direction.UP then
97 -- y+h touching the y of the current frame, with same x
98 for i, f in pairs(spoonfish.spaces[space_id].frames) do
99 if f.rect.y + f.rect.h == cur.y and f.rect.x == cur.x then
100 found[i] = true
101 end
102 end
103 -- or just y+h touching the y of the current frame
104 for i, f in pairs(spoonfish.spaces[space_id].frames) do
105 if f.rect.y + f.rect.h == cur.y then
106 found[i] = true
107 end
108 end
109 else
110 error("frame_find_touching: bogus direction")
111 end
112
113 return table.keys(found)
114end
115
116spoonfish.frame_rect_with_gap = function(space_id, frame_id)
117 local rect = spoonfish.spaces[space_id].frames[frame_id].rect
118 local hgap = spoonfish.gap / 2
119 local grect = spoonfish.inset(rect, hgap)
120 local srect = spoonfish.spaces[space_id].rect
121
122 if rect.x == srect.x then
123 -- touching left side
124 grect.x = grect.x + hgap
125 grect.w = grect.w - hgap
126 end
127
128 if rect.y == srect.y then
129 -- touching top
130 grect.y = grect.y + hgap
131 grect.h = grect.h - hgap
132 end
133
134 if (rect.x + rect.w) == (srect.x + srect.w) then
135 -- touching right side
136 grect.w = grect.w - hgap
137 end
138
139 if (rect.y + rect.h) == (srect.y + srect.h) then
140 -- touching bottom
141 grect.h = grect.h - hgap
142 end
143
144 return grect
145end
146
147-- give focus to frame and raise its active window
148spoonfish.frame_focus = function(space_id, frame_id, raise)
149 if spoonfish.spaces[space_id].frames[frame_id] == nil then
150 error("bogus frame " .. frame_id .. " on space " .. space_id)
151 return
152 end
153
154 local fc = spoonfish.spaces[space_id].frame_current
155 local wof = spoonfish.frame_top_window(space_id, frame_id)
156 if wof then
157 spoonfish.window_reframe(wof)
158 if raise then
159 wof["win"]:focus()
160 end
161 spoonfish.window_reborder(wof)
162 end
163
164 if frame_id ~= fc then
165 spoonfish.spaces[space_id].frame_current = frame_id
166 spoonfish.spaces[space_id].frame_previous = fc
167
168 spoonfish.frame_message(space_id, frame_id, "Frame " .. frame_id)
169 end
170end
171
172-- split a frame horizontally
173spoonfish.frame_horizontal_split = function(space_id, frame_id)
174 return spoonfish.frame_split(space_id, frame_id, false)
175end
176
177-- raise a window in a given frame, assigning it to that frame
178spoonfish.frame_raise_window = function(space_id, frame_id, win)
179 spoonfish.window_reframe(win)
180 spoonfish.window_show(win)
181 win["win"]:focus()
182 spoonfish.window_restack(win, spoonfish.position.FRONT)
183 spoonfish.window_reborder(win)
184end
185
186spoonfish.frame_resize_interactively = function(space_id, frame_id)
187 if table.count(spoonfish.spaces[space_id].frames) == 1 then
188 spoonfish.frame_message(space_id,
189 table.keys(spoonfish.spaces[space_id].frames)[1],
190 "Cannot resize only frame", false)
191 return
192 end
193
194 spoonfish.resizing = true
195 spoonfish.frame_message(space_id, frame_id, "Resize frame", true)
196end
197
198-- shrink or grow a given frame
199spoonfish.frame_resize = function(space_id, frame_id, dir)
200 local origrect = spoonfish.inset(
201 spoonfish.spaces[space_id].frames[frame_id].rect, 0)
202 local srect = spoonfish.spaces[space_id].rect
203 local resized = {}
204
205 if dir == spoonfish.direction.LEFT or dir == spoonfish.direction.RIGHT or
206 dir == spoonfish.direction.UP or dir == spoonfish.direction.DOWN then
207 local amt = spoonfish.resize_unit
208 local xy = "x"
209 local wh = "w"
210
211 if dir == spoonfish.direction.LEFT or dir == spoonfish.direction.UP then
212 -- shrink
213 amt = -amt
214 end
215
216 if dir == spoonfish.direction.UP or dir == spoonfish.direction.DOWN then
217 xy = "y"
218 wh = "h"
219 end
220
221 if (origrect[xy] == srect[xy]) and
222 (origrect[xy] + origrect[wh] == srect[xy] + srect[wh]) then
223 -- the original frame can't be resized in this direction, don't bother
224 -- resizing any others
225 return
226 end
227
228 for i, f in pairs(spoonfish.spaces[space_id].frames) do
229 if (f.rect[xy] == srect[xy]) and
230 (f.rect[xy] + f.rect[wh] == srect[xy] + srect[wh]) then
231 -- this frame can't be resized in this direction
232 else
233 -- shrink/grow frames with this frame's coord
234 if f.rect[xy] == origrect[xy] then
235 if (f.rect[xy] + f.rect[wh]) == (srect[xy] + srect[wh]) then
236 -- frame is on the right/bottom screen edge, keep its edge there by
237 -- moving it left or up
238 f.rect[xy] = f.rect[xy] - amt
239 end
240 f.rect[wh] = f.rect[wh] + amt
241 resized[i] = true
242 end
243
244 -- grow/shrink frames with edge of this frame's shrunken/grown edge
245 if (f.rect[xy] + f.rect[wh] == origrect[xy]) or
246 (f.rect[xy] == origrect[xy] + origrect[wh]) then
247 if (f.rect[xy] + f.rect[wh]) == (srect[xy] + srect[wh]) then
248 -- frame is on the right/bottom screen edge, keep its edge there
249 f.rect[xy] = f.rect[xy] + amt
250 end
251 f.rect[wh] = f.rect[wh] - amt
252 resized[i] = true
253 end
254 end
255 end
256 else
257 error("frame_resize: bogus direction")
258 return
259 end
260
261 spoonfish.draw_frames(space_id)
262
263 for _, w in ipairs(spoonfish.windows) do
264 if w["space"] == space_id and resized[w["frame"]] then
265 spoonfish.window_reframe(w)
266 end
267 end
268end
269
270-- split a frame vertically or horizontally
271spoonfish.frame_split = function(space_id, frame_id, vertical)
272 local old_rect = spoonfish.spaces[space_id].frames[frame_id].rect
273 local new_rect = {}
274
275 -- halve current frame
276 if vertical then
277 new_rect = hs.geometry.rect(
278 old_rect.x,
279 old_rect.y,
280 math.floor(old_rect.w / 2),
281 old_rect.h
282 )
283 else
284 new_rect = hs.geometry.rect(
285 old_rect.x,
286 old_rect.y,
287 old_rect.w,
288 math.floor(old_rect.h / 2)
289 )
290 end
291 spoonfish.spaces[space_id].frames[frame_id].rect = new_rect
292
293 -- reframe all windows in that old frame
294 for _, w in ipairs(spoonfish.windows) do
295 if w["space"] == space_id and w["frame"] == frame_id then
296 spoonfish.window_reframe(w)
297 end
298 end
299
300 local new_frame_id = table.count(spoonfish.spaces[space_id].frames) + 1
301 local new_frame_rect = {}
302
303 if vertical then
304 new_frame_rect = hs.geometry.rect(
305 new_rect.x + new_rect.w,
306 new_rect.y,
307 old_rect.w - new_rect.w,
308 new_rect.h
309 )
310 else
311 new_frame_rect = hs.geometry.rect(
312 new_rect.x,
313 new_rect.y + new_rect.h,
314 new_rect.w,
315 old_rect.h - new_rect.h
316 )
317 end
318 spoonfish.spaces[space_id].frames[new_frame_id] = { rect = new_frame_rect }
319
320 spoonfish.draw_frames(space_id)
321 spoonfish.frame_focus(space_id, new_frame_id, true)
322
323 -- we'll probably want to go to this frame on tab
324 spoonfish.spaces[space_id].frame_previous = new_frame_id
325end
326
327-- swap the front-most windows of two frames
328spoonfish.frame_swap = function(space_id, frame_id_from, frame_id_to)
329 local fwin = spoonfish.frame_top_window(space_id, frame_id_from)
330 local twin = spoonfish.frame_top_window(space_id, frame_id_to)
331
332 if fwin ~= nil then
333 fwin["frame"] = frame_id_to
334 spoonfish.window_reframe(fwin)
335 end
336 if twin ~= nil then
337 twin["frame"] = frame_id_from
338 spoonfish.window_reframe(twin)
339 end
340 spoonfish.frame_focus(space_id, frame_id_to, true)
341end
342
343-- remove current frame
344spoonfish.frame_remove = function(space_id)
345 if table.count(spoonfish.spaces[space_id].frames) == 1 then
346 spoonfish.frame_message(space_id,
347 table.keys(spoonfish.spaces[space_id].frames)[1],
348 "Cannot remove only frame", false)
349 return
350 end
351
352 local id_removing = spoonfish.spaces[space_id].frame_current
353
354 -- reframe all windows in the current frame and renumber higher frames
355 for _, w in ipairs(spoonfish.windows) do
356 if w["space"] == space_id then
357 if w["frame"] == id_removing then
358 w["frame"] = 0
359 spoonfish.window_reframe(w)
360 elseif w["frame"] > id_removing then
361 w["frame"] = w["frame"] - 1
362 end
363 end
364 end
365
366 -- shift other frame numbers down
367 table.remove(spoonfish.spaces[space_id].frames, id_removing)
368
369 if spoonfish.spaces[space_id].frame_previous > id_removing then
370 spoonfish.spaces[space_id].frame_previous =
371 spoonfish.spaces[space_id].frame_previous - 1
372 end
373
374 -- TODO: actually resize other frames
375
376 spoonfish.frame_focus(space_id, spoonfish.spaces[space_id].frame_previous,
377 true)
378end
379
380-- split a frame vertically
381spoonfish.frame_vertical_split = function(space_id, frame_id)
382 return spoonfish.frame_split(space_id, frame_id, true)
383end
384
385-- return the first window in this frame
386spoonfish.frame_top_window = function(space_id, frame_id)
387 for _, w in ipairs(spoonfish.windows) do
388 if w["space"] == space_id and w["frame"] == frame_id then
389 return w
390 end
391 end
392end
393
394spoonfish.frame_message_timer = nil
395spoonfish._frame_message = nil
396spoonfish.frame_message = function(space_id, frame_id, message, sticky)
397 if spoonfish.frame_message_timer ~= nil then
398 spoonfish.frame_message_timer:stop()
399 end
400
401 if spoonfish._frame_message ~= nil then
402 spoonfish._frame_message:delete()
403 spoonfish._frame_message = nil
404 end
405
406 if message == nil then
407 return
408 end
409
410 if spoonfish.spaces[space_id].frames[frame_id] == nil then
411 return
412 end
413 local frame = spoonfish.spaces[space_id].frames[frame_id].rect
414
415 local textFrame = hs.drawing.getTextDrawingSize(message,
416 { size = spoonfish.frame_message_font_size })
417 local lwidth = textFrame.w + 30
418 local lheight = textFrame.h + 10
419
420 spoonfish._frame_message = hs.canvas.new {
421 x = frame.x + (frame.w / 2) - (lwidth / 2),
422 y = frame.y + (frame.h / 2) - (lheight / 2),
423 w = lwidth,
424 h = lheight,
425 }:level(hs.canvas.windowLevels.popUpMenu)
426
427 spoonfish._frame_message[1] = {
428 id = "1",
429 type = "rectangle",
430 action = "fill",
431 center = {
432 x = lwidth / 2,
433 y = lheight / 2,
434 },
435 fillColor = { green = 0, blue = 0, red = 0, alpha = 0.9 },
436 roundedRectRadii = { xRadius = 15, yRadius = 15 },
437 }
438 spoonfish._frame_message[2] = {
439 id = "2",
440 type = "text",
441 frame = {
442 x = 0,
443 y = ((lheight - spoonfish.frame_message_font_size) / 2) - 1,
444 h = "100%",
445 w = "100%",
446 },
447 textAlignment = "center",
448 textColor = { white = 1.0 },
449 textSize = spoonfish.frame_message_font_size,
450 text = message,
451 }
452
453 spoonfish._frame_message:show()
454
455 if not sticky then
456 spoonfish.frame_message_timer = hs.timer.doAfter(
457 spoonfish.frame_message_secs, function()
458 if spoonfish._frame_message ~= nil then
459 spoonfish._frame_message:delete()
460 spoonfish._frame_message = nil
461 end
462 spoonfish.frame_message_timer = nil
463 end)
464 end
465end
466
467-- for debugging
468spoonfish.draw_frames = function(space_id)
469 if not spoonfish.debug_frames then
470 return
471 end
472
473 for i, f in pairs(spoonfish.spaces[space_id].frames) do
474 if f.outline == nil then
475 f.outline = hs.drawing.rectangle(f.rect)
476 else
477 f.outline:setFrame(f.rect)
478 end
479 f.outline:setLevel(hs.drawing.windowLevels.normal)
480 f.outline:setStrokeColor({ ["hex"] = "#ff0000" })
481 f.outline:setStrokeWidth(4)
482 f.outline:setFill(false)
483 f.outline:show()
484 end
485end