object-cmac app
1. 背景
最近在帮一个朋友写一些程序把一些繁琐的工作做成自动化,帮他节省时间和精力。但是作为一个后端程序员,做出来的东西对外来说无非就是 http 接口 或者一个可执行的二进制文件。如果 http 接口还好,找个自己的服务器部署上去,写个简单的页面(我能力仅限于简单的页面)交给他人使用即可。但是有些需求可能在对方的电脑运行更方便(比如处理本地的一些文件,或者涉及到敏感信息等),这个时候就麻烦了,我给对方一个二进制文件让他用,对方也是一脸懵逼,运行失败了,报错了或者其他情况对方都不知道发生了什么,就很不友好。
所以我迫切希望一个可以通过后端语言生成一些简单页面化的 app(一开始相关 terminal gui,但还是太 geek)。试着搜了一下 go 语言开发 mac app,居然搜到了一个对我帮助很大的文章(原文连接)。看到里面提到的第二个例子,简直就是我想要的,直接在 mac 的 status bar 多一个入口,点击下来多个菜单,我可以把我开发的能力放到这里,用户一点就触发,就觉得很 nice。
官方例子如下:
这是我开发后的效果:
其中状态栏显示的文字,可以在运行时实时更新,这样可以在状态栏就可以看到当前运行情况和进度了。
项目叫 MacDriver,是通过 go 语言调用 mac api的框架。
2. 开发
我本人对 Mac APP 的开发以及 Mac 的 API 几乎完全不懂,所以本项目对这现有的 example 慢慢啃下来然后实现了自己的需求。
而 MacDriver 项目提供的能力和能做出来的东西远比我在这里实现的复杂和高级,如果有同学对这个十分感兴趣可以先看看项目的源码,大概了解一下已有的能力。
我需求比较简单,就是拉取最近未读邮件然后对其中需要处理的(自己指定了一些匹配规则)进行后台处理并回复一条自动邮件。
因为我的处理需求和匹配规则跟邮件内容有关,所以没办法使用邮箱提供的收信规则简单处理,所以自己动手写了一个程序。
2.1. 初始化 APP
1
2
3
4
5
6
7
8
9
func main() {
runtime.LockOSThread()
cocoa.TerminateAfterWindowsClose = false
app := cocoa.NSApp_WithDidLaunch(func(n objc.Object) {
// all code in here
}
app.Run()
}
cocoa.NSApp_WithDidLaunch
2.2. 初始化 status bar
这里是定义程序启动时,默认是展示文字。
1
2
3
4
5
6
app := cocoa.NSApp_WithDidLaunch(func(n objc.Object) {
obj := cocoa.NSStatusBar_System().StatusItemWithLength(cocoa.NSVariableStatusItemLength)
obj.Retain()
obj.Button().SetTitle("📧 准备就绪") // 初始化 status bar 的展示文本
// ...省略 code
}
2.3. 运行时动态更新 status bar
运行过程中我希望能实时更新处理的进度以及状态。
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
app := cocoa.NSApp_WithDidLaunch(func(n objc.Object) {
obj := cocoa.NSStatusBar_System().StatusItemWithLength(cocoa.NSVariableStatusItemLength)
obj.Retain()
obj.Button().SetTitle("📧 准备就绪")
var (
eventChan = make(chan string, 1)
indexChan = make(chan int, count)
)
go func() {
for {
select {
case <-time.After(1 * time.Second):
case e := <-eventChan:
// 这里我更新各类事件的实时情况和状态
core.Dispatch(func() {
obj.Button().SetTitle(fmt.Sprintf("🏷 %s", e))
})
case i := <-indexChan:
// 这里我实时更新处理到第几封邮件
core.Dispatch(func() {
obj.Button().SetTitle(fmt.Sprintf("✴️ 处理邮件中 %d/%d", i, count))
})
}
}
}()
// .. 省略 code
2.4. 添加 menu
上面初始化了展示的 status bar 的文字,现在我们添加 menu 菜单,不同的 menu 处理不同的事件。
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
app := cocoa.NSApp_WithDidLaunch(func(n objc.Object) {
// ... 省略code
// set quit action
itemQuit := cocoa.NSMenuItem_New()
itemQuit.SetTitle("退出")
itemQuit.SetAction(objc.Sel("terminate:"))
// 设置自定义 menu 和处理方法
checkAndSetSeen := cocoa.NSMenuItem_New()
checkAndSetSeen.SetTitle(fmt.Sprintf("处理最新%d封邮件✉️(并且设为已读)", count))
checkAndSetSeen.SetAction(objc.Sel("checkAndSet:"))
cocoa.DefaultDelegateClass.AddMethod("checkAndSet:", func(_ objc.Object) {
// 这里就可以放我们自己的逻辑了
go func() {
defer deferFunc(obj)
log.Println("email start")
run(indexChan, eventChan, onlyCheckMode|setSeenMode)
}()
})
setAndReply := cocoa.NSMenuItem_New()
setAndReply.SetTitle(fmt.Sprintf("处理最新%d封邮件✉️(并且设为已读和回复邮件)", count))
setAndReply.SetAction(objc.Sel("setAndReply:"))
cocoa.DefaultDelegateClass.AddMethod("setAndReply:", func(_ objc.Object) {
go func() {
defer deferFunc(obj)
log.Println("email start")
run(indexChan, eventChan, onlyCheckMode|setSeenMode|replyMailMode)
}()
})
// menu 注册进去
menu := cocoa.NSMenu_New()
menu.AddItem(checkAndSetSeen)
menu.AddItem(setAndReply)
menu.AddItem(itemQuit)
obj.SetMenu(menu)
}
到这里 status bar 的开发就完成了,业务逻辑代码我就不贴了。
3. 编译部署
我一开始以为是需要各类的开发者账号或者 xcode 才能将代码运行起来,但是实际上简单的让人我怀疑(因为我知道苹果由于生态封闭 app 的开发就比较复杂)
编译:
1
go build main.go
运行:
1
./main
这就 OK 了,完全不需要其他任何操作,非常清爽。
4. 总结
本篇讲述内容如下:
- 讲述用 go 开发 mac app 的背景
- 介绍基于 go 语言调用 Mac api 的开源库 MacDriver
- 讲述基于 MacDriver 开发一个简单状态栏 app 的过程
- 讲述如何编译部署开发的 app