Skip to content
Open
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
20 changes: 10 additions & 10 deletions BrewServicesManager.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,6 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = UUW59LGK2E;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
Expand Down Expand Up @@ -381,7 +380,6 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = UUW59LGK2E;
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
Expand All @@ -407,10 +405,12 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = BrewServicesManager/BrewServicesManager.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = UUW59LGK2E;
DEVELOPMENT_TEAM = X784EY4HLD;
ENABLE_APP_SANDBOX = NO;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
Expand All @@ -423,9 +423,10 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
MARKETING_VERSION = 1.0;
MARKETING_VERSION = 1.1;
PRODUCT_BUNDLE_IDENTIFIER = dev.mertcandemir.BrewServicesManager;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
REGISTER_APP_GROUPS = YES;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
Expand All @@ -441,10 +442,12 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = BrewServicesManager/BrewServicesManager.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = UUW59LGK2E;
DEVELOPMENT_TEAM = X784EY4HLD;
ENABLE_APP_SANDBOX = NO;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
Expand All @@ -457,9 +460,10 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
MARKETING_VERSION = 1.0;
MARKETING_VERSION = 1.1;
PRODUCT_BUNDLE_IDENTIFIER = dev.mertcandemir.BrewServicesManager;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
REGISTER_APP_GROUPS = YES;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
Expand All @@ -476,7 +480,6 @@
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = UUW59LGK2E;
GENERATE_INFOPLIST_FILE = YES;
MACOSX_DEPLOYMENT_TARGET = 15.7;
MARKETING_VERSION = 1.0;
Expand All @@ -497,7 +500,6 @@
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = UUW59LGK2E;
GENERATE_INFOPLIST_FILE = YES;
MACOSX_DEPLOYMENT_TARGET = 15.7;
MARKETING_VERSION = 1.0;
Expand All @@ -517,7 +519,6 @@
buildSettings = {
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = UUW59LGK2E;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = dev.mertcandemir.BrewServicesManagerUITests;
Expand All @@ -536,7 +537,6 @@
buildSettings = {
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = UUW59LGK2E;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = dev.mertcandemir.BrewServicesManagerUITests;
Expand Down
107 changes: 106 additions & 1 deletion BrewServicesManager/Brew/BrewServiceInfoEntry.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,81 @@ struct BrewServiceInfoEntry: Codable, Identifiable, Hashable, Sendable {
case interval
case cron
}


// MARK: - Initializer

init(
name: String,
serviceName: String?,
status: BrewServiceStatus,
running: Bool?,
loaded: Bool?,
schedulable: Bool?,
pid: Int?,
exitCode: Int?,
user: String?,
file: String?,
registered: Bool?,
loadedFile: String?,
command: String?,
workingDir: String?,
rootDir: String?,
logPath: String?,
errorLogPath: String?,
interval: Int?,
cron: String?,
detectedPorts: [ServicePort]? = nil
) {
self.name = name
self.serviceName = serviceName
self.status = status
self.running = running
self.loaded = loaded
self.schedulable = schedulable
self.pid = pid
self.exitCode = exitCode
self.user = user
self.file = file
self.registered = registered
self.loadedFile = loadedFile
self.command = command
self.workingDir = workingDir
self.rootDir = rootDir
self.logPath = logPath
self.errorLogPath = errorLogPath
self.interval = interval
self.cron = cron
self.detectedPorts = detectedPorts
}

// Custom Decodable implementation to handle runtime-only detectedPorts
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)

name = try container.decode(String.self, forKey: .name)
serviceName = try container.decodeIfPresent(String.self, forKey: .serviceName)
status = try container.decode(BrewServiceStatus.self, forKey: .status)
running = try container.decodeIfPresent(Bool.self, forKey: .running)
loaded = try container.decodeIfPresent(Bool.self, forKey: .loaded)
schedulable = try container.decodeIfPresent(Bool.self, forKey: .schedulable)
pid = try container.decodeIfPresent(Int.self, forKey: .pid)
exitCode = try container.decodeIfPresent(Int.self, forKey: .exitCode)
user = try container.decodeIfPresent(String.self, forKey: .user)
file = try container.decodeIfPresent(String.self, forKey: .file)
registered = try container.decodeIfPresent(Bool.self, forKey: .registered)
loadedFile = try container.decodeIfPresent(String.self, forKey: .loadedFile)
command = try container.decodeIfPresent(String.self, forKey: .command)
workingDir = try container.decodeIfPresent(String.self, forKey: .workingDir)
rootDir = try container.decodeIfPresent(String.self, forKey: .rootDir)
logPath = try container.decodeIfPresent(String.self, forKey: .logPath)
errorLogPath = try container.decodeIfPresent(String.self, forKey: .errorLogPath)
interval = try container.decodeIfPresent(Int.self, forKey: .interval)
cron = try container.decodeIfPresent(String.self, forKey: .cron)

// Runtime-only property not decoded from JSON
detectedPorts = nil
}

// MARK: - Computed Properties

var fileURL: URL? {
Expand All @@ -89,4 +163,35 @@ struct BrewServiceInfoEntry: Codable, Identifiable, Hashable, Sendable {
guard let errorLogPath else { return nil }
return URL(filePath: errorLogPath)
}

// MARK: - Runtime State (not from JSON)

/// Detected listening ports (populated at runtime)
let detectedPorts: [ServicePort]?

/// Returns a new instance with updated detected ports
func withDetectedPorts(_ ports: [ServicePort]) -> BrewServiceInfoEntry {
BrewServiceInfoEntry(
name: name,
serviceName: serviceName,
status: status,
running: running,
loaded: loaded,
schedulable: schedulable,
pid: pid,
exitCode: exitCode,
user: user,
file: file,
registered: registered,
loadedFile: loadedFile,
command: command,
workingDir: workingDir,
rootDir: rootDir,
logPath: logPath,
errorLogPath: errorLogPath,
interval: interval,
cron: cron,
detectedPorts: ports
)
}
}
8 changes: 8 additions & 0 deletions BrewServicesManager/BrewServicesManager.entitlements
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.application-identifier</key>
<string>$(AppIdentifierPrefix)$(CFBundleIdentifier)</string>
</dict>
</plist>
4 changes: 3 additions & 1 deletion BrewServicesManager/BrewServicesManagerApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@ import SwiftUI
struct BrewServicesManagerApp: App {
@State private var servicesStore = ServicesStore()
@State private var appSettings = AppSettings()

@State private var serviceLinksStore = ServiceLinksStore()

var body: some Scene {
MenuBarExtra {
MenuBarRootView()
.environment(servicesStore)
.environment(appSettings)
.environment(serviceLinksStore)
} label: {
Label("Brew Services Manager", systemImage: iconName)
.labelStyle(.iconOnly)
Expand Down
4 changes: 3 additions & 1 deletion BrewServicesManager/MenuBar/MainMenuContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ struct MainMenuContentView: View {
let onSettings: () -> Void
let onServiceInfo: (BrewServiceListEntry) -> Void
let onStopWithOptions: (BrewServiceListEntry) -> Void
let onManageLinks: (BrewServiceListEntry) -> Void
let onGlobalAction: (GlobalActionType) -> Void

var body: some View {
Expand All @@ -24,7 +25,8 @@ struct MainMenuContentView: View {
// Services section
MainMenuServicesSectionView(
onServiceInfo: onServiceInfo,
onStopWithOptions: onStopWithOptions
onStopWithOptions: onStopWithOptions,
onManageLinks: onManageLinks
)

Divider()
Expand Down
4 changes: 4 additions & 0 deletions BrewServicesManager/MenuBar/MainMenuServicesSectionView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ struct MainMenuServicesSectionView: View {

let onServiceInfo: (BrewServiceListEntry) -> Void
let onStopWithOptions: (BrewServiceListEntry) -> Void
let onManageLinks: (BrewServiceListEntry) -> Void

var body: some View {
Group {
Expand Down Expand Up @@ -92,6 +93,9 @@ struct MainMenuServicesSectionView: View {
},
onStopWithOptions: {
onStopWithOptions(service)
},
onManageLinks: {
onManageLinks(service)
}
)
.padding(.horizontal)
Expand Down
44 changes: 41 additions & 3 deletions BrewServicesManager/MenuBar/MenuBarRootView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@ import SwiftUI
struct MenuBarRootView: View {
@Environment(ServicesStore.self) private var store
@Environment(AppSettings.self) private var settings

@State private var pendingGlobalAction: GlobalActionType?
@State private var serviceToStop: BrewServiceListEntry?
@State private var showingSettings = false
@State private var showingServiceInfo: BrewServiceInfoEntry?
@State private var managingLinksFor: (service: String, ports: [ServicePort])?

var body: some View {
let menuContentWidth: CGFloat = if serviceToStop != nil {
Expand All @@ -23,6 +24,8 @@ struct MenuBarRootView: View {
LayoutConstants.settingsMenuWidth
} else if showingServiceInfo != nil {
LayoutConstants.serviceInfoMenuWidth
} else if managingLinksFor != nil {
LayoutConstants.serviceInfoMenuWidth
} else {
LayoutConstants.mainMenuWidth
}
Expand All @@ -37,7 +40,7 @@ struct MenuBarRootView: View {
},
onServiceInfo: { service in
Task {
await store.fetchServiceInfo(
await store.fetchServiceInfoWithPorts(
service.name,
domain: settings.selectedDomain,
sudoServiceUser: settings.validatedSudoServiceUser,
Expand All @@ -53,11 +56,32 @@ struct MenuBarRootView: View {
onStopWithOptions: { service in
serviceToStop = service
},
onManageLinks: { service in
Task {
var ports: [ServicePort] = []

if let info = store.selectedServiceInfo, info.name == service.name {
ports = info.detectedPorts ?? []
} else {
await store.fetchServiceInfoWithPorts(
service.name,
domain: settings.selectedDomain,
sudoServiceUser: settings.validatedSudoServiceUser,
debugMode: settings.debugMode
)
ports = store.selectedServiceInfo?.detectedPorts ?? []
}

withAnimation(.easeInOut(duration: 0.2)) {
managingLinksFor = (service.name, ports)
}
}
},
onGlobalAction: { action in
pendingGlobalAction = action
}
)
.opacity(showingSettings || showingServiceInfo != nil ? 0 : 1)
.opacity(showingSettings || showingServiceInfo != nil || managingLinksFor != nil ? 0 : 1)

// Settings overlay
if showingSettings {
Expand All @@ -78,6 +102,20 @@ struct MenuBarRootView: View {
}
.transition(.move(edge: .trailing))
}

// Service Links Management overlay
if let managingLinks = managingLinksFor {
ServiceLinksManagementView(
serviceName: managingLinks.service,
suggestedPorts: managingLinks.ports,
onDismiss: {
withAnimation(.easeInOut(duration: 0.2)) {
managingLinksFor = nil
}
}
)
.transition(.move(edge: .trailing))
}
}
.frame(width: menuContentWidth)
.task(id: "\(settings.selectedDomain.rawValue)|\(settings.autoRefreshInterval)|\(settings.validatedSudoServiceUser ?? "")|\(settings.debugMode)") {
Expand Down
Loading