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
9 changes: 6 additions & 3 deletions platforms/macos/Sources/Models/ProcessGroup.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,15 @@ import Foundation
/// their owning process. This provides a hierarchical view where users can
/// expand/collapse processes to see all their associated ports.
struct ProcessGroup: Identifiable, Sendable {
/// Process ID (PID) - used as stable identifier
let id: Int
/// Process name - used as stable identifier for grouping
let id: String

/// Name of the process owning these ports
let processName: String

/// All ports owned by this process
/// All PIDs in this group
let pids: [Int]

/// All ports owned by this process (across all PIDs)
let ports: [PortInfo]
}
18 changes: 10 additions & 8 deletions platforms/macos/Sources/Services/PortGroupingService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,12 @@ actor PortGroupingService {
/// // groups[0] might contain: ProcessGroup(id: 1234, processName: "node", ports: [3000, 3001])
/// ```
func groupByProcess(_ ports: [PortInfo]) -> [ProcessGroup] {
let grouped = Dictionary(grouping: ports) { $0.pid }
return grouped.map { pid, ports in
let grouped = Dictionary(grouping: ports) { $0.processName }
return grouped.map { name, ports in
ProcessGroup(
id: pid,
processName: ports.first?.processName ?? "Unknown",
id: name,
processName: name,
pids: Array(Set(ports.map(\.pid))).sorted(),
ports: ports.sorted { $0.port < $1.port }
)
}.sorted { $0.processName.localizedCaseInsensitiveCompare($1.processName) == .orderedAscending }
Expand All @@ -54,11 +55,12 @@ actor PortGroupingService {
/// - watched: Set of watched port numbers
/// - Returns: Array of ProcessGroup instances, sorted by priority then name
func groupByProcessWithPriority(_ ports: [PortInfo], favorites: Set<Int>, watched: Set<Int>) -> [ProcessGroup] {
let grouped = Dictionary(grouping: ports) { $0.pid }
return grouped.map { pid, ports in
let grouped = Dictionary(grouping: ports) { $0.processName }
return grouped.map { name, ports in
ProcessGroup(
id: pid,
processName: ports.first?.processName ?? "Unknown",
id: name,
processName: name,
pids: Array(Set(ports.map(\.pid))).sorted(),
ports: ports.sorted { $0.port < $1.port }
)
}.sorted { a, b in
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,12 @@ struct ProcessGroupListRow: View {
}
.frame(width: 150, alignment: .leading)

// PID (aligned with PID column of header)
Text("\(group.id)")
// PID(s) (aligned with PID column of header)
Text(group.pids.count == 1 ? "\(group.pids[0])" : "\(group.pids.count) PIDs")
.font(.system(.body, design: .monospaced))
.foregroundStyle(.secondary)
.frame(width: 70, alignment: .leading)
.help(group.pids.map(String.init).joined(separator: ", "))

// Port Count Badge (aligned with Type column of header effectively)
if !showConfirm {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ struct MenuBarPortList: View {
let filteredPortForwardConnections: [PortForwardConnectionState]
let groupedByProcess: [ProcessGroup]
let useTreeView: Bool
@Binding var expandedProcesses: Set<Int>
@Binding var expandedProcesses: Set<String>
@Binding var confirmingKillPort: String?
@Bindable var state: AppState

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ struct MenuBarProcessGroupRow: View {
if group.ports.contains(where: { state.isWatching($0.port) }) { Image(systemName: "eye.fill").font(.caption2).foregroundStyle(.blue) }
}
Spacer()
Text("PID \(String(group.id))").font(.caption2).foregroundStyle(.secondary)
Text(group.pids.count == 1 ? "PID \(group.pids[0])" : "\(group.pids.count) PIDs").font(.caption2).foregroundStyle(.secondary)
if !(isHovered || showConfirm) {
Text("\(group.ports.count)").font(.caption2).foregroundStyle(.secondary).padding(.horizontal, 5).background(.tertiary.opacity(0.5)).clipShape(Capsule())
} else if !showConfirm {
Expand Down
11 changes: 6 additions & 5 deletions platforms/macos/Sources/Views/MenuBar/MenuBarView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ struct MenuBarView: View {
@State private var confirmingKillAll = false
@State private var confirmingKillPort: String?
@State private var hoveredPort: String?
@State private var expandedProcesses: Set<Int> = []
@State private var expandedProcesses: Set<String> = []
@Default(.useTreeView) private var useTreeView
@Default(.hideSystemProcesses) private var hideSystemProcesses

Expand Down Expand Up @@ -70,11 +70,12 @@ struct MenuBarView: View {
}

// Compute groups from cached filtered ports
let grouped = Dictionary(grouping: cachedFilteredPorts) { $0.pid }
cachedGroups = grouped.map { pid, ports in
let grouped = Dictionary(grouping: cachedFilteredPorts) { $0.processName }
cachedGroups = grouped.map { name, ports in
ProcessGroup(
id: pid,
processName: ports.first?.processName ?? "Unknown",
id: name,
processName: name,
pids: Array(Set(ports.map(\.pid))).sorted(),
ports: ports.sorted { $0.port < $1.port }
)
}.sorted { a, b in
Expand Down
11 changes: 6 additions & 5 deletions platforms/macos/Sources/Views/PortTable/PortTableView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ struct PortTableView: View {
@State private var sortOrder: SortOrder = .port
@State private var sortAscending = true
@Default(.useTreeView) private var useTreeView
@State private var expandedProcesses: Set<Int> = []
@State private var expandedProcesses: Set<String> = []

var body: some View {
VStack(spacing: 0) {
Expand Down Expand Up @@ -206,11 +206,12 @@ struct PortTableView: View {

/// Groups ports by process for tree view
private var groupedPorts: [ProcessGroup] {
let grouped = Dictionary(grouping: appState.filteredPorts) { $0.pid }
return grouped.map { pid, ports in
let grouped = Dictionary(grouping: appState.filteredPorts) { $0.processName }
return grouped.map { name, ports in
ProcessGroup(
id: pid,
processName: ports.first?.processName ?? "Unknown",
id: name,
processName: name,
pids: Array(Set(ports.map(\.pid))).sorted(),
ports: ports.sorted { $0.port < $1.port }
)
}.sorted { $0.processName.localizedCaseInsensitiveCompare($1.processName) == .orderedAscending }
Expand Down
Loading