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
43 changes: 43 additions & 0 deletions platforms/macos/Sources/AppState+PortLabels.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import AppKit
import Defaults

extension AppState {
/// Returns the custom label for a port, if any
func portLabel(for port: Int) -> String? {
let label = Defaults[.portLabels][String(port)]
return (label?.isEmpty ?? true) ? nil : label
}

/// Sets a custom label for a port
func setPortLabel(_ label: String, for port: Int) {
if label.isEmpty {
Defaults[.portLabels].removeValue(forKey: String(port))
} else {
Defaults[.portLabels][String(port)] = label
}
}

/// Removes the custom label for a port
func removePortLabel(for port: Int) {
Defaults[.portLabels].removeValue(forKey: String(port))
}

/// Prompts user to set a port label using NSAlert
func promptForPortLabel(port: Int) {
let alert = NSAlert()
alert.messageText = "Set Label for Port \(port)"
alert.informativeText = "Enter a custom name to identify this port."
alert.addButton(withTitle: "Save")
alert.addButton(withTitle: "Cancel")

let textField = NSTextField(frame: NSRect(x: 0, y: 0, width: 260, height: 24))
textField.placeholderString = "e.g., Frontend Dev Server"
textField.stringValue = portLabel(for: port) ?? ""
alert.accessoryView = textField
alert.window.initialFirstResponder = textField

if alert.runModal() == .alertFirstButtonReturn {
setPortLabel(textField.stringValue, for: port)
}
}
}
3 changes: 3 additions & 0 deletions platforms/macos/Sources/AppState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ extension Defaults.Keys {
// Process type overrides (processName → ProcessType.rawValue)
static let processTypeOverrides = Key<[String: String]>("processTypeOverrides", default: [:])

// Port labels (port number string → custom name)
static let portLabels = Key<[String: String]>("portLabels", default: [:])

// Kubernetes-related keys
static let customNamespaces = Key<[String]>("customNamespaces", default: [])

Expand Down
19 changes: 19 additions & 0 deletions platforms/macos/Sources/Views/Components/PortContextMenu.swift
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,25 @@ struct PortContextMenu: View {
systemImage: appState.isWatching(port.port) ? "eye.slash" : "eye"
)
}

Divider()

Button {
appState.promptForPortLabel(port: port.port)
} label: {
Label(
appState.portLabel(for: port.port) != nil ? "Edit Label" : "Set Label",
systemImage: "pencil"
)
}

if appState.portLabel(for: port.port) != nil {
Button {
appState.removePortLabel(for: port.port)
} label: {
Label("Remove Label", systemImage: "pencil.slash")
}
}
}

@ViewBuilder
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,30 @@ struct PortStatusIndicator: View {

// MARK: - Port Number Display

/// Displays port number in monospaced font
/// Displays port number in monospaced font with optional custom label
struct PortNumberDisplay: View {
let port: Int
let isActive: Bool
var font: Font = .system(.body, design: .monospaced)
var fontWeight: Font.Weight = .medium
var showLabel: Bool = true

@Environment(AppState.self) private var appState

var body: some View {
Text(String(port))
.font(font)
.fontWeight(fontWeight)
.opacity(isActive ? 1 : 0.6)
HStack(spacing: 4) {
Text(String(port))
.font(font)
.fontWeight(fontWeight)
.opacity(isActive ? 1 : 0.6)

if showLabel, let label = appState.portLabel(for: port) {
Text(label)
.font(.caption)
.foregroundStyle(.orange)
.lineLimit(1)
}
}
}
}

Expand Down
18 changes: 13 additions & 5 deletions platforms/macos/Sources/Views/Components/PortRow/PortRowView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -256,11 +256,19 @@ struct PortRowView: View {
.frame(width: 100, alignment: .leading)
.opacity(isKilling ? 0.5 : 1)

// Process name
Text(port.processName)
.font(.callout)
.lineLimit(1)
.opacity(isKilling ? 0.5 : 1)
// Process name + label
HStack(spacing: 4) {
Text(port.processName)
.font(.callout)
.lineLimit(1)
if let label = appState.portLabel(for: port.port) {
Text("(\(label))")
.font(.caption)
.foregroundStyle(.orange)
.lineLimit(1)
}
}
.opacity(isKilling ? 0.5 : 1)

Spacer()

Expand Down
17 changes: 14 additions & 3 deletions platforms/macos/Sources/Views/PortDetailView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,19 @@ struct PortDetailView: View {
.fontWeight(.semibold)
.lineLimit(1)

Text("Port \(String(port.port))")
.font(.subheadline)
.foregroundStyle(.secondary)
HStack(spacing: 4) {
Text("Port \(String(port.port))")
.font(.subheadline)
.foregroundStyle(.secondary)

if let label = appState.portLabel(for: port.port) {
Text("·")
.foregroundStyle(.secondary)
Text(label)
.font(.subheadline)
.foregroundStyle(.orange)
}
}
}

Spacer()
Expand Down Expand Up @@ -120,6 +130,7 @@ struct PortDetailView: View {
GridItem(.flexible())
], alignment: .leading, spacing: 16) {
DetailRow(title: "Port", value: String(port.port))
DetailRow(title: "Label", value: appState.portLabel(for: port.port) ?? "—")
DetailRow(title: "PID", value: String(port.pid))
DetailRow(title: "Address", value: port.address)
DetailRow(title: "User", value: port.user)
Expand Down
Loading