From 94f14495d19850a4d6ebdfe3249370822e7d4363 Mon Sep 17 00:00:00 2001 From: Howard Abrams Date: Mon, 10 Aug 2026 14:24:01 -0700 Subject: [PATCH] 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. --- ha-org.org | 70 +++++++++++++++++++++++----------------- hammerspoon.org | 86 +++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 118 insertions(+), 38 deletions(-) diff --git a/ha-org.org b/ha-org.org index 3d36385..5c705b2 100644 --- a/ha-org.org +++ b/ha-org.org @@ -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-" . ha-focus-interrupt) ("s-" . 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. I’m 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: diff --git a/hammerspoon.org b/hammerspoon.org index 8f4465f..982774b 100644 --- a/hammerspoon.org +++ b/hammerspoon.org @@ -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 /can’t 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 -I’ve created left and right ~Meh~ keys on my Moonlanders: +I’ve 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 Zoom’s 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 Zoom’s 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, I’m /looking for features/ to use Hammerspoon. @@ -182,10 +234,19 @@ I want to use Org’s 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: