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
6 changes: 6 additions & 0 deletions platforms/macos/Sources/AppState+PortOperations.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
24 changes: 24 additions & 0 deletions platforms/macos/Sources/AppState+ProcessTypeNotifications.swift
Original file line number Diff line number Diff line change
@@ -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."
)
}
}
}
3 changes: 3 additions & 0 deletions platforms/macos/Sources/AppState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<Set<String>>("notifyProcessTypes", default: [])

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

Expand Down
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
}
}
3 changes: 3 additions & 0 deletions platforms/macos/Sources/Views/Settings/SettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ struct SettingsView: View {
// MARK: - Port Forwarding
PortForwardingSettingsSection()

// MARK: - Notifications
NotificationsSettingsSection()

// MARK: - Cloudflare Tunnels
CloudflaredSettingsSection()

Expand Down
Loading