-
Notifications
You must be signed in to change notification settings - Fork 70
/
windows.fnl
565 lines (491 loc) · 15.2 KB
/
windows.fnl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
(local {: filter
: get-in
: count
: concat
: contains?
: map
: for-each
: split} (require :lib.functional))
(local {:global-filter global-filter} (require :lib.utils))
(local {:atom atom
:deref deref
:swap! swap!
:reset! reset!} (require :lib.atom))
(require-macros :lib.advice.macros)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; History
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(global history {})
(fn history.push
[self]
"
Append current window frame geometry to history.
self refers to history table instance
"
(let [win (hs.window.focusedWindow)
id (when win (win:id))
tbl (. self id)]
(when win
(if (= (type tbl) :nil)
(tset self id [(win:frame)])
(let [last-el (. tbl (length tbl))]
(when (~= last-el (win:frame))
(table.insert tbl (win:frame))))))))
(fn history.pop
[self]
"
Go back to previous window frame geometry in history.
self refers to history table instance
"
(let [win (hs.window.focusedWindow)
id (when win (win:id))
tbl (. self id)]
(when (and win tbl)
(let [el (table.remove tbl)
num-of-undos (length tbl)]
(if el
(do
(win:setFrame el)
(when (< 0 num-of-undos)
(alert (.. num-of-undos " undo steps available"))))
(alert "nothing to undo"))))))
(fn undo
[]
(: history :pop))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Shared Functions
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn highlight-active-window
[]
"
Draw a border around the active window for a short period to highlight
"
(let [rect (hs.drawing.rectangle (: (hs.window.focusedWindow) :frame))]
(: rect :setStrokeColor {:red 1 :blue 0 :green 1 :alpha 1})
(: rect :setStrokeWidth 5)
(: rect :setFill false)
(: rect :show)
(hs.timer.doAfter .3 (fn [] (: rect :delete)))))
(fn maximize-window-frame
[]
(: history :push)
(: (hs.window.focusedWindow) :maximize 0)
(highlight-active-window))
(defn position-window-center
[ratio-str window screen]
"
Takes the center-ratio key from config, or default value if not
provided, and the window center-window-frame was called with,
and the current screen.
Should calculate the centered dimensions of the target window
using the ratio values
This function is advisable.
"
(let [frame (: screen :fullFrame)
[w-percent h-percent] (split ":" ratio-str)
w-percent (/ (tonumber w-percent) 100)
h-percent (/ (tonumber h-percent) 100)
update {:w (* w-percent frame.w)
:h (* h-percent frame.h)
:x 0
:y 0}]
(doto window
(: :setFrameInScreenBounds update)
(: :centerOnScreen))
(highlight-active-window)))
(fn center-window-frame
[]
(: history :push)
(let [win (hs.window.focusedWindow)
prev-duration hs.window.animationDuration
config (get-config)
ratio (or (?. config :modules :windows :center-ratio) "80:50")
screen (hs.screen.primaryScreen)]
(tset hs.window :animationDuration 0)
(position-window-center ratio win screen)
(tset hs.window :animationDuration prev-duration)))
(fn activate-app
[app-name]
(hs.application.launchOrFocus app-name)
(let [app (hs.application.find app-name)]
(when app
(: app :activate)
(hs.timer.doAfter .05 highlight-active-window)
(: app :unhide))))
(fn set-mouse-cursor-at
[app-title]
(let [sf (: (: (hs.application.find app-title) :focusedWindow) :frame)
desired-point (hs.geometry.point (- (+ sf._x sf._w)
(/ sf._w 2))
(- (+ sf._y sf._h)
(/ sf._h 2)))]
(hs.mouse.setAbsolutePosition desired-point)))
(fn show-grid
[]
(: history :push)
(hs.grid.show))
(fn jump-to-last-window
[]
(-> (global-filter)
(: :getWindows hs.window.filter.sortByFocusedLast)
(. 2)
(: :focus)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Jumping Windows
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(fn jump-window
[arrow]
"
Navigate to the window nearest the current active window
For instance if you open up emacs to the left of a web browser, activate
emacs, then run (jump-window :l) hammerspoon will move active focus
to the browser.
Takes an arrow like :h :j :k :l to support vim key bindings.
Performs side effects
Returns nil
"
(let [dir {:h "West" :j "South" :k "North" :l "East"}
frontmost-win (hs.window.frontmostWindow)
focus-dir (.. :focusWindow (. dir arrow))]
(: hs.window.filter.defaultCurrentSpace focus-dir frontmost-win true true)
(highlight-active-window)))
(fn jump-window-left
[]
(jump-window :h))
(fn jump-window-above
[]
(jump-window :j))
(fn jump-window-below
[]
(jump-window :k))
(fn jump-window-right
[]
(jump-window :l))
(fn allowed-app?
[window]
(if (: window :isStandard)
true
false))
(fn jump []
"
Displays hammerspoon's window jump UI
"
(let [wns (->> (hs.window.allWindows)
(filter allowed-app?))]
(hs.hints.windowHints wns nil true)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Movement\Resizing Constants
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(local
arrow-map
{:k {:half [0 0 1 .5] :movement [ 0 -20] :complement :h :resize "Shorter"}
:j {:half [0 .5 1 .5] :movement [ 0 20] :complement :l :resize "Taller"}
:h {:half [0 0 .5 1] :movement [-20 0] :complement :j :resize "Thinner"}
:l {:half [.5 0 .5 1] :movement [ 20 0] :complement :k :resize "Wider"}})
(fn grid
[method direction]
"
Moves, expands, or shrinks the active window by the next grid dimension. Grid
settings are specified in config.fnl.
"
(let [fn-name (.. method direction)
f (. hs.grid fn-name)]
(f (hs.window.focusedWindow))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Resize window by half
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(fn rect
[rct]
"
Change a window's rect geometry which includes x, y, width, and height
Takes a rectangle table
Performs side-effects to move or resize the active window and update history.
Returns nil
"
(: history :push)
(let [win (hs.window.focusedWindow)]
(when win (: win :move rct))))
(fn resize-window-halve
[arrow]
"
Resize a window by half the grid dimensions specified in config.fnl.
Takes an :h :j :k or :l arrow
Performs a side effect to resize the active window's frame rect
Returns nil
"
(: history :push)
(rect (. arrow-map arrow :half)))
(fn resize-half-left
[]
(resize-window-halve :h))
(fn resize-half-right
[]
(resize-window-halve :l))
(fn resize-half-top
[]
(resize-window-halve :k))
(fn resize-half-bottom
[]
(resize-window-halve :j))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Resize window by increments
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(fn resize-by-increment
[arrow]
"
Resize the active window by the next window increment
Let's say we make the grid dimensions 4x4 and we place a window in the 1x1
meaning first column in the first row.
We then resize an increment right. The dimensions would now be 2x1
Takes an arrow like :h :j :k :l
Performs a side-effect to resize the current window to the next grid increment
Returns nil
"
(let [directions {:h "Left"
:j "Down"
:k "Up"
:l "Right"}]
(: history :push)
(when (or (= arrow :h) (= arrow :l))
(hs.grid.resizeWindowThinner (hs.window.focusedWindow)))
(when (or (= arrow :j) (= arrow :k))
(hs.grid.resizeWindowShorter (hs.window.focusedWindow)))
(grid :pushWindow (. directions arrow))))
(fn resize-inc-left
[]
(resize-by-increment :h))
(fn resize-inc-bottom
[]
(resize-by-increment :j))
(fn resize-inc-top
[]
(resize-by-increment :k))
(fn resize-inc-right
[]
(resize-by-increment :l))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Resize windows
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(fn resize-window
[arrow]
"
Resizes a window against the grid specifed in config.fnl
Takes an arrow string like :h :k :j :l
Performs a side effect to resize the current window.
Returns nil
"
(: history :push)
;; hs.grid.resizeWindowShorter/Taller/Thinner/Wider
(grid :resizeWindow (. arrow-map arrow :resize)))
(fn resize-left
[]
(resize-window :h))
(fn resize-up
[]
(resize-window :j))
(fn resize-down
[]
(resize-window :k))
(fn resize-right
[]
(resize-window :l))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Resize to grid preset
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(fn resize-to-grid
[grid]
(: history :push)
(hs.grid.set (hs.window.focusedWindow) grid))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Move to screen directions
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(fn move-to-screen
[screen]
"Moves current window onto given hs.screen instance"
(let [w (hs.window.focusedWindow)
no-resize true]
(: w :moveToScreen screen no-resize)))
(fn move-screen
[method]
"
Moves a window to the display in the specified direction
:north ^ :south v :east -> :west <-
Takes a method name of the hammer spoon window instance.
You probably will not be using this function directly.
Performs a side effect that will move a window the next screen in specified
direction.
Returns nil
"
(let [window (hs.window.focusedWindow)]
(: window method nil true)))
(fn move-north
[]
(move-screen :moveOneScreenNorth))
(fn move-south
[]
(move-screen :moveOneScreenSouth))
(fn move-east
[]
(move-screen :moveOneScreenEast))
(fn move-west
[]
(move-screen :moveOneScreenWest))
(local canvas (require :hs.canvas))
(local screen-number-canvases (atom []))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Move to screen by number
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(fn show-display-number
[idx screen]
"Shows a big number at the corner of hs.screen.
To be used as for multi-monitor setups, to easily identify index of each
screen."
(let [cs (canvas.new {})
font-size (/ (. (: screen :frame) :w) 10)]
(swap! screen-number-canvases (fn [t] (concat t [cs])))
(doto cs
(: :frame (: screen :frame))
(: :appendElements
[{:action :fill
:type :text
:frame {:x "0.93" :y 0 :h "1" :w "1"}
:text (hs.styledtext.new
idx
{:font {:size font-size}
:color {:red 1 :green 0.5 :blue 0 :alpha 1}})
:withShadow true}])
(: :show))))
(fn show-display-numbers
[screens]
"Shows big number at the corner of each screen.
To be used as for multi-monitor setups, to easily identify index of each screen."
(let [ss (hs.screen.allScreens)]
(when (< 1 (count ss))
(each [idx display (ipairs (hs.screen.allScreens))]
(show-display-number idx display)))))
(fn hide-display-numbers
[]
"Hides big numbers at the corner of each screen that are used for guidance in
multi-monitor setups."
(for-each
(fn [c] (: c :delete .4))
(deref screen-number-canvases))
(reset! screen-number-canvases []))
(fn monitor-item
[screen i]
"
Creates a menu item to move the frontMost window to the specified screen index
Takes a hs.screen instance and an index integer
Returns a table-map to add to a config.fnl modal menu
"
{:title (.. "Monitor " i)
:key (tostring i)
:group :monitor
:action (fn []
(when screen
(move-to-screen screen)))})
(fn remove-monitor-items
[menu]
"
Removes the monitor items from a menu
Takes a menu table-map
Mutates the menu object to remove items with :group :monitor flags
Returns mutated table-map
"
(->> menu.items
(filter #(not (= (. $ :group) :monitor)))
(tset menu :items))
menu)
(fn add-monitor-items
[menu screens]
"
Update a menu by adding an item for each connected monitor
Takes a menu table-map and a table-list of hs.screens
Mutates the menu.items by adding items for each monitor
Returns mutated modal menu table-map
"
(->> screens
(map monitor-item)
(concat menu.items)
(tset menu :items))
menu)
(fn enter-window-menu
[menu]
"
Handler that can be used when entering the windows menu
Takes modal menu table-map
- Hides any previous display numbers
- Shows display numbers at top right of each screen
- Removes previous monitor items if any were added
- Adds monitor items based on currently connected monitors
Returns mutated modal menu table-map for threading or chaining
"
(let [screens (hs.screen.allScreens)]
(hide-display-numbers)
(show-display-numbers screens)
(remove-monitor-items menu)
(add-monitor-items menu screens))
menu)
(fn exit-window-menu
[menu]
"
Handler that can be used when exiting the windows menu
- Removes previous monitor items if any were added
Returns mutated modal menu table-map for threading or chaining
"
(hide-display-numbers)
menu)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Initialization
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(fn init
[config]
"
Initializes the windows module
Performs side effects:
- Set grid margins from config.fnl like {:grid {:margins [10 10]}}
- Set the grid dimensions from config.fnl like {:grid {:size \"3x2\"}}
"
(hs.grid.setMargins (or (get-in [:grid :margins] config) [0 0]))
(hs.grid.setGrid (or (get-in [:grid :size] config) "3x2")))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Exports
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
{: activate-app
: center-window-frame
: enter-window-menu
: exit-window-menu
: hide-display-numbers
: highlight-active-window
: init
: jump
: jump-to-last-window
: jump-window-above
: jump-window-below
: jump-window-left
: jump-window-right
: maximize-window-frame
: move-east
: move-north
: move-south
: move-to-screen
: move-west
: position-window-center
: rect
: resize-down
: resize-half-bottom
: resize-half-left
: resize-half-right
: resize-half-top
: resize-inc-bottom
: resize-inc-left
: resize-inc-right
: resize-inc-top
: resize-left
: resize-right
: resize-up
: resize-to-grid
: set-mouse-cursor-at
: show-display-numbers
: show-grid
: undo}