写了两个MenuBar应用

我应该是MenuBar APP的忠实粉丝。

很喜欢打开一些操作性或者辅助性的软件的「菜单栏」选项,这样就可以在任何的窗口、工作区中操作或查看这些软件。

常驻的Menu Bar APP

  • MenuBarX: 菜单栏浏览器
  • Surge:代理工具
  • iStat Menu:状态查看
  • OrbStack:Docker容器管理

但是总有一些需求找不到相关的APP,既然这样,那不如自己写一个!

锁定通知:HemuLock

不知道是不是年龄大了,每次下班的时候总会怀疑公司的电脑没有锁定,虽然已经开启了「闲置x分钟」锁定,但是不知道电脑有没有锁定心里还是不踏实。

HemuLock就只做一件事,响应系统事件:当系统解锁、锁定的时候发送一个通知,只要查看最近的通知就知道电脑有没有锁定了。

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

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条历史记录。

签名

因为没有开发者账号,由于苹果严格的检查机制,打开时可能会遇到警告拦截。所以可以进行本地签名:

  1. 安装 Command Line Tools:
xcode-select --install
  1. 打开终端并执行:
sudo codesign --force --deep --sign - /Applications/Zapp.app/

出现 「replacing existing signature」 即本地签名成功。