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) "•")))))))
#+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
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))))))
#+end_src
** 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]],
#+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-timer-left)))
#+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
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))
#+END_SRC
*** Definitions
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
#+date: 2025-11-24
#+filetags: emacs hamacs
#+lastmod: [2026-06-15 Mon]
#+lastmod: [2026-08-10 Mon]
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()
#+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
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)
- 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
----------------------------------------------------------------------
-- Launcher replaces iCanHazShortcuts
@ -33,7 +80,7 @@ To create special key bindings, I can:
end)
hs.hotkey.bind({"alt", "ctrl", "shift"}, "F", function()
hs.application.launchOrFocus("Safari")
hs.application.launchOrFocus("Vivaldi")
end)
hs.hotkey.bind({"alt", "ctrl", "shift"}, "C", function()
@ -53,6 +100,10 @@ To create special key bindings, I can:
hs.application.launchOrFocus("KeepassXC")
end)
hs.hotkey.bind({"alt", "ctrl", "shift"}, "G", function()
hs.application.launchOrFocus("Ghostty")
end)
hs.hotkey.bind({"alt", "ctrl", "shift"}, "W", function()
hs.application.launchOrFocus("VLC")
end)
@ -82,13 +133,13 @@ To create special key bindings, I can:
end)
#+END_SRC
* 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
git clone https://github.com/jpf/Zoom.spoon.git
#+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:
@ -166,7 +217,8 @@ Leaving my home office and hopping on the train can be socially awkward when my
lastSSID = newSSID
end
hs.wifi.watcher.new(ssidChangedCallback):start()
wifiWatcher = hs.wifi.watcher.new(ssidChangedCallback)
wifiWatcher:start()
#+END_SRC
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
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()
#+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
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
hs.pathwatcher.new(os.getenv("HOME") .. "/.hammerspoon/", reloadConfig):start()
configWatcher = hs.pathwatcher.new(os.getenv("HOME") .. "/.hammerspoon/", reloadConfig)
configWatcher:start()
#+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:
#+BEGIN_SRC lua
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: 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
# Local Variables:
# jinx-local-words: "Lua"
# End: