Hammerspoon

An icon of a key

The contents of this section is included directly from my real Hammerspoon config file at https://github.com/zzamboni/dot-hammerspoon/blob/master/init.org, using the following Org directive (the first 16 lines are skipped because they contain some global document directives which do not apply in this context):

1 #+include: "~/.hammerspoon/init.org" :lines "17-"

This is my Hammerspoon configuration file.

This file is written in literate programming style using org-mode. See init.lua for the generated file. You can see this in a nicer format on my blog post My Hammerspoon Configuration, With Commentary.

If you want to learn more about Hammerspoon, check out my book Learning Hammerspoon!

General variables and configuration

Global log level. Per-spoon log level can be configured in each Install:andUse block below.

1 hs.logger.defaultLogLevel="info"

I use hyper, shift_hyper and ctrl_cmd as the modifiers for most of my key bindings, so I define them as variables here for easier use.

1 hyper       = {"cmd","alt","ctrl"}
2 shift_hyper = {"cmd","alt","ctrl","shift"}
3 ctrl_cmd    = {"cmd","ctrl"}

Set up an abbreviation for hs.drawing.color.x11 since I use it repeatedly later on.

1 col = hs.drawing.color.x11

Work’s logo, which I use in some of my Seal shortcuts later on.

1 work_logo = hs.image.imageFromPath(hs.configdir .. "/files/work_logo_2x.png")

Spoon Management

Set up SpoonInstall - this is the only spoon that needs to be manually installed (it is already there if you check out this repository), all the others are installed and configured automatically.

1 hs.loadSpoon("SpoonInstall")

Configuration of my personal spoon repository, which contains Spoons that have not been merged in the main repo. See the descriptions at https://zzamboni.github.io/zzSpoons/.

1 spoon.SpoonInstall.repos.zzspoons = {
2   url = "https://github.com/zzamboni/zzSpoons",
3   desc = "zzamboni's spoon repository",
4 }

I prefer sync notifications, makes them easier to read.

1 spoon.SpoonInstall.use_syncinstall = true

This is just a shortcut to make the declarations below look more readable, i.e. Install:andUse instead of spoon.SpoonInstall:andUse.

1 Install=spoon.SpoonInstall

BetterTouchTool integration (experimental)

I’m currently working on a new BetterTouchTool.spoon which provides integration with the BetterTouchTool AppleScript API. This is in heavy development! See the configuration for the Hammer spoon in System and UI for an example of how to use it.

1 -- Install:andUse("BetterTouchTool", { loglevel = 'debug' })
2 -- BTT = spoon.BetterTouchTool

URL dispatching to site-specific browsers

The URLDispatcher spoon makes it possible to open URLs with different browsers. Currently I use the following:

  • Different Chrome profiles for various work-related purposes (e.g. one profile for each of my customers, another one for internal sites), which allows me to keep site-specific bookmarks, search settings, etc.;
  • Brave for non-work browsing. I also use the url_redir_decoders parameter to rewrite some URLs before they are opened, both to redirect certain URLs directly to their corresponding applications (instead of going through the web browser) and to fix a bug I have experienced in opening URLs from PDF documents using Preview.

The URLDispatcher spoon requires application IDs, so I define a function which gets the path of the application and returns its ID.

1 -- Returns the bundle ID of an application, given its path.
2 function appID(app)
3   if hs.application.infoForBundlePath(app) then
4     return hs.application.infoForBundlePath(app)['CFBundleIdentifier']
5   end
6 end

The chromeProfile function returns a function which opens a URL with the given Chrome Profile, and which can be used directly as the value in the url_patterns parameter below.

 1 -- Returns a function that takes a URL and opens it in the given Chrome profile
 2 -- Note: the value of `profile` must be the name of the profile directory under
 3 -- ~/Library/Application Support/Google/Chrome/
 4 function chromeProfile(profile)
 5   return function(url)
 6     hs.task.new("/usr/bin/open", nil, { "-n",
 7                                         "-a", "Google Chrome",
 8                                         "--args",
 9                                         "--profile-directory="..profile,
10                                         url }):start()
11   end
12 end

First I define variables containing the application IDs of the various applications that might be used to open URLs (not all of these are currently used, but I leave them here for convenience).

1 -- Define the IDs of the various applications used to open URLs
2 chromeBrowser  = appID('/Applications/Google Chrome.app')
3 braveBrowser   = appID('/Applications/Brave Browser.app')
4 safariBrowser  = appID('/Applications/Safari.app')
5 firefoxBrowser = appID('/Applications/Firefox.app')
6 arcBrowser     = appID('/Applications/Arc.app')
7 teamsApp       = appID('/Applications/Microsoft Teams.app')
8 quipApp        = appID('/Applications/Quip.app')
9 chimeApp       = appID('/Applications/Amazon Chime.app')

The browsers array stores the browsers used for different sets of URLs. I store them here mostly for readability and convenience, I can then refer to things like browsers.work in the configuration below.

1 -- Define my default browsers for various purposes
2 browsers = {
3   default    = arcBrowser,
4   awsConsole = firefoxBrowser,
5   work       = chromeProfile("Default"),
6   customer1  = chromeProfile("Profile 1")
7 }

Similarly, I store in an array the paths (relative to ~/.hammerspoon) of the files in which I store the lists of URLs to be opened by different browsers. This avoids having to include potentially private or sensitive URLs in my public configuration file. Since URLDispatcher automatically reloads the files when they are updated, this also makes it possible to update the lists without restarting Hammerspoon every time.

1 -- Read URL patterns from text files
2 URLfiles = {
3   work      = "local/work_urls.txt",
4   customer1 = "local/customer1_urls.txt"
5 }

Finally, I load and configure URLDispatcher.

 1 Install:andUse("URLDispatcher",
 2                {
 3                  config = {
 4                    default_handler = browsers.default,
 5                    url_patterns = {
 6                      -- URLs that get redirected to applications
 7                      { "https://quip%-amazon%.com/"      , quipApp },
 8                      { "https://teams%.microsoft%.com/"  , teamsApp },
 9                      { "chime://"                        , chimeApp },
10                      -- Customer-specific URLs open in their own Chrome profile
11                      { URLfiles.customer1                , browsers.customer1 },
12                      -- AWS console URLs open by default in Firefox because it
13                      -- has better plugins to improve the experience. This comes
14                      -- after customer1 URLs because I have patterns for that
15                      -- customer's accounts to open in its corresponding
16                      -- profile.
17                      { "console%.aws%.amazon%.com/.*"    , browsers.awsConsole },
18                      -- Work-related URLs open in the default Chrome profile
19                      { URLfiles.work                     , browsers.work },
20                    },
21                    url_redir_decoders = {
22                      -- Most URLs opened from within MS Teams are normally sent
23                      -- through a redirect which messes the matching, so we
24                      -- extract the final URL before dispatching it. The final
25                      -- URL is passed as parameter "url" to the redirect URL,
26                      -- which makes it easy to extract it using a function-based
27                      -- decoder.
28                      -- Some URLs (e.g. Sharepoint) are not sent through the
29                      -- decoder, so if there is no url parameter, we process the
30                      -- full original URL.
31                      { "MS Teams links", function(_, _, params,fullUrl)
32                          if params.url then
33                            return params.url
34                          else
35                            return fullUrl
36                          end
37                      end, nil, true, "Microsoft Teams" },
38                      -- URLs within a tracking link
39                      { "awstrack.me links", "https://.*%.awstrack%.me/.-/(.*)", "%1" },
40                      -- Chime meeting URLs get rewritten to open in the Chime app
41                      { "Chime meeting links", "https://chime%.aws/(%d+)", "chime://meeting?pin=%1" }
42                    }
43                  },
44                  start = true,
45                  -- loglevel = 'debug'
46                }
47 )

Window and screen manipulation

The WindowHalfsAndThirds spoon sets up multiple key bindings for manipulating the size and position of windows. This was one of the first spoons I wrote, and I still use it for window resizing.

 1 Install:andUse("WindowHalfsAndThirds",
 2                {
 3                  config = {
 4                    use_frame_correctness = true
 5                  },
 6                  hotkeys = 'default',
 7 --                 loglevel = 'debug'
 8                  disable = true
 9                }
10 )

The WindowGrid spoon sets up a key binding (Hyper-g here) to overlay a grid that allows resizing windows by specifying their opposite corners.

1 myGrid = { w = 6, h = 4 }
2 Install:andUse("WindowGrid",
3                {
4                  config = { gridGeometries =
5                               { { myGrid.w .."x" .. myGrid.h } } },
6                  hotkeys = {show_grid = {hyper, "g"}},
7                  start = true
8                }
9 )

The WindowScreenLeftAndRight spoon sets up key bindings for moving windows between multiple screens.

 1 Install:andUse("WindowScreenLeftAndRight",
 2                {
 3                  config = {
 4                    animationDuration = 0
 5                  },
 6                  hotkeys = 'default',
 7 --                 loglevel = 'debug'
 8                  disable = true
 9                }
10 )

The ToggleScreenRotation spoon sets up a key binding to rotate the external screen (the spoon can set up keys for multiple screens if needed, but by default it rotates the first external screen).

1 Install:andUse("ToggleScreenRotation",
2                {
3                  hotkeys = { first = {hyper, "f15"} }
4                }
5 )

Organization and Productivity

Universal Archiving

The UniversalArchive spoon sets up a single key binding (Ctrl-Cmd-a) to archive the current item in Evernote, Mail and Outlook.

 1 Install:andUse("UniversalArchive",
 2                {
 3                  config = {
 4                    evernote_archive_notebook = ".Archive",
 5                    archive_notifications = false,
 6                    outlook_archive_folder = "Archive (myemail@work.com)"
 7                  },
 8                  hotkeys = { archive = { { "ctrl", "cmd" }, "a" } }
 9                }
10 )

Filing to Omnifocus

Note: I no longer use OmniFocus so the Spoon below is diabled, but this section is still here as an example.

The SendToOmniFocus spoon sets up a single key binding (Hyper-t) to send the current item to OmniFocus from multiple applications. We use the fn attribute of Install:andUse to call a function which registers some of the Epichrome site-specific-browsers I use, so that the Spoon knows how to collect items from them.

1 function chrome_item(n)
2   return { apptype = "chromeapp", itemname = n }
3 end
1 function OF_register_additional_apps(s)
2   s:registerApplication("Collab", chrome_item("tab"))
3   s:registerApplication("Wiki", chrome_item("wiki page"))
4   s:registerApplication("Jira", chrome_item("issue"))
5   s:registerApplication("Brave Browser Dev", chrome_item("page"))
6 end
 1 Install:andUse("SendToOmniFocus",
 2                {
 3                  disable = true,
 4                  config = {
 5                    quickentrydialog = false,
 6                    notifications = false
 7                  },
 8                  hotkeys = {
 9                    send_to_omnifocus = { hyper, "t" }
10                  },
11                  fn = OF_register_additional_apps,
12                }
13 )

Capturing to Org mode

I now use Org-mode for task tracking and capturing. The following snippet runs the ~/.emacs.d/bin/org-capture script to bring up an Emacs window which allows me to capture things from anywhere in the system. The code is a bit convoluted because it needs to capture the current window and restore it after the org-capture window closes, otherwise Emacs is brought to the front.

 1 org_capture_path = os.getenv("HOME").."/.hammerspoon/files/org-capture.lua"
 2 script_file = io.open(org_capture_path, "w")
 3 script_file:write([[local win = hs.window.frontmostWindow()
 4 local o,s,t,r = hs.execute("~/.emacs.d/bin/org-capture", true)
 5 if not s then
 6   print("Error when running org-capture: "..o.."\n")
 7 end
 8 win:focus()
 9 ]])
10 script_file:close()
11 
12 hs.hotkey.bindSpec({hyper, "t"},
13   function ()
14     hs.task.new("/bin/bash", nil, { "-l", "-c", "/usr/local/bin/hs "..org_capture_path }):start()
15   end
16 )

Evernote filing and tagging

The EvernoteOpenAndTag spoon sets up some missing key bindings for note manipulation in Evernote. I no longer use Evernote for GTD, so I have it disabled for now.

 1 Install:andUse("EvernoteOpenAndTag",
 2                {
 3                  disable = true,
 4                  hotkeys = {
 5                    open_note = { hyper, "o" },
 6                    ["open_and_tag-+work"] = { hyper, "w" },
 7                    ["open_and_tag-+personal"] = { hyper, "p" },
 8                    ["tag-@zzdone"] = { hyper, "z" }
 9                  }
10                }
11 )

Clipboard history

The TextClipboardHistory spoon implements a clipboard history, only for text items. It is invoked with Cmd-Shift-v.

Note: This is disabled for the moment as I experiment with BetterTouchTool’s built-in clipboard history, which I have bound to the same key combination for consistency in my workflow.

 1 Install:andUse("TextClipboardHistory",
 2                {
 3                  disable = true,
 4                  config = {
 5                    show_in_menubar = false,
 6                  },
 7                  hotkeys = {
 8                    toggle_clipboard = { { "cmd", "shift" }, "v" } },
 9                  start = true,
10                }
11 )

System and UI

General Hammerspoon utilities

The BTT_restart_Hammerspoon function sets up a BetterTouchTool widget which also executes the config_reload action from the spoon. This gets assigned to the fn config parameter in the configuration of the Hammer spoon below, which has the effect of calling the function with the Spoon object as its parameter.

This is still manual - the uuid parameter contains the ID of the BTT widget to configure, and for now you have to get it by hand from BTT and paste it here.

 1 function BTT_restart_hammerspoon(s)
 2   BTT:bindSpoonActions(s, {
 3                          config_reload = {
 4                            kind = 'touchbarButton',
 5                            uuid = "FF8DA717-737F-4C42-BF91-E8826E586FA1",
 6                            name = "Restart",
 7                            icon = hs.image.imageFromName(
 8                              hs.image.systemImageNames.ApplicationIcon),
 9                            color = hs.drawing.color.x11.orange,
10   }})
11 end

The Hammer spoon (get it? hehe) is a simple wrapper around some common Hammerspoon configuration variables. Note that this gets loaded from my personal repo, since it’s not in the official repository.

 1 Install:andUse("Hammer",
 2                {
 3                  repo = 'zzspoons',
 4                  config = { auto_reload_config = false },
 5                  hotkeys = {
 6                    config_reload = {hyper, "r"},
 7                    toggle_console = {hyper, "y"}
 8                  },
 9 --                 fn = BTT_restart_Hammerspoon,
10                  start = true
11                }
12 )

Caffeine: Control system/display sleep

The Caffeine spoon allows preventing the display and the machine from sleeping. I use it frequently when playing music from my machine, to avoid having to unlock the screen whenever I want to change the music. In this case we also create a function BTT_caffeine_widget to configure the widget to both execute the corresponding function, and to set its icon according to the current state.

 1 function BTT_caffeine_widget(s)
 2   BTT:bindSpoonActions(s, {
 3                          toggle = {
 4                            kind = 'touchbarWidget',
 5                            uuid = '72A96332-E908-4872-A6B4-8A6ED2E3586F',
 6                            name = 'Caffeine',
 7                            widget_code = [[
 8 do
 9   title = " "
10   icon = hs.image.imageFromPath(spoon.Caffeine.spoonPath.."/caffeine-off.pdf")
11   if (hs.caffeinate.get('displayIdle')) then
12     icon = hs.image.imageFromPath(spoon.Caffeine.spoonPath.."/caffeine-on.pdf")
13   end
14   print(hs.json.encode({ text = title,
15                          icon_data = BTT:hsimageToBTTIconData(icon) }))
16 end
17       ]],
18                            code = "spoon.Caffeine.clicked()",
19                            widget_interval = 1,
20                            color = hs.drawing.color.x11.black,
21                            icon_only = true,
22                            icon_size = hs.geometry.size(15,15),
23                            BTTTriggerConfig = {
24                              BTTTouchBarFreeSpaceAfterButton = 0,
25                              BTTTouchBarItemPadding = -6,
26                            },
27                          }
28   })
29 end
1 Install:andUse("Caffeine", {
2                  start = true,
3                  hotkeys = {
4                    toggle = { hyper, "1" }
5                  },
6 --                 fn = BTT_caffeine_widget,
7 })

Colorize menubar according to keyboard layout

The MenubarFlag spoon colorizes the menubar according to the selected keyboard language or layout (functionality inspired by ShowyEdge). I use English, Spanish and German, so those are the colors I have defined.

 1 Install:andUse("MenubarFlag",
 2                {
 3                  config = {
 4                    colors = {
 5                      ["U.S."] = { },
 6                      Spanish = {col.green, col.white, col.red},
 7                      ["Latin American"] = {col.green, col.white, col.red},
 8                      German = {col.black, col.red, col.yellow},
 9                    }
10                  },
11                  start = true
12                }
13 )

Locating the mouse

The MouseCircle spoon shows a circle around the mouse pointer when triggered. I have it disabled for now because I have the macOS shake-to-grow feature enabled.

 1 Install:andUse("MouseCircle",
 2                {
 3                  disable = true,
 4                  config = {
 5                    color = hs.drawing.color.x11.rebeccapurple
 6                  },
 7                  hotkeys = {
 8                    show = { hyper, "m" }
 9                  }
10                }
11 )

Finding colors

One of my original bits of Hammerspoon code, now made into a spoon (although I keep it disabled, since I don’t really use it). The ColorPicker spoon shows a menu of the available color palettes, and when you select one, it draws swatches in all the colors in that palette, covering the whole screen. You can click on any of them to copy its name to the clipboard, or cmd-click to copy its RGB code.

 1 Install:andUse("ColorPicker",
 2                {
 3                  disable = true,
 4                  hotkeys = {
 5                    show = { hyper, "z" }
 6                  },
 7                  config = {
 8                    show_in_menubar = false,
 9                  },
10                  start = true,
11                }
12 )

Homebrew information popups

I use Homebrew, and when I run brew update, I often wonder about what some of the formulas shown are (names are not always obvious). The BrewInfo spoon allows me to point at a Formula or Cask name and press Hyper-b or Hyper-c (for Casks) to have the output of the info command in a popup window, or the same key with Shift-Hyper to open the URL of the Formula/Cask.

 1 Install:andUse("BrewInfo",
 2                {
 3                  config = {
 4                    brew_info_style = {
 5                      textFont = "Inconsolata",
 6                      textSize = 14,
 7                      radius = 10 }
 8                  },
 9                  hotkeys = {
10                    -- brew info
11                    show_brew_info = {hyper, "b"},
12                    open_brew_url = {shift_hyper, "b"},
13                    -- brew cask info - not needed anymore, the above now do both
14                    -- show_brew_cask_info = {shift_hyper, "c"},
15                    -- open_brew_cask_url = {hyper, "c"},
16                  }
17                }
18 )

Displaying keyboard shortcuts

The KSheet spoon traverses the current application’s menus and builds a cheatsheet of the keyboard shortcuts, showing it in a nice popup window.

1 Install:andUse("KSheet",
2                {
3                  hotkeys = {
4                    toggle = { hyper, "/" }
5 }})

TimeMachine backup monitoring

The TimeMachineProgress spoon shows an indicator about the progress of the ongoing Time Machine backup. The indicator disappears when there is no backup going on.

1 Install:andUse("TimeMachineProgress",
2                {
3                  start = true
4                }
5 )

Disabling Turbo Boost

The TurboBoost spoon shows an indicator of the CPU’s Turbo Boost status, and allows disabling/enabling. This requires Turbo Boost Switcher to be installed.

(disabled because I ended up buying Turbo Boost Switcher Pro - it’s a great utility and offers a few great extra features for an excellent price, it deserves our support)

 1 Install:andUse("TurboBoost",
 2                {
 3                  disable = true,
 4                  config = {
 5                    disable_on_start = true
 6                  },
 7                  hotkeys = {
 8                    toggle = { hyper, "0" }
 9                  },
10                  start = true,
11                  --                   loglevel = 'debug'
12                }
13 )

Unmounting external disks on sleep

The EjectMenu spoon automatically ejects all external disks before the system goes to sleep. I use this to avoid warnings from macOS when I close my laptop and disconnect it from my hub without explicitly unmounting my backup disk before. I disable the menubar icon, which is shown by default by the Spoon.

 1 Install:andUse("EjectMenu", {
 2                  config = {
 3                    eject_on_lid_close = false,
 4                    eject_on_sleep = false,
 5                    show_in_menubar = true,
 6                    notify = true,
 7                  },
 8                  hotkeys = { ejectAll = { hyper, "=" } },
 9                  start = true,
10 --                 loglevel = 'debug'
11 })

Other applications

The HeadphoneAutoPause spoon implements auto-pause/resume for iTunes, Spotify and others when the headphones are unplugged. Note that this goes unused since I started using wireless headphones.

1 Install:andUse("HeadphoneAutoPause",
2                {
3                  start = true,
4                  disable = true,
5                }
6 )

Seal application launcher/controller

The Seal spoon is a powerhouse. It implements a Spotlight-like launcher, but which allows for infinite configurability of what can be done or searched from the launcher window. I use Seal as my default launcher, triggered with Cmd-space, although I still keep Spotlight around under Hyper-space, mainly for its search capabilities.

We start by loading the spoon, and specifying which plugins we want.

 1 Install:andUse("Seal",
 2                {
 3                  hotkeys = { show = { {"alt"}, "space" } },
 4                  fn = function(s)
 5                    s:loadPlugins({"apps", "calc", "safari_bookmarks",
 6                                   "screencapture", "useractions"})
 7                    s.plugins.safari_bookmarks.always_open_with_safari = false
 8                    s.plugins.useractions.actions =
 9                      {
10                          <<useraction-definitions>>
11                      }
12                    s:refreshAllCommands()
13                  end,
14                  start = true,
15                }
16 )

The useractions Seal plugin allows me to define my own shortcuts. For example, a bookmark to the Hammerspoon documentation page:

Figure 44. «useraction-definitions»≡
1 ["Hammerspoon docs webpage"] = {
2   url = "http://hammerspoon.org/docs/",
3   icon = hs.image.imageFromName(hs.image.systemImageNames.ApplicationIcon),
4 },

Or to manually trigger my work/non-work transition scripts (see below):

Figure 45. «useraction-definitions»≡
 1 ["Leave corpnet"] = {
 2   fn = function()
 3     spoon.WiFiTransitions:processTransition('foo', 'corpnet01')
 4   end,
 5   icon = work_logo,
 6 },
 7 ["Arrive in corpnet"] = {
 8   fn = function()
 9     spoon.WiFiTransitions:processTransition('corpnet01', 'foo')
10   end,
11   icon = work_logo,
12 },

Or to translate things using dict.leo.org:

Figure 46. «useraction-definitions»≡
1 ["Translate using Leo"] = {
2   url = "http://dict.leo.org/englisch-deutsch/${query}",
3   icon = 'favicon',
4   keyword = "leo",
5 }

Network transitions

The WiFiTransitions spoon allows triggering arbitrary actions when the SSID changes. I am interested in the change from my work network (corpnet01) to other networks, mainly because at work I need a proxy for all connections to the Internet. I have two applications which don’t handle these transitions gracefully on their own: Spotify and Adium. So I have written a couple of functions for helping them along.

The reconfigSpotifyProxy function quits Spotify, updates the proxy settings in its config file, and restarts it.

 1 function reconfigSpotifyProxy(proxy)
 2   local spotify = hs.appfinder.appFromName("Spotify")
 3   local lastapp = nil
 4   if spotify then
 5     lastapp = hs.application.frontmostApplication()
 6     spotify:kill()
 7     hs.timer.usleep(40000)
 8   end
 9   -- I use CFEngine to reconfigure the Spotify preferences
10   cmd = string.format(
11     "/usr/local/bin/cf-agent -K -f %s/files/spotify-proxymode.cf%s",
12     hs.configdir, (proxy and " -DPROXY" or " -DNOPROXY"))
13   output, status, t, rc = hs.execute(cmd)
14   if spotify and lastapp then
15     hs.timer.doAfter(
16       3,
17       function()
18         if not hs.application.launchOrFocus("Spotify") then
19           hs.notify.show("Error launching Spotify", "", "")
20         end
21         if lastapp then
22           hs.timer.doAfter(0.5, hs.fnutils.partial(lastapp.activate, lastapp))
23         end
24     end)
25   end
26 end

The reconfigAdiumProxy function uses AppleScript to tell Adium about the change without having to restart it - only if Adium is already running.

 1 function reconfigAdiumProxy(proxy)
 2   app = hs.application.find("Adium")
 3   if app and app:isRunning() then
 4     local script = string.format([[
 5   tell application "Adium"
 6     repeat with a in accounts
 7       if (enabled of a) is true then
 8         set proxy enabled of a to %s
 9       end if
10     end repeat
11     go offline
12     go online
13   end tell
14   ]], hs.inspect(proxy))
15     hs.osascript.applescript(script)
16   end
17 end

Functions to stop applications that are disallowed in the work network.

 1 function stopApp(name)
 2   app = hs.application.get(name)
 3   if app and app:isRunning() then
 4     app:kill()
 5   end
 6 end
 7 
 8 function forceKillProcess(name)
 9   hs.execute("pkill " .. name)
10 end
11 
12 function startApp(name)
13   hs.application.open(name)
14 end

The configuration for the WiFiTransitions spoon invoked these functions with the appropriate parameters.

 1 Install:andUse("WiFiTransitions",
 2                {
 3                  config = {
 4                    actions = {
 5                      -- { -- Test action just to see the SSID transitions
 6                      --    fn = function(_, _, prev_ssid, new_ssid)
 7                      --       hs.notify.show("SSID change",
 8                      --          string.format("From '%s' to '%s'",
 9                      --          prev_ssid, new_ssid), "")
10                      --    end
11                      -- },
12                      { -- Enable proxy config when joining corp network
13                        to = "corpnet01",
14                        fn = {hs.fnutils.partial(reconfigSpotifyProxy, true),
15                              hs.fnutils.partial(reconfigAdiumProxy, true),
16                              hs.fnutils.partial(forceKillProcess, "Dropbox"),
17                              hs.fnutils.partial(stopApp, "Evernote"),
18                        }
19                      },
20                      { -- Disable proxy config when leaving corp network
21                        from = "corpnet01",
22                        fn = {hs.fnutils.partial(reconfigSpotifyProxy, false),
23                              hs.fnutils.partial(reconfigAdiumProxy, false),
24                              hs.fnutils.partial(startApp, "Dropbox"),
25                        }
26                      },
27                    }
28                  },
29                  start = true,
30                }
31 )

Pop-up translation

I live in Switzerland, and my German is far from perfect, so the PopupTranslateSelection spoon helps me a lot. It allows me to select some text and, with a keystroke, translate it to any of three languages using Google Translate. Super useful! Usually, Google’s auto-detect feature works fine, so the translate_to_<lang> keys are sufficient. I have some translate_<from>_<to> keys set up for certain language pairs for when this doesn’t quite work (I don’t think I’ve ever needed them).

 1 local wm=hs.webview.windowMasks
 2 Install:andUse("PopupTranslateSelection",
 3                {
 4                  disable = true,
 5                  config = {
 6                    popup_style = wm.utility|wm.HUD|wm.titled|
 7                      wm.closable|wm.resizable,
 8                  },
 9                  hotkeys = {
10                    translate_to_en = { hyper, "e" },
11                    translate_to_de = { hyper, "d" },
12                    translate_to_es = { hyper, "s" },
13                    translate_de_en = { shift_hyper, "e" },
14                    translate_en_de = { shift_hyper, "d" },
15                  }
16                }
17 )

I am now testing DeepLTranslate, based on PopupTranslateSelection but which uses the DeepL translator (this is disabled because I have the DeepL app installed, which binds its own global hotkeys).

 1 Install:andUse("DeepLTranslate",
 2                {
 3                  disable = true,
 4                  config = {
 5                    popup_style = wm.utility|wm.HUD|wm.titled|
 6                      wm.closable|wm.resizable,
 7                  },
 8                  hotkeys = {
 9                    translate = { hyper, "e" },
10                  }
11                }
12 )

Leanpub integration

The Leanpub spoon provides monitoring of book build jobs. You can read more about how I use this in my blog post Automating Leanpub book publishing with Hammerspoon and CircleCI.

 1 Install:andUse("Leanpub",
 2                {
 3                  config = {
 4                    watch_books = {
 5                      -- api_key gets set in init-local.lua like this:
 6                      -- spoon.Leanpub.api_key = "my-api-key"
 7                      { slug = "learning-hammerspoon" },
 8                      { slug = "learning-cfengine" },
 9                      { slug = "emacs-org-leanpub" },
10                      -- { slug = "be-safe-on-the-internet" },
11                      { slug = "lit-config"  },
12                      { slug = "zztestbook" },
13                      -- { slug = "cisspexampreparationguide" },
14                    },
15                    books_sync_to_dropbox = true,
16                  },
17                  start = false,
18                  -- loglevel = 'debug'
19 })

Showing application keybindings

The KSheet spoon provides for showing the keybindings for the currently active application.

1 Install:andUse("KSheet", {
2                  hotkeys = {
3                    toggle = { hyper, "/" }
4                  }
5 })

Loading private configuration

In init-local.lua I keep experimental or private stuff (like API tokens) that I don’t want to publish in my main config. This file is not committed to any publicly accessible git repositories.

1 local localfile = hs.configdir .. "/local/init-local.lua"
2 if hs.fs.attributes(localfile) then
3   dofile(localfile)
4 end

If the Leanpub API key is defined, then we start the spoon.

1 if spoon.Leanpub.api_key and spoon.Leanpub.api_key ~= "" then
2   spoon.Leanpub:start()
3 end

End-of-config animation

The FadeLogo spoon simply shows an animation of the Hammerspoon logo to signal the end of the config load.

1 Install:andUse("FadeLogo",
2                {
3                  config = {
4                    default_run = 1.0,
5                  },
6                  start = true
7                }
8 )

If you don’t want to use FadeLogo, you can have a regular notification.

1 -- hs.notify.show("Welcome to Hammerspoon", "Have fun!", "")