写了两个MenuBar应用
我应该是MenuBar APP的忠实粉丝。
很喜欢打开一些操作性或者辅助性的软件的「菜单栏」选项,这样就可以在任何的窗口、工作区中操作或查看这些软件。
常驻的Menu Bar APP
但是总有一些需求找不到相关的APP,既然这样,那不如自己写一个!
锁定通知:HemuLock
不知道是不是年龄大了,每次下班的时候总会怀疑公司的电脑没有锁定,虽然已经开启了「闲置x分钟」锁定,但是不知道电脑有没有锁定心里还是不踏实。
HemuLock就只做一件事,响应系统事件:当系统解锁、锁定的时候发送一个通知,只要查看最近的通知就知道电脑有没有锁定了。

可以监听系统以下事件:
- 屏幕睡眠
- 屏幕唤醒
- 系统睡眠
- 系统唤醒
- 系统锁定
- 系统解锁
扩展性
为了可以扩展功能,HemuLock同样支持执行shell脚本,只要在~/Library/Application Scripts/com.ch.hades.HemuLock/script中编写需要的代码,就可以响应相应事件的时候执行脚本。
比如,一个发送 Pushover 的脚本示例:
#!/bin/bash
PUSHOVER_TOKEN="xxx"
API_TOKEN="xxx"
push() {
curl -s \
--form-string "sk=$API_TOKEN" \
--form-string "token=$PUSHOVER_TOKEN" \
--form-string "user=xx" \
--form-string "title=HemuLock's Notify" \
--form-string "content=$1" \
https://api.mayuko.cn/v1/push.message
}
case $1 in
SYSTEM_LOCK)
push "系统锁定"
;;
SYSTEM_UNLOCK)
push "系统解锁"
;;
SYSTEM_SLEEP)
push "系统进入睡眠模式"
;;
esac
case方法的参数是系统事件,所有的参数有:
| Event | Argument |
|---|---|
| 屏幕唤醒 | SCREEN_WAKE |
| 屏幕睡眠 | SCREEN_SLEEP |
| 系统唤醒 | SYSTEM_WAKE |
| 系统睡眠 | SYSTEM_SLEEP |
| 系统锁定 | SYSTEM_LOCK |
| 系统解锁 | SYSTEM_UNLOCK |
勿扰模式
如果希望某段时间内不监听系统事件,可以勾选开启勿扰。当时间在勿扰时间段时,将不会发送通知或执行事件。
勿扰时间设置请在「偏好设置」中进行修改。
状态
| 状态 | 说明 |
|---|---|
| 🟢 | 正在运行 |
| 🟡 | 勿扰模式 |
二维码解析:Zapp
最近写项目的时候需要经常解析一个网页上的二维码进行登录,但是每次都要用手机扫码再将登录链接复制到电脑上很是繁琐,尤其是Handoff失灵的时候简直抓狂。

Zapp可以解析屏幕上二维码,目前提供了三种方式:
| 方式 | 快捷键 | 描述 |
|---|---|---|
| 从屏幕截图 | ⌘+X | 通过系统截图应用截取屏幕二维码 |
| 从剪贴板导入 | ⌘+C | 将解析剪贴板中的图像 |
| 从iPhone或iPad导入 | ⌘+Q | 通过「连续互通相机」API调用同一个iCloud下iPhone或iPad的相机进行拍摄 |
屏幕截图
屏幕截图仍然调用了系统命令:/usr/sbin/screencapture,然后获取到图像进行解析:
/**
Use system screencapture cli to capture image.
*/
public func capture() -> URL? {
let destination = genDestination()
let task = Process()
task.launchPath = "/usr/sbin/screencapture"
task.arguments = ["-i", "-r", "-x", destination.path]
task.launch()
task.waitUntilExit()
// Make sure the file exist.
if FileManager.default.fileExists(atPath: destination.path) {
return destination
}
return nil
}
连续互通
为了实现可以通过手机解析其他位置的二维码,需要使用到macOS「连续互通相机」的API,但是除了官方的一个简单的Demo,几乎找不到其他任何资料,最后还是使用了一个ViewController勉强实现。😞
import Foundation
import AppKit
class ContinuityViewController: NSViewController {
var appState: AppStateContainer!
override func viewDidLoad() {
super.viewDidLoad()
}
override func loadView() {
let view = NSView(frame: NSRect(x: 0, y: 0, width: 200, height: 200))
view.wantsLayer = true
let stackView = NSStackView()
stackView.orientation = .vertical
let symbol = NSImage(systemSymbolName: "camera.viewfinder", accessibilityDescription: nil)!
symbol.size = NSSize(width: 60, height: 60)
let symbolView = NSImageView(image: symbol)
symbolView.imageScaling = .scaleProportionallyUpOrDown
symbolView.frame = NSRect(x: 0, y: 0, width: symbol.size.width, height: symbol.size.height)
stackView.addArrangedSubview(symbolView)
stackView.translatesAutoresizingMaskIntoConstraints = false
let button = NSButton(title: "Import".localized, target: nil, action: #selector(showMenu(_:)))
let buttonMenu = NSMenu()
button.menu = buttonMenu
button.translatesAutoresizingMaskIntoConstraints = false
stackView.addArrangedSubview(button)
view.addSubview(stackView)
NSLayoutConstraint.activate([
stackView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
symbolView.widthAnchor.constraint(equalToConstant: symbol.size.width),
symbolView.heightAnchor.constraint(equalToConstant: symbol.size.height)
])
self.view = view
}
override func validRequestor(forSendType sendType: NSPasteboard.PasteboardType?, returnType: NSPasteboard.PasteboardType?) -> Any? {
if let pasteboardType = returnType,
// The service is image-related.
NSImage.imageTypes.contains(pasteboardType.rawValue) {
return self
} else {
// Let objects in the responder chain handle the message.
return super.validRequestor(forSendType: sendType, returnType: returnType)
}
}
@objc func readSelectionFromPasteboard(_ pasteboard: NSPasteboard) -> Bool
{
guard pasteboard.canReadItem(withDataConformingToTypes: NSImage.imageTypes) else { return false }
guard let image = NSImage(pasteboard: pasteboard) else { return false }
appState.importQRCodeFromContinuityCamera(image: image)
closeWindow()
return true
}
@objc func closeWindow() {
view.window?.close()
}
@objc func showMenu(_ sender: NSButton) {
guard let menu = sender.menu else { return }
guard let event = NSApplication.shared.currentEvent else { return }
// AppKit uses the Responder Chain to figure out where to insert the Continuity Camera menu items.
// So making ourselves `firstResponder` here is important.
self.view.window?.makeFirstResponder(self)
NSMenu.popUpContextMenu(menu, with: event, for: sender)
}
}
如果二维码有效,二维码的内容将直接复制到剪贴板中,方便使用。同时Zapp也可以保存最近N条历史记录。
签名
因为没有开发者账号,由于苹果严格的检查机制,打开时可能会遇到警告拦截。所以可以进行本地签名:
- 安装 Command Line Tools:
xcode-select --install
- 打开终端并执行:
sudo codesign --force --deep --sign - /Applications/Zapp.app/
出现 「replacing existing signature」 即本地签名成功。