little-ff: pass the window through the placement steps
check-browser re-queried the focused window at every step, so resize and
place each looked up what wait-for-little-ff had already found.
wait-for-little-ff now hands the window to its callback and the rest take
it as an argument.

focused-browser-window, formerly is-browser-focused which read as a
boolean but returns a window, matches on browser.bundle-id directly.
match unifies against outer locals, so the multisym compares rather than
binds; case shadows it instead, and rejects multisyms outright.

aerospace writes failures to stderr and leaves stdout empty, so the
fallback that picked between the two streams is gone.

Assisted-by: Claude Opus 5 via Claude Code
change sqpqxntyxlwzqllsswqysxzxzlzwrwvq
commit 00fdeb6272e37fc582a2d6e8fee7f1ce5d0f7d6d
author Alpha Chen <alpha@kejadlen.dev>
date
parent owqnyktx
diff --git a/.hammerspoon/little-ff.fnl b/.hammerspoon/little-ff.fnl
index 4263991e..8e52b72f 100644
--- a/.hammerspoon/little-ff.fnl
+++ b/.hammerspoon/little-ff.fnl
@@ -4,28 +4,22 @@
 
 (local aerospace :/opt/homebrew/bin/aerospace)
 
-(local ff {:bin "/Applications/Firefox Developer Edition.app/Contents/MacOS/firefox"
-           :bundle-id :org.mozilla.firefoxdeveloperedition
-           :name "Firefox Developer Addition"})
-
-(local glide {:bin :/Applications/Glide.app/Contents/MacOS/glide
-              :bundle-id :app.glide-browser.glide
-              :name :Glide})
-
-(local browser glide)
-
-(λ is-ff-focused []
-  ;; focusedWindow returns nil when nothing is focused, which happens often
-  ;; enough while polling for a window that is still being created.
-  (let [win (hs.window.focusedWindow)
-        app (when win (win:application))
-        bundle-id (when app (app:bundleID))]
-    (when (= bundle-id browser.bundle-id)
-      win)))
-
-(λ check-ff [cb]
-  (let [win (is-ff-focused)]
-    (if win (cb win) (log:w "Firefox not focused"))))
+(local browser (let [_ff {:bin "/Applications/Firefox Developer Edition.app/Contents/MacOS/firefox"
+                          :bundle-id :org.mozilla.firefoxdeveloperedition
+                          :name "Firefox Developer Addition"}
+                     glide {:bin :/Applications/Glide.app/Contents/MacOS/glide
+                            :bundle-id :app.glide-browser.glide
+                            :name :Glide}]
+                 glide))
+
+(λ focused-browser-window []
+  ;; -?> because focusedWindow returns nil when nothing is focused, which
+  ;; happens often while polling for a window that is still being created.
+  (let [win (hs.window.focusedWindow)]
+    (match (-?> win
+                (: :application)
+                (: :bundleID))
+      browser.bundle-id win)))
 
 ;; Never run these synchronously. Opening a URL activates Hammerspoon, and
 ;; aerospace answers an activation by querying the app over the accessibility
@@ -33,15 +27,15 @@
 ;; Hammerspoon's main thread while Hammerspoon waits on aerospace — until
 ;; aerospace's 3 second AX timeout fires, once per call.
 (λ aerospace-run [args cb]
-  (: (hs.task.new aerospace (fn [rc out err]
-                              (if (= rc 0)
-                                  (cb (chomp out))
-                                  (do
-                                    (log:w :aerospace (table.concat args " ")
-                                           "failed:"
-                                           (chomp (if (= err "") out err)))
-                                    (cb nil))))
-                  args) :start))
+  "Calls `cb` with aerospace's output, or nil when it fails"
+  (let [on-exit (fn [rc out err]
+                  (if (= rc 0)
+                      (cb (chomp out))
+                      (do
+                        (log:w :aerospace (table.concat args " ") "failed:"
+                               (chomp err))
+                        (cb))))]
+    (: (hs.task.new aerospace on-exit args) :start)))
 
 (λ focused-workspace [cb]
   "Calls `cb` with the name of the focused aerospace workspace, or nil"
@@ -55,51 +49,52 @@
                   "--"
                   workspace] cb))
 
-(λ resize-little-ff []
-  (check-ff (fn [win]
-              (let [hide-sidebar #(hs.eventtap.keyStroke [:ctrl] :z)]
-                (with-ax-hotfix win
-                  #(win:moveToUnit "[20,10,80,90]"))
-                (hide-sidebar)))))
+(λ resize-little-ff [win]
+  (let [hide-sidebar #(hs.eventtap.keyStroke [:ctrl] :z)]
+    (with-ax-hotfix win
+      #(win:moveToUnit "[20,10,80,90]"))
+    (hide-sidebar)))
 
-(λ place-little-ff [workspace]
-  "Moves the new window back to `workspace`, then sizes it.
+(λ place-little-ff [win workspace]
+  "Moves `win` to `workspace`, then sizes it.
 
   Launching the browser activates the app, which pulls aerospace over to
   whatever workspace the browser already lives on, so the new window is born
   there rather than where the URL was opened from."
-  (check-ff (fn [win]
-              (let [before (win:screen)
-                    ;; moveToUnit sizes against the window's current screen, so
-                    ;; give the window a beat when the move crossed monitors.
-                    finish (fn []
-                             (if (= (: (win:screen) :id) (before:id))
-                                 (resize-little-ff)
-                                 (hs.timer.doAfter 0.1 resize-little-ff)))]
-                (if workspace
-                    (move-to-workspace win workspace finish)
-                    (finish))))))
+  (let [before (win:screen)
+        ;; moveToUnit sizes against the window's current screen, so give the
+        ;; window a beat when the move crossed monitors.
+        resize #(if (= (: (win:screen) :id) (before:id))
+                    (resize-little-ff win)
+                    (hs.timer.doAfter 0.1 #(resize-little-ff win)))]
+    (if workspace
+        (move-to-workspace win workspace resize)
+        (resize))))
 
 ;; A cold browser can take several seconds to put the new window on screen.
 ;; Give up eventually so a failed launch doesn't leave a timer polling forever.
 (local window-timeout 10)
 
 (λ wait-for-little-ff [cb]
-  (let [timer (hs.timer.waitUntil is-ff-focused cb 0.05)]
+  "Calls `cb` with the browser window once it is focused"
+  (let [on-focus #(let [win (focused-browser-window)]
+                    (if win
+                        (cb win)
+                        (log:w "Lost the window before placing it")))
+        timer (hs.timer.waitUntil focused-browser-window on-focus 0.05)]
     (hs.timer.doAfter window-timeout
                       #(when (timer:running)
-                         (log:w "Gave up waiting for the window after"
-                                window-timeout :seconds)
+                         (log:w "Gave up waiting for a" browser.name
+                                "window after" window-timeout :seconds)
                          (timer:stop)))))
 
 (λ open [url]
   ;; Read the workspace before launching: activating the browser moves
   ;; aerospace's focus to wherever the browser already has windows.
   (focused-workspace (fn [workspace]
-                       (let [args [:--new-window url]]
-                         (log:d "Launching:" browser.bin
-                                (table.concat args " "))
-                         (: (hs.task.new browser.bin nil args) :start)
-                         (wait-for-little-ff #(place-little-ff workspace))))))
+                       (log:d :Launching browser.name :with url)
+                       (: (hs.task.new browser.bin nil [:--new-window url])
+                          :start)
+                       (wait-for-little-ff #(place-little-ff $1 workspace)))))
 
-{: open : is-ff-focused}
+{: open : focused-browser-window}