Original Configuration: https://github.com/zzamboni/dot-hammerspoon

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"

组合键(hyper, shift_hyper and ctrl_cmd):

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

hs.drawing.color.x11 的缩写:

1
col = hs.drawing.color.x11

Logo:

1
work_logo = hs.image.imageFromPath(hs.configdir .. "/img/cheng.png")

Spoon Management

SpoonInstall: spoon 管理器,需要手动安装。

1
2
hs.loadSpoon("SpoonInstall")
hs.loadSpoon("ModalMgr")

同步通知:

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

Start ModalMgr

Start:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
----------------------------------------------------------------------------------------------------
-- Then we create/register all kinds of modal keybindings environments.
----------------------------------------------------------------------------------------------------
-- Register windowHints (Register a keybinding which is NOT modal environment with modal supervisor)
hswhints_keys = hswhints_keys or {"alt", "tab"}
if string.len(hswhints_keys[2]) > 0 then
  spoon.ModalMgr.supervisor:bind(hswhints_keys[1], hswhints_keys[2], 'Show Window Hints', function()
                                   spoon.ModalMgr:deactivateAll()
                                   hs.hints.windowHints()
  end)
end

BEGIN Alt+R

1
local cmodal

WinWin

WinWin: Window management with short keys after toggle on:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
----------------------------------------------------------------------------------------------------
-- resizeM modal environment
Install:andUse("WinWin", {
                 fn = function (s)
                   spoon.ModalMgr:new("resizeM")
                   cmodal = spoon.ModalMgr.modal_list["resizeM"]
                   cmodal:bind('', 'escape', 'Deactivate resizeM', function() spoon.ModalMgr:deactivate({"resizeM"}) end)
                   cmodal:bind('', 'Q', 'Deactivate resizeM', function() spoon.ModalMgr:deactivate({"resizeM"}) end)
                   cmodal:bind('', 'tab', 'Toggle Cheatsheet', function() spoon.ModalMgr:toggleCheatsheet() end)
                   -------------------------------- Movement --------------------------------
                   cmodal:bind('', 'A', 'Move Leftward', function() s:stepMove("left") end, nil, function() sj:stepMove("left") end)
                   cmodal:bind('', 'D', 'Move Rightward', function() s:stepMove("right") end, nil, function() s:stepMove("right") end)
                   cmodal:bind('', 'W', 'Move Upward', function() s:stepMove("up") end, nil, function() s:stepMove("up") end)
                   cmodal:bind('', 'S', 'Move Downward', function() s:stepMove("down") end, nil, function() s:stepMove("down") end)
                   cmodal:bind('shift', 'H', 'Move Leftward', function() s:stepResize("left") end, nil, function() s:stepResize("left") end)
                   cmodal:bind('shift', 'L', 'Move Rightward', function() s:stepResize("right") end, nil, function() s:stepResize("right") end)
                   cmodal:bind('shift', 'K', 'Move Upward', function() s:stepResize("up") end, nil, function() s:stepResize("up") end)
                   cmodal:bind('shift', 'J', 'Move Downward', function() s:stepResize("down") end, nil, function() s:stepResize("down") end)
                   -------------------------------- Half Split --------------------------------
                   cmodal:bind('', 'H', 'Lefthalf of Screen', function() s:moveAndResize("halfleft") end)
                   cmodal:bind('', 'L', 'Righthalf of Screen', function() s:moveAndResize("halfright") end)
                   cmodal:bind('', 'K', 'Uphalf of Screen', function() s:moveAndResize("halfup") end)
                   cmodal:bind('', 'J', 'Downhalf of Screen', function() s:moveAndResize("halfdown") end)
                   cmodal:bind('', 'F', 'Fullscreen', function() s:moveAndResize("fullscreen") end)
                   cmodal:bind('', 'C', 'Center Window', function() s:moveAndResize("center") end)
                   cmodal:bind('', 'M', 'Maximize Window', function() s:moveAndResize("maximize") end)
                   cmodal:bind('shift', 'M', 'Maximize Window', function() s:moveAndResize("minimize") end)
                   cmodal:bind('ctrl', 'H', 'NorthWest Corner', function() s:moveAndResize("cornerNW") end)
                   cmodal:bind('ctrl', 'L', 'NorthEast Corner', function() s:moveAndResize("cornerNE") end)
                   cmodal:bind('ctrl', 'J', 'SouthWest Corner', function() s:moveAndResize("cornerSW") end)
                   cmodal:bind('ctrl', 'K', 'SouthEast Corner', function() s:moveAndResize("cornerSE") end)
                   cmodal:bind('', '=', 'Stretch Outward', function() s:moveAndResize("expand") end, nil, function() s:moveAndResize("expand") end)
                   cmodal:bind('', '-', 'Shrink Inward', function() s:moveAndResize("shrink") end, nil, function() s:moveAndResize("shrink") end)
                   -------------------------------- Monitor Movement --------------------------------
                   cmodal:bind('', 'left', 'Move to Left Monitor', function() s:moveToScreen("left") end)
                   cmodal:bind('', 'right', 'Move to Right Monitor', function() s:moveToScreen("right") end)
                   cmodal:bind('', 'up', 'Move to Above Monitor', function() s:moveToScreen("up") end)
                   cmodal:bind('', 'down', 'Move to Below Monitor', function() s:moveToScreen("down") end)
                   cmodal:bind('', 'space', 'Move to Next Monitor', function() s:moveToScreen("next") end)
                   -------------------------------- Re&Undo --------------------------------
                   cmodal:bind('', '[', 'Undo Window Manipulation', function() s:undo() end)
                   cmodal:bind('', ']', 'Redo Window Manipulation', function() s:redo() end)
                   cmodal:bind('', '`', 'Center Cursor', function() s:centerCursor() end)
                 end
})

WindowHalfsAndThirds

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
Install:andUse("WindowHalfsAndThirds",
               {
                 config = {
                   use_frame_correctness = true
                 },
                 -- hotkeys = 'default',
                 fn = function (s)
                   --- 1/3 ---
                   cmodal:bind('cmd', 'H', 'Left Screen/3', function() s:thirdLeft() end)
                   cmodal:bind('cmd', 'L', 'Right Screen/3', function() s:thirdRight() end)
                   cmodal:bind('cmd', 'J', 'Top Screen/3', function() s:thirdUp() end)
                   cmodal:bind('cmd', 'K', 'Bottom Screen/3', function() s:thirdDown() end)
                 end
               }
)

END Alt+R

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
-- Register resizeM with modal supervisor
hsresizeM_keys = hsresizeM_keys or {"alt", "R"}
if string.len(hsresizeM_keys[2]) > 0 then
  spoon.ModalMgr.supervisor:bind(hsresizeM_keys[1], hsresizeM_keys[2], "Enter resizeM Environment", function()
                                   -- Deactivate some modal environments or not before activating a new one
                                   spoon.ModalMgr:deactivateAll()
                                   -- Show an status indicator so we know we're in some modal environment now
                                   spoon.ModalMgr:activate({"resizeM"}, "#B22222")
  end)
end

URL dispatching to site-specific browsers

The URLDispatcher spoon makes it possible to open URLs with different browsers. I have created different site-specific browsers using Epichrome, which allows me to keep site-specific bookmarks, search settings, etc. I also use Edge as my work browser (since it integrated with my work account), while using Brave for everything else. 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.

1
2
3
function appID(app)
  return hs.application.infoForBundlePath(app)['CFBundleIdentifier']
end

Window and screen manipulation

WindowScreenLeftAndRight 多屏间移动

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

  1. move to left screen: ctrl + alt + cmd + <Left>

  2. move to right screen: ctrl + alt + cmd + <Right>

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

WindowGrid 网格布局

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

cmd + alt + ctrl + g

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

ToggleScreenRotation 旋转屏幕

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).

ctrl + alt + cmd + <f15>

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

HSaria2

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
-- Install:andUse("HSaria2",
--                {
--                  fn = function (s)
--                    -- First we need to connect to aria2 rpc host
--                    hsaria2_host = hsaria2_host or "http://localhost:6700/jsonrpc"
--                    hsaria2_secret = hsaria2_secret or "token"
--                    s:connectToHost(hsaria2_host, hsaria2_secret)

--                    hsaria2_keys = hsaria2_keys or {"alt", "D"}
--                    if string.len(hsaria2_keys[2]) > 0 then
--                      spoon.ModalMgr.supervisor:bind(hsaria2_keys[1], hsaria2_keys[2], 'Toggle aria2 Panel', function() s:togglePanel() end)
--                    end
--                  end
--                }
-- )

I

#+end_src

Organization and Productivity

Capturing to Org mode(Not Response)

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.

cmd + alt + ctrl + t

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

hs.hotkey.bindSpec({hyper, "t"},
  function ()
    hs.task.new("/bin/bash", nil, { "-l", "-c", "/usr/local/bin/hs "..org_capture_path }):start()
  end
)

System and UI

Basic

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
----------------------------------------------------------------------------------------------------
-- Register lock screen
hslock_keys = hslock_keys or {"alt", "L"}
if string.len(hslock_keys[2]) > 0 then
    spoon.ModalMgr.supervisor:bind(hslock_keys[1], hslock_keys[2], "Lock Screen", function()
        hs.caffeinate.lockScreen()
    end)
end

----------------------------------------------------------------------------------------------------
-- Register AClock
if spoon.AClock then
    hsaclock_keys = hsaclock_keys or {"alt", "T"}
    if string.len(hsaclock_keys[2]) > 0 then
        spoon.ModalMgr.supervisor:bind(hsaclock_keys[1], hsaclock_keys[2], "Toggle Floating Clock", function() spoon.AClock:toggleShow() end)
    end
end

Get Current Tab Url

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
-- ----------------------------------------------------------------------------------------------------
-- -- Register browser tab typist: Type URL of current tab of running browser in markdown format. i.e. [title](link)
-- hstype_keys = hstype_keys or {"alt", "V"}
-- if string.len(hstype_keys[2]) > 0 then
--     spoon.ModalMgr.supervisor:bind(hstype_keys[1], hstype_keys[2], "Type Browser Link", function()
--         local safari_running = hs.application.applicationsForBundleID("com.apple.Safari")
--         local chrome_running = hs.application.applicationsForBundleID("com.google.Chrome")
--         -- if #safari_running > 0 then
--         --     local stat, data = hs.applescript('tell application "Safari" to get {URL, name} of current tab of window 1')
--         --     if stat then hs.eventtap.keyStrokes("[" .. data[2] .. "](" .. data[1] .. ")") end
--         if #chrome_running > 0 then
--             local stat, data = hs.applescript('tell application "Google Chrome" to get {URL, title} of active tab of window 1')
--             -- Markdown Format
--             -- if stat then hs.eventtap.keyStrokes("[" .. data[2] .. "](" .. data[1] .. ")") end
--             -- Org Format
--             if stat then hs.eventtap.keyStrokes("[[" .. data[1] .. "][" .. data[2] .. "]]") end
--         end
--     end)
-- end

AClock( Disabled )

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
-- Install:andUse("AClock",
--                {
--                  config = {
--                    format = "%H:%M"
--                  },
--                  fn = function(s)
--                    hsaclock_keys = hsaclock_keys or {"alt", "T"}
--                    if string.len(hsaclock_keys[2]) > 0 then
--                      spoon.ModalMgr.supervisor:bind(hsaclock_keys[1], hsaclock_keys[2], "Toggle Floating Clock", function() s:toggleShow() end)
--                    end
--                  end

--                  -- start = true
--                }
-- )

General Hammerspoon utilities

BTT: BetterTouchTool(付费)

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

cmd + alt + ctrl + r: reload config

cmd + alt + ctrl + y: toggle console

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

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

Finding colors( Disabled )

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
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
Install:andUse("ColorPicker",
               {
                 -- 太卡了
                 disable = true,
                 hotkeys = {
                   show = { hyper, "z" }
                 },
                 config = {
                   show_in_menubar = false,
                 },
                 start = true,
               }
)

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
2
3
4
5
Install:andUse("KSheet",
               {
                 hotkeys = {
                   toggle = { hyper, "/" }
}})

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

End ModalMgr

1
2
3
----------------------------------------------------------------------------------------------------
-- Finally we initialize ModalMgr supervisor
spoon.ModalMgr.supervisor:enter()