feat(processes): 改为可选模块并支持活动监视器回退
This commit is contained in:
1 parent
dd099950b1
commit
e19ea53771
25 files changed
+229
-12
No files matched your search
@@ -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 عند تفعيل الميزة، أو مراقب نشاط النظام عند إيقافها.
|
||||
|
@@ -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.
|
||||
|
@@ -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.
|
||||
|
@@ -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.
|
||||
|
@@ -1615,3 +1615,7 @@ GitHub 项目 GitHub プロジェクト
|
||||
国旗 国旗
|
||||
地区文字 地域名
|
||||
与网络详情中的 IP 归属地共用查询结果和刷新,不额外联网。需开启公网 IP 查询。 ネットワーク詳細の IP 地域と照会結果・更新を共有します。追加通信は行いません。公開 IP の照会が必要です。
|
||||
查看全部进程、搜索排序与结束进程,按需开启。 全プロセスの表示、検索、並べ替え、終了ができます。必要なときに有効にしてください。
|
||||
活动监视器 アクティビティモニタ
|
||||
打开系统活动监视器 システムのアクティビティモニタを開く
|
||||
进程功能开启时打开 XStats 进程页,关闭时打开系统活动监视器。 プロセス機能がオンの場合は XStats のプロセスページを、オフの場合はシステムのアクティビティモニタを開きます。
|
||||
|
@@ -1615,3 +1615,7 @@ GitHub 项目 GitHub 프로젝트
|
||||
国旗 국기
|
||||
地区文字 지역 이름
|
||||
与网络详情中的 IP 归属地共用查询结果和刷新,不额外联网。需开启公网 IP 查询。 네트워크 상세 정보의 IP 위치 조회 결과와 새로고침을 공유합니다. 추가 통신은 하지 않으며 공인 IP 조회가 필요합니다.
|
||||
查看全部进程、搜索排序与结束进程,按需开启。 모든 프로세스를 보고 검색, 정렬하거나 종료합니다. 필요할 때 켜세요.
|
||||
活动监视器 활성 상태 보기
|
||||
打开系统活动监视器 시스템 활성 상태 보기 열기
|
||||
进程功能开启时打开 XStats 进程页,关闭时打开系统活动监视器。 프로세스 기능이 켜져 있으면 XStats 프로세스 페이지를, 꺼져 있으면 시스템 활성 상태 보기를 엽니다.
|
||||
|
@@ -1616,3 +1616,7 @@ GitHub 项目 GitHub 專案
|
||||
国旗 國旗
|
||||
地区文字 地區文字
|
||||
与网络详情中的 IP 归属地共用查询结果和刷新,不额外联网。需开启公网 IP 查询。 與網路詳情中的 IP 歸屬地共用查詢結果和重新整理,不額外連網。需開啟公網 IP 查詢。
|
||||
查看全部进程、搜索排序与结束进程,按需开启。 查看全部程序、搜尋排序與結束程序,按需啟用。
|
||||
活动监视器 活動監視器
|
||||
打开系统活动监视器 開啟系統活動監視器
|
||||
进程功能开启时打开 XStats 进程页,关闭时打开系统活动监视器。 程序功能開啟時開啟 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))
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -55,6 +55,7 @@ brew install --cask xstats
|
||||
|
||||
**オプションモジュール**(初期状態はオフ):
|
||||
|
||||
- プロセス:全プロセスの表示、検索、並べ替え、アプリごとのグループ化、終了に対応します。
|
||||
- メニューバーカレンダー:旧暦、中国の祝日と振替出勤日、暦注に加え、カレンダーの予定とリマインダーも表示します。
|
||||
- ポモドーロと目の休憩:すべてのディスプレイに休憩画面を表示し、いつでもスキップ、一時停止、ミニ HUD への縮小ができます。
|
||||
- AI の使用量と利用枠:Codex / Claude Code のローカルのトークン使用量とサブスクリプション利用枠を確認します。
|
||||
|
||||
@@ -55,6 +55,7 @@ brew install --cask xstats
|
||||
|
||||
**선택 모듈**(기본값 꺼짐):
|
||||
|
||||
- 프로세스: 모든 프로세스를 보고 검색, 정렬, 앱별 그룹화 및 종료를 할 수 있습니다.
|
||||
- 메뉴 막대 달력: 음력, 중국 공휴일과 대체 근무일, 역서에 더해 캘린더 일정과 미리 알림도 표시합니다.
|
||||
- 뽀모도로와 눈 휴식: 모든 디스플레이에 휴식 화면을 띄우며 언제든 건너뛰거나 일시 정지하거나 미니 HUD로 줄일 수 있습니다.
|
||||
- AI 사용량 및 한도: Codex / Claude Code의 로컬 토큰 사용량과 구독 한도를 확인합니다.
|
||||
|
||||
Reference in new issue
Block a user