macOS效率神器-Hammerspoon

Hammerspoon 是 macOS 上一个强大的自动化工具,是一款开源软件,但安装之后 Hammerspoon 默认什么功能也没有,所有的功能都在 Lua 脚本中,需要用户自己编写。Hammerspoon 可以让用户通过 Lua 脚本直接调用 macOS 提供的 API,能做的事情既有自定义快捷键这种简单操作,也能实现连上家里 Wi-Fi 后自动打开某视频网站、到办公室后自动静音等复杂功能。官方提供的 API 已经非常丰富,包括管理应用程序、管理系统音频设备、画图、网格化窗口、控制系统电源状态、操纵显示屏、控制鼠标、执行HTTP请求、HTTP服务器、执行shell/applescript/javascript代码等等。

安装

安装非常简单,这当然是直接使用Homebrew,执行brew install hammerspoon即可。启动 Hammerspoon 后,程序会自动加载配置,配置文件位于 ~/.hammerspoon/init.lua

使用

在使用之前,你需要花几分钟简单的学习一下lua语言http://www.runoob.com/lua/lua-tutorial.html
然后对照着hammerspoon提供的api,实现你想实现的一切功能。http://www.hammerspoon.org/docs/

如果你不想写,也可以看看其他大神提供的脚本,看看都实现了些什么功能,拿来改吧改吧也能用呢。
https://github.com/search?q=hammerspoon

实战

这里我先参考文档,和其他脚本,实现了几个小功能(显示天气预报/网速监控/内存监控/窗口管理)

Jietu20190425-213726-HD.gif

image.png

Jietu20190425-213952-HD.gif

1123123123.gif

首先是快捷键绑定,由于开发阶段需要频繁的加载配置文件,每次都去找到Hammerspoon然后点一下Relod Config还是比较烦人的事情。

1
2
3
4
5
6
local hotkey = require('core.hotkey')

hotkey.bindWithCtrlCmdAlt('R', '重新加载配置文件', function()
hs.alert.show('加载配置文件中..')
hs.timer.doAfter(0.1, function()hs.reload()end)
end)

这里的core.hotkey是对hs.hotkey的封装,代码如下:

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
hotkey = {
registeredHotkey = {}
}
local strkit = require('core.strkit')

function hotkey.bind(mods, key, desc, fn)
hs.hotkey.bind(mods, key, fn)

--///////////注册快捷键////////////////
local info = ''
for _, k in pairs(mods) do
info = info .. (info ~= '' and '+' or '') .. strkit.firstUp(k)
end
info = (info .. '+' .. strkit.firstUp(key))

table.insert(hotkey.registeredHotkey, {
key = info,
desc = desc
})
hs.printf('[注册快捷键]%s -> %s', info, desc)
end

function hotkey.bindWithCtrl(key, desc, fn)
hotkey.bind({ 'CTRL'}, key, desc, fn)
end

function hotkey.bindWithCmd(key, desc, fn)
hotkey.bind({ 'CMD'}, key, desc, fn)
end

function hotkey.bindWithShift(key, desc, fn)
hotkey.bind({ 'Shift'}, key, desc, fn)
end

function hotkey.bindWithAlt(key, desc, fn)
hotkey.bind({ 'Alt'}, key, desc, fn)
end

function hotkey.bindWithCmdAlt(key, desc, fn)
hotkey.bind({ 'CMD', 'ALT' }, key, desc, fn)
end


function hotkey.bindWithCtrlCmd(key, desc, fn)
hotkey.bind({ 'CTRL', 'CMD' }, key, desc, fn)
end

function hotkey.bindWithCtrlCmdAlt(key, desc, fn)
hotkey.bind({ 'CTRL', 'CMD', 'ALT' }, key, desc, fn)
end

function hotkey.bindWithCtrlAlt(key, desc, fn)
hotkey.bind({ 'CTRL', 'ALT' }, key, desc, fn)
end

function hotkey.bindWithCtrlShift(key, desc, fn)
hotkey.bind({ 'CTRL', 'SHIFT' }, key, desc, fn)
end

function hotkey.bindWithCtrlShiftCmd(key, desc, fn)
hotkey.bind({ 'CTRL', 'SHIFT', 'CMD' }, key, desc, fn)
end

function hotkey.bindWithCtrlShiftAlt(key, desc, fn)
hotkey.bind({ 'CTRL', 'SHIFT', 'ALT' }, key, desc, fn)
end

function hotkey.bindWithShiftAlt(key, desc, fn)
hotkey.bind({ 'SHIFT', 'ALT' }, key, desc, fn)
end

function hotkey.bindWithShiftCmd(key, desc, fn)
hotkey.bind({ 'SHIFT', 'CMD' }, key, desc, fn)
end

function hotkey.bindWithShiftCmdAlt(key, desc, fn)
hotkey.bind({ 'SHIFT', 'CMD', 'ALT' }, key, desc, fn)
end

hotkey.bindWithCtrlCmdAlt('K', '显示所有快捷键', function()
allHotKey = ""
for _, v in pairs(hotkey.registeredHotkey) do
allHotKey = allHotKey .. '▶︎ (' .. v.key .. ') ☞' .. v.desc .. '\n'
end
hs.dialog.blockAlert("已注册的快捷键", allHotKey, "我知道了")
end)

return hotkey

然后进行模块划分,后续可能会加很多功能进来, 不可能全部写在~/.hammerspoon/init.lua文件中。
image.png

init.lua文件中,按需加载即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
require('modules.default')
require('modules.reload')
require('modules.config')
require('modules.hosts')
require('modules.lockscreen')
require('modules.weather')
require('modules.speed')
require('modules.memory')
require('modules.window')

--// 加载私有模块
if (string.find(hs.execute('whoami'), 'wuwenze') ~= nil) then
require('private.helpdesk')
end

天气预报

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
local apiUrl = "https://www.tianqiapi.com/api/?version=v1"

local weatherIcon = {
loading = hs.image.imageFromPath('assets/weather/loading.ico'):setSize({ w = 20, h = 20 }),
reload = hs.image.imageFromPath('assets/weather/reload.ico'):setSize({ w = 20, h = 20 }),
lei = hs.image.imageFromPath('assets/weather/lei.ico'):setSize({ w = 20, h = 20 }),
qing = hs.image.imageFromPath('assets/weather/qing.ico'):setSize({ w = 20, h = 20 }),
wu = hs.image.imageFromPath('assets/weather/wu.ico'):setSize({ w = 20, h = 20 }),
xue = hs.image.imageFromPath('assets/weather/xue.ico'):setSize({ w = 20, h = 20 }),
yu = hs.image.imageFromPath('assets/weather/yu.ico'):setSize({ w = 20, h = 20 }),
yujiaxue = hs.image.imageFromPath('assets/weather/yujiaxue.ico'):setSize({ w = 20, h = 20 }),
yun = hs.image.imageFromPath('assets/weather/yun.ico'):setSize({ w = 20, h = 20 }),
zhenyu = hs.image.imageFromPath('assets/weather/zhenyu.ico'):setSize({ w = 20, h = 20 }),
yin = hs.image.imageFromPath('assets/weather/yin.ico'):setSize({ w = 20, h = 20 }),
xiaoyu = hs.image.imageFromPath('assets/weather/xiaoyu.ico'):setSize({ w = 20, h = 20 }),
bingbao = hs.image.imageFromPath('assets/weather/bingbao.ico'):setSize({ w = 20, h = 20 }),
taifeng = hs.image.imageFromPath('assets/weather/taifeng.ico'):setSize({ w = 20, h = 20 }),
shachen = hs.image.imageFromPath('assets/weather/shachen.png'):setSize({ w = 20, h = 20 }),
}

local weatherBar = hs.menubar.new()
weatherBar:setIcon(weatherIcon['loading'])
-- weatherBar:setTitle('查询天气中..')

local reloadItem = {
title = '重新加载',
image = weatherIcon['reload'],
fn = function() fetchWeatherInfo(true) end
}

local function fetchWeatherInfo(isReload)
hs.http.asyncGet(apiUrl, nil, function(status, body, _)
if status ~= 200 then
hs.alert.show('fetchWeatherInfo error, status = ' .. status)
return
end

local weatherMenu = {}
json = hs.json.decode(body)
for i, v in pairs(json.data) do
if i == 1 then
-- weatherBar:setTitle(v.wea)
weatherBar:setIcon(weatherIcon[v.wea_img])
end

weatherMenu[i] = {
image = weatherIcon[v.wea_img],
title = string.format('%s%s %s %s', v.day, v.wea, v.tem, v.win_speed)
}
end
table.insert(weatherMenu, reloadItem)
weatherBar:setMenu(weatherMenu)

if (isReload) then
hs.alert.show(string.format('%s 天气预报已更新', json.city))
end
end)
end

fetchWeatherInfo(false)

内存监控

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
local memoryIcon = {
icon = hs.image.imageFromPath('assets/memory/icon.png'):setSize({ w = 20, h = 20 }),
clean = hs.image.imageFromPath('assets/memory/clean.png'):setSize({ w = 20, h = 20 }),
}
local fetchTimer = nil
local isCleaning = false
local memoryBar = hs.menubar.new()

memoryBar:setTitle('0.00%') --used_rate
memoryBar:setIcon(memoryIcon['icon'])
memoryBar:setTooltip('0M used (0M wired), 0M unused')
memoryBar:setClickCallback(function()
isCleaning = true
if (fetchTimer ~= nil) then
fetchTimer:stop()
end

memoryBar:setTitle('清理中..')
memoryBar:setIcon(memoryIcon['clean'])
hs.execute('sudo purge')
if (fetchTimer ~= nil) then
fetchTimer:start()
end
end)

local function fetchPhysMem()
if (isCleaning) then
memoryBar:setIcon(memoryIcon['icon'])
isCleaning = false
end

-- PhysMem: 9032M used (2148M wired), 7351M unused.
physMem = hs.execute('top -l 1 | head -n 10 | grep PhysMem')

-- 9032M used
used_text = string.match(physMem, '[%d]+%a used')
wired_text = string.match(physMem, '[%d]+%a wired')
unused_text = string.match(physMem, '[%d]+%a unused')

-- M or G
used_unit = string.match(used_text, '%u')
wired_unit = string.match(wired_text, '%u')
unused_unit = string.match(unused_text, '%u')

-- 9032
used = string.match(used_text, '[%d]+')
if (used_unit == 'G') then
used = used * 1024
end
wired = string.match(wired_text, '[%d]+')
if (wired_unit == 'G') then
wired = wired * 1024
end
unused = string.match(unused_text, '[%d]+')
if (unused_unit == 'G') then
unused = unused * 1024
end

-- used_rate = (used - wired) / (used + unused) ?
used_rate = used / (used + unused)
memoryBar:setTitle((string.gsub(string.format("%6.0f", used_rate * 100), "^%s*(.-)%s*$", "%1"))..'%')
memoryBar:setTooltip(string.format('%dM used (%dM wired), %dM unused',used, wired, unused))
end
fetchTimer = hs.timer.doEvery(5, fetchPhysMem)
fetchPhysMem()

窗口管理

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
local hotkey = require('core.hotkey')
local function move(x, y, w, h)
return function()
win = hs.window.focusedWindow()
if win then
win_f = win:frame()
screen_f = win:screen():frame()
print('input: x='..x..',y='..y..',w='..w..',h='..h)
print('screen: x='..screen_f.x..',y='..screen_f.y..',w='..screen_f.w..',h='..screen_f.h)
print('begin_window: x='..win_f.x..',y='..win_f.y..',w='..win_f.w..',h='..win_f.h)
print('win_f.x = screen_f.w * x + screen_f.x -> '.. screen_f.w * x + screen_f.x)
print('win_f.y = screen_f.h * y -> '.. screen_f.h * y)
print('win_f.w = screen_f.w * w -> '.. screen_f.w * w)
print('win_f.h = screen_f.h * h -> '.. screen_f.h * h)

win_f.x = screen_f.w * x + screen_f.x
win_f.y = screen_f.h * y
win_f.w = screen_f.w * w
win_f.h = screen_f.h * h

print('end_window: x='..win_f.x..',y='..win_f.y..',w='..win_f.w..',h='..win_f.h)
win:setFrame(win_f, 0)
end
end
end

hotkey.bindWithCtrlShift('Up', '[窗口管理]向上移动窗口', move(0, 0, 1, 0.5))
hotkey.bindWithCtrlShift('Right', '[窗口管理]向右移动窗口', move(0.5, 0, 0.5, 1))
hotkey.bindWithCtrlShift('Down', '[窗口管理]向下移动窗口', move(0, 0.5, 1, 0.5))
hotkey.bindWithCtrlShift('Left', '[窗口管理]向左移动窗口', move(0, 0, 0.5, 1))
hotkey.bindWithCtrlShift('M', '[窗口管理]最大化窗口', move(0, 0, 1, 1))
hotkey.bindWithCtrlShift('C', '[窗口管理]居中窗口', move(0.05, 0.08, 0.9, 0.9))
hotkey.bindWithCmdAlt('Left', '[窗口管理]向左上角移动窗口', move(0, 0, 0.5, 0.5))
hotkey.bindWithShiftCmdAlt('Left', '[窗口管理]向左下角移动窗口', move(0, 0.5, 0.5, 0.5))
hotkey.bindWithCmdAlt('Right', '[窗口管理]向右上角移动窗口', move(0.5, 0, 0.5, 0.5))
hotkey.bindWithShiftCmdAlt('Right', '[窗口管理]向右下角移动窗口', move(0.5, 0.5, 0.5, 0.5))

网速监控

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
local speedBar = hs.menubar.new()
speedBar:setTitle('0.00 KB/s')
speedBar:setIcon(hs.image.imageFromPath('assets/speed/down.ico'):setSize({ w = 20, h = 20 }))

local interface = hs.network.primaryInterfaces();
if interface then
local netstat_down = 'netstat -ibn | grep -e ' .. interface .. ' -m 1 | awk \'{print $7}\''
local netstat_up = 'netstat -ibn | grep -e ' .. interface .. ' -m 1 | awk \'{print $10}\''
local prev_speed_down = hs.execute(netstat_down)
local prev_speed_up = hs.execute(netstat_up)

hs.timer.doEvery(1, function()
speed_down = hs.execute(netstat_down)
speed_up = hs.execute(netstat_up)
speed_down_show = format_show(speed_down - prev_speed_down)
speed_up_show = format_show(speed_up - prev_speed_up)
prev_speed_down = speed_down
prev_speed_up = speed_up
speedBar:setTitle(speed_down_show)
speedBar:setTooltip('UP:'..speed_up_show..', DOWN:'..speed_down_show)
end)

function format_show(diff)
if diff/1024 > 1024 then
return trim(string.format("%6.2f MB/s", diff/1024/1024))
end
return trim(string.format("%6.2f KB/s", diff/1024))
end

function trim (s)
return (string.gsub(s, "^%s*(.-)%s*$", "%1"))
end
end

有了如此强大开放的工具,我还装那些乱七八糟的辅助软件干什么呢?自己实现一个,才是最极客的方式!

脚本地址:https://gitee.com/wuwenze/hammerspoon-config,后续我会持续更新新的功能进来的,哈哈。

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×