Fix bug between Emacs and Hammerspoon

Now, on my Mac laptop, if I am clocked into a task, shutting my
laptop, clocks out of the task before going to sleep.
This commit is contained in:
Howard Abrams 2026-08-10 14:24:01 -07:00
parent 5ed5bcf446
commit 94f14495d1
2 changed files with 118 additions and 38 deletions

View file

@ -248,31 +248,6 @@ And I would like to have cute little icons for those states:
(0 (prog1 () (compose-region (match-beginning 1) (match-end 1) "•"))))))) (0 (prog1 () (compose-region (match-beginning 1) (match-end 1) "•")))))))
#+end_src #+end_src
What was
#+BEGIN_SRC emacs-lisp
(defun display-latest-clocked-task ()
"Display the name of the latest task that was clocked in."
(interactive)
(let ((latest-task (car (org-clock-get-clock-string))))
(if latest-task
(message "Latest clocked task: %s" latest-task)
(message "No task is currently clocked in."))))
#+END_SRC
Need to be able to clock out of a task even if no /clock-in/ has occurred.
#+BEGIN_SRC emacs-lisp
(defun ha-clock-out ()
"Safely clock out of the current task.
This will not throw an error if not active clock exists."
(interactive)
(ignore-errors
(org-clock-out)
(message "Clocked out.")))
#+END_SRC
** Meetings ** Meetings
I've notice that while showing a screen while taking meeting notes, I don't always like showing other windows, so I created this function to remove distractions during a meeting. I've notice that while showing a screen while taking meeting notes, I don't always like showing other windows, so I created this function to remove distractions during a meeting.
@ -821,9 +796,6 @@ Splitting out HTML snippets is often a way that I can transfer org-formatted con
hard-newline)))))) hard-newline))))))
#+end_src #+end_src
** Focused Work ** Focused Work
:LOGBOOK:
CLOCK: [2022-02-11 Fri 11:05]--[2022-02-11 Fri 11:21] => 0:16
:END:
I've been working on my own [[http://www.howardism.org/Technical/Emacs/focused-work.html][approach to focused work]], I've been working on my own [[http://www.howardism.org/Technical/Emacs/focused-work.html][approach to focused work]],
#+begin_src emacs-lisp #+begin_src emacs-lisp
@ -840,6 +812,47 @@ I've been working on my own [[http://www.howardism.org/Technical/Emacs/focused-w
("S-<f12>" . ha-focus-interrupt) ("S-<f12>" . ha-focus-interrupt)
("s-<f12>" . ha-focus-timer-left))) ("s-<f12>" . ha-focus-timer-left)))
#+end_src #+end_src
Need to be able to clock out of a task even if no /clock-in/ has occurred.
#+begin_src emacs-lisp
(defun ha-clock-out ()
"Safely clock out of the current task.
This will not throw an error if no active clock exists."
(interactive)
(ignore-errors
(org-clock-out)
(message "Clocked out.")))
#+end_src
I call this function from [[file:hammerspoon.org::*Clocking out a Task][my Hammerspoon configuration]] to clock out of a task when I close my laptop.
What was I working on? Opening the laptop after a break seldom answers that, so this reports whatever clock is still running, and when none is, the one I stopped last, which is the more likely question after a break.
#+begin_src emacs-lisp
(defun ha-display-latest-clocked-task ()
"Display the task clocked in now, or the one most recently clocked out."
(interactive)
(cond
((org-clocking-p)
(message "Currently clocked in: %s"
(substring-no-properties (org-clock-get-clock-string))))
((and org-clock-out-time
(stringp org-clock-heading)
(not (string-empty-p org-clock-heading)))
(let ((seconds (float-time (time-subtract nil org-clock-out-time))))
(message "Clocked out of \"%s\" %s ago" org-clock-heading
(if (< seconds 60)
"less than a minute"
(format-seconds "%D %H %M%z" seconds)))))
(t (message "No task is currently clocked in."))))
#+end_src
The =%z= at the /end/ of the format drops the larger units while they read zero, turning =0 days 0 hours 25 minutes= into =25 minutes=. It has nothing to lean on when every unit is zero, so the first minute gets phrased by hand:
** Spell Checking ** Spell Checking
Let's hook some spell checking into org files, and actually all text files. Im making this particularly delicious. Let's hook some spell checking into org files, and actually all text files. Im making this particularly delicious.
@ -921,7 +934,6 @@ The key-bindings, keystrokes, and key-connections work well with a hyper-command
:bind ("s-t" . powerthesaurus-lookup-dwim)) :bind ("s-t" . powerthesaurus-lookup-dwim))
#+END_SRC #+END_SRC
*** Definitions *** Definitions
Since the /definitions/ do not work, so let's use the [[https://github.com/abo-abo/define-word][define-word]] project: Since the /definitions/ do not work, so let's use the [[https://github.com/abo-abo/define-word][define-word]] project:

View file

@ -2,7 +2,7 @@
#+author: Howard X. Abrams #+author: Howard X. Abrams
#+date: 2025-11-24 #+date: 2025-11-24
#+filetags: emacs hamacs #+filetags: emacs hamacs
#+lastmod: [2026-06-15 Mon] #+lastmod: [2026-08-10 Mon]
A literate programming file for configuring Hammerspoon. A literate programming file for configuring Hammerspoon.
@ -14,12 +14,59 @@ Simple to use. Use =hs.execute= to run a script, =ha.alert.show= to print a smal
hs.notify.new({title="Hammerspoon", informativeText="Hello World"}):send() hs.notify.new({title="Hammerspoon", informativeText="Hello World"}):send()
#+END_SRC #+END_SRC
** The Command Line
The =hs= binary talks to a running Hammerspoon over a Mach port, and that port only exists while the =hs.ipc= module is loaded. Without this line, the tool answers every request with /cant access Hammerspoon message port/, however well it is installed:
#+BEGIN_SRC lua
require("hs.ipc")
#+END_SRC
For instance, using the [[*Zoom][Zoom spoon]], I can toggle the mute button from a script:
#+BEGIN_SRC sh :tangle no
hs -c "spoon.Zoom:toggleMute()" # your Zoom spoon, from anywhere
#+END_SRC
Or callable from Emacs.
Other ideas:
1. Tail the Hammerspoon console in a terminal with =hs -C=, which shows anything called with =printf=.
2. While I already built out [[https://howardism.org/Technical/Emacs/beep-for-emacs.html][a notification system]], I could use Hammerspoon to replace =beep= CLI:
#+BEGIN_SRC sh
make build; hs -c 'hs.notify.new({title="Build", informativeText="done"}):send()'
#+END_SRC
3. Query Hammerspoon to query the system:
#+BEGIN_SRC bash :results replace raw output
hs -c 'return hs.wifi.currentNetwork()'
#+END_SRC
This should return =iTerm2=, or =Emacs= when running from my literate config:
#+BEGIN_SRC sh :results replace raw output
hs -c "return hs.application.frontmostApplication():name()"
#+END_SRC
Or screwing with my monitors:
#+BEGIN_SRC sh :results replace raw output
hs -c "return hs.screen.mainScreen():name()"
#+END_SRC
Or the volume level as a float value:
#+BEGIN_SRC sh :results replace raw output
hs -c "return hs.audiodevice.defaultOutputDevice():volume()"
#+END_SRC
Flags worth knowing, from the binary's usage text:
* =-i= interactive REPL
* =-q= quiet (only the result, ideal for scripting)
* =-c= repeatable
* =-A= auto-launch Hammerspoon if not running, and a bare file path to run a =.lua= file.
* Shortcut Keybindings * Shortcut Keybindings
Ive created left and right ~Meh~ keys on my Moonlanders: Ive created left and right ~Meh~ keys on my Moonlander keyboard:
- Left Meh Key: ~C-M-S-s~ (Control Option/Meta Command Shift) - Left Meh Key: ~C-M-S-s~ (Control Option/Meta Command Shift)
- Right Meh Key: ~C-M-S~ (Control Option/Meta Shift) - Right Meh Key: ~C-M-S~ (Control Option/Meta Shift)
To create special key bindings, I can: To create specific key bindings for starting applications, I can:
#+BEGIN_SRC lua #+BEGIN_SRC lua
---------------------------------------------------------------------- ----------------------------------------------------------------------
-- Launcher replaces iCanHazShortcuts -- Launcher replaces iCanHazShortcuts
@ -33,7 +80,7 @@ To create special key bindings, I can:
end) end)
hs.hotkey.bind({"alt", "ctrl", "shift"}, "F", function() hs.hotkey.bind({"alt", "ctrl", "shift"}, "F", function()
hs.application.launchOrFocus("Safari") hs.application.launchOrFocus("Vivaldi")
end) end)
hs.hotkey.bind({"alt", "ctrl", "shift"}, "C", function() hs.hotkey.bind({"alt", "ctrl", "shift"}, "C", function()
@ -53,6 +100,10 @@ To create special key bindings, I can:
hs.application.launchOrFocus("KeepassXC") hs.application.launchOrFocus("KeepassXC")
end) end)
hs.hotkey.bind({"alt", "ctrl", "shift"}, "G", function()
hs.application.launchOrFocus("Ghostty")
end)
hs.hotkey.bind({"alt", "ctrl", "shift"}, "W", function() hs.hotkey.bind({"alt", "ctrl", "shift"}, "W", function()
hs.application.launchOrFocus("VLC") hs.application.launchOrFocus("VLC")
end) end)
@ -82,13 +133,13 @@ To create special key bindings, I can:
end) end)
#+END_SRC #+END_SRC
* Zoom * Zoom
Library extensions to Hammerspoon are called /spoons/, and most of these are a simple Lua script, =init.lua= stored in the =Spoons= subdirectory. We grab these with a =git clone=, typically. To use the Zoom spoon, clone it: /Spoons/ are library extensions to Hammerspoon: usally small Lua scripts, like =init.lua=, stored in the =Spoons= subdirectory. Installable from a =git clone=, typically. To use the Zoom spoon, clone it:
#+BEGIN_SRC sh :dir ~/.hammerspoon/Spoons :tangle no :results silent #+BEGIN_SRC sh :dir ~/.hammerspoon/Spoons :tangle no :results silent
git clone https://github.com/jpf/Zoom.spoon.git git clone https://github.com/jpf/Zoom.spoon.git
#+END_SRC #+END_SRC
I noticed that it does its works by calling Zooms menus, and with the latest version of Zoom, they changed the /case/ of the menu, meaning that I needed to create a pull request. I noticed that this library does most of its work by calling Zooms menus, and with the latest version of Zoom, they changed the /case/ of the menu, meaning that I needed to create a pull request.
From this [[https://developer.okta.com/blog/2020/10/22/set-up-a-mute-indicator-light-for-zoom-with-hammerspoon][nice essay]], we create a menu bar item that shows the status, as well as allowing me to click it to toggle: From this [[https://developer.okta.com/blog/2020/10/22/set-up-a-mute-indicator-light-for-zoom-with-hammerspoon][nice essay]], we create a menu bar item that shows the status, as well as allowing me to click it to toggle:
@ -166,7 +217,8 @@ Leaving my home office and hopping on the train can be socially awkward when my
lastSSID = newSSID lastSSID = newSSID
end end
hs.wifi.watcher.new(ssidChangedCallback):start() wifiWatcher = hs.wifi.watcher.new(ssidChangedCallback)
wifiWatcher:start()
#+END_SRC #+END_SRC
Why yes, Im /looking for features/ to use Hammerspoon. Why yes, Im /looking for features/ to use Hammerspoon.
@ -182,10 +234,19 @@ I want to use Orgs task clocking ability, but I often forget to /clock out/.
end end
end end
sleepWatcher = hs.caffeinate.watcher.new(sleepWatch)
sleepWatcher:start()
#+END_SRC
Discovered a subtle bug, solving it meant assigning the =watcher= to a variable, rather than starting it and dropping the value. For instance, if I call the =start= this way:
#+BEGIN_SRC lua :tangle no
hs.caffeinate.watcher.new(sleepWatch):start() hs.caffeinate.watcher.new(sleepWatch):start()
#+END_SRC #+END_SRC
Not that we could also do something for Wake: Since nothing else refers to that object once the line finishes, Lua may collect it, and a collected watcher stops delivering events without a word about it.
Note if I want to do something for Wake:
#+BEGIN_SRC lua :tangle no #+BEGIN_SRC lua :tangle no
if (eventType == hs.caffeinate.watcher.systemDidWake) then if (eventType == hs.caffeinate.watcher.systemDidWake) then
@ -267,9 +328,12 @@ Whenever my configuration file is altered (or with a ~Meh-R~ key), I reload the
end end
end end
hs.pathwatcher.new(os.getenv("HOME") .. "/.hammerspoon/", reloadConfig):start() configWatcher = hs.pathwatcher.new(os.getenv("HOME") .. "/.hammerspoon/", reloadConfig)
configWatcher:start()
#+END_SRC #+END_SRC
Every watcher in this file now lives in a variable of its own, for the reason described under [[*Clocking out a Task][clocking out]]. This one failing is the quietest of the three, as a dead configuration watcher looks exactly like a file that saved without incident.
* Technical Artifacts :noexport: * Technical Artifacts :noexport:
#+BEGIN_SRC lua #+BEGIN_SRC lua
hs.alert.show("Hammerspoon Configuration") hs.alert.show("Hammerspoon Configuration")
@ -285,3 +349,7 @@ Whenever my configuration file is altered (or with a ~Meh-R~ key), I reload the
#+OPTIONS: num:nil toc:nil todo:nil tasks:nil tags:nil date:nil #+OPTIONS: num:nil toc:nil todo:nil tasks:nil tags:nil date:nil
#+OPTIONS: skip:nil author:nil email:nil creator:nil timestamp:nil #+OPTIONS: skip:nil author:nil email:nil creator:nil timestamp:nil
#+INFOJS_OPT: view:nil toc:nil ltoc:t mouse:underline buttons:0 path:http://orgmode.org/org-info.js #+INFOJS_OPT: view:nil toc:nil ltoc:t mouse:underline buttons:0 path:http://orgmode.org/org-info.js
# Local Variables:
# jinx-local-words: "Lua"
# End: