Improve process table columns and memory display
This commit is contained in:
@@ -9,6 +9,7 @@ Validated with automated tests, debug and release builds, packaged-app code-sign
|
||||
- Live process list with CPU, memory, disk I/O, and network usage
|
||||
- Cached native application icons throughout process groups, app history, startup apps, users, services, properties, and dependency views
|
||||
- Search and sortable process columns, including live Disk and Network rates
|
||||
- Drag-resizable, persisted Processes columns with memory hover details showing both resident bytes and physical-memory percentage
|
||||
- Column sorting automatically flattens process categories for a true global order; the header menu can restore Group by type
|
||||
- Expandable application groups plus an advanced Details table with persisted optional columns for threads, open handles, architecture, priority/nice, parent PID, CPU time, and other diagnostics
|
||||
- Collapsible Windows-style Apps, Background processes, and macOS processes categories
|
||||
|
||||
@@ -15,6 +15,40 @@ private enum ResourceDisplayMode: String, CaseIterable {
|
||||
case percentages = "Percentages"
|
||||
}
|
||||
|
||||
private let processNumericTextInset: CGFloat = 7
|
||||
|
||||
private enum ProcessColumn: String, CaseIterable, Codable {
|
||||
case name, pid, status, cpu, memory, disk, network
|
||||
|
||||
var defaultWidth: CGFloat {
|
||||
switch self {
|
||||
case .name: 300
|
||||
case .pid: 70
|
||||
case .status: 80
|
||||
case .cpu: 86
|
||||
case .memory: 110
|
||||
case .disk, .network: 88
|
||||
}
|
||||
}
|
||||
|
||||
var minimumWidth: CGFloat {
|
||||
switch self {
|
||||
case .name: 160
|
||||
case .pid: 54
|
||||
case .status: 68
|
||||
case .cpu: 64
|
||||
case .memory: 86
|
||||
case .disk, .network: 76
|
||||
}
|
||||
}
|
||||
|
||||
var maximumWidth: CGFloat { self == .name ? 720 : 260 }
|
||||
|
||||
static var defaultWidths: [ProcessColumn: CGFloat] {
|
||||
Dictionary(uniqueKeysWithValues: allCases.map { ($0, $0.defaultWidth) })
|
||||
}
|
||||
}
|
||||
|
||||
private enum ProcessCategory: String, CaseIterable, Identifiable {
|
||||
case apps = "Apps"
|
||||
case background = "Background processes"
|
||||
@@ -35,6 +69,10 @@ struct ProcessesView: View {
|
||||
@AppStorage("processSortAscending") private var ascending = false
|
||||
@AppStorage("processGroupByType") private var groupByType = true
|
||||
@AppStorage("processResourceDisplayMode") private var resourceDisplayMode: ResourceDisplayMode = .values
|
||||
@AppStorage("processColumnWidths") private var columnWidthsStorage = ""
|
||||
@State private var columnWidths = ProcessColumn.defaultWidths
|
||||
@State private var groupsFrozenForResize: [ProcessGroup]?
|
||||
@State private var isResizingColumn = false
|
||||
@State private var terminationRequest: TerminationRequest?
|
||||
@State private var groupTerminationRequest: ProcessGroup?
|
||||
@State private var inspectedProcess: ProcessRecord?
|
||||
@@ -65,6 +103,8 @@ struct ProcessesView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private var displayedGroups: [ProcessGroup] { groupsFrozenForResize ?? grouped }
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
PageHeader(title: "Processes", subtitle: "\(monitor.processes.count) running processes") {
|
||||
@@ -74,17 +114,31 @@ struct ProcessesView: View {
|
||||
}
|
||||
}
|
||||
ResourceSummaryBar(snapshot: monitor.snapshot)
|
||||
ScrollView(.horizontal) {
|
||||
VStack(spacing: 0) {
|
||||
ProcessTableHeader(
|
||||
sort: $sort,
|
||||
ascending: $ascending,
|
||||
groupByType: $groupByType,
|
||||
resourceDisplayMode: $resourceDisplayMode
|
||||
resourceDisplayMode: $resourceDisplayMode,
|
||||
columnWidths: $columnWidths,
|
||||
onResizeStarted: {
|
||||
isResizingColumn = true
|
||||
if groupsFrozenForResize == nil { groupsFrozenForResize = grouped }
|
||||
},
|
||||
onResizeEnded: {
|
||||
persistColumnWidths()
|
||||
isResizingColumn = false
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.18) {
|
||||
if !isResizingColumn { groupsFrozenForResize = nil }
|
||||
}
|
||||
}
|
||||
)
|
||||
ScrollView {
|
||||
LazyVStack(spacing: 0) {
|
||||
if groupByType {
|
||||
ForEach(ProcessCategory.allCases) { category in
|
||||
let categoryGroups = grouped.filter { $0.category == category }
|
||||
let categoryGroups = displayedGroups.filter { $0.category == category }
|
||||
if !categoryGroups.isEmpty {
|
||||
ProcessCategoryHeader(
|
||||
category: category,
|
||||
@@ -106,13 +160,16 @@ struct ProcessesView: View {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ForEach(grouped) { group in
|
||||
ForEach(displayedGroups) { group in
|
||||
processGroup(group)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.leading, 10)
|
||||
}
|
||||
}
|
||||
.frame(width: processTableWidth, alignment: .leading)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
Divider()
|
||||
HStack {
|
||||
UpdateStatus()
|
||||
@@ -162,6 +219,29 @@ struct ProcessesView: View {
|
||||
.sheet(item: $inspectedProcess) { process in
|
||||
ProcessPropertiesView(process: process)
|
||||
}
|
||||
.onAppear(perform: restoreColumnWidths)
|
||||
}
|
||||
|
||||
private var processTableWidth: CGFloat {
|
||||
ProcessColumn.allCases.reduce(0) { $0 + (columnWidths[$1] ?? $1.defaultWidth) }
|
||||
+ CGFloat(ProcessColumn.allCases.count - 1) * 12
|
||||
+ 32
|
||||
}
|
||||
|
||||
private func restoreColumnWidths() {
|
||||
guard let data = columnWidthsStorage.data(using: .utf8),
|
||||
let stored = try? JSONDecoder().decode([String: Double].self, from: data) else { return }
|
||||
for column in ProcessColumn.allCases {
|
||||
guard let value = stored[column.rawValue] else { continue }
|
||||
columnWidths[column] = min(column.maximumWidth, max(column.minimumWidth, CGFloat(value)))
|
||||
}
|
||||
}
|
||||
|
||||
private func persistColumnWidths() {
|
||||
let stored = Dictionary(uniqueKeysWithValues: columnWidths.map { ($0.key.rawValue, Double($0.value)) })
|
||||
guard let data = try? JSONEncoder().encode(stored),
|
||||
let value = String(data: data, encoding: .utf8) else { return }
|
||||
columnWidthsStorage = value
|
||||
}
|
||||
|
||||
private func expansionBinding(for group: ProcessGroup) -> Binding<Bool> {
|
||||
@@ -178,27 +258,16 @@ struct ProcessesView: View {
|
||||
private func processGroup(_ group: ProcessGroup) -> some View {
|
||||
if group.processes.count > 1 {
|
||||
VStack(spacing: 0) {
|
||||
HStack(spacing: 0) {
|
||||
Button {
|
||||
let binding = expansionBinding(for: group)
|
||||
binding.wrappedValue.toggle()
|
||||
} label: {
|
||||
Image(systemName: "chevron.right")
|
||||
.font(.caption.weight(.semibold))
|
||||
.rotationEffect(.degrees(expansionBinding(for: group).wrappedValue ? 90 : 0))
|
||||
.frame(width: 16, height: 48)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.foregroundStyle(.secondary)
|
||||
.help(expansionBinding(for: group).wrappedValue ? "Collapse \(group.name)" : "Expand \(group.name)")
|
||||
.accessibilityLabel(expansionBinding(for: group).wrappedValue ? "Collapse \(group.name)" : "Expand \(group.name)")
|
||||
|
||||
ProcessGroupRow(
|
||||
group: group,
|
||||
selected: selectedGroupID == group.id,
|
||||
displayMode: resourceDisplayMode,
|
||||
leadingPadding: 0
|
||||
columnWidths: columnWidths,
|
||||
isExpanded: expansionBinding(for: group),
|
||||
onToggleExpansion: {
|
||||
let binding = expansionBinding(for: group)
|
||||
binding.wrappedValue.toggle()
|
||||
}
|
||||
)
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
@@ -213,7 +282,6 @@ struct ProcessesView: View {
|
||||
onShowProperties: { inspectedProcess = $0 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if expansionBinding(for: group).wrappedValue {
|
||||
ForEach(group.processes.sorted { $0.pid < $1.pid }) { process in
|
||||
@@ -227,7 +295,13 @@ struct ProcessesView: View {
|
||||
}
|
||||
|
||||
private func processRow(_ process: ProcessRecord, isChild: Bool) -> some View {
|
||||
ProcessRow(process: process, selected: selectedPID == process.pid, isChild: isChild, displayMode: resourceDisplayMode)
|
||||
ProcessRow(
|
||||
process: process,
|
||||
selected: selectedPID == process.pid,
|
||||
isChild: isChild,
|
||||
displayMode: resourceDisplayMode,
|
||||
columnWidths: columnWidths
|
||||
)
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
selectedPID = process.pid
|
||||
@@ -362,16 +436,24 @@ private struct ProcessTableHeader: View {
|
||||
@Binding var ascending: Bool
|
||||
@Binding var groupByType: Bool
|
||||
@Binding var resourceDisplayMode: ResourceDisplayMode
|
||||
@Binding var columnWidths: [ProcessColumn: CGFloat]
|
||||
let onResizeStarted: () -> Void
|
||||
let onResizeEnded: () -> Void
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 12) {
|
||||
header("Name", field: .name).frame(maxWidth: .infinity, alignment: .leading)
|
||||
header("PID", field: .pid).frame(width: 70, alignment: .trailing)
|
||||
Text("Status").frame(width: 80, alignment: .leading)
|
||||
header("CPU", field: .cpu).frame(width: 86, alignment: .trailing)
|
||||
header("Memory", field: .memory).frame(width: 110, alignment: .trailing)
|
||||
header("Disk", field: .disk).frame(width: 88, alignment: .trailing)
|
||||
header("Network", field: .network).frame(width: 88, alignment: .trailing)
|
||||
resizable(.name, alignment: .leading) { header("Name", field: .name, column: .name) }
|
||||
resizable(.pid, alignment: .leading) { header("PID", field: .pid, column: .pid) }
|
||||
resizable(.status, alignment: .leading) {
|
||||
Text("Status")
|
||||
.lineLimit(1)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(.trailing, processNumericTextInset)
|
||||
}
|
||||
resizable(.cpu, alignment: .leading) { header("CPU", field: .cpu, column: .cpu) }
|
||||
resizable(.memory, alignment: .leading) { header("Memory", field: .memory, column: .memory) }
|
||||
resizable(.disk, alignment: .leading) { header("Disk", field: .disk, column: .disk) }
|
||||
resizable(.network, alignment: .leading) { header("Network", field: .network, column: .network) }
|
||||
}
|
||||
.font(.caption.weight(.medium))
|
||||
.foregroundStyle(.secondary)
|
||||
@@ -394,17 +476,100 @@ private struct ProcessTableHeader: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func header(_ title: String, field: ProcessSort) -> some View {
|
||||
Button {
|
||||
private func resizable<Content: View>(
|
||||
_ column: ProcessColumn,
|
||||
alignment: Alignment,
|
||||
@ViewBuilder content: @escaping () -> Content
|
||||
) -> some View {
|
||||
ResizableProcessHeaderCell(
|
||||
width: Binding(
|
||||
get: { columnWidths[column] ?? column.defaultWidth },
|
||||
set: { columnWidths[column] = $0 }
|
||||
),
|
||||
minimumWidth: column.minimumWidth,
|
||||
maximumWidth: column.maximumWidth,
|
||||
alignment: alignment,
|
||||
onResizeStarted: onResizeStarted,
|
||||
onResizeEnded: onResizeEnded,
|
||||
content: content
|
||||
)
|
||||
}
|
||||
|
||||
private func header(
|
||||
_ title: String,
|
||||
field: ProcessSort,
|
||||
column: ProcessColumn
|
||||
) -> some View {
|
||||
HStack(spacing: 0) {
|
||||
Text(title).lineLimit(1)
|
||||
Spacer(minLength: 4)
|
||||
if sort == field {
|
||||
Color.clear.frame(width: 6, height: 0)
|
||||
Image(systemName: ascending ? "chevron.up" : "chevron.down")
|
||||
.frame(width: 10)
|
||||
}
|
||||
Color.clear.frame(width: processNumericTextInset, height: 0)
|
||||
}
|
||||
.frame(width: columnWidths[column] ?? column.defaultWidth)
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
groupByType = false
|
||||
if sort == field { ascending.toggle() } else { sort = field; ascending = false }
|
||||
} label: {
|
||||
HStack(spacing: 3) {
|
||||
Text(title)
|
||||
if sort == field { Image(systemName: ascending ? "chevron.up" : "chevron.down") }
|
||||
}
|
||||
.accessibilityAddTraits(.isButton)
|
||||
.accessibilityLabel(title)
|
||||
.accessibilityValue(sort == field ? (ascending ? "Sorted ascending" : "Sorted descending") : "Not sorted")
|
||||
}
|
||||
}
|
||||
|
||||
private struct ResizableProcessHeaderCell<Content: View>: View {
|
||||
@Binding var width: CGFloat
|
||||
let minimumWidth: CGFloat
|
||||
let maximumWidth: CGFloat
|
||||
let alignment: Alignment
|
||||
let onResizeStarted: () -> Void
|
||||
let onResizeEnded: () -> Void
|
||||
@ViewBuilder let content: () -> Content
|
||||
@State private var dragStartWidth: CGFloat?
|
||||
@State private var proposedWidth: CGFloat?
|
||||
@State private var isHandleHovered = false
|
||||
|
||||
var body: some View {
|
||||
content()
|
||||
.frame(width: width, alignment: alignment)
|
||||
.clipped()
|
||||
.overlay(alignment: .trailing) {
|
||||
ZStack {
|
||||
Rectangle()
|
||||
.fill(isHandleHovered || dragStartWidth != nil ? Color.accentColor : Color.secondary.opacity(0.25))
|
||||
.frame(width: isHandleHovered || dragStartWidth != nil ? 2 : 1, height: 22)
|
||||
}
|
||||
.frame(width: 10, height: 34)
|
||||
.offset(x: (proposedWidth ?? width) - width + 5)
|
||||
.contentShape(Rectangle())
|
||||
.onHover { hovering in
|
||||
isHandleHovered = hovering
|
||||
(hovering ? NSCursor.resizeLeftRight : NSCursor.arrow).set()
|
||||
}
|
||||
.highPriorityGesture(
|
||||
DragGesture(minimumDistance: 0)
|
||||
.onChanged { value in
|
||||
if dragStartWidth == nil {
|
||||
dragStartWidth = width
|
||||
onResizeStarted()
|
||||
}
|
||||
let proposed = (dragStartWidth ?? width) + value.translation.width
|
||||
proposedWidth = min(maximumWidth, max(minimumWidth, proposed))
|
||||
}
|
||||
.onEnded { _ in
|
||||
if let proposedWidth { width = proposedWidth }
|
||||
proposedWidth = nil
|
||||
dragStartWidth = nil
|
||||
onResizeEnded()
|
||||
}
|
||||
)
|
||||
.help("Drag to resize column")
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -414,6 +579,7 @@ private struct ProcessRow: View {
|
||||
let selected: Bool
|
||||
let isChild: Bool
|
||||
let displayMode: ResourceDisplayMode
|
||||
let columnWidths: [ProcessColumn: CGFloat]
|
||||
|
||||
private var activity: ProcessActivityRate { monitor.processActivity[process.pid] ?? ProcessActivityRate() }
|
||||
|
||||
@@ -424,25 +590,33 @@ private struct ProcessRow: View {
|
||||
Image(systemName: "arrow.turn.down.right")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.tertiary)
|
||||
.frame(width: 12)
|
||||
.frame(width: 16)
|
||||
} else {
|
||||
Color.clear.frame(width: 16)
|
||||
}
|
||||
ProcessIcon(name: process.displayName, path: process.executablePath)
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(process.displayName).lineLimit(1)
|
||||
Text(process.user).font(.caption).foregroundStyle(.secondary).lineLimit(1)
|
||||
}
|
||||
Text(process.displayName).lineLimit(1).truncationMode(.tail)
|
||||
Text(process.user).font(.caption).foregroundStyle(.secondary).lineLimit(1).truncationMode(.tail)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
Text("\(process.pid)").monospacedDigit().frame(width: 70, alignment: .trailing)
|
||||
Text(process.state.hasPrefix("R") ? "Running" : "").frame(width: 80, alignment: .leading)
|
||||
}
|
||||
.frame(width: width(.name), alignment: .leading)
|
||||
.clipped()
|
||||
Text("\(process.pid)").monospacedDigit().lineLimit(1).minimumScaleFactor(0.75)
|
||||
.padding(.trailing, processNumericTextInset)
|
||||
.frame(width: width(.pid), alignment: .trailing).clipped()
|
||||
Text(process.state.hasPrefix("R") ? "Running" : "").lineLimit(1).truncationMode(.tail)
|
||||
.frame(width: width(.status), alignment: .leading).clipped()
|
||||
HeatCell(text: Formatters.percent(process.cpu), intensity: process.cpu / 100)
|
||||
.frame(width: 86, alignment: .trailing)
|
||||
.frame(width: width(.cpu), alignment: .trailing)
|
||||
HeatCell(text: memoryText, intensity: process.memoryPercent / 30)
|
||||
.frame(width: 110, alignment: .trailing)
|
||||
.frame(width: width(.memory), alignment: .trailing)
|
||||
.help(memoryHelp)
|
||||
HeatCell(text: diskText, intensity: activity.diskTotal / 10_000_000)
|
||||
.frame(width: 88, alignment: .trailing)
|
||||
.frame(width: width(.disk), alignment: .trailing)
|
||||
HeatCell(text: networkText, intensity: activity.networkTotal / 5_000_000)
|
||||
.frame(width: 88, alignment: .trailing)
|
||||
.frame(width: width(.network), alignment: .trailing)
|
||||
}
|
||||
.font(.callout)
|
||||
.padding(.horizontal, 16)
|
||||
@@ -455,6 +629,14 @@ private struct ProcessRow: View {
|
||||
displayMode == .percentages ? Formatters.percent(process.memoryPercent) : Formatters.bytes.string(fromByteCount: Int64(process.residentBytes))
|
||||
}
|
||||
|
||||
private var memoryHelp: String {
|
||||
"Resident memory: \(Formatters.bytes.string(fromByteCount: Int64(process.residentBytes))) • \(Formatters.percent(process.memoryPercent)) of physical memory"
|
||||
}
|
||||
|
||||
private func width(_ column: ProcessColumn) -> CGFloat {
|
||||
columnWidths[column] ?? column.defaultWidth
|
||||
}
|
||||
|
||||
private var diskText: String {
|
||||
displayMode == .percentages ? share(activity.diskTotal, of: monitor.snapshot.diskThroughput) : Formatters.rate(activity.diskTotal)
|
||||
}
|
||||
@@ -474,35 +656,53 @@ private struct ProcessGroupRow: View {
|
||||
let group: ProcessGroup
|
||||
let selected: Bool
|
||||
let displayMode: ResourceDisplayMode
|
||||
let leadingPadding: CGFloat
|
||||
let columnWidths: [ProcessColumn: CGFloat]
|
||||
@Binding var isExpanded: Bool
|
||||
let onToggleExpansion: () -> Void
|
||||
|
||||
private var activity: ProcessActivityRate { group.activity(using: monitor.processActivity) }
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 12) {
|
||||
HStack(spacing: 10) {
|
||||
Button(action: onToggleExpansion) {
|
||||
Image(systemName: "chevron.right")
|
||||
.font(.caption.weight(.semibold))
|
||||
.rotationEffect(.degrees(isExpanded ? 90 : 0))
|
||||
.frame(width: 16, height: 44)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.foregroundStyle(.secondary)
|
||||
.help(isExpanded ? "Collapse \(group.name)" : "Expand \(group.name)")
|
||||
.accessibilityLabel(isExpanded ? "Collapse \(group.name)" : "Expand \(group.name)")
|
||||
ProcessIcon(name: group.name, path: group.appPath ?? group.primary.executablePath)
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("\(group.name) (\(group.processes.count))").lineLimit(1)
|
||||
Text("Application group").font(.caption).foregroundStyle(.secondary)
|
||||
}
|
||||
Text("\(group.name) (\(group.processes.count))").lineLimit(1).truncationMode(.tail)
|
||||
Text("Application group").font(.caption).foregroundStyle(.secondary).lineLimit(1).truncationMode(.tail)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
Text("—").frame(width: 70, alignment: .trailing).foregroundStyle(.secondary)
|
||||
}
|
||||
.frame(width: width(.name), alignment: .leading)
|
||||
.clipped()
|
||||
Text("—").lineLimit(1)
|
||||
.padding(.trailing, processNumericTextInset)
|
||||
.frame(width: width(.pid), alignment: .trailing).foregroundStyle(.secondary).clipped()
|
||||
Text(group.processes.contains(where: { $0.state.hasPrefix("R") }) ? "Running" : "")
|
||||
.frame(width: 80, alignment: .leading)
|
||||
.lineLimit(1).truncationMode(.tail)
|
||||
.frame(width: width(.status), alignment: .leading).clipped()
|
||||
HeatCell(text: Formatters.percent(group.cpu), intensity: group.cpu / 100)
|
||||
.frame(width: 86, alignment: .trailing)
|
||||
.frame(width: width(.cpu), alignment: .trailing)
|
||||
HeatCell(text: memoryText, intensity: group.memoryPercent / 30)
|
||||
.frame(width: 110, alignment: .trailing)
|
||||
.frame(width: width(.memory), alignment: .trailing)
|
||||
.help(memoryHelp)
|
||||
HeatCell(text: diskText, intensity: activity.diskTotal / 10_000_000)
|
||||
.frame(width: 88, alignment: .trailing)
|
||||
.frame(width: width(.disk), alignment: .trailing)
|
||||
HeatCell(text: networkText, intensity: activity.networkTotal / 5_000_000)
|
||||
.frame(width: 88, alignment: .trailing)
|
||||
.frame(width: width(.network), alignment: .trailing)
|
||||
}
|
||||
.font(.callout.weight(.medium))
|
||||
.padding(.leading, leadingPadding)
|
||||
.padding(.trailing, 16)
|
||||
.padding(.horizontal, 16)
|
||||
.frame(height: 48)
|
||||
.background(selected ? Color.accentColor.opacity(0.18) : Color.clear)
|
||||
.overlay(alignment: .bottom) { Divider().opacity(0.45) }
|
||||
@@ -515,6 +715,14 @@ private struct ProcessGroupRow: View {
|
||||
displayMode == .percentages ? Formatters.percent(group.memoryPercent) : Formatters.bytes.string(fromByteCount: Int64(group.residentBytes))
|
||||
}
|
||||
|
||||
private var memoryHelp: String {
|
||||
"Resident memory: \(Formatters.bytes.string(fromByteCount: Int64(group.residentBytes))) • \(Formatters.percent(group.memoryPercent)) of physical memory"
|
||||
}
|
||||
|
||||
private func width(_ column: ProcessColumn) -> CGFloat {
|
||||
columnWidths[column] ?? column.defaultWidth
|
||||
}
|
||||
|
||||
private var diskText: String {
|
||||
displayMode == .percentages ? share(activity.diskTotal, of: monitor.snapshot.diskThroughput) : Formatters.rate(activity.diskTotal)
|
||||
}
|
||||
@@ -610,9 +818,13 @@ private struct HeatCell: View {
|
||||
var body: some View {
|
||||
Text(text)
|
||||
.monospacedDigit()
|
||||
.lineLimit(1)
|
||||
.minimumScaleFactor(0.72)
|
||||
.truncationMode(.tail)
|
||||
.padding(.vertical, 9)
|
||||
.padding(.horizontal, 7)
|
||||
.padding(.horizontal, processNumericTextInset)
|
||||
.frame(maxWidth: .infinity, alignment: .trailing)
|
||||
.clipped()
|
||||
.background(Color.accentColor.opacity(0.05 + min(0.32, max(0, intensity) * 0.32)))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 4))
|
||||
}
|
||||
|
||||
+13
-2
@@ -3,8 +3,12 @@ set -euo pipefail
|
||||
|
||||
PROJECT_DIR="${0:A:h:h}"
|
||||
APP_DIR="$PROJECT_DIR/dist/Task Manager.app"
|
||||
CONTENTS_DIR="$APP_DIR/Contents"
|
||||
ICON_SOURCE="$PROJECT_DIR/mactaskmanager.icon"
|
||||
STAGING_ROOT="$(mktemp -d /tmp/mactaskmanager-build.XXXXXX)"
|
||||
STAGED_APP="$STAGING_ROOT/Task Manager.app"
|
||||
CONTENTS_DIR="$STAGED_APP/Contents"
|
||||
|
||||
trap '/bin/rm -rf -- "$STAGING_ROOT"' EXIT
|
||||
|
||||
cd "$PROJECT_DIR"
|
||||
swift build -c release
|
||||
@@ -23,5 +27,12 @@ if [[ -d "$ICON_SOURCE" ]]; then
|
||||
--output-partial-info-plist "$CONTENTS_DIR/Resources/IconInfo.plist" >/dev/null
|
||||
fi
|
||||
|
||||
codesign --force --deep --sign - "$APP_DIR" >/dev/null
|
||||
# Finder and cloud-sync metadata can make an otherwise valid app bundle
|
||||
# unsignable when rebuilding in place.
|
||||
xattr -cr "$STAGED_APP"
|
||||
codesign --force --deep --sign - "$STAGED_APP" >/dev/null
|
||||
codesign --verify --deep --strict "$STAGED_APP"
|
||||
|
||||
mkdir -p "$PROJECT_DIR/dist"
|
||||
ditto --norsrc "$STAGED_APP" "$APP_DIR"
|
||||
print "Built $APP_DIR"
|
||||
|
||||
Reference in New Issue
Block a user