Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 34 additions & 4 deletions Sources/ProcessBarMonitor/MonitorViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,16 @@ final class MonitorViewModel: ObservableObject {
}
}
}
@Published var displayTemplate: MenuBarDisplayTemplate {
didSet {
settings.set(displayTemplate.rawValue, forKey: Keys.displayTemplate)
}
}
@Published var moduleVisibility: PopupModuleVisibility {
didSet {
settings.set(moduleVisibility.rawValue, forKey: Keys.moduleVisibility)
}
}

private var currentSummaryRefreshInterval: UInt64 = 2_000_000_000
private var currentProcessRefreshInterval: TimeInterval = 10
Expand All @@ -82,6 +92,8 @@ final class MonitorViewModel: ObservableObject {
static let temperatureMode = "temperatureMode"
static let menuBarDisplayMode = "menuBarDisplayMode"
static let refreshRatePreset = "refreshRatePreset"
static let displayTemplate = "displayTemplate"
static let moduleVisibility = "moduleVisibility"
}

init(defaults: UserDefaults = .standard) {
Expand Down Expand Up @@ -111,19 +123,37 @@ final class MonitorViewModel: ObservableObject {
} else {
refreshRatePreset = .balanced
}

if let rawDisplayTemplate = settings.string(forKey: Keys.displayTemplate),
let parsedTemplate = MenuBarDisplayTemplate(savedValue: rawDisplayTemplate) {
displayTemplate = parsedTemplate
} else {
displayTemplate = .standard
}

if let savedRaw = settings.int(forKey: Keys.moduleVisibility) {
let visibility = PopupModuleVisibility(rawValue: savedRaw)
if visibility.isEmpty || (visibility.rawValue & ~PopupModuleVisibility.all.rawValue) != 0 {
moduleVisibility = .defaultVisibility
} else {
moduleVisibility = visibility
}
} else {
moduleVisibility = .defaultVisibility
}
}

var menuBarTitle: String {
let cpu = String(format: "%.0f%%", summary.cpuPercent)
let mem = String(format: "%.0f%%", summary.memoryPressurePercent)
let temp = summary.cpuTemperatureC.map { String(format: "%.0f°", $0) } ?? "--°"

switch menuBarDisplayMode {
case .compact:
switch displayTemplate {
case .minimal:
return L10n.format("menu_bar_title.compact", cpu, mem, temp)
case .labeled:
case .standard:
return L10n.format("menu_bar_title.labeled", cpu, mem, temp)
case .temperatureFirst:
case .detailed:
return L10n.format("menu_bar_title.temperature_first", temp, cpu, mem)
}
}
Expand Down
10 changes: 10 additions & 0 deletions Sources/ProcessBarMonitor/Resources/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,13 @@
"refresh_rate.power_saving" = "Power Saving";
"refresh_rate.balanced" = "Balanced";
"refresh_rate.real_time" = "Real-time";
"picker.display_template" = "Display Template";
"template.minimal" = "Minimal";
"template.standard" = "Standard";
"template.detailed" = "Detailed";
"settings.section.modules" = "Popup Modules";
"toggle.sparklines" = "Sparklines";
"toggle.top_cpu" = "Top CPU Apps";
"toggle.top_memory" = "Top Memory Apps";
"toggle.temperature_hint" = "Temperature Hint";
"toggle.diagnostics" = "Diagnostics";
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,13 @@
"refresh_rate.power_saving" = "省电";
"refresh_rate.balanced" = "平衡";
"refresh_rate.real_time" = "实时";
"picker.display_template" = "显示模板";
"template.minimal" = "极简";
"template.standard" = "标准";
"template.detailed" = "详情";
"settings.section.modules" = "弹窗模块";
"toggle.sparklines" = "趋势图";
"toggle.top_cpu" = "CPU 占用最高应用";
"toggle.top_memory" = "内存占用最高应用";
"toggle.temperature_hint" = "温度提示";
"toggle.diagnostics" = "诊断信息";
31 changes: 31 additions & 0 deletions Sources/ProcessBarMonitor/SettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,41 @@ struct SettingsView: View {
Text(preset.title).tag(preset)
}
}

Picker(L10n.string("picker.display_template"), selection: $viewModel.displayTemplate) {
ForEach(MenuBarDisplayTemplate.allCases) { template in
Text(template.title).tag(template)
}
}
} header: {
Text(L10n.string("settings.section.display"))
}

Section {
Toggle(L10n.string("toggle.sparklines"), isOn: Binding(
get: { viewModel.moduleVisibility.contains(.sparklines) },
set: { viewModel.moduleVisibility = $0 ? viewModel.moduleVisibility.union(.sparklines) : viewModel.moduleVisibility.subtracting(.sparklines) }
))
Toggle(L10n.string("toggle.top_cpu"), isOn: Binding(
get: { viewModel.moduleVisibility.contains(.topCPU) },
set: { viewModel.moduleVisibility = $0 ? viewModel.moduleVisibility.union(.topCPU) : viewModel.moduleVisibility.subtracting(.topCPU) }
))
Toggle(L10n.string("toggle.top_memory"), isOn: Binding(
get: { viewModel.moduleVisibility.contains(.topMemory) },
set: { viewModel.moduleVisibility = $0 ? viewModel.moduleVisibility.union(.topMemory) : viewModel.moduleVisibility.subtracting(.topMemory) }
))
Toggle(L10n.string("toggle.temperature_hint"), isOn: Binding(
get: { viewModel.moduleVisibility.contains(.temperatureHint) },
set: { viewModel.moduleVisibility = $0 ? viewModel.moduleVisibility.union(.temperatureHint) : viewModel.moduleVisibility.subtracting(.temperatureHint) }
))
Toggle(L10n.string("toggle.diagnostics"), isOn: Binding(
get: { viewModel.moduleVisibility.contains(.diagnostics) },
set: { viewModel.moduleVisibility = $0 ? viewModel.moduleVisibility.union(.diagnostics) : viewModel.moduleVisibility.subtracting(.diagnostics) }
))
} header: {
Text(L10n.string("settings.section.modules"))
}

Section {
Toggle(L10n.string("toggle.launch_at_login"), isOn: Binding(
get: { viewModel.launchAtLogin.isEnabled },
Expand Down
49 changes: 49 additions & 0 deletions Sources/ProcessBarMonitor/SystemModels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,55 @@ enum MenuBarDisplayMode: String, CaseIterable, Identifiable {
}
}

enum MenuBarDisplayTemplate: String, CaseIterable, Identifiable {
case minimal
case standard
case detailed

var id: String { rawValue }

var title: String {
switch self {
case .minimal:
return L10n.string("template.minimal")
case .standard:
return L10n.string("template.standard")
case .detailed:
return L10n.string("template.detailed")
}
}

init?(savedValue: String) {
if let template = MenuBarDisplayTemplate(rawValue: savedValue) {
self = template
return
}

switch savedValue {
case "Minimal": self = .minimal
case "Standard": self = .standard
case "Detailed": self = .detailed
default: return nil
}
}
}

struct PopupModuleVisibility: OptionSet, Equatable {
let rawValue: Int

static let sparklines = PopupModuleVisibility(rawValue: 1 << 0)
static let topCPU = PopupModuleVisibility(rawValue: 1 << 1)
static let topMemory = PopupModuleVisibility(rawValue: 1 << 2)
static let temperatureHint = PopupModuleVisibility(rawValue: 1 << 3)
static let diagnostics = PopupModuleVisibility(rawValue: 1 << 4)

static let all: PopupModuleVisibility = [.sparklines, .topCPU, .topMemory, .temperatureHint, .diagnostics]

static var defaultVisibility: PopupModuleVisibility {
[.sparklines, .topCPU, .topMemory]
}
}

struct ProcessChildStat: Identifiable, Hashable {
let pid: Int
let command: String
Expand Down
166 changes: 88 additions & 78 deletions Sources/ProcessBarMonitor/Views.swift
Original file line number Diff line number Diff line change
Expand Up @@ -179,49 +179,53 @@ struct MenuBarContentView: View {
SummaryCardView(title: L10n.string("summary.thermal"), value: viewModel.thermalText(viewModel.summary.thermalState), accent: .pink)
}

VStack(alignment: .leading, spacing: 4) {
Text(healthLine)
.font(.subheadline.weight(.medium))
Text(viewModel.summary.architectureNote)
.font(.caption)
.foregroundStyle(.secondary)
.lineLimit(2)
if viewModel.summary.cpuTemperatureC == nil, let hint = viewModel.summary.temperatureHint {
Text(hint)
.font(.caption.weight(.medium))
.foregroundStyle(.orange)
if viewModel.moduleVisibility.contains(.temperatureHint) {
VStack(alignment: .leading, spacing: 4) {
Text(healthLine)
.font(.subheadline.weight(.medium))
Text(viewModel.summary.architectureNote)
.font(.caption)
.foregroundStyle(.secondary)
.lineLimit(2)
if viewModel.summary.cpuTemperatureC == nil, let hint = viewModel.summary.temperatureHint {
Text(hint)
.font(.caption.weight(.medium))
.foregroundStyle(.orange)
.lineLimit(2)
}
}
}

HStack(spacing: 8) {
SparklineView(
title: L10n.string("trend.cpu"),
points: viewModel.cpuHistory,
color: .primary,
valueText: String(format: "%.1f%%", viewModel.summary.cpuPercent),
fixedMax: 100,
warningThreshold: 60,
criticalThreshold: 85
)
SparklineView(
title: L10n.string("trend.ram"),
points: viewModel.memoryHistory,
color: .blue,
valueText: String(format: "%.0f%%", viewModel.summary.memoryPressurePercent),
fixedMax: 100,
warningThreshold: 75,
criticalThreshold: 90
)
SparklineView(
title: L10n.string("trend.temp"),
points: viewModel.temperatureHistory,
color: .green,
valueText: viewModel.summary.cpuTemperatureC.map { String(format: "%.1f°C", $0) } ?? "--",
fixedMax: nil,
warningThreshold: 70,
criticalThreshold: 85
)
if viewModel.moduleVisibility.contains(.sparklines) {
HStack(spacing: 8) {
SparklineView(
title: L10n.string("trend.cpu"),
points: viewModel.cpuHistory,
color: .primary,
valueText: String(format: "%.1f%%", viewModel.summary.cpuPercent),
fixedMax: 100,
warningThreshold: 60,
criticalThreshold: 85
)
SparklineView(
title: L10n.string("trend.ram"),
points: viewModel.memoryHistory,
color: .blue,
valueText: String(format: "%.0f%%", viewModel.summary.memoryPressurePercent),
fixedMax: 100,
warningThreshold: 75,
criticalThreshold: 90
)
SparklineView(
title: L10n.string("trend.temp"),
points: viewModel.temperatureHistory,
color: .green,
valueText: viewModel.summary.cpuTemperatureC.map { String(format: "%.1f°C", $0) } ?? "--",
fixedMax: nil,
warningThreshold: 70,
criticalThreshold: 85
)
}
}

Divider()
Expand Down Expand Up @@ -277,58 +281,64 @@ struct MenuBarContentView: View {
}
}

Divider()
Divider()

VStack(alignment: .leading, spacing: 4) {
Text(L10n.string("section.top_apps_cpu"))
.font(.headline)
Text(L10n.string("section.top_apps_cpu_desc"))
.font(.caption)
.foregroundStyle(.secondary)
processHeader
ForEach(viewModel.topCPUProcesses) { process in
ProcessRowView(
process: process,
expanded: expandedCPUApps.contains(process.id),
onToggle: {
if expandedCPUApps.contains(process.id) { expandedCPUApps.remove(process.id) }
else { expandedCPUApps.insert(process.id) }
}
)
if viewModel.moduleVisibility.contains(.topCPU) {
VStack(alignment: .leading, spacing: 8) {
Text(L10n.string("section.top_apps_cpu"))
.font(.headline)
Text(L10n.string("section.top_apps_cpu_desc"))
.font(.caption)
.foregroundStyle(.secondary)
processHeader
ForEach(viewModel.topCPUProcesses) { process in
ProcessRowView(
process: process,
expanded: expandedCPUApps.contains(process.id),
onToggle: {
if expandedCPUApps.contains(process.id) { expandedCPUApps.remove(process.id) }
else { expandedCPUApps.insert(process.id) }
}
)
}
}
}

Divider()
Divider()
}

VStack(alignment: .leading, spacing: 4) {
Text(L10n.string("section.top_apps_memory"))
.font(.headline)
Text(L10n.string("section.top_apps_memory_desc"))
.font(.caption)
.foregroundStyle(.secondary)
processHeader
ForEach(viewModel.topMemoryProcesses) { process in
ProcessRowView(
process: process,
expanded: expandedMemoryApps.contains(process.id),
onToggle: {
if expandedMemoryApps.contains(process.id) { expandedMemoryApps.remove(process.id) }
else { expandedMemoryApps.insert(process.id) }
}
)
if viewModel.moduleVisibility.contains(.topMemory) {
VStack(alignment: .leading, spacing: 8) {
Text(L10n.string("section.top_apps_memory"))
.font(.headline)
Text(L10n.string("section.top_apps_memory_desc"))
.font(.caption)
.foregroundStyle(.secondary)
processHeader
ForEach(viewModel.topMemoryProcesses) { process in
ProcessRowView(
process: process,
expanded: expandedMemoryApps.contains(process.id),
onToggle: {
if expandedMemoryApps.contains(process.id) { expandedMemoryApps.remove(process.id) }
else { expandedMemoryApps.insert(process.id) }
}
)
}
}
}

Divider()
Divider()
}

HStack {
Button(action: { Task { await viewModel.refresh(forceProcesses: true) } }) {
Text(viewModel.isRefreshing ? L10n.string("button.refreshing") : L10n.string("button.refresh_now"))
}
.disabled(viewModel.isRefreshing)

Button(L10n.string("button.copy_diagnostics")) {
viewModel.copyDiagnosticsToPasteboard()
if viewModel.moduleVisibility.contains(.diagnostics) {
Button(L10n.string("button.copy_diagnostics")) {
viewModel.copyDiagnosticsToPasteboard()
}
}

Button(L10n.string("button.quit")) {
Expand Down
Loading