feat(processes): 改为可选模块并支持活动监视器回退

This commit is contained in:
ysicing committed 2026-09-29 22:13:53 +08:00
1 parent dd099950b1
commit e19ea53771
25 files changed
+229 -12

No files matched your search

+10
View File
@@ -13,6 +13,16 @@ samples only that: CPU always; memory and network cheaply; GPU, disk, processes,
fans only when a visible surface needs them. Disk is read at most every 30 s, battery every
10 s. The loop pauses on screen sleep, system sleep and session switch.
The full process manager is an optional module, disabled by default through `processesEnabled`.
Its sidebar entry and process-explanation shortcuts are available only when enabled. The process
hotkey remains registered: it opens XStats's process page when enabled, or macOS Activity Monitor
when disabled, without changing the module preference. Its settings label follows the destination;
the process-page header also provides a direct Activity Monitor button.
Disabling the module returns an open process page to General settings, cancels an explanation,
and removes full-system process sampling demand. Existing CPU, memory, disk and overview app-usage
lists retain their own visibility-based sampling. The preference is backed up; older documents without
it preserve the current setting. Saved process-page routes fall back to General settings when disabled.
The sandboxed WidgetKit extension samples CPU, memory, disk, and battery through `Metrics`
independently, so it remains useful when the app is not running.
AI, IP, and calendar widgets instead read a credential-free App Group snapshot. The main app
@@ -1615,3 +1615,7 @@ GitHub 项目 مشروع GitHub
国旗 العلم
地区文字 اسم المنطقة
与网络详情中的 IP 归属地共用查询结果和刷新,不额外联网。需开启公网 IP 查询。 يشارك نتيجة الموقع والتحديث مع تفاصيل الشبكة دون طلبات إضافية. يتطلب الاستعلام عن IP العام.
查看全部进程、搜索排序与结束进程,按需开启。 عرض جميع العمليات والبحث عنها وفرزها وإنهاؤها. فعّله عند الحاجة.
活动监视器 مراقب النشاط
打开系统活动监视器 فتح مراقب نشاط النظام
进程功能开启时打开 XStats 进程页,关闭时打开系统活动监视器。 يفتح صفحة عمليات XStats عند تفعيل الميزة، أو مراقب نشاط النظام عند إيقافها.
1 # Copyright (C) 2026 ysicing
1615 国旗
1616 地区文字
1617 与网络详情中的 IP 归属地共用查询结果和刷新,不额外联网。需开启公网 IP 查询。
1618 查看全部进程、搜索排序与结束进程,按需开启。
1619 活动监视器
1620 打开系统活动监视器
1621 进程功能开启时打开 XStats 进程页,关闭时打开系统活动监视器。
@@ -1615,3 +1615,7 @@ GitHub 项目 GitHub-Projekt
国旗 Flagge
地区文字 Regionsname
与网络详情中的 IP 归属地共用查询结果和刷新,不额外联网。需开启公网 IP 查询。 Nutzt dasselbe Standortergebnis und dieselbe Aktualisierung wie die Netzwerkdetails, ohne zusätzliche Anfragen. Benötigt die öffentliche IP-Abfrage.
查看全部进程、搜索排序与结束进程,按需开启。 Alle Prozesse anzeigen, suchen, sortieren und beenden. Bei Bedarf aktivieren.
活动监视器 Aktivitätsanzeige
打开系统活动监视器 macOS-Aktivitätsanzeige öffnen
进程功能开启时打开 XStats 进程页,关闭时打开系统活动监视器。 Öffnet bei aktivierter Prozessfunktion die XStats-Prozessseite, andernfalls die macOS-Aktivitätsanzeige.
1 # Copyright (C) 2026 ysicing
1615 国旗
1616 地区文字
1617 与网络详情中的 IP 归属地共用查询结果和刷新,不额外联网。需开启公网 IP 查询。
1618 查看全部进程、搜索排序与结束进程,按需开启。
1619 活动监视器
1620 打开系统活动监视器
1621 进程功能开启时打开 XStats 进程页,关闭时打开系统活动监视器。
@@ -1615,3 +1615,7 @@ GitHub 项目 Proyecto en GitHub
国旗 Bandera
地区文字 Nombre de región
与网络详情中的 IP 归属地共用查询结果和刷新,不额外联网。需开启公网 IP 查询。 Comparte el resultado de ubicación y la actualización con los detalles de red, sin solicitudes adicionales. Requiere consultar la IP pública.
查看全部进程、搜索排序与结束进程,按需开启。 Consulta todos los procesos, busca, ordena y finaliza procesos. Actívalo cuando lo necesites.
活动监视器 Monitor de Actividad
打开系统活动监视器 Abrir el Monitor de Actividad de macOS
进程功能开启时打开 XStats 进程页,关闭时打开系统活动监视器。 Abre Procesos de XStats si la función está activada; si está desactivada, abre el Monitor de Actividad de macOS.
1 # Copyright (C) 2026 ysicing
1615 国旗
1616 地区文字
1617 与网络详情中的 IP 归属地共用查询结果和刷新,不额外联网。需开启公网 IP 查询。
1618 查看全部进程、搜索排序与结束进程,按需开启。
1619 活动监视器
1620 打开系统活动监视器
1621 进程功能开启时打开 XStats 进程页,关闭时打开系统活动监视器。
@@ -1615,3 +1615,7 @@ GitHub 项目 Projet GitHub
国旗 Drapeau
地区文字 Nom de région
与网络详情中的 IP 归属地共用查询结果和刷新,不额外联网。需开启公网 IP 查询。 Partage le résultat de localisation et l’actualisation des détails réseau, sans requête supplémentaire. Nécessite la recherche d’IP publique.
查看全部进程、搜索排序与结束进程,按需开启。 Affichez tous les processus, recherchez, triez et arrêtez-les. À activer selon vos besoins.
活动监视器 Moniteur d’activité
打开系统活动监视器 Ouvrir le Moniteur d’activité de macOS
进程功能开启时打开 XStats 进程页,关闭时打开系统活动监视器。 Ouvre les processus XStats si la fonction est activée, sinon le Moniteur d’activité de macOS.
1 # Copyright (C) 2026 ysicing
1615 国旗
1616 地区文字
1617 与网络详情中的 IP 归属地共用查询结果和刷新,不额外联网。需开启公网 IP 查询。
1618 查看全部进程、搜索排序与结束进程,按需开启。
1619 活动监视器
1620 打开系统活动监视器
1621 进程功能开启时打开 XStats 进程页,关闭时打开系统活动监视器。
@@ -1615,3 +1615,7 @@ GitHub 项目 GitHub プロジェクト
国旗 国旗
地区文字 地域名
与网络详情中的 IP 归属地共用查询结果和刷新,不额外联网。需开启公网 IP 查询。 ネットワーク詳細の IP 地域と照会結果・更新を共有します。追加通信は行いません。公開 IP の照会が必要です。
查看全部进程、搜索排序与结束进程,按需开启。 全プロセスの表示、検索、並べ替え、終了ができます。必要なときに有効にしてください。
活动监视器 アクティビティモニタ
打开系统活动监视器 システムのアクティビティモニタを開く
进程功能开启时打开 XStats 进程页,关闭时打开系统活动监视器。 プロセス機能がオンの場合は XStats のプロセスページを、オフの場合はシステムのアクティビティモニタを開きます。
1 # Copyright (C) 2026 ysicing
1615 国旗
1616 地区文字
1617 与网络详情中的 IP 归属地共用查询结果和刷新,不额外联网。需开启公网 IP 查询。
1618 查看全部进程、搜索排序与结束进程,按需开启。
1619 活动监视器
1620 打开系统活动监视器
1621 进程功能开启时打开 XStats 进程页,关闭时打开系统活动监视器。
@@ -1615,3 +1615,7 @@ GitHub 项目 GitHub 프로젝트
国旗 국기
地区文字 지역 이름
与网络详情中的 IP 归属地共用查询结果和刷新,不额外联网。需开启公网 IP 查询。 네트워크 상세 정보의 IP 위치 조회 결과와 새로고침을 공유합니다. 추가 통신은 하지 않으며 공인 IP 조회가 필요합니다.
查看全部进程、搜索排序与结束进程,按需开启。 모든 프로세스를 보고 검색, 정렬하거나 종료합니다. 필요할 때 켜세요.
活动监视器 활성 상태 보기
打开系统活动监视器 시스템 활성 상태 보기 열기
进程功能开启时打开 XStats 进程页,关闭时打开系统活动监视器。 프로세스 기능이 켜져 있으면 XStats 프로세스 페이지를, 꺼져 있으면 시스템 활성 상태 보기를 엽니다.
1 # Copyright (C) 2026 ysicing
1615 国旗
1616 地区文字
1617 与网络详情中的 IP 归属地共用查询结果和刷新,不额外联网。需开启公网 IP 查询。
1618 查看全部进程、搜索排序与结束进程,按需开启。
1619 活动监视器
1620 打开系统活动监视器
1621 进程功能开启时打开 XStats 进程页,关闭时打开系统活动监视器。
@@ -1616,3 +1616,7 @@ GitHub 项目 GitHub 專案
国旗 國旗
地区文字 地區文字
与网络详情中的 IP 归属地共用查询结果和刷新,不额外联网。需开启公网 IP 查询。 與網路詳情中的 IP 歸屬地共用查詢結果和重新整理,不額外連網。需開啟公網 IP 查詢。
查看全部进程、搜索排序与结束进程,按需开启。 查看全部程序、搜尋排序與結束程序,按需啟用。
活动监视器 活動監視器
打开系统活动监视器 開啟系統活動監視器
进程功能开启时打开 XStats 进程页,关闭时打开系统活动监视器。 程序功能開啟時開啟 XStats 程序頁,關閉時開啟系統活動監視器。
1 # Copyright (c) 2026 GiantAccel, LLC
1616 国旗
1617 地区文字
1618 与网络详情中的 IP 归属地共用查询结果和刷新,不额外联网。需开启公网 IP 查询。
1619 查看全部进程、搜索排序与结束进程,按需开启。
1620 活动监视器
1621 打开系统活动监视器
1622 进程功能开启时打开 XStats 进程页,关闭时打开系统活动监视器。
@@ -1424,5 +1424,9 @@ Homebrew 将按执行时的状态清理旧版本与缓存,不自动移除依
国旗 Flag
地区文字 Region name
与网络详情中的 IP 归属地共用查询结果和刷新,不额外联网。需开启公网 IP 查询。 Shares the location result and refresh with network details; no extra requests. Requires public IP lookup.
查看全部进程、搜索排序与结束进程,按需开启。 View all processes, search, sort, and end processes. Enable when needed.
活动监视器 Activity Monitor
打开系统活动监视器 Open macOS Activity Monitor
进程功能开启时打开 XStats 进程页,关闭时打开系统活动监视器。 Opens XStats Processes when enabled, or macOS Activity Monitor when the feature is off.
"""#
}
@@ -186,7 +186,8 @@ public final class AppController: NSObject, NSApplicationDelegate {
egressWindow.show()
}
// 开发调试:--explain-process <进程名> 打开进程页并用 Apple 智能解释该进程
if let index = arguments.firstIndex(of: "--explain-process"), let name = arguments.dropFirst(index + 1).first {
if model.settings.processesEnabled, let index = arguments.firstIndex(of: "--explain-process"),
let name = arguments.dropFirst(index + 1).first {
mainWindow.show(tab: .processes)
DispatchQueue.main.asyncAfter(deadline: .now() + 4) { [weak self] in
guard let self else { return }
@@ -248,7 +249,7 @@ public final class AppController: NSObject, NSApplicationDelegate {
if mainWindow.isVisible && NSApp.isActive { mainWindow.close() } else { mainWindow.show(tab: nil) }
case .showProcesses:
menuBar.dismissPopovers()
mainWindow.show(tab: .processes)
model.openProcessMonitor()
case .toggleKeepAwake:
Task { await model.keepAwake.setActive(!model.keepAwake.isActive) }
case .purgeMemory:
@@ -308,6 +309,7 @@ public final class AppController: NSObject, NSApplicationDelegate {
_ = model.settings.menuBarLayout
_ = model.settings.menuBarStyle
_ = model.settings.networkStyle
_ = model.settings.processesEnabled
_ = model.settings.networkLocationStyle
_ = model.settings.publicIPLookup
_ = model.network.publicAddresses?.countryCode
@@ -328,6 +330,7 @@ public final class AppController: NSObject, NSApplicationDelegate {
Task { @MainActor in
guard let self else { return }
self.applyLanguageIfChanged()
if !self.model.settings.processesEnabled { self.model.explainer.dismiss() }
self.model.alerts.applySettings()
await self.model.hub.update(self.model.demand)
self.model.bluetooth.setDemand(self.model.bluetoothDemand)
@@ -18,6 +18,10 @@ public enum HotKeyAction: String, CaseIterable, Identifiable, Codable, Sendable
}
}
func title(processesEnabled: Bool) -> String {
self == .showProcesses && !processesEnabled ? tr("打开系统活动监视器") : title
}
/// Carbon 热键编号,从 1 开始
var carbonID: UInt32 { UInt32(Self.allCases.firstIndex(of: self)! + 1) }
}
@@ -206,6 +206,7 @@ private struct MainSidebar: View {
switch tab {
case .rest: model.settings.restEnabled
case .cleaner: model.settings.cleanerEnabled
case .processes: model.settings.processesEnabled
default: true
}
})
@@ -341,6 +342,18 @@ private struct PageHeader: View {
}
}
if tab == .memory { PurgeMemoryButton() }
if tab == .processes {
Button {
model.openActivityMonitor()
} label: {
Label(tr("活动监视器"), systemImage: "arrow.up.forward.app")
.lineLimit(1)
.fixedSize()
}
.buttonStyle(DSButtonStyle(kind: .secondary))
.help(tr("打开系统活动监视器"))
.accessibilityLabel(tr("打开系统活动监视器"))
}
if tab == .cleaner {
Button { model.openProjectPurgeWindow() } label: {
HStack(spacing: DS.Space.s1) {
@@ -475,11 +475,13 @@ private struct TopProcessesCard: View {
NSPasteboard.general.clearContents()
NSPasteboard.general.setString(String(process.pid), forType: .string)
}
if ProcessExplainer.isSupported {
if model.settings.processesEnabled && ProcessExplainer.isSupported {
Button(tr("用 Apple 智能解释")) { model.explainProcess(.init(process)) }
}
Divider()
Button(tr("查看全部进程")) { model.settings.panelTab = .processes }
if model.settings.processesEnabled {
Divider()
Button(tr("查看全部进程")) { model.settings.panelTab = .processes }
}
} label: {
icon
}
@@ -602,7 +602,7 @@ private struct ExplainableRow<Content: View>: View {
var body: some View {
content
.contentShape(Rectangle())
.contextMenu(isSnapshot ? nil : ContextMenu {
.contextMenu(isSnapshot || !model.settings.processesEnabled ? nil : ContextMenu {
Button(tr("用 Apple 智能解释")) { model.explainProcess(subject) }
})
}
@@ -105,6 +105,13 @@ struct GeneralSettings: View {
}
SettingsGroup(caption: tr("可选功能")) {
GroupRow(showsDivider: false) {
SettingRow(title: tr("进程"),
subtitle: tr("查看全部进程、搜索排序与结束进程,按需开启。"),
icon: "list.bullet.rectangle") {
DSToggle(isOn: $settings.processesEnabled, label: tr("进程"))
}
}
GroupRow(showsDivider: false) {
SettingRow(title: tr("番茄钟与护眼休息"),
subtitle: tr("专注计时、每日目标与多屏休息幕布。"),
@@ -258,9 +265,16 @@ struct HotKeySettings: View {
let settings = model.settings
SettingsGroup(caption: tr("全局快捷键(在任何应用中都能使用,需要包含 ⌘、⌥ 或 ⌃)")) {
ForEach(Array(HotKeyAction.allCases.enumerated()), id: \.element) { index, action in
let subtitle: String? = if model.hotKeyConflicts.contains(action) {
tr("这个快捷键已被其他应用或系统占用,请换一个")
} else if action == .showProcesses {
tr("进程功能开启时打开 XStats 进程页,关闭时打开系统活动监视器。")
} else {
nil
}
GroupRow(showsDivider: index > 0) {
SettingRow(title: action.title,
subtitle: model.hotKeyConflicts.contains(action) ? tr("这个快捷键已被其他应用或系统占用,请换一个") : nil) {
SettingRow(title: action.title(processesEnabled: settings.processesEnabled),
subtitle: subtitle) {
ShortcutRecorder(hotKey: Binding(get: { settings.hotKeys[action] },
set: { settings.hotKeys[action] = $0 }))
}
@@ -29,6 +29,7 @@ enum SnapshotRenderer {
settings.menuBarItems = [.cpu, .gpu, .memory, .network, .disk, .temperature, .battery, .aiUsage]
settings.aiUsageEnabled = true
settings.cleanerEnabled = true
settings.processesEnabled = true
// 历史页用示例数据:最近 24 小时每分钟一条,中间留一段“睡眠”空档
let historyURL = FileManager.default.temporaryDirectory.appendingPathComponent("xstats-snapshot-history.sqlite")
try? FileManager.default.removeItem(at: historyURL)
@@ -1,4 +1,5 @@
import AIUsage
import AppKit
import Foundation
import Metrics
import Observation
@@ -63,6 +64,9 @@ public final class AppModel {
public private(set) var launchAtLoginError: String?
@ObservationIgnored var openSettings: () -> Void = {}
@ObservationIgnored var openActivityMonitor: () -> Void = {
NSWorkspace.shared.open(URL(fileURLWithPath: "/System/Applications/Utilities/Activity Monitor.app"))
}
@ObservationIgnored var openCalendar: () -> Void = {}
@ObservationIgnored var restoreCalendarEntry: () -> Void = {}
@ObservationIgnored var openAIUsageSettings: () -> Void = {}
@@ -113,6 +117,7 @@ public final class AppModel {
var demand = MetricsDemand()
let tab = settings.panelTab
let window = isMainWindowVisible
let processPage = settings.processesEnabled && window && tab == .processes
let popover = openPopover
let menu = settings.menuBarItems
let thermalPopover = popover == .temperature || popover == .fan
@@ -121,7 +126,7 @@ public final class AppModel {
let liveWindow = window && [.overview, .system, .cpu, .gpu, .memory, .disk,
.network, .thermal, .battery].contains(tab)
// 进程页要读全系统进程(启动 ps),每 2 秒刷新一次足够,也更省电
demand.interval = window && tab == .processes && popover == nil ? .seconds(2)
demand.interval = processPage && popover == nil ? .seconds(2)
: liveWindow || popover != nil || isCombinedPopoverOpen ? .seconds(1) : .seconds(settings.refreshSeconds)
demand.memory = true
demand.network = true
@@ -131,9 +136,9 @@ public final class AppModel {
demand.diskDetail = showing(.disk, .disk)
demand.battery = (window && [.overview, .system, .keepAwake].contains(tab)) || keepAwake.lidClosedActive
|| menu.contains(.battery) || showing(.battery, .battery)
demand.processes = (window && [.processes, .overview, .disk].contains(tab)) || showing(.cpu, .cpu) || showing(.memory, .memory)
demand.processes = processPage || (window && [.overview, .disk].contains(tab)) || showing(.cpu, .cpu) || showing(.memory, .memory)
|| popover == .disk
demand.systemProcesses = window && tab == .processes
demand.systemProcesses = processPage
var groups = Set<TemperatureGroup>()
if window && tab == .overview { groups.formUnion([.cpu, .gpu]) }
@@ -206,8 +211,15 @@ public final class AppModel {
}
}
/// 进程快捷键始终可用;关闭内置模块时交给系统应用,不隐式开启模块。
func openProcessMonitor() {
if settings.processesEnabled { openMainWindow(.processes) }
else { openActivityMonitor() }
}
/// 用 Apple 智能解释进程:结果显示在主窗口的进程页
func explainProcess(_ subject: ProcessExplainer.Subject) {
guard settings.processesEnabled else { return }
settings.panelTab = .processes
if !isMainWindowVisible { openMainWindow(.processes) }
explainer.explain(subject)
@@ -541,6 +541,13 @@ public final class AppSettings {
if !aiUsageEnabled && panelTab == .aiUsage { panelTab = .settingsGeneral }
}
}
/// 独立进程管理器默认关闭;其他监控页仍可按需读取应用用量。
public var processesEnabled: Bool {
didSet {
defaults.set(processesEnabled, forKey: Keys.processesEnabled)
if !processesEnabled && panelTab == .processes { panelTab = .settingsGeneral }
}
}
/// 清理工具默认关闭;扫描只在打开页面后按需执行。
public var cleanerEnabled: Bool {
didSet {
@@ -578,7 +585,11 @@ public final class AppSettings {
didSet { defaults.set(fanSafetyTemperature, forKey: Keys.fanSafetyTemperature) }
}
public var panelTab: PanelTab {
didSet { defaults.set(panelTab.rawValue, forKey: Keys.panelTab) }
didSet {
// 统一拦截旧路由与外部打开请求,关闭的进程模块不能被其他入口重新打开。
if panelTab == .processes && !processesEnabled { panelTab = .settingsGeneral }
defaults.set(panelTab.rawValue, forKey: Keys.panelTab)
}
}
public var appearance: AppearanceMode {
didSet { defaults.set(appearance.rawValue, forKey: Keys.appearance) }
@@ -753,6 +764,8 @@ public final class AppSettings {
aiUsageEnabled = isAIUsageEnabled
let isCleanerEnabled = defaults.bool(forKey: Keys.cleanerEnabled)
cleanerEnabled = isCleanerEnabled
let isProcessesEnabled = defaults.bool(forKey: Keys.processesEnabled)
processesEnabled = isProcessesEnabled
aiUsageSources = defaults.stringArray(forKey: Keys.aiUsageSources)
.map { Set($0.compactMap(AIProviderID.init(rawValue:))) } ?? Set(AIProviderID.allCases)
aiUsageShowsLocalUsage = defaults.object(forKey: Keys.aiUsageShowsLocalUsage) as? Bool ?? true
@@ -772,6 +785,7 @@ public final class AppSettings {
|| (savedPanelTab == .aiUsage && !isAIUsageEnabled)
|| (savedPanelTab == .rest && !isRestEnabled)
|| (savedPanelTab == .cleaner && !isCleanerEnabled)
|| (savedPanelTab == .processes && !isProcessesEnabled)
? .settingsGeneral : savedPanelTab
appearance = defaults.string(forKey: Keys.appearance).flatMap(AppearanceMode.init(rawValue:)) ?? .system
showDockIcon = defaults.bool(forKey: Keys.showDockIcon)
@@ -867,6 +881,7 @@ public final class AppSettings {
static let refreshSeconds = "refreshSeconds"
static let aiUsageEnabled = "aiUsageEnabled"
static let cleanerEnabled = "cleanerEnabled"
static let processesEnabled = "processesEnabled"
static let aiUsageSources = "aiUsageSources"
static let aiUsageShowsLocalUsage = "aiUsageShowsLocalUsage"
static let aiUsageRefreshMinutes = "aiUsageRefreshMinutes"
@@ -59,6 +59,7 @@ public struct SettingsDocument: Codable, Equatable, Sendable {
public var autoCheckUpdates: Bool?
public var updateCheckSchedule: String?
public var cleanerEnabled: Bool?
public var processesEnabled: Bool?
public var cleanPrefersTrash: Bool?
public init() {}
@@ -111,6 +112,7 @@ extension AppSettings {
doc.autoCheckUpdates = autoCheckUpdates
doc.updateCheckSchedule = updateCheckSchedule.rawValue
doc.cleanerEnabled = cleanerEnabled
doc.processesEnabled = processesEnabled
doc.cleanPrefersTrash = cleanPrefersTrash
return doc
}
@@ -184,6 +186,7 @@ extension AppSettings {
assign(\.autoCheckUpdates, doc.autoCheckUpdates)
}
assign(\.cleanerEnabled, doc.cleanerEnabled)
assign(\.processesEnabled, doc.processesEnabled)
assign(\.cleanPrefersTrash, doc.cleanPrefersTrash)
}
@@ -0,0 +1,103 @@
// Copyright (C) 2026 ysicing
// SPDX-License-Identifier: AGPL-3.0-or-later
import AppKit
import Foundation
import Localization
import Testing
@testable import XStatsUI
@MainActor
struct ProcessFeatureTests {
private func defaults() -> UserDefaults {
UserDefaults(suiteName: "ProcessFeatureTests.\(UUID())")!
}
@Test func defaultsOffAndRejectsSavedOrDirectProcessRoutes() {
let defaults = defaults()
defaults.set("processes", forKey: "panelTab")
let settings = AppSettings(defaults: defaults)
#expect(!settings.processesEnabled)
#expect(settings.panelTab == .settingsGeneral)
settings.panelTab = .processes
#expect(settings.panelTab == .settingsGeneral)
#expect(defaults.string(forKey: "panelTab") == "settingsGeneral")
settings.processesEnabled = true
settings.panelTab = .processes
let restored = AppSettings(defaults: defaults)
#expect(restored.processesEnabled && restored.panelTab == .processes)
settings.processesEnabled = false
#expect(settings.panelTab == .settingsGeneral)
#expect(!AppSettings(defaults: defaults).processesEnabled)
}
@Test func disablingProcessPageStopsSystemSamplingWithoutDisablingCPUDetails() {
let settings = AppSettings(defaults: defaults())
let model = AppModel(settings: settings, historyURL: nil, aiUsageProviders: [])
model.isMainWindowVisible = true
settings.processesEnabled = true
settings.panelTab = .processes
#expect(model.demand.processes && model.demand.systemProcesses)
settings.processesEnabled = false
#expect(!model.demand.processes && !model.demand.systemProcesses)
model.openPopover = .cpu
#expect(model.demand.processes && !model.demand.systemProcesses)
model.openPopover = nil
settings.processesEnabled = true
settings.panelTab = .processes
model.isMainWindowVisible = false
#expect(!model.demand.processes && !model.demand.systemProcesses)
}
@Test func shortcutKeepsItsBindingAndRoutesToTheEnabledDestination() {
let settings = AppSettings(defaults: defaults())
let model = AppModel(settings: settings, historyURL: nil, aiUsageProviders: [])
var systemLaunches = 0
var pages: [PanelTab?] = []
model.openActivityMonitor = { systemLaunches += 1 }
model.openMainWindow = { pages.append($0) }
let key = HotKey(keyCode: 35, modifiers: [.command, .option], key: "p")
settings.hotKeys[.showProcesses] = key
let initialPage = settings.panelTab
model.openProcessMonitor()
#expect(systemLaunches == 1 && pages.isEmpty)
#expect(!settings.processesEnabled && settings.panelTab == initialPage)
#expect(HotKeyAction.showProcesses.title(processesEnabled: false) == tr("打开系统活动监视器"))
settings.processesEnabled = true
model.openProcessMonitor()
#expect(systemLaunches == 1 && pages == [.processes])
#expect(HotKeyAction.showProcesses.title(processesEnabled: true) == tr("打开进程页"))
settings.processesEnabled = false
model.openProcessMonitor()
#expect(systemLaunches == 2 && pages == [.processes])
#expect(settings.hotKeys[.showProcesses] == key)
}
@Test func disabledFeatureDoesNotStartAnExplanationOrOpenItsPage() {
let settings = AppSettings(defaults: defaults())
let model = AppModel(settings: settings, historyURL: nil, aiUsageProviders: [])
var opened = false
model.openMainWindow = { _ in opened = true }
model.explainProcess(.init(pid: 42, name: "fixture", displayName: "fixture", executablePath: nil,
appBundlePath: nil, cpu: nil, memory: nil, networkRate: nil))
#expect(!opened && settings.panelTab != .processes)
#expect(model.explainer.subject == nil && model.explainer.phase == .idle)
}
@Test func backupRoundTripsPreferenceAndOldDocumentsPreserveIt() throws {
let source = AppSettings(defaults: defaults())
source.processesEnabled = true
let data = try JSONEncoder().encode(source.exportDocument())
let decoded = try JSONDecoder().decode(SettingsDocument.self, from: data)
let target = AppSettings(defaults: defaults())
target.apply(decoded)
#expect(target.processesEnabled)
target.apply(try JSONDecoder().decode(SettingsDocument.self, from: Data("{}".utf8)))
#expect(target.processesEnabled)
var disabled = SettingsDocument()
disabled.processesEnabled = false
target.panelTab = .processes
target.apply(disabled)
#expect(!target.processesEnabled && target.panelTab == .settingsGeneral)
}
}
@@ -451,6 +451,7 @@ private func isolatedDefaults() -> UserDefaults {
@Test func processesPageRefreshesEveryTwoSeconds() {
let model = model()
model.settings.processesEnabled = true
model.isMainWindowVisible = true
model.settings.panelTab = .processes
#expect(model.demand.interval == .seconds(2))
+1
View File
@@ -55,6 +55,7 @@ brew install --cask xstats
**Optional modules**, off by default:
- Processes: view all processes, search, sort, group by app, and end processes.
- Menu bar calendar: Chinese lunar calendar, public holidays and make-up workdays, almanac, plus your calendar events and reminders.
- Pomodoro and eye-rest breaks: a break screen on every display that you can skip, pause or shrink to a mini HUD at any time.
- AI usage and quotas: local Codex / Claude Code token usage and subscription quota checks.
+1
View File
@@ -55,6 +55,7 @@ brew install --cask xstats
**オプションモジュール**(初期状態はオフ):
- プロセス:全プロセスの表示、検索、並べ替え、アプリごとのグループ化、終了に対応します。
- メニューバーカレンダー:旧暦、中国の祝日と振替出勤日、暦注に加え、カレンダーの予定とリマインダーも表示します。
- ポモドーロと目の休憩:すべてのディスプレイに休憩画面を表示し、いつでもスキップ、一時停止、ミニ HUD への縮小ができます。
- AI の使用量と利用枠:Codex / Claude Code のローカルのトークン使用量とサブスクリプション利用枠を確認します。
+1
View File
@@ -55,6 +55,7 @@ brew install --cask xstats
**선택 모듈**(기본값 꺼짐):
- 프로세스: 모든 프로세스를 보고 검색, 정렬, 앱별 그룹화 및 종료를 할 수 있습니다.
- 메뉴 막대 달력: 음력, 중국 공휴일과 대체 근무일, 역서에 더해 캘린더 일정과 미리 알림도 표시합니다.
- 뽀모도로와 눈 휴식: 모든 디스플레이에 휴식 화면을 띄우며 언제든 건너뛰거나 일시 정지하거나 미니 HUD로 줄일 수 있습니다.
- AI 사용량 및 한도: Codex / Claude Code의 로컬 토큰 사용량과 구독 한도를 확인합니다.
+1
View File
@@ -55,6 +55,7 @@ brew install --cask xstats
**可选模块**,默认关闭,按需打开:
- 进程:查看全部进程,支持搜索、排序、按应用分组与结束进程。
- 菜单栏日历:农历、节假日与调休、黄历,可显示系统日程和提醒事项。
- 番茄钟与护眼休息:多屏休息幕布,可随时跳过、暂停或收起到迷你 HUD。
- AI 用量与额度:统计 Codex / Claude Code 本机 Token 用量,查询订阅额度。