这是用户在 2024-11-12 21:43 为 https://www.hammerspoon.org/go/ 保存的双语快照页面,由 沉浸式翻译 提供双语支持。了解如何保存?

Getting Started with Hammerspoon
开始使用 Hammerspoon

What is Hammerspoon? 什么是 Hammerspoon?

Hammerspoon is a desktop automation tool for macOS. It bridges various system level APIs into a Lua scripting engine, allowing you to have powerful effects on your system by writing Lua scripts.
Hammerspoon 是一款 macOS 的桌面自动化工具。它将各种系统级 API 集成到 Lua 脚本引擎中,通过编写 Lua 脚本,让您能够对系统产生强大的影响。

What is Lua? Lua 是一种轻量级的编程语言

Lua is a simple programming language. If you have never programmed in Lua before, you may want to run through Learn Lua in Y minutes before you begin.
Lua 是一种简单的编程语言。如果你之前从未使用过 Lua 进行编程,你可能想在开始之前运行“Y 分钟学会 Lua”。

Setup 设置

  • Download the latest release of Hammerspoon and drag it to your /Applications folder
    下载最新版本的 Hammerspoon 并将其拖到您的 /Applications 文件夹中
  • Run Hammerspoon.app and follow the prompts to enable Accessibility access for the app
    运行 Hammerspoon.app 并按照提示为应用程序启用辅助功能访问权限
  • Click on the Hammerspoon menu bar icon and choose Open Config from the menu
    点击 Hammerspoon 菜单栏图标并从菜单中选择 Open Config
  • Open the Hammerspoon API docs in your browser, to explore the extensions we provide, and the functions they offer
    打开浏览器中的 Hammerspoon API 文档,以探索我们提供的扩展及其提供的功能

Table of Contents 目录

Hello World 你好,世界

All good programming tutorials start with a Hello World example of some kind, so we will use Hammerspoon’s ability to bind keyboard hotkeys to demonstrate saying Hello World with a simple notification.
所有好的编程教程都以某种形式的“Hello World”示例开始,因此我们将使用 Hammerspoon 将键盘热键绑定到功能的能力,通过一个简单的通知来演示如何说出“Hello World”。

In your init.lua place the following:
在你的 init.lua 位置放置以下内容:

hs.hotkey.bind({"cmd", "alt", "ctrl"}, "W", function()
  hs.alert.show("Hello World!")
end)

Then save the file, click on the Hammerspoon menubar icon and choose Reload Config. You should now find that pressing ++ctrl+W will display a Hello World notification on your screen.
然后保存文件,点击 Hammerspoon 菜单栏图标并选择 Reload Config 。现在你应该会发现按下 + + ctrl + W 会在屏幕上显示一个“Hello World”通知。

What is happening here is that we’re telling Hammerspoon to bind an anonymous function to a particular hotkey. The hotkey is specified by a table of modifier keys (, and ctrl in this case) and a normal key (W). An anonymous function is simply one that doesn’t have a name. We could have defined the alert function separately with a name and passed that name to hs.hotkey.bind(), but Lua makes it easy to define the functions inline.
这里发生的事情是我们告诉 Hammerspoon 将一个匿名函数绑定到一个特定的热键上。热键由一个修饰键表(在这种情况下为 ctrl )和一个普通键( W )指定。匿名函数就是没有名字的函数。我们本来可以单独定义一个带有名称的 alert 函数,并将其名称传递给 hs.hotkey.bind() ,但 Lua 使得定义函数时直接内联变得简单。

Fancier Hello World 更复杂的 Hello World

While hs.alert is useful, you might prefer to use the macOS native notifications instead, which you can do by simply modifying the previous example to:
hs.alert 很有用的时候,你可能更喜欢使用 macOS 的原生通知,你可以通过简单地修改前面的示例来实现:

hs.hotkey.bind({"cmd", "alt", "ctrl"}, "W", function()
  hs.notify.new({title="Hammerspoon", informativeText="Hello World"}):send()
end)

Introduction to Spoons 介绍勺子

Spoons are pre-made plugins for Hammerspoon.
勺子是 Hammerspoon 的预制插件。

Have a look at the official documentation for Spoons here about how they work and how to use them.
查看 Spoons 的官方文档,了解它们的工作原理和使用方法。

The official website with the listing of Spoons is https://www.hammerspoon.org/Spoons/ and the official repository is https://github.com/Hammerspoon/Spoons.
官方网站上关于 Spoons 的列表是 https://www.hammerspoon.org/Spoons/,官方仓库是 https://github.com/Hammerspoon/Spoons。

Using a Spoon 使用勺子

For this example we’ll use the “AClock” Spoon. To install it, please download the zip file from our Spoon repository, uncompress the zip and double click the AClock.spoon file. Hammerspoon will then remove it from your Downloads folder as it installs.
为此示例,我们将使用“AClock”勺子。要安装它,请从我们的勺子存储库下载 zip 文件,解压缩 zip 文件并双击 AClock.spoon 文件。Hammerspoon 将将其从您的下载文件夹中删除,并在安装过程中将其移除。

Installing a Spoon doesn’t mean it’s going to run by default, so we’ll now add some configuration to load and use this Spoon:
安装勺子并不意味着它将默认运行,因此我们现在将添加一些配置来加载和使用这个勺子:

hs.loadSpoon("AClock")
hs.hotkey.bind({"cmd", "alt", "ctrl"}, "C", function()
  spoon.AClock:toggleShow()
end)

Introduction to window movement
窗口移动简介

One of the most immediately useful things you can do with Hammerspoon is to manipulate the windows on your screen. We’ll start off with a simple example and build up to something more complicated.
Hammerspoon 中最有用的事情之一就是操作屏幕上的窗口。我们将从一个简单的例子开始,逐步构建更复杂的功能。

Add the following to your init.lua:
添加以下内容到您的 init.lua 中:

hs.hotkey.bind({"cmd", "alt", "ctrl"}, "H", function()
  local win = hs.window.focusedWindow()
  local f = win:frame()

  f.x = f.x - 10
  win:setFrame(f)
end)

This will now cause ++ctrl+H to make move the currently focused window 10 pixels to the left. You can see that we fetch the currently focused window and then obtain its frame. This describes the location and size of the window. We can then modify the frame and apply it back to the window using setFrame().
这将现在使 + + ctrl + H 将当前聚焦的窗口向左移动 10 像素。您可以看到我们获取当前聚焦的窗口并获取其框架。这描述了窗口的位置和大小。然后我们可以修改框架并使用 setFrame() 将其重新应用到窗口上。

A quick aside on colon syntax
关于冒号语法的简要说明

You might have noticed that sometimes we’re using dots in function calls, and sometimes we’re using colons. The colon syntax means you’re calling one of that object’s methods. It’s still a function call, but it implicitly passes the object to the method as a self argument.
您可能已经注意到,有时我们在函数调用中使用点,有时使用冒号。冒号语法表示您正在调用该对象的一个方法。这仍然是一个函数调用,但它隐式地将对象作为 self 参数传递给方法。

A quick aside about variable lifecycles
关于变量生命周期的简要说明

Lua uses Garbage Collection to clean up its memory usage - any object it believes is no longer in use, will be destroyed at some point in the future (exactly when, can be very unpredictable, but it’s based around how active your Lua code is).
Lua 使用垃圾回收来清理其内存使用 - 任何它认为不再使用的对象,将在未来的某个时刻被销毁(确切的时间可能非常不可预测,但它是基于你的 Lua 代码的活跃程度)。

This means that a variable which only exists inside a function/loop/etc will be available for garbage collection as soon as the function/loop has finished executing. This includes your init.lua, which is considered to be a single scope that finishes when the final line of code has run.
这意味着仅存在于函数/循环等内部的变量,在函数/循环执行完毕后即可进行垃圾回收。这包括你的 init.lua ,它被视为一个在代码最后一行运行完毕时结束的单个作用域。

If you create any objects in your init.lua, you must capture them in a variable, or they will be silently destroyed at some point in the future. For example:
如果您在您的 init.lua 中创建了任何对象,您必须将它们捕获到变量中,否则它们将在未来的某个时刻被静默销毁。例如:

hs.pathwatcher.new(.....):start()

The object returned here, an hs.pathwatcher object, is not being captured, so it is available for Garbage Collection as soon as your init.lua is finished. It will likely not be destroyed for some minutes/hours after, but you will then be confused why your pathwatcher is not running. Instead, this version will survive for until you reload your config, or quit Hammerspoon:
此处返回的对象,一个 hs.pathwatcher 对象,没有被捕获,因此当你的 init.lua 执行完毕后,它将可用于垃圾回收。之后几分钟/几小时之内它可能不会被销毁,但那时你可能会困惑为什么你的路径监视器没有运行。相反,这个版本将一直存活,直到你重新加载配置或退出 Hammerspoon。

myWatcher = hs.pathwatcher.new(.....):start()

The myWatcher variable is a global variable, so will never go out of scope.
myWatcher 变量是一个全局变量,所以永远不会超出作用域。

As a further aside about the lifecycle of variables - in the Console window, each time you type a line and hit enter, a distinct Lua scope is created, executed and finished. This means that local variables created in the Console window will immediately become inaccessible when you hit Enter, because their scope has closed.
作为关于变量生命周期的进一步说明 - 在控制台窗口中,每次你输入一行并按回车键,就会创建、执行并完成一个独立的 Lua 作用域。这意味着在控制台窗口中创建的 local 变量,在你按下回车键后立即变得不可访问,因为它们的作用域已经关闭。

More complex window movement
更复杂的窗口移动

We can build on the simple window movement example to implement a set of keyboard shortcuts that allow us to move a window in all directions, using the nethack movement keys:
我们可以基于简单的窗口移动示例来实现一组键盘快捷键,允许我们在所有方向上移动窗口,使用 nethack 移动键:

y   k   u
h       l
b   j   n

To do this, we simply need to repeat the previous hs.hotkey.bind() call with slightly different frame modifications:
为此,我们只需将之前的 hs.hotkey.bind() 调用重复一遍,并进行一些微小的帧修改:

hs.hotkey.bind({"cmd", "alt", "ctrl"}, "Y", function()
  local win = hs.window.focusedWindow()
  local f = win:frame()

  f.x = f.x - 10
  f.y = f.y - 10
  win:setFrame(f)
end)

hs.hotkey.bind({"cmd", "alt", "ctrl"}, "K", function()
  local win = hs.window.focusedWindow()
  local f = win:frame()

  f.y = f.y - 10
  win:setFrame(f)
end)

hs.hotkey.bind({"cmd", "alt", "ctrl"}, "U", function()
  local win = hs.window.focusedWindow()
  local f = win:frame()

  f.x = f.x + 10
  f.y = f.y - 10
  win:setFrame(f)
end)

hs.hotkey.bind({"cmd", "alt", "ctrl"}, "H", function()
  local win = hs.window.focusedWindow()
  local f = win:frame()

  f.x = f.x - 10
  win:setFrame(f)
end)

hs.hotkey.bind({"cmd", "alt", "ctrl"}, "L", function()
  local win = hs.window.focusedWindow()
  local f = win:frame()

  f.x = f.x + 10
  win:setFrame(f)
end)

hs.hotkey.bind({"cmd", "alt", "ctrl"}, "B", function()
  local win = hs.window.focusedWindow()
  local f = win:frame()

  f.x = f.x - 10
  f.y = f.y + 10
  win:setFrame(f)
end)

hs.hotkey.bind({"cmd", "alt", "ctrl"}, "J", function()
  local win = hs.window.focusedWindow()
  local f = win:frame()

  f.y = f.y + 10
  win:setFrame(f)
end)

hs.hotkey.bind({"cmd", "alt", "ctrl"}, "N", function()
  local win = hs.window.focusedWindow()
  local f = win:frame()

  f.x = f.x + 10
  f.y = f.y + 10
  win:setFrame(f)
end)

Try it out! 试试看!

Window sizing 窗口大小

In this section we’ll implement the common window management feature of moving a window so it occupies either the left or right half of the screen, allowing you to tile two windows next to each other for Productivity™.
在这个部分,我们将实现常见的窗口管理功能,即移动窗口使其占据屏幕的左侧或右侧,让您可以将两个窗口并排放置以提高生产力™。

hs.hotkey.bind({"cmd", "alt", "ctrl"}, "Left", function()
  local win = hs.window.focusedWindow()
  local f = win:frame()
  local screen = win:screen()
  local max = screen:frame()

  f.x = max.x
  f.y = max.y
  f.w = max.w / 2
  f.h = max.h
  win:setFrame(f)
end)

Here we are binding ++ctrl+ (as in the left cursor key) to a function that will fetch the focused window, then fetch the screen that the focused window is on, fetch the frame of the screen (note that hs.screen.frame() does not include the menubar and dock, see hs.screen.fullFrame() if you need that) and set the frame of the window to occupy the left half of the screen.
这里我们将 + + ctrl + (即左光标键)绑定到一个函数,该函数将获取焦点窗口,然后获取焦点窗口所在的屏幕,获取屏幕的框架(注意 hs.screen.frame() 不包括菜单栏和 Dock,如需包括请查看 hs.screen.fullFrame() )。接着将窗口框架设置为占据屏幕的左侧一半。

To round that out, we’ll add a function to move the window to the right half of the screen:
为了完善这一点,我们将添加一个函数将窗口移动到屏幕的右半部分:

hs.hotkey.bind({"cmd", "alt", "ctrl"}, "Right", function()
  local win = hs.window.focusedWindow()
  local f = win:frame()
  local screen = win:screen()
  local max = screen:frame()

  f.x = max.x + (max.w / 2)
  f.y = max.y
  f.w = max.w / 2
  f.h = max.h
  win:setFrame(f)
end)

A good exercise here would be to see if you can now write functions for yourself that bind the Up/Down cursor keys to resizing windows to the top/bottom half of the screen, respectively.
这里一个好的练习是看看你能否现在为自己编写函数,将上/下光标键绑定到调整窗口到屏幕顶部/底部一半大小。

Multi-window layouts 多窗口布局

When you want to keep several apps open all the time, and have their windows arranged in a particular way, you can use the hs.layout extension:
当你想始终打开几个应用,并按特定方式排列它们的窗口时,可以使用 hs.layout 扩展程序:

    local laptopScreen = "Color LCD"
    local windowLayout = {
        {"Safari",  nil,          laptopScreen, hs.layout.left50,    nil, nil},
        {"Mail",    nil,          laptopScreen, hs.layout.right50,   nil, nil},
        {"iTunes",  "iTunes",     laptopScreen, hs.layout.maximized, nil, nil},
        {"iTunes",  "MiniPlayer", laptopScreen, nil, nil, hs.geometry.rect(0, -48, 400, 48)},
    }
    hs.layout.apply(windowLayout)

To break this down a little, we start off by creating a variable with the name of the main screen on a Mac. You can find these names with the :name() method on an hs.screen object (e.g. typing hs.screen.allScreens()[1]:name() in the Hammerspoon Console).
为了稍微分解一下,我们首先创建一个变量,其名称为 Mac 上的主屏幕。您可以通过在 hs.screen 对象上使用 :name() 方法来找到这些名称(例如,在 Hammerspoon 控制台中输入 hs.screen.allScreens()[1]:name() )。

We then create a table that describes the layout we want. Each entry in the windowLayout table is another table that selects the windows we are interested in, and specifies their desired position and size.
然后我们创建一个表格来描述我们想要的布局。 windowLayout 表格中的每一项都是一个选择我们感兴趣的窗口的另一个表格,并指定它们的期望位置和大小。

The first item in the table is the name of an app we wish to affect, and the second item is the title of a window we wish to affect. Either of these items can be nil, but not both. If the application name is nil then we will match the given window title across all applications. If the window title item is nil then we will match all windows of the given application.
表格中的第一项是我们希望影响的软件名称,第二项是我们希望影响的窗口标题。这两项中的任何一项可以是 nil ,但不能同时是。如果应用程序名称是 nil ,则我们将匹配所有应用程序中的给定窗口标题。如果窗口标题项是 nil ,则我们将匹配给定应用程序的所有窗口。

The third item is the name of the screen to place the window on, as described above (see the API docs for more ways to specify the screen).
第三项是放置窗口的屏幕名称,如上所述(有关更多指定屏幕的方法,请参阅 API 文档)。

The fourth, fifth and sixth items are used to describe the layout of matched windows, in different ways. Only one of these items can have a value, and that value should be a table containing four items, x, y, w and h (horizontal position, vertical position, width and height, respectively).
第四、五和第六项用于以不同方式描述匹配窗口的布局。其中只有一项可以有值,该值应为一个包含四个项的表格,分别是 x (水平位置)、 y (垂直位置)、 w (宽度)和 h (高度)。

The fourth item is a rect that will be given to hs.window:moveToUnit(). The x, y, w, and h values of this rect, are values between 0.0 and 1.0, allowing you to position windows as fractions of the display, without having to be concerned about the precise resolution of the display (e.g. hs.layout.left50 is a pre-defined rect of {x=0, y=0, w=0.5, h=1}).
第四项是一个矩形,将被分配给 hs.window:moveToUnit() 。此矩形的 xywh 值在 0.01.0 之间,允许您将窗口定位为显示区域的一部分,无需担心显示器的精确分辨率(例如, hs.layout.left50 是预定义的 {x=0, y=0, w=0.5, h=1} 矩形)。

The fifth item is a rect that will be given to hs.window:setFrame() and should specify the position/size values as pixel positions on the screen, but without the OS menubar and dock taken into account.
第五项是一个矩形,将分配给 hs.window:setFrame() ,应指定屏幕上的位置/大小值作为像素位置,但不考虑操作系统菜单栏和 Dock。

The sixth item is similar to the fifth, except it does take the OS menubar and dock into account. This is shown in our example above, which will place the iTunes Mini Player window at the very bottom left of the screen, even if the dock is there. Note that we’re using the hs.geometry.rect() helper function to construct the rect table and that the y value is negative, meaning that the top of the window should start 48 pixels above the bottom of the display.
第六项与第五项类似,只是它确实考虑了操作系统菜单栏和 Dock。这在上面的示例中有所体现,它将 iTunes Mini Player 窗口放置在屏幕的左下角,即使有 Dock 也是如此。请注意,我们正在使用 hs.geometry.rect() 辅助函数来构建矩形表,并且 y 的值是负数,这意味着窗口的顶部应该从显示器的底部开始向上 48 像素。

This may seem like a fairly complex set of options, but it’s worth spending some time learning, as it allows for extremely powerful window layouts, particularly in reaction to system events (such as the number of screens changing when you plug in a monitor, or even just press a particular hotkey to restore sanity to your windows).
这可能看起来是一组相当复杂的选项,但花些时间学习是值得的,因为它允许实现非常强大的窗口布局,尤其是在系统事件(如插入显示器时屏幕数量变化,或者只需按下特定的热键来恢复窗口的秩序)的反应中。

Window filters 窗口过滤器

Wouldn’t it be useful to have hotkeys bound in certain contexts or applications but not others? Organize windows and react to events on the basis of position, size, workflow, or any combination thereof? The extremely versatile hs.window.filter module allows this, enabling you to create complex window groupings and behaviors with filtering rules and event watchers. The best way to demonstrate the power of this module is through examples.
是否在特定上下文或应用程序中绑定快捷键而不在其他情况下绑定会有用?根据位置、大小、工作流程或任何组合来组织窗口并响应用件?极其通用的 hs.window.filter 模块允许这样做,使您能够通过过滤规则和事件监视器创建复杂的窗口分组和行为。展示此模块功能的最有效方法是举例说明。

When copying and pasting content from Safari to Messages.app, all links are jarringly expanded, making the text hard to read:
当从 Safari 复制粘贴内容到 Messages.app 时,所有链接都会令人不悦地展开,使得文本难以阅读:

Thrushes make up the Turdidae, a family <https://en.wikipedia.org/wiki/Family_(biology)> of passerine <https://en.wikipedia.org/wiki/Passerine> birds <https://en.wikipedia.org/wiki/Bird> that occurs worldwide.

Only the Safari/Messages pair suffers from this and other macOS applications generally copy and paste without surprises. This annoyance is cleanly rectified with the help of a windowfilter:
只有 Safari/Messages 这一对组合会受到影响,而其他 macOS 应用程序通常复制粘贴都不会出现意外。这个烦恼可以通过窗口过滤器轻松解决。

local function cleanPasteboard()
  local pb = hs.pasteboard.contentTypes()
  local contains = hs.fnutils.contains
  if contains(pb, "com.apple.webarchive") and contains(pb, "public.rtf") then
    hs.pasteboard.setContents(hs.pasteboard.getContents())
  end
end

local messagesWindowFilter = hs.window.filter.new(false):setAppFilter('Messages')
messagesWindowFilter:subscribe(hs.window.filter.windowFocused, cleanPasteboard)

The cleanPasteboard function replaces the Safari ‘rich text’ on the pasteboard with plain text after checking the pasteboard content metadata types. This ensures that copying and pasting of images from Safari still works. An empty windowfilter is created by initializing with false to excludes all windows by default. A Messages ‘appfilter’ is added so that this windowfilter only observes Messages windows. We then subscribe to the windowfilter so that cleanPasteboard is called each time a Messages window gains focus. You can similarly enable/disable custom hotkeys when certain windows or applications have focus.
cleanPasteboard 函数在检查剪贴板内容元数据类型后,将 Safari 的“富文本”替换为纯文本。这确保了从 Safari 复制和粘贴图片仍然有效。通过使用 false 初始化创建一个空的 windowfilter,默认排除所有窗口。添加了一个 Messages 的‘appfilter’,以便这个 windowfilter 只观察 Messages 窗口。然后我们订阅了 windowfilter,以便每次 Messages 窗口获得焦点时调用 cleanPasteboard 。您还可以在特定窗口或应用程序获得焦点时启用/禁用自定义快捷键。

Windowfilters are dynamic, filtering automatically in the background according to the constraints set. By initializing a windowfilter with a predicate function, you can create arbitrarily complex filtering rules:
窗口过滤器是动态的,根据设置的约束自动在后台进行过滤。通过使用谓词函数初始化窗口过滤器,您可以创建任意复杂的过滤规则:

local wf = hs.window.filter.new(function(win)
    local fw = hs.window.focusedWindow()
    return (
      win:isStandard() and
      win:application() == fw:application() and
      win:screen() == fw:screen()
    )
  end)

This windowfilter contains all standard (unhidden, non-modal) windows that share both the application and screen of the currently focused window. The windowfilter continually updates so that the currently focused window determines the set of windows in play. This can be used to cycle through focused application windows on the current screen using hs.window.switcher or your own custom cycler.
此窗口过滤器包含所有标准(非隐藏、非模态)窗口,这些窗口与当前聚焦窗口的应用程序和屏幕共享。窗口过滤器持续更新,以便当前聚焦窗口确定正在播放的窗口集。这可以用来使用 hs.window.switcher 或您自己的自定义循环器在当前屏幕上循环聚焦应用程序窗口。

Simple configuration reloading
简单配置重新加载

You may have noticed that while you’re editing the config, it’s a little bit annoying to have to keep choosing the Reload Config menu item every time you make a change. We can fix that by adding a keyboard shortcut to reload the config:
您可能已经注意到,在编辑配置时,每次您进行更改时都必须选择 Reload Config 菜单项,这有点令人烦恼。我们可以通过添加一个键盘快捷键来重新加载配置来解决这个问题:

hs.hotkey.bind({"cmd", "alt", "ctrl"}, "R", function()
  hs.reload()
end)
hs.alert.show("Config loaded")

We have now bound +++R to a function that will reload the config and display a simple alert banner on the screen for a couple of seconds.
我们现在将 + + + R 绑定到一个函数,该函数将重新加载配置并在屏幕上显示一个简单的警告横幅几秒钟。

One important detail to call out here is that hs.reload() destroys the current Lua interpreter and creates a new one. If we had any code after hs.reload() in this function, it would not be called.
这里需要指出一个重要细节,即 hs.reload() 会销毁当前的 Lua 解释器并创建一个新的。如果我们在这个函数的 hs.reload() 之后有任何代码,它将不会被调用。

Fancy configuration reloading
花哨的配置重新加载

So we can now manually force a reload, but why should we even have to do that when the computer could do it for us‽
所以我们现在可以手动强制刷新,但为什么我们甚至需要这样做,当电脑可以为我们做这件事时‽

The following snippet introduces another new extension, pathwatcher which will allow us to automatically reload the config whenever the file changes:
以下代码片段介绍了另一个新扩展 pathwatcher ,它将允许我们在文件更改时自动重新加载配置:

function reloadConfig(files)
    doReload = false
    for _,file in pairs(files) do
        if file:sub(-4) == ".lua" then
            doReload = true
        end
    end
    if doReload then
        hs.reload()
    end
end
myWatcher = hs.pathwatcher.new(os.getenv("HOME") .. "/.hammerspoon/", reloadConfig):start()
hs.alert.show("Config loaded")

There are several things worth breaking down about this example. Firstly, we’re using a Lua function called os.getenv() to fetch the HOME variable from your system’s environment. This will tell us where your home directory is. We then use Lua’s .. operator to join that string to the part of the config file’s path that we do know, the /.hammerspoon/ part. This gives us the full path of Hammerspoon’s configuration directory.
该示例有几个值得分解的地方。首先,我们使用了一个名为 os.getenv() 的 Lua 函数来从您的系统环境中获取 HOME 变量。这将告诉我们您的家目录在哪里。然后我们使用 Lua 的 .. 运算符将这个字符串与我们知道的部分配置文件路径 /.hammerspoon/ 部分连接起来。这给出了 Hammerspoon 配置目录的完整路径。

We then create a new path watcher using this path, and tell it to call our reloadConfig function whenever something changes in the .hammerspoon directory. We then immediately call start() on the path watcher object, so it begins its work.
然后我们使用此路径创建一个新的路径监视器,并告诉它在 .hammerspoon 目录中任何内容发生变化时调用我们的 reloadConfig 函数。然后我们立即在路径监视器对象上调用 start() ,使其开始工作。

In this example we’ve implemented the config reloading function as a separate, named function, which we pass as an argument to hs.pathwatcher.new(). It’s entirely up to you whether you pass around named functions, or use anonymous ones in-line.
在这个例子中,我们将配置重新加载函数实现为一个单独的、命名的函数,并将其作为参数传递给 hs.pathwatcher.new() 。是否传递命名函数或使用匿名函数直接内联,完全取决于你。

This function accepts a single argument, which is a table containing all the names of files that have been modified. It iterates over that list and checks each file to see if it ends with .lua. If any Lua files have been changed, it then tells Hammerspoon to destroy the current Lua setup and reload its configuration files.
此函数接受一个参数,该参数是一个包含所有已修改文件名称的表。它遍历该列表,检查每个文件是否以 .lua 结尾。如果有任何 Lua 文件被更改,它将告诉 Hammerspoon 销毁当前的 Lua 设置并重新加载其配置文件。

Smart configuration reloading with Spoons
智能配置重新加载使用 Spoons

Hammerspoon supports Lua plugins that we call “Spoons”. They allow anyone to build useful functionality with Hammerspoon’s APIs and then distribute that to other people.
Hammerspoon 支持我们称之为“Spoons”的 Lua 插件。它们允许任何人使用 Hammerspoon 的 API 构建有用的功能,然后将这些功能分发给其他人。

Since configuration reloading is something that many users are likely to want, it’s an ideal candidate for a Spoon, and one exists in the official Spoons repository here.
由于配置重新加载是许多用户可能希望做的事情,它是一个理想的 Spoon 候选,官方 Spoon 仓库中已经有一个了。

To start with, click on the Download link on the Spoon’s webpage - this should download the Zip file and extract it to your Downloads folder, where it will appear with a spoon icon. Open that file and Hammerspoon will automatically import the Spoon to ~/.hammerspoon/Spoons/.
首先,点击 Spoon 网页上的下载链接 - 这应该会下载 Zip 文件并将其提取到您的 Downloads 文件夹中,其中它将以勺子图标出现。打开该文件,Hammerspoon 将自动将 Spoon 导入到 ~/.hammerspoon/Spoons/

Then add the following to your init.lua and you’re done:
然后将其添加到您的 init.lua 中,完成即可:

hs.loadSpoon("ReloadConfiguration")
spoon.ReloadConfiguration:start()

Interacting with application menus
与应用程序菜单交互

Sometimes the only way to automate something is to interact with the GUI of an application, which is not ideal, but is often necessary to get something done.
有时,自动化某事的唯一方法是与应用程序的 GUI 交互,这并不理想,但通常为了完成任务是必要的。

To illustrate this, we’re going to build a hotkey that cycles Safari between multiple User Agent strings (i.e. how it identifies itself to web servers). To do this, you’ll need to have the Safari Develop menu enabled, which you can do by ticking Show Develop menu in menu bar in Safari→Preferences→Advanced.
为了说明这一点,我们将构建一个快捷键,使 Safari 在多个用户代理字符串之间循环(即它向网络服务器标识自己的方式)。为此,您需要启用 Safari 的 Develop 菜单,您可以通过在 Safari→Preferences→Advanced 中勾选 Show Develop menu in menu bar 来实现。

function cycle_safari_agents()
    hs.application.launchOrFocus("Safari")
    local safari = hs.appfinder.appFromName("Safari")

    local str_default = {"Develop", "User Agent", "Default (Automatically Chosen)"}
    local str_edge = {"Develop", "User Agent", "Microsoft Edge — macOS"}
    local str_chrome = {"Develop", "User Agent", "Google Chrome — Windows"}

    local default = safari:findMenuItem(str_default)
    local edge = safari:findMenuItem(str_edge)
    local chrome = safari:findMenuItem(str_chrome)

    if (default and default["ticked"]) then
        safari:selectMenuItem(str_edge)
        hs.alert.show("Edge")
    end
    if (edge and edge["ticked"]) then
        safari:selectMenuItem(str_chrome)
        hs.alert.show("Chrome")
    end
    if (chrome and chrome["ticked"]) then
        safari:selectMenuItem(str_default)
        hs.alert.show("Safari")
    end
end
hs.hotkey.bind({"cmd", "alt", "ctrl"}, '7', cycle_safari_agents)

What we are doing here is first launching Safari or bringing it to the front if it is already running. This is an important step in any menu interaction - menus for apps that are not currently focused, will usually be disabled.
我们在这里所做的是首先启动 Safari,如果它已经在运行,则将其带到前台。这是任何菜单交互中的重要步骤——对于当前未聚焦的应用的菜单通常会被禁用。

We then get a reference to Safari itself using hs.appfinder.appFromName(). Using this object we can search the available menu items and interact with them. Specifically, we are looking for the current state of three of the User Agent strings in Develop→User Agent. We then check to see which of them is ticked, and then select the next one.
然后我们通过 hs.appfinder.appFromName() 引用 Safari 本身。使用这个对象,我们可以搜索可用的菜单项并与它们交互。具体来说,我们正在寻找 Develop→User Agent 中三个用户代理字符串的当前状态。然后我们检查哪个被勾选,然后选择下一个。

Thus, pressing +++7 repeatedly will cycle between the default user agent string, an Edge user agent, and a Chrome user agent. Each time, we display a simple on-screen alert with the name of the user agent we have cycled to.
因此,重复按 + + + 7 将在默认用户代理字符串、Edge 用户代理和 Chrome 用户代理之间循环。每次,我们都会显示一个简单的屏幕警报,显示我们已切换到的用户代理名称。

Creating a simple menubar item
创建一个简单的菜单栏项

Lots of Mac utilities place a small icon in the system menubar to display their status and let you interact with them. We’re going to use two of Hammerspoon’s extensions to whip up a very simple replacement for the popular utility Caffeine.
许多 Mac 实用程序会在系统菜单栏中放置一个小图标,以显示其状态并允许您与之交互。我们将使用 Hammerspoon 的两个扩展来制作一个非常简单的流行实用程序 Caffeine 的替代品。

caffeine = hs.menubar.new()
function setCaffeineDisplay(state)
    if state then
        caffeine:setTitle("AWAKE")
    else
        caffeine:setTitle("SLEEPY")
    end
end

function caffeineClicked()
    setCaffeineDisplay(hs.caffeinate.toggle("displayIdle"))
end

if caffeine then
    caffeine:setClickCallback(caffeineClicked)
    setCaffeineDisplay(hs.caffeinate.get("displayIdle"))
end

This code snippet will create a menubar item that displays either the text SLEEPY if your machine is allowed to go to sleep when you’re not using it, or AWAKE if it will refuse to sleep. The hs.caffeinate extension provides the ability to prevent the display from sleeping, but hs.menubar is providing the menubar item.
这段代码片段将创建一个菜单栏项目,显示文本 SLEEPY 如果您的机器在您不使用时允许进入睡眠状态,或者显示 AWAKE 如果它将拒绝进入睡眠。 hs.caffeinate 扩展提供了防止显示进入睡眠的能力,但 hs.menubar 是提供菜单栏项目。

In this case we create the menubar item and connect a callback (in this case caffeineClicked()) to click events on the menubar item. You can also use icons instead of text, by placing small image files in ~/.hammerspoon/ and using the :setIcon() method on your menubar object. See the full API docs for hs.menubar for more information about this.
在这种情况下,我们创建菜单栏项并将回调(在这种情况下为 caffeineClicked() )连接到菜单栏项的点击事件。您还可以使用图标代替文本,通过在 ~/.hammerspoon/ 中放置小图像文件并使用您的菜单栏对象的 :setIcon() 方法来实现。有关更多信息,请参阅 hs.menubar 的完整 API 文档。

Reacting to application events
响应应用程序事件

Using the hs.application.watcher callback we can react to various application level events, such as applications being launched, exiting, hiding, and activating.
使用 hs.application.watcher 回调,我们可以响应各种应用程序级别的事件,例如应用程序启动、退出、隐藏和激活。

We can demonstrate this by creating a very simple callback which will make sure that when you activate the Finder application, all of its windows will be brought to the front of the display.
我们可以通过创建一个非常简单的回调函数来演示这一点,该函数将确保当您激活 Finder 应用程序时,所有窗口都将被带到显示的前面。

function applicationWatcher(appName, eventType, appObject)
    if (eventType == hs.application.watcher.activated) then
        if (appName == "Finder") then
            -- Bring all Finder windows forward when one gets activated
            appObject:selectMenuItem({"Window", "Bring All to Front"})
        end
    end
end
appWatcher = hs.application.watcher.new(applicationWatcher)
appWatcher:start()

To start with, we define a callback function which accepts three parameters and in it we check if the type of event that triggers the function, is an application being activated. Then we check if the application being activated is Finder. If it is, we select its menu item to bring all of its windows to the front.
首先,我们定义一个接受三个参数的回调函数,在其中我们检查触发函数的事件类型是否为应用程序激活。然后我们检查被激活的应用程序是否为 Finder。如果是,我们选择其菜单项以将其所有窗口置于前台。

We then create an application watcher object that will call our function, and tell it to start.
然后我们创建一个应用程序监视器对象,该对象将调用我们的函数,并让它开始。

Note that we kept a reference to the watcher object, rather than simply calling hs.application.watcher.new(applicationWatcher):start(). The reason for this is so that we can call :stop() on the watcher later if we need to (for example in a function that reloads our config - see the Fancy Config Reloading example for information on how to reload Hammerspoon’s configuration automatically).
请注意,我们保留了监视器对象的引用,而不是简单地调用 hs.application.watcher.new(applicationWatcher):start() 。这样做的原因是,如果需要(例如在重新加载我们配置的函数中 - 请参阅 Fancy Config Reloading 示例以获取有关自动重新加载 Hammerspoon 配置的信息),我们可以在稍后调用 :stop() 监视器。

Reacting to wifi events
响应 WiFi 事件

If you use a MacBook then you probably have a WiFi network at home. It’s very simple with Hammerspoon to trigger events when you are either arriving home and joining your WiFi network, or departing home and leaving the network. In this case we’ll do something simple and adjust the audio volume of the MacBook such that it’s at zero when you’re away from home (protecting you from the shame of opening your MacBook in a coffee shop and blaring out the music you had playing at home!)
如果你使用 MacBook,那么你家里可能有一个 WiFi 网络。使用 Hammerspoon 触发事件非常简单,无论是你回家加入 WiFi 网络,还是离开家离开网络。在这种情况下,我们将做一些简单的事情,调整 MacBook 的音量,这样当你不在家时,音量就会调至零(保护你免受在咖啡店打开 MacBook 并播放家里播放的音乐的尴尬!)

wifiWatcher = nil
homeSSID = "MyHomeNetwork"
lastSSID = hs.wifi.currentNetwork()

function ssidChangedCallback()
    newSSID = hs.wifi.currentNetwork()

    if newSSID == homeSSID and lastSSID ~= homeSSID then
        -- We just joined our home WiFi network
        hs.audiodevice.defaultOutputDevice():setVolume(25)
    elseif newSSID ~= homeSSID and lastSSID == homeSSID then
        -- We just departed our home WiFi network
        hs.audiodevice.defaultOutputDevice():setVolume(0)
    end

    lastSSID = newSSID
end

wifiWatcher = hs.wifi.watcher.new(ssidChangedCallback)
wifiWatcher:start()

Here we have created a callback function that compares the current WiFi network’s name to the previous network’s name and examines whether we have moved from our pre-defined home network to something else, or vice versa, and then uses hs.audiodevice to adjust the system volume.
这里我们创建了一个回调函数,该函数比较当前 WiFi 网络的名称与之前网络的名称,检查我们是否从预定义的家庭网络移动到了其他网络,或者反之,然后使用 hs.audiodevice 来调整系统音量。

Reacting to USB events
响应 USB 事件

If you have a piece of USB hardware that you want to be able to react to, hs.usb.watcher is the extension for you. In the example below, we’ll automatically start the software for a scanner when it is plugged in, and then kill the software when the scanner is unplugged.
如果您有一块希望能够响应的 USB 硬件, hs.usb.watcher 就是您需要的扩展。在下面的示例中,当扫描仪插入时,我们将自动启动软件,当扫描仪拔出时,我们将终止软件。

usbWatcher = nil

function usbDeviceCallback(data)
    if (data["productName"] == "ScanSnap S1300i") then
        if (data["eventType"] == "added") then
            hs.application.launchOrFocus("ScanSnap Manager")
        elseif (data["eventType"] == "removed") then
            app = hs.appfinder.appFromName("ScanSnap Manager")
            app:kill()
        end
    end
end

usbWatcher = hs.usb.watcher.new(usbDeviceCallback)
usbWatcher:start()

Defeating paste blocking
击败粘贴阻止

You may have noticed that some programs and websites try very hard to stop you from pasting in your password. They seem to think it makes them more secure, but in the age of strongly encrypted password managers, this is, of course, nonsense.
你可能已经注意到,一些程序和网站非常努力地阻止你粘贴密码。它们似乎认为这会使它们更安全,但在强加密密码管理器的时代,这当然是胡说八道。

Fortunately, we can route around their damage by emitting fake keyboard events to type the contents of the clipboard:
幸运的是,我们可以通过发出伪造的键盘事件来输入剪贴板内容,从而绕过他们的损坏:

hs.hotkey.bind({"cmd", "alt"}, "V", function() hs.eventtap.keyStrokes(hs.pasteboard.getContents()) end)

Running AppleScript 运行 AppleScript

Sometimes the automation you need is locked away in an application, which seems like it would be impossible to control from Hammerspoon, except that many applications expose their functionality via AppleScript, which Hammerspoon can execute for you:
有时您需要的自动化功能被锁定在应用程序中,这似乎意味着从 Hammerspoon 无法控制,但许多应用程序通过 AppleScript 暴露其功能,而 Hammerspoon 可以为您执行这些操作:

ok,result = hs.applescript('tell Application "iTunes" to artist of the current track as string')
hs.alert.show(result)

This will go and ask iTunes for the artist of the track it is currently playing, and then display that on-screen using hs.alert.
这将去询问当前播放的曲目艺术家,然后使用 hs.alert 在屏幕上显示。

However, before you rush out and start writing lots of iTunes related AppleScript, check out the next entry in this guide.
然而,在你急忙开始编写大量与 iTunes 相关的 AppleScript 之前,请查看本指南的下一项内容。

Controlling iTunes/Spotify
控制 iTunes/Spotify

Using hs.itunes and hs.spotify we can interrogate/control various aspects of iTunes and Spotify, for example, if you were to need to switch between one app and the other:
使用 hs.ituneshs.spotify ,我们可以查询/控制 iTunes 和 Spotify 的各个方面,例如,如果您需要在这两个应用之间切换:

hs.itunes.pause()
hs.spotify.play()
hs.spotify.displayCurrentTrack()

Drawing on the screen
在屏幕上绘制

Sometimes you just cannot find your mouse pointer. You’re sure you left it somewhere, but it’s hiding on one of your monitors and wiggling the mouse isn’t helping you to spot it. Fortunately, we can interrogate and control the mouse pointer, and we can draw things on the screen, which means we can do something like this:
有时你根本找不到你的鼠标指针。你确信你把它放在了某个地方,但它正隐藏在你的某个显示器上,晃动鼠标并不能帮助你找到它。幸运的是,我们可以查询和控制鼠标指针,我们还可以在屏幕上绘制东西,这意味着我们可以这样做:

mouseCircle = nil
mouseCircleTimer = nil

function mouseHighlight()
    -- Delete an existing highlight if it exists
    if mouseCircle then
        mouseCircle:delete()
        if mouseCircleTimer then
            mouseCircleTimer:stop()
        end
    end
    -- Get the current co-ordinates of the mouse pointer
    mousepoint = hs.mouse.absolutePosition()
    -- Prepare a big red circle around the mouse pointer
    mouseCircle = hs.drawing.circle(hs.geometry.rect(mousepoint.x-40, mousepoint.y-40, 80, 80))
    mouseCircle:setStrokeColor({["red"]=1,["blue"]=0,["green"]=0,["alpha"]=1})
    mouseCircle:setFill(false)
    mouseCircle:setStrokeWidth(5)
    mouseCircle:show()

    -- Set a timer to delete the circle after 3 seconds
    mouseCircleTimer = hs.timer.doAfter(3, function()
      mouseCircle:delete()
      mouseCircle = nil
    end)
end
hs.hotkey.bind({"cmd","alt","shift"}, "D", mouseHighlight)

There are several different types of drawing object currently supported - lines, circles, boxes, text and images. Different drawing types can have different properties, which are all fully documented in the API documentation.
当前支持多种不同的绘图对象类型 - 线条、圆形、矩形、文本和图像。不同的绘图类型可能具有不同的属性,这些属性在 API 文档中都有详细说明。

Drawing objects can be placed either on top of all other windows, or behind desktop icons - this makes them useful for displaying contextual overlays on top of the screen (such as this mouse finding example), and more permanent information displays behind all the windows (e.g. the kinds of status information people use GeekTool for).
绘图对象可以放置在所有其他窗口的上方,或者桌面图标的下方——这使得它们在屏幕上显示上下文叠加(如这个鼠标查找示例)和更持久的所有窗口背后的信息显示(例如,人们使用 GeekTool 的状态信息类型)。

Sending iMessage/SMS messages
发送 iMessage/SMS 消息

Rather than explain what this is doing, see if you can figure it out. You may recognise the wifi parts from Reacting to wifi events:
而不是解释这个在做什么,看看你是否能弄懂。你可能能从对 wifi 事件的反应中认出 wifi 的部分。

coffeeShopWifi = "Baristartisan_Guest"
lastSSID = hs.wifi.currentNetwork()
wifiWatcher = nil

function ssidChanged()
    newSSID = hs.wifi.currentNetwork()

    if newSSID == coffeeShopWifi and lastSSID ~= coffeeShopWifi then
        -- We have arrived at the coffee shop
        hs.messages.iMessage("iphonefriend@hipstermail.com", "Hey! I'm at Baristartisan's, come join me!")
        hs.messages.SMS("+1234567890", "Hey, you don't have an iPhone, but you should still come for a coffee")
    end

    lastSSID = newSSID
end

wifiWatcher = hs.wifi.watcher.new(ssidChanged)
wifiWatcher:start()

As you doubtless noticed, this will send two messages to people whenever your Mac arrives at your favourite trendy coffee shop. You’ll need to have macOS’s Messages app configured and working for sending both iMessages and SMS (the latter via an iPhone using SMS Relay) for this to work.
正如您无疑已经注意到的那样,每当您的 Mac 到达您最喜欢的时尚咖啡店时,这将会向人们发送两条消息。为了使此功能正常工作,您需要将 macOS 的“消息”应用配置并设置为可以发送 iMessage 和 SMS(后者通过 iPhone 使用短信中继)。

Automating Hammerspoon with URLs
自动化 Hammerspoon 通过 URL

Sometimes you need to automate your automation tools, and Hammerspoon is automatable in several ways. The first way we’ll cover here is with URLs. Specifically, URLs that begin with hammerspoon://. Given this simple snippet:
有时您需要自动化您的自动化工具,Hammerspoon 可以通过多种方式实现自动化。在这里我们将介绍第一种方法,即使用 URL。具体来说,是以 hammerspoon:// 开头的 URL。给定这个简单的片段:

hs.urlevent.bind("someAlert", function(eventName, params)
    hs.alert.show("Received someAlert")
end)

We have now bound a URL event handler for an event named someAlert that will show a little on-screen text alert. To trigger this event, in a Terminal, run open -g hammerspoon://someAlert. Many applications have the ability to open URLs, so this becomes a very simple way to automate Hammerspoon into taking some action. See the next section for a more concrete (and complex) example of this. Note that the -g option for open causes the URL to be opened in the background, so as to avoid opening Hammerspoon’s Console Window, or giving it keyboard focus.
我们现在为名为 someAlert 的事件绑定了一个 URL 事件处理器,该事件将在屏幕上显示一个小文本警告。要触发此事件,请在终端中运行 open -g hammerspoon://someAlert 。许多应用程序都有打开 URL 的能力,因此这成为了一种将 Hammerspoon 自动化为执行某些操作的非常简单的方法。请参阅下一节,以获取此方法的更具体(且更复杂)的示例。请注意, -g 选项用于 open 会导致 URL 在后台打开,以避免打开 Hammerspoon 的控制台窗口或将其聚焦到键盘上。

Credits 致谢

This guide owes a huge debt to Joseph Holsten and his Mjolnir guide
这篇指南在很大程度上得益于约瑟夫·霍尔斯滕和他的 Mjolnir 指南