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
;; fennel-ls doesn't support arbitrary allowed globals, so
;; unwrap `hs` here to localize it to just one place
(local {: eventtap
: execute
:fnutils {: map}
: fs
: hotkey
: loadSpoon
: logger
: notify
: pasteboard
: pathwatcher
: screen
: urlevent
: window} hs)
;; Enables the `hs` CLI, which drives this config from a shell.
(require :hs.ipc)
(local log (logger.new :init :info))
; (set logger.defaultLogLevel :info)
(local {: mash : smash : modal-bind} (require :hotkey))
(local {: chomp : debounce : paste : replace-selection : run} (require :utils))
(local little-ff (require :little-ff))
;; debugging
; (hotkey.bind mash :d #(little-ff.open "https://google.com"))
;; ⌘⌥V - defeat paste blocking
(hotkey.bind [:cmd :alt] :v #(eventtap.keyStrokes (pasteboard.getContents)))
;; ⌘⌥⌃E - edit selected text in neovide, inspired by quickcursor (hence the temporary filenme)
(let [editor "/opt/homebrew/bin/neovide --no-fork"
cb (fn [text]
(let [home (os.getenv :HOME)
date (chomp (run "date -Iseconds -u"))
file (.. home :/.quickcursor. date)]
(pasteboard.setContents text)
(run (.. "pbpaste > " file) (.. editor " " file)
(.. "pbcopy < " file) (.. "rm " file))
(pasteboard.getContents)))]
(hotkey.bind mash :e #(replace-selection cb)))
;; mash-, modal hotkeys
(modal-bind mash "," nil
;; mash-, mash-l: create a markdown link using the selected
;; text as the title and pastboard contents as the link
[[mash
:l
nil
(fn []
(replace-selection #(.. "[" $1 "]" "(" $2 ")")))]])
;;; quitter
(let [quitter (require :quitter)]
(quitter:start))
;;; reload sketchybar on screen changes
(set _G.screen-watcher
(screen.watcher.new (debounce 1.0
(fn []
(log:i "Screen configuration changed, reloading sketchybar")
(execute "/opt/homebrew/bin/sketchybar --reload")))))
(_G.screen-watcher:start)
;;; reload aerospace on config changes
(let [home (os.getenv :HOME)
aerospace-config (.. home :/.config/aerospace/aerospace.toml)]
(set _G.aerospace-watcher
(pathwatcher.new aerospace-config
(fn []
(execute "/opt/homebrew/bin/aerospace reload-config")
(let [n (notify.new {:title :Aerospace
:informativeText "Config reloaded"
:withdrawAfter 2})]
(n:send)))))
(_G.aerospace-watcher:start))
;;; wm
(let [{: init : step} (require :wm)]
(init)
(hotkey.bind mash :m step))
;;; Spoons
(loadSpoon :SpoonInstall)
(local {:SpoonInstall Install} spoon)
(set Install.use_syncinstall true)
(set window.animationDuration 0.0)
(Install:andUse :SleepCorners {:config {:feedbackSize 25 :neverSleepCorner "*"}
:start true})
(fn sanitize-url [url]
;; Skip trurl when it would do harm without benefit:
;; - AWS SSO URLs — trurl encodes slashes in hash-routed fragments.
;; - Query-less URLs — tracking params only live in the query, so there
;; is nothing to trim, and trurl percent-encodes path colons
;; (e.g. bsky's `did:plc:…` becomes `did%3aplc%3a…`).
(if (or (string.match url "%.awsapps%.com/start/#/")
(not (string.find url "?" 1 true)))
url
(let [tracking-params [:utm_* :uta_* :fbclid :gclid]
trurl-cmd (.. :/opt/homebrew/bin/trurl " "
(table.concat (map tracking-params
#(.. "--qtrim " $1))
" "))]
(chomp (execute (.. trurl-cmd " --url \"" url "\""))))))
(let [handlers {:firefox-dev :org.mozilla.firefoxdeveloperedition
:firefox :org.mozilla.firefox
:glide :app.glide-browser.glide
:safari :com.apple.Safari
:zoom :us.zoom.xos}
;; domains to be opened in Safari, most for Apple Pay
safari-domains [:apps.apple.com
:account.apple.com
:goodluckbread.com
:patagonia.com
:squareupmessagine.com
:store.apple.com]
safari-patterns (icollect [_ domain (ipairs safari-domains)]
(.. "^https://(.*%.?)" (string.gsub domain "%." "%%.")
"/?"))
url-patterns [["^https://(.*%.?)zoom.us/j/%d+" handlers.zoom]
[safari-patterns handlers.safari]]
url-redir-decoders [[:trurl-sanitize #(sanitize-url $4) nil true]
[:reddit "://www%.reddit%.com" "://old.reddit.com"]]]
(Install:andUse :URLDispatcher {:config {:url_patterns url-patterns
:url_redir_decoders url-redir-decoders
:default_handler little-ff.open
:set_system_handler true}
:start true}))
(Install:andUse :ReloadConfiguration {:start true})
;; Local overrides
(when (fs.attributes :local.fnl)
(require :local))
(let [n (notify.new {:title :Hammerspoon
:informativeText "Config loaded"
:withdrawAfter 2})]
(n:send))
;; hold onto globals so they don't get GC'ed?
{}