diff --git a/platforms/macos/Sources/AppState+PortOperations.swift b/platforms/macos/Sources/AppState+PortOperations.swift index 55e5912..cee7481 100644 --- a/platforms/macos/Sources/AppState+PortOperations.swift +++ b/platforms/macos/Sources/AppState+PortOperations.swift @@ -16,9 +16,15 @@ extension AppState { isScanning = true let scanned = await scanner.scanPorts() + let previousPorts = ports let didChange = updatePorts(scanned) didChangeAny = didChangeAny || didChange + // Check process type notifications for newly appeared ports + if didChange { + checkProcessTypeNotifications(oldPorts: previousPorts, newPorts: scanned) + } + // Always update watcher state to keep transition baseline accurate. checkWatchedPorts() isScanning = false diff --git a/platforms/macos/Sources/AppState+ProcessTypeNotifications.swift b/platforms/macos/Sources/AppState+ProcessTypeNotifications.swift new file mode 100644 index 0000000..39a706e --- /dev/null +++ b/platforms/macos/Sources/AppState+ProcessTypeNotifications.swift @@ -0,0 +1,24 @@ +import Foundation +import Defaults + +extension AppState { + /// Checks for new ports matching enabled process type notifications. + /// Call after each scan to detect newly appeared ports. + func checkProcessTypeNotifications(oldPorts: [PortInfo], newPorts: [PortInfo]) { + let enabledTypes = Defaults[.notifyProcessTypes] + guard !enabledTypes.isEmpty else { return } + + let oldPortPids = Set(oldPorts.map { "\($0.port)-\($0.pid)" }) + + for port in newPorts { + let key = "\(port.port)-\(port.pid)" + guard !oldPortPids.contains(key) else { continue } + guard enabledTypes.contains(port.processType.rawValue) else { continue } + + NotificationService.shared.notify( + title: "New \(port.processType.rawValue) on Port \(port.port)", + body: "\(port.processName) started listening." + ) + } + } +} diff --git a/platforms/macos/Sources/AppState.swift b/platforms/macos/Sources/AppState.swift index 2d106cf..589cb72 100644 --- a/platforms/macos/Sources/AppState.swift +++ b/platforms/macos/Sources/AppState.swift @@ -20,6 +20,9 @@ extension Defaults.Keys { // Port labels (port number string → custom name) static let portLabels = Key<[String: String]>("portLabels", default: [:]) + // Process type notification filters (rawValues of enabled types, empty = disabled) + static let notifyProcessTypes = Key>("notifyProcessTypes", default: []) + // Kubernetes-related keys static let customNamespaces = Key<[String]>("customNamespaces", default: []) diff --git a/platforms/macos/Sources/Views/Settings/NotificationsSettingsSection.swift b/platforms/macos/Sources/Views/Settings/NotificationsSettingsSection.swift new file mode 100644 index 0000000..f087a2e --- /dev/null +++ b/platforms/macos/Sources/Views/Settings/NotificationsSettingsSection.swift @@ -0,0 +1,49 @@ +import SwiftUI +import Defaults + +struct NotificationsSettingsSection: View { + @Default(.notifyProcessTypes) private var enabledTypes + + var body: some View { + SettingsGroup("Port Notifications", icon: "bell.fill") { + VStack(spacing: 0) { + SettingsRowContainer { + VStack(alignment: .leading, spacing: 2) { + Text("Notify on new ports by process type") + .fontWeight(.medium) + Text("Get notified when a port opens for selected process types") + .font(.caption) + .foregroundStyle(.secondary) + } + } + + ForEach(ProcessType.allCases) { type in + SettingsDivider() + processTypeToggle(type) + } + } + } + } + + private func processTypeToggle(_ type: ProcessType) -> some View { + SettingsRowContainer { + Toggle(isOn: Binding( + get: { enabledTypes.contains(type.rawValue) }, + set: { enabled in + if enabled { + Defaults[.notifyProcessTypes].insert(type.rawValue) + } else { + Defaults[.notifyProcessTypes].remove(type.rawValue) + } + } + )) { + HStack(spacing: 8) { + Image(systemName: type.icon) + .foregroundStyle(type.color) + .frame(width: 20) + Text(type.rawValue) + } + } + } + } +} diff --git a/platforms/macos/Sources/Views/Settings/SettingsView.swift b/platforms/macos/Sources/Views/Settings/SettingsView.swift index 0a3d0ff..a615156 100644 --- a/platforms/macos/Sources/Views/Settings/SettingsView.swift +++ b/platforms/macos/Sources/Views/Settings/SettingsView.swift @@ -36,6 +36,9 @@ struct SettingsView: View { // MARK: - Port Forwarding PortForwardingSettingsSection() + // MARK: - Notifications + NotificationsSettingsSection() + // MARK: - Cloudflare Tunnels CloudflaredSettingsSection()