Back
Featured image of post MacOS神器之Alfred workflow概览

MacOS神器之Alfred workflow概览

号称Mac神器的Alfred究竟有什么能力,我们能拿它来干啥,为什么使用过之后你就再也回不去了,你的习惯会得到不可逆的转变。又有哪一些对日常工作娱乐有用的插件呢?甚至如何自己创造一个插件,这篇文章都会告诉你。

背景

关于打断

“有研究人员说,专注力一旦被打断,再恢复到原来的状态至少需要15分钟时间。 不用多,如果您一天找3次东西的话,至少就损失了45分钟。”

关于工具

摆脱日常繁琐的重复,提升自动化水平,提升幸福感。

关于软件

本文所说的Alfred是一款免费+可付费升级功能的MacOS下应用软件,可完全替代OS自带的Spotlight(聚焦搜索)并有惊天之扩展性。

Alfred的基础能力

我发现很多同事,包括工作多年的人,不甚注意工作提效。比如:

  • 往来于窗口间的拷贝粘贴却连个多剪切版都没有
  • 查找一个文件还需要目录层级人肉检索而却没有个趁手的搜索工具
  • 打开一个APP还要回到桌面寻找一翻却没有快捷指令
  • 调整一个窗口还要找到边界来回拉扯却没有一键切换

更别说查找单词,搜寻网页等的低效操作了。想象如果在一边的我看着这1秒钟能做的事用了10秒,只能摇头叹息却又无可奈何。有时无意识的低效却不自知,即使你用Windows,这些功能也有它的解法:比如Everything搜索文件,甚至你倒是打开Windows白送(自带)的剪切版历史功能啊!

当然在Mac下也有各种解法,如Paste简洁美观的剪切板管理工具,Moom的窗口管理,Yoink拖拽辅助工具等等。而我们要讲的Alfred则集众多能力于一身。比如下图能看到一些基本能力。 alfred-clipboard

常见的:

  • 快速搜索并打开文件/应用/Web等
  • 剪切板/计算机/系统控制
  • 随时使用的计算器
  • 代码片段Snippets …

如果你不曾使用过,当你操练起来后,这些基础能力或许已经能大幅改善你的使用体验了,但更牛的还在后头。

Alfred的Workflows

当你开启了Powerpack后,便可以使用很多workflow。互联网上已经有成百上千种供你随意挑选了,官方也提供了一些推荐的,若不满足需求你还可以很方便的自我创造。这里不妨分享一下我常用的Workflows。

个人常用Workflow及简介

  1. 有道翻译(Youdao Translate)

    Tip

    随时英/汉互查,写代码或看文档不再自我打断,当然Mac的三指点击有类似功能,但这个更快捷,还可发声等。

    alfred-youdao

  2. 一键进入腾讯会议

    Tip

    你可随时对着一串有会议号的文本按快捷键,就会自动帮你进去会议。别人还在找软件,你已经签好到了:)下载地址

    alfred-tm

  3. 进程监控/管理(Top Processes)

    Tip

    让你在Mac下管理进程,查看哪个是机器卡死的罪魁祸首。top/kill,你手指舞动下,进程灰飞烟灭~ 下载地址

    alfred-top

  4. 终端打开某目录(Open iTerm At Current Finder Path)

    Tip

    当你在Finder浏览目录时,又想用终端到这个目录整点活,它适合不过了。默认快捷键.。你根本不用去记刚才那个文件在哪,机票直达~

  5. ssh快捷工具

    Tip

    相信有不少人像我一样有很多远程机器可以登录,你也会把机器管理在~/.ssh/config下。但是当你死活不记得某机器的名称时,或者你不想按部就班的打开终端,打开窗口再敲命令。用它吧,支持模糊搜索,选择即可自动打开终端进入相应机器,有它必须得香啊:) 下载地址

    alfred-ssh

  6. 进制转换(Convert Number)

    Tip

    程序员们经常要在十六进制、二进制和十进制转来转去,一个命令解君愁啊~

    alfred-convert-number

  7. 查看我的IP

    Tip

    你是否还要在一堆ifconfig/ipconfig的结果中肉眼查看自己的IP,并且还要打开某些网站查看自己公网IP呢?

    alfred-ip

  8. 天气APP

    Tip

    一键查看后面的天气,可设置全球任意地点,可查询未来几天的天气情况,也可以未来小时计的天气变化。 下载地址

    Warning

    天气API有两个,其中openweathermap.org不付费基础功能太弱,推荐使用https://app.tomorrow.io/来查询,免费次数够用。这个使用需要一次设置,请看说明文档,也比较简单即可用起来。

    alfred-ip

  9. Chrome/Safari等Tab搜索切换

    Tip

    我们有时候打开太多Tab后,当每个Tab在浏览器中已经没有空间显示其标题时,找起来简直是轮询试错,突出一个低效。而有了这款插件后,只要还记得大概的标题名称,就能助你快速切换过去,支持当前很多常用浏览器,简直不要太方便~ 下载地址

    alfred-ip

打造自己的Workflow

如果你有一些想法,即在网上搜索不到相关的Workflow,恭喜你,可以考虑自己动手来搞起。不要怕复杂,其实了解了原理真的很简单~我准备了几个小示例,来跟我一步步学起?

Info

这里要先推荐一下官方的Workflow文档,一步步教得挺详细的,对于零基础入门挺好。如果你有基础(程序基础),那就更棒了!

我这里假定你有一点程序,那么我们搞点事。

时间戳转换

程序员们也不免要在时间戳间转换,以往我会用date -d @xxxx等命令来回转成可读时间,但毕竟重复多了心累。我们只要基于Workflow的Script Filter写一个很简单的脚本即可完成目标,就代替我们本来要执行的那几条命令。

 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
#by kevin @2017-11-2 16:48
# 回头看好多年前写的这个脚本居然一直运行至今,兼容性相当不错啊:)

# 这里判断一下是否大概符合一个时间戳的格式,否则尝试将它解释为时间
is_second=`echo {query} | egrep "^[0-9]{9,10}$"`
if [ $? -eq 0 ]
then
    result=`date -r "{query}" "+%Y-%m-%d %H:%M:%S"`
else
    if [ {query} == "now" ]
    then
        result=`date +%s`
    else
        result=`date -j -f "%Y-%m-%d %H:%M:%S" "{query}" +%s`
    fi
fi

# 这里以xml格式输出Alfred workflow的展示结构
# 设置了arg参数,这个当回车时,就会将这个结果返回,而后我们可以挂一个`Copy to Clipboard`就会拷贝到剪切板了。
echo "<?xml version=\"1.0\"?>"
echo "<items>"
echo "  <item uid=\"1\" arg=\"${result}\">"
echo "      <title> ${result} </title>"
echo "      <subtitle>select to copy to clipboard</subtitle>"
echo "  </item>"
echo "</items>"

我们给Script Filter的关键词定义为date,然后把它的输出连接到Copy to Clipboard就完成了。 alfred-date 使用效果如下: alfred-date alfred-date

万年历

作为中国人,难免会有些场景想知道农历日期,甚至黄道吉日,让我看看今天适不适合写代码呢:P 这时候我们就可能要写个复杂点的Workflow了,比涉及一些网络功能等。在社区已经有成熟的Workflow封装了,不论你是用pythongolang都可以很容易的写一些功能逻辑而不用关心Alfred Workflow细节,框架帮你做好了。比如这里我使用awgo框架实现了一个万年历。

  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
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
package main

import (
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"strconv"
	"time"

	aw "github.com/deanishe/awgo"
)

var (
	// 获取时间的HTTP 接口
	dateAPI = "https://api.m.taobao.com/rest/api3.do?api=mtop.common.getTimestamp"

	// 获取农历信息HTTP接口
	lunarAPI = "https://v.juhe.cn/laohuangli/d"
	// 上面接口的调用KEY,放在环境变量中
	lunarAPIKey = ""
	// Our Workflow object
	wf *aw.Workflow
)

func fetchCurrentDate() (time.Time, error) {
	resp, err := http.Get(dateAPI)
	if err != nil {
		return time.Now(), err
	}

	defer resp.Body.Close()
	body, err := io.ReadAll(resp.Body)

	type taobaoDateResult struct {
		API  string   `json:"api"`
		V    string   `json:"v"`
		Ret  []string `json:"ret"`
		Data struct {
			T string `json:"t"`
		} `json:"data"`
	}

	var res taobaoDateResult
	if err = json.Unmarshal(body, &res); err != nil {
		log.Fatal(err)
	}

	log.Println("res:", res)
	msec, err := strconv.ParseInt(res.Data.T, 10, 64)
	if err != nil {
		log.Fatal(err)
	}

	return time.Unix(msec/1000, 0), nil
}

type LunarInfo struct {
	Reason string `json:"reason"`
	Result struct {
		ID        string `json:"id"`
		Yangli    string `json:"yangli"`
		Yinli     string `json:"yinli"`
		Wuxing    string `json:"wuxing"`
		Chongsha  string `json:"chongsha"`
		Baiji     string `json:"baiji"`
		Jishen    string `json:"jishen"`
		Yi        string `json:"yi"`
		Xiongshen string `json:"xiongshen"`
		Ji        string `json:"ji"`
	} `json:"result"`
	ErrorCode int `json:"error_code"`
}

func lunarInfo(d time.Time) (*LunarInfo, error) {
	lunarAPIKey = wf.Config.GetString("lunarAPIKey", "")
	if lunarAPIKey == "" {
		log.Fatal("must set lunarAPIKey in env or config")
	}
	url := fmt.Sprintf("%s?date=%s&key=%s", lunarAPI, d.Format("2006-01-02"), lunarAPIKey)
	log.Println(url)
	resp, err := http.Get(url)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	body, err := io.ReadAll(resp.Body)
	var res LunarInfo
	if err := json.Unmarshal(body, &res); err != nil {
		log.Fatal(err)
	}

	log.Println(res)
	return &res, nil
}

func addItem(title string, now time.Time, local bool) {
	// notify
	timeBaseStr := "基于网络时间得出"
	if local {
		timeBaseStr = "基于本地时间得出"
	}

	curDateString := now.Format("2006-01-02 15:04:05")
	// 功能名称
	fun := wf.NewItem(title)
	fun.Subtitle("选择相应信息,回车拷贝至剪切板")

	// 时间显示
	t := wf.NewItem(fmt.Sprintf("时间: %s", curDateString))
	t.Arg(curDateString)
	t.Valid(true)
	t.NewModifier(aw.ModCmd).Subtitle(timeBaseStr)
	t.NewModifier(aw.ModAlt).Subtitle(timeBaseStr)

	// 时间戳显示
	tsString := strconv.FormatInt(now.Unix(), 10)
	ts := wf.NewItem(fmt.Sprintf("时间戳: %s", tsString))
	ts.Arg(tsString)
	ts.Valid(true)
	ts.NewModifier(aw.ModCmd).Subtitle(timeBaseStr)
	ts.NewModifier(aw.ModAlt).Subtitle(timeBaseStr)

	// 农历获取
	info, err := lunarInfo(now)
	if err != nil {
		lunar := wf.NewItem("农历信息获取失败")
		lunar.Subtitle("请检查网络或接口调用权限")
		return
	}

	if info.ErrorCode != 0 {
		log.Println("info.ErrorCode:", info.ErrorCode)
		return
	}

	wf.NewItem(fmt.Sprintf("农历: %s", info.Result.Yinli)).Valid(true)
	wf.NewItem(fmt.Sprintf("宜: %s", info.Result.Yi)).Valid(true)
	wf.NewItem(fmt.Sprintf("忌: %s", info.Result.Ji)).Valid(true)
}

// run executes the Script Filter.
func run() {
	// ----------------------------------------------------------------
	// Handle CLI arguments
	// ----------------------------------------------------------------

	// 没有参数时,直接显示当前时间
	if len(wf.Args()) == 0 {
		now, err := fetchCurrentDate()
		if err == nil {
			addItem("从网络获取当前时间", now, false)
		}
	}

	now := time.Now()
	if len(wf.Args()) == 1 {
		// 时间的偏移: 格式 +10h -2s
		// 支持: s,m,h, M,d,y
		offsetStr := wf.Args()[0]
		log.Println("offset:", offsetStr)

		tips := "未来"
		if offsetStr[0] == '-' {
			tips = "过去"
		}

		offset, err := strconv.ParseInt(offsetStr[:len(offsetStr)-1], 10, 64)
		if err != nil {
			log.Fatal(err)
		}

		newTime := now
		// golang中时间处理有两个函数:Add() AddDate()
		switch offsetStr[len(offsetStr)-1] {
		case 's', 'm', 'h':
			td, err := time.ParseDuration(offsetStr[1:])
			if err != nil {
				log.Fatal(err)
			}
			newTime = now.Add(td)
		case 'd', 'D':
			newTime = now.AddDate(0, 0, int(offset))
		case 'M':
			newTime = now.AddDate(0, int(offset), 0)
		case 'y', 'Y':
			newTime = now.AddDate(int(offset), 0, 0)
		default:
			log.Fatal("时间单位错误,支持: [smhdMy]")
		}

		addItem(fmt.Sprintf("计算%s时间", tips), newTime, true)

	}
	// Show a warning in Alfred if there are no items
	wf.WarnEmpty("获取时间失败", "请检查网络状态")

	// Send JSON to Alfred. After calling this function, you can't send
	// any more results to Alfred.
	wf.SendFeedback()
}

func main() {
	// Initialise workflow
	wf = aw.New()
	// Call workflow via `Run` wrapper to catch any errors, log them
	// and display an error message in Alfred.
	wf.Run(run)
}

短短的200来行代码,实现了一系列特性:

  • 从互联网取即时时间(方便你必要时对时)
  • 基于当前时间的偏移功能,可增减(秒/分/时/天/月/年)
  • 获得某天的对应的农历
  • 展示黄历等

大概的成果如下(以下示例基于当前时间往后+100天的情况): alfred-now

工作上的助力

事实上,在工作中我也可以用它来实现不少自己特性化功能。比如我就因为日常CodeReview较多,让它自动拉取当前Merge Requests的状态,然后方便我跳转处理。我们可以定制化调起(基于URL Scheme)相关程序来处理,就不在此一一细表了。

后话

我多年前就想写一下如何更好地使用Alfred,它适合那些想精益求精的追求效率之人,但一直拖拉,最近给团队分享个人研发提效介绍到Alfred,然后基于初稿完善了一些形成此文。

在使用Workflow上每个人都也自己的喜好,而Workflow也不停在成长中。一路从Alfred2升级到当前Alfred5,转眼多年了。它迭代较块,时不时更新来点新特性,就像最近支持的Automations Task,它把操作系统的诸多底层能力都暴露出来可供用户使用了。

当年给它开了Mege Supporter后并没有后悔,终身免费升级还是很人性化。同时因为已经用了多年,我无法分清哪些是免费,哪些又是付费(Powerpack)的能力了,这点还请看官们自行摸索。

有同学提到是否设置可漫游,这对Alfred很Easy,可通过iCloud或其它云盘,将其配置目录放在云上即可,多台电脑感受一致体验。

行文至此就接近尾声啦,因为没有收到Alfred的广告费,我就假装一般般推荐一下就好啦。

PS:本篇文章纯手工敲入,没有ChatGPT帮助,对此我很遗憾。

-EOF