前言:菜单栏自定义的进阶之路

macOS的菜单栏是系统信息展示和快捷操作的核心区域。从系统时钟到Wi-Fi状态,从电池电量到输入法切换,菜单栏承载了大量日常交互。然而,对于追求个性化的黑苹果用户而言,系统原生的菜单栏信息远远不够——你需要实时监控CPU温度、显示网络流量、追踪内存使用率、甚至将自定义脚本输出显示在菜单栏上。SwiftBar正是为这一需求而生的工具。它将Unix哲学的"一切皆文本"理念引入菜单栏自定义,让你用最简单的Shell脚本就能创建功能丰富的菜单栏插件。本文将全面讲解SwiftBar的插件开发,从基础Shell脚本到Swift原生插件,打造完全个性化的系统监控面板。

一、SwiftBar基础

1.1 SwiftBar简介

SwiftBar是一个受BitBar启发的macOS菜单栏自定义工具,由Swift编写。它通过执行脚本并将其输出渲染为菜单栏项目来实现自定义。核心特性:

  • 支持任何可执行脚本(Shell、Python、Ruby、Swift、Node.js等)
  • BitBar插件完全兼容
  • 支持xbar(BitBar继任者)插件格式
  • 内置插件浏览器,一键安装社区插件
  • 支持插件变量和刷新控制
  • 原生Swift实现,极低资源占用

1.2 安装SwiftBar

# 方法一:通过Homebrew安装
brew install --cask swiftbar

# 方法二:从GitHub下载
# https://github.com/swiftbar/SwiftBar/releases

# 首次启动配置
# 1. 设置插件目录(建议:~/.swiftbar-plugins)
# 2. 授予辅助功能权限(系统偏好设置 → 安全性与隐私 → 隐私 → 辅助功能)

1.3 插件基本格式

SwiftBar插件的本质是一个可执行脚本,其输出遵循特定的格式规范:

#!/bin/bash
# 文件名格式:plugin_name.{time}.{ext}
# {time} 定义刷新间隔,如 1s, 5m, 1h
# 示例:cpu_temp.5s.sh(每5秒刷新一次)

# 第一行输出显示在菜单栏上
echo "CPU: 45°C"

# --- 以下内容在下拉菜单中显示 ---
echo "---"
echo "刷新 | refresh=true"
echo "打开活动监视器 | href='x-apple.systempreferences:com.apple.Activity-Monitor'"

二、Shell脚本插件开发

2.1 CPU温度监控插件

黑苹果上监控CPU温度是最常见的需求之一:

#!/bin/bash
# cpu_temp.5s.sh
# CPU温度监控插件

# 使用osx-cpu-temp获取温度
# brew install osx-cpu-temp

TEMP=$(osx-cpu-temp 2>/dev/null | grep -oE '[0-9]+\.[0-9]+' || echo "N/A")

# 根据温度设置图标和颜色
if [ "$TEMP" != "N/A" ]; then
    TEMP_INT=${TEMP%.*}
    if [ "$TEMP_INT" -lt 50 ]; then
        COLOR="green"
        ICON="❄️"
    elif [ "$TEMP_INT" -lt 70 ]; then
        COLOR="yellow"
        ICON="🌡️"
    elif [ "$TEMP_INT" -lt 85 ]; then
        COLOR="orange"
        ICON="🔥"
    else
        COLOR="red"
        ICON="⚠️"
    fi
    echo "$ICON ${TEMP}°C | color=$COLOR"
else
    echo "🌡️ N/A | color=gray"
fi

echo "---"
echo "温度阈值:"
echo "  正常: <50°C (绿色)"
echo "  注意: 50-70°C (黄色)"
echo "  警告: 70-85°C (橙色)"
echo "  危险: >85°C (红色)"
echo "---"
echo "刷新 | refresh=true"

2.2 内存使用监控插件

#!/bin/bash
# memory_usage.10s.sh
# 内存使用监控插件

# 获取内存信息
MEM_TOTAL=$(sysctl -n hw.memsize)
MEM_TOTAL_GB=$(echo "scale=1; $MEM_TOTAL / 1073741824" | bc)

# 使用vm_stat获取详细内存统计
VM_STAT=$(vm_stat)

PAGES_FREE=$(echo "$VM_STAT" | grep "Pages free" | awk '{print $3}' | tr -d '.')
PAGES_ACTIVE=$(echo "$VM_STAT" | grep "Pages active" | awk '{print $3}' | tr -d '.')
PAGES_INACTIVE=$(echo "$VM_STAT" | grep "Pages inactive" | awk '{print $3}' | tr -d '.')
PAGES_SPECULATIVE=$(echo "$VM_STAT" | grep "Pages speculative" | awk '{print $3}' | tr -d '.')
PAGES_COMPRESSED=$(echo "$VM_STAT" | grep "Pages occupied by compressor" | awk '{print $4}' | tr -d '.')

PAGE_SIZE=4096

# 计算各项内存使用(GB)
ACTIVE_GB=$(echo "scale=1; $PAGES_ACTIVE * $PAGE_SIZE / 1073741824" | bc)
INACTIVE_GB=$(echo "scale=1; $PAGES_INACTIVE * $PAGE_SIZE / 1073741824" | bc)
COMPRESSED_GB=$(echo "scale=1; $PAGES_COMPRESSED * $PAGE_SIZE / 1073741824" | bc)
USED_GB=$(echo "scale=1; ($PAGES_ACTIVE + $PAGES_COMPRESSED) * $PAGE_SIZE / 1073741824" | bc)

# 计算使用百分比
USED_PCT=$(echo "scale=0; $USED_GB * 100 / $MEM_TOTAL_GB" | bc)

# 设置颜色
if [ "$USED_PCT" -lt 60 ]; then
    COLOR="green"
elif [ "$USED_PCT" -lt 80 ]; then
    COLOR="yellow"
elif [ "$USED_PCT" -lt 90 ]; then
    COLOR="orange"
else
    COLOR="red"
fi

echo "RAM: ${USED_GB}G/${MEM_TOTAL_GB}G (${USED_PCT}%) | color=$COLOR"
echo "---"
echo "活跃: ${ACTIVE_GB} GB"
echo "非活跃: ${INACTIVE_GB} GB"
echo "压缩: ${COMPRESSED_GB} GB"
echo "---"

# Swap使用情况
SWAP_INFO=$(sysctl vm.swapusage 2>/dev/null)
if [ -n "$SWAP_INFO" ]; then
    SWAP_USED=$(echo "$SWAP_INFO" | grep "used" | awk '{print $7}' | cut -d'M' -f1)
    SWAP_TOTAL=$(echo "$SWAP_INFO" | grep "total" | awk '{print $7}' | cut -d'M' -f1)
    echo "Swap: ${SWAP_USED}MB / ${SWAP_TOTAL}MB"
else
    echo "Swap: 未启用"
fi

echo "---"
echo "刷新 | refresh=true"
echo "打开活动监视器 | href='x-apple.systempreferences:com.apple.Activity-Monitor'"

2.3 网络流量监控插件

#!/bin/bash
# network_speed.5s.sh
# 网络流量监控插件

# 网络接口(根据实际情况修改)
IFACE="en0"

# 读取当前流量统计
RX1=$(netstat -ibn | grep -e "$IFACE" -m 1 | awk '{print $7}')
TX1=$(netstat -ibn | grep -e "$IFACE" -m 1 | awk '{print $10}')

# 等待1秒
sleep 1

# 再次读取
RX2=$(netstat -ibn | grep -e "$IFACE" -m 1 | awk '{print $7}')
TX2=$(netstat -ibn | grep -e "$IFACE" -m 1 | awk '{print $10}')

# 计算速率(Bytes/s → KB/s 或 MB/s)
RX_RATE=$((RX2 - RX1))
TX_RATE=$((TX2 - TX1))

# 格式化下载速率
if [ "$RX_RATE" -gt 1048576 ]; then
    RX_DISPLAY=$(echo "scale=1; $RX_RATE / 1048576" | bc)"MB/s"
else
    RX_DISPLAY=$(echo "scale=0; $RX_RATE / 1024" | bc)"KB/s"
fi

# 格式化上传速率
if [ "$TX_RATE" -gt 1048576 ]; then
    TX_DISPLAY=$(echo "scale=1; $TX_RATE / 1048576" | bc)"MB/s"
else
    TX_DISPLAY=$(echo "scale=0; $TX_RATE / 1024" | bc)"KB/s"
fi

echo "⬇${RX_DISPLAY} ⬆${TX_DISPLAY}"
echo "---"

# IP地址
IP_V4=$(ifconfig "$IFACE" | grep "inet " | awk '{print $2}')
IP_V6=$(ifconfig "$IFACE" | grep "inet6" | awk '{print $2}' | head -1)
echo "IPv4: ${IP_V4:-未连接}"
echo "IPv6: ${IP_V6:-未连接}"

echo "---"

# Wi-Fi信息
WIFI_SSID=$(/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport -I 2>/dev/null | grep "SSID" | awk -F': ' '{print $2}')
WIFI_RSSI=$(/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport -I 2>/dev/null | grep "rssi" | awk -F': ' '{print $2}')

if [ -n "$WIFI_SSID" ]; then
    echo "Wi-Fi: $WIFI_SSID"
    echo "信号强度: ${WIFI_RSSI} dBm"
else
    echo "Wi-Fi: 未连接"
fi

echo "---"
echo "刷新 | refresh=true"
echo "网络偏好设置 | href='x-apple.systempreferences:com.apple.Network-Settings'"

2.4 磁盘空间监控插件

#!/bin/bash
# disk_space.1m.sh
# 磁盘空间监控插件

# 系统盘
ROOT_INFO=$(df -h / | tail -1)
ROOT_SIZE=$(echo "$ROOT_INFO" | awk '{print $2}')
ROOT_USED=$(echo "$ROOT_INFO" | awk '{print $3}')
ROOT_AVAIL=$(echo "$ROOT_INFO" | awk '{print $4}')
ROOT_PCT=$(echo "$ROOT_INFO" | awk '{print $5}' | tr -d '%')

# 设置颜色
if [ "$ROOT_PCT" -lt 70 ]; then
    COLOR="green"
elif [ "$ROOT_PCT" -lt 85 ]; then
    COLOR="yellow"
elif [ "$ROOT_PCT" -lt 95 ]; then
    COLOR="orange"
else
    COLOR="red"
fi

echo "💾 ${ROOT_PCT}% | color=$COLOR"
echo "---"
echo "系统盘 (/)"
echo "  总量: $ROOT_SIZE"
echo "  已用: $ROOT_USED"
echo "  可用: $ROOT_AVAIL"

# EFI分区
EFI_PARTITION=$(diskutil list | grep EFI | head -1 | awk '{print $NF}')
if [ -n "$EFI_PARTITION" ]; then
    sudo diskutil mount /dev/$EFI_PARTITION 2>/dev/null || true
    EFI_INFO=$(df -h /Volumes/EFI 2>/dev/null | tail -1)
    if [ -n "$EFI_INFO" ]; then
        EFI_SIZE=$(echo "$EFI_INFO" | awk '{print $2}')
        EFI_USED=$(echo "$EFI_INFO" | awk '{print $3}')
        EFI_AVAIL=$(echo "$EFI_INFO" | awk '{print $4}')
        echo "---"
        echo "EFI分区"
        echo "  总量: $EFI_SIZE"
        echo "  已用: $EFI_USED"
        echo "  可用: $EFI_AVAIL"
    fi
    sudo diskutil unmount /dev/$EFI_PARTITION 2>/dev/null || true
fi

echo "---"
echo "刷新 | refresh=true"

三、Python脚本插件

3.1 黑苹果兼容性检查插件

#!/usr/bin/env python3
# hackintosh_status.5m.py
# 黑苹果系统状态综合检查

# <swiftbar>about.title=黑苹果状态检查</swiftbar>

import subprocess
import json

def run_cmd(cmd):
    try:
        return subprocess.check_output(cmd, shell=True, text=True).strip()
    except:
        return "N/A"

# 获取系统信息
macos_ver = run_cmd("sw_vers -productVersion")
kernel_ver = run_cmd("uname -r")
opencore_ver = run_cmd("nvram 4D1FDA02-38C7-4A6A-9CC6-4BCCA8B30102:opencore-version 2>/dev/null | awk -F: '{print $2}'")
smbios_model = run_cmd("sysctl -n hw.model")
cpu_model = run_cmd("sysctl -n machdep.cpu.brand_string")
mem_gb = round(int(run_cmd("sysctl -n hw.memsize")) / 1073741824)

# SIP状态
sip_status = run_cmd("csrutil status")
sip_enabled = "启用" if "enabled" in sip_status else "禁用"

# 输出
print(f"🍎 {macos_ver} | color=green")
print("---")
print(f"macOS: {macos_ver}")
print(f"内核: {kernel_ver}")
print(f"OpenCore: {opencore_ver}")
print(f"SMBIOS: {smbios_model}")
print("---")
print(f"CPU: {cpu_model}")
print(f"内存: {mem_gb} GB")
print(f"SIP: {sip_enabled}")
print("---")
print("刷新 | refresh=true")

四、Swift原生插件开发

4.1 为什么使用Swift

虽然Shell脚本足够简单,但Swift原生插件具有以下优势:

  • 直接调用macOS原生API(SystemConfiguration、IOKit等)
  • 更低的CPU和内存占用
  • 更精确的数据获取(无需解析文本输出)
  • 更好的错误处理和类型安全
  • 可编译为单一二进制文件,无外部依赖

4.2 Swift插件:高级CPU监控

#!/usr/bin/env swift
# cpu_monitor.5s.swift

import Foundation

// 获取CPU使用率(通过host_statistics)
func getCPUUsage() -> (user: Double, system: Double, idle: Double) {
    var numCPU: natural_t = 0
    var cpuInfo: processor_info_array_t?
    var numCPUInfo: mach_msg_type_number_t = 0
    
    let result = withUnsafeMutablePointer(to: &numCPU) { numCPUPtr in
        withUnsafeMutablePointer(to: &cpuInfo) { cpuInfoPtr in
            withUnsafeMutablePointer(to: &numCPUInfo) { numCPUInfoPtr in
                host_processor_info(mach_host_self(), PROCESSOR_CPU_LOAD_INFO,
                                   numCPUPtr, cpuInfoPtr, numCPUInfoPtr)
            }
        }
    }
    
    guard result == KERN_SUCCESS, let cpuInfo = cpuInfo else {
        return (0, 0, 100)
    }
    
    var totalUser: Double = 0
    var totalSystem: Double = 0
    var totalIdle: Double = 0
    
    for i in 0..<Int(numCPU) {
        let user = Double(cpuInfo[i * 4 + 0])
        let system = Double(cpuInfo[i * 4 + 1])
        let idle = Double(cpuInfo[i * 4 + 3])
        totalUser += user
        totalSystem += system
        totalIdle += idle
    }
    
    let total = totalUser + totalSystem + totalIdle
    
    vm_deallocate(mach_task_self_, vm_address_t(bitPattern: cpuInfo), 
                  vm_size_t(numCPUInfo) * 4)
    
    return (totalUser / total * 100, totalSystem / total * 100, totalIdle / total * 100)
}

let usage = getCPUUsage()
let total = 100 - usage.idle

let color = total < 30 ? "green" : total < 60 ? "yellow" : total < 85 ? "orange" : "red"
let icon = total < 30 ? "🟢" : total < 60 ? "🟡" : total < 85 ? "🟠" : "🔴"

print("\(icon) CPU: \(String(format: "%.0f", total))% | color=\(color)")
print("---")
print("用户: \(String(format: "%.1f", usage.user))%")
print("系统: \(String(format: "%.1f", usage.system))%")
print("空闲: \(String(format: "%.1f", usage.idle))%")
print("---")

// CPU核心数
let cores = ProcessInfo.processInfo.processorCount
print("物理核心: \(cores)")
print("逻辑核心: \(ProcessInfo.processInfo.activeProcessorCount)")

4.3 Swift插件:GPU信息监控

#!/usr/bin/env swift
# gpu_info.30s.swift

import Foundation
import IOKit

func getGPUInfo() -> [(name: String, vendor: String)] {
    var gpus: [(name: String, vendor: String)] = []
    
    let matchingDict = IOServiceMatching("IOPCIDevice")
    var iterator: io_iterator_t = 0
    
    let result = IOServiceGetMatchingServices(kIOMainPortDefault, matchingDict, &iterator)
    guard result == KERN_SUCCESS else { return gpus }
    
    var service: io_object_t = IOIteratorNext(iterator)
    while service != 0 {
        if let modelData = IORegistryEntryCreateCFProperty(service, "model" as CFString, 
                                                           kCFAllocatorDefault, 0) {
            let model = modelData.takeRetainedValue() as? Data
            if let model = model {
                let name = String(data: model, encoding: .utf8)?.trimmingCharacters(in: .controlCharacters) ?? "Unknown"
                var vendor = "Unknown"
                if name.contains("AMD") || name.contains("Radeon") {
                    vendor = "AMD"
                } else if name.contains("Intel") {
                    vendor = "Intel"
                } else if name.contains("NVIDIA") || name.contains("GeForce") {
                    vendor = "NVIDIA"
                }
                gpus.append((name: name, vendor: vendor))
            }
        }
        IOObjectRelease(service)
        service = IOIteratorNext(iterator)
    }
    
    IOObjectRelease(iterator)
    return gpus
}

let gpus = getGPUInfo()

if gpus.isEmpty {
    print("GPU: 未检测到 | color=gray")
} else {
    let primary = gpus[0]
    let shortName = primary.name.count > 30 ? String(primary.name.prefix(27)) + "..." : primary.name
    let icon = primary.vendor == "AMD" ? "🔴" : primary.vendor == "Intel" ? "🔵" : "🟢"
    print("\(icon) \(shortName)")
    print("---")
    for (i, gpu) in gpus.enumerated() {
        print("GPU \(i): \(gpu.name)")
        print("厂商: \(gpu.vendor)")
        if i < gpus.count - 1 { print("---") }
    }
}

print("---")
print("刷新 | refresh=true")

五、插件组合与仪表盘

5.1 创建统一监控面板

将多个插件组合,创建一个综合系统监控面板:

#!/bin/bash
# system_dashboard.10s.sh
# 综合系统监控仪表盘

# 获取各项指标
CPU_USAGE=$(ps -A -o %cpu | awk '{s+=$1} END {printf "%.0f", s}')

MEM_TOTAL=$(sysctl -n hw.memsize)
MEM_TOTAL_GB=$(echo "scale=0; $MEM_TOTAL / 1073741824" | bc)
MEM_USED_GB=$(vm_stat | grep "Pages active" | awk '{printf "%.1f", $3 * 4096 / 1073741824}' | tr -d '.')
MEM_PCT=$(echo "scale=0; $MEM_USED_GB * 100 / $MEM_TOTAL_GB" | bc)

DISK_PCT=$(df -h / | tail -1 | awk '{print $5}' | tr -d '%')

# 计算综合健康分数
HEALTH=100
[ "$CPU_USAGE" -gt 50 ] && HEALTH=$((HEALTH - 20))
[ "$CPU_USAGE" -gt 80 ] && HEALTH=$((HEALTH - 20))
[ "$MEM_PCT" -gt 70 ] && HEALTH=$((HEALTH - 15))
[ "$MEM_PCT" -gt 90 ] && HEALTH=$((HEALTH - 15))
[ "$DISK_PCT" -gt 80 ] && HEALTH=$((HEALTH - 10))
[ "$DISK_PCT" -gt 95 ] && HEALTH=$((HEALTH - 10))

if [ "$HEALTH" -ge 80 ]; then
    COLOR="green"; ICON="✅"
elif [ "$HEALTH" -ge 60 ]; then
    COLOR="yellow"; ICON="⚠️"
else
    COLOR="red"; ICON="🔴"
fi

echo "$ICON 系统: ${HEALTH}分 | color=$COLOR"
echo "---"
echo "CPU: ${CPU_USAGE}% | color=$([ "$CPU_USAGE" -gt 80 ] && echo red || echo green)"
echo "RAM: ${MEM_PCT}% | color=$([ "$MEM_PCT" -gt 85 ] && echo red || echo green)"
echo "磁盘: ${DISK_PCT}% | color=$([ "$DISK_PCT" -gt 90 ] && echo red || echo green)"
echo "---"
echo "运行时间: $(uptime | sed 's/.*up //' | sed 's/,.*//')"
echo "负载: $(sysctl -n vm.loadavg | awk '{print $2, $3, $4}')"
echo "---"
echo "刷新 | refresh=true"
echo "活动监视器 | href='x-apple.systempreferences:com.apple.Activity-Monitor'"

5.2 插件目录结构

~/.swiftbar-plugins/
├── cpu_temp.5s.sh           # CPU温度(5秒刷新)
├── memory_usage.10s.sh      # 内存使用(10秒刷新)
├── network_speed.5s.sh      # 网络流量(5秒刷新)
├── disk_space.1m.sh         # 磁盘空间(1分钟刷新)
├── gpu_info.30s.swift       # GPU信息(30秒刷新)
├── hackintosh_status.5m.py  # 黑苹果状态(5分钟刷新)
└── system_dashboard.10s.sh  # 综合仪表盘(10秒刷新)

六、性能优化与注意事项

6.1 刷新间隔优化

过于频繁的刷新会消耗大量CPU资源,特别是调用外部命令时:

  • 温度/网络流量:5-10秒
  • CPU/内存使用率:10-30秒
  • 磁盘空间/系统信息:1-5分钟
  • GPU信息/硬件检测:30秒-5分钟

6.2 脚本执行超时

SwiftBar默认5秒执行超时。如果脚本执行时间过长,会被强制终止。优化方法:

  • 缓存计算结果到临时文件
  • 使用增量计算而非全量计算
  • 避免在脚本中调用sleep
  • 减少外部命令调用次数

6.3 黑苹果特有的注意事项

  • 部分系统信息命令在黑苹果上可能返回异常值,需要做容错处理
  • 温度传感器可能不完整,osx-cpu-temp可能无法读取所有核心温度
  • GPU信息可能不如真机完整,特别是AMD显卡的显存和频率信息
  • 电源状态相关数据(电池、充电)在台式黑苹果上不可用

七、总结

SwiftBar为黑苹果用户提供了无限可能的菜单栏自定义能力。从简单的Shell脚本到Swift原生插件,从单一指标到综合仪表盘,你可以根据需求构建完全个性化的系统监控面板。核心建议:

  1. 从Shell脚本入手:快速验证想法,逐步优化
  2. 合理设置刷新间隔:在实时性和性能之间取平衡
  3. 善用颜色和图标:让关键信息一目了然
  4. 必要时迁移到Swift:当Shell脚本性能不足时,使用Swift直接调用系统API
  5. 加入交互功能:利用href和bash参数实现快捷操作

菜单栏是macOS桌面最具特色的设计之一。通过SwiftBar,你不仅能实时掌握黑苹果的系统状态,更能让菜单栏成为你个人效率和审美品味的体现。开始写你的第一个SwiftBar插件吧!

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。