mirror of
https://github.com/nicoverbruggen/phpmon.git
synced 2026-03-27 14:30:08 +01:00
✨ Add new alert for startup
This commit is contained in:
@@ -221,6 +221,38 @@
|
||||
03EC943D2F47297B00231276 /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03EC943B2F47297100231276 /* ErrorView.swift */; };
|
||||
03EC943E2F47297B00231276 /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03EC943B2F47297100231276 /* ErrorView.swift */; };
|
||||
03EC943F2F47297B00231276 /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03EC943B2F47297100231276 /* ErrorView.swift */; };
|
||||
EE4FA5D074AD5C9D09C25A55 /* StartupAlertViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B900AAECE035041E9BE09DCF /* StartupAlertViewModel.swift */; };
|
||||
7E19EE251E743286BC95EBA3 /* StartupAlertViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B900AAECE035041E9BE09DCF /* StartupAlertViewModel.swift */; };
|
||||
FEA10615F601BEC79BBDA45B /* StartupAlertViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B900AAECE035041E9BE09DCF /* StartupAlertViewModel.swift */; };
|
||||
1D1173AE9AC9C8315E899D06 /* StartupAlertViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B900AAECE035041E9BE09DCF /* StartupAlertViewModel.swift */; };
|
||||
959BE2F3A5743862AC8D1E7D /* StartupAlertView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 349EE572012BA8A554E276E4 /* StartupAlertView.swift */; };
|
||||
C57636BF2B81A9D147B28D1C /* StartupAlertView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 349EE572012BA8A554E276E4 /* StartupAlertView.swift */; };
|
||||
9297DB3F2A8903D70E1D6426 /* StartupAlertView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 349EE572012BA8A554E276E4 /* StartupAlertView.swift */; };
|
||||
88DD3A08057A23955913FB70 /* StartupAlertView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 349EE572012BA8A554E276E4 /* StartupAlertView.swift */; };
|
||||
E56493248D049CA2EBB82B21 /* StartupAlertWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D51912F2DA8FD7D8A1578644 /* StartupAlertWindowController.swift */; };
|
||||
148796E43977CCBC22AAB5CC /* StartupAlertWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D51912F2DA8FD7D8A1578644 /* StartupAlertWindowController.swift */; };
|
||||
8A8AB3D1A6DAE99C91DFBEFF /* StartupAlertWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D51912F2DA8FD7D8A1578644 /* StartupAlertWindowController.swift */; };
|
||||
A42F1F6EF455F42769E63FF5 /* StartupAlertWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D51912F2DA8FD7D8A1578644 /* StartupAlertWindowController.swift */; };
|
||||
BFF65E73753B67381C23D65E /* MarkdownText.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4C0BBF856B022EA51606492 /* MarkdownText.swift */; };
|
||||
288730259CFF37771C773EA2 /* MarkdownText.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4C0BBF856B022EA51606492 /* MarkdownText.swift */; };
|
||||
4AAB0E1F7037BEFAE0037AFF /* MarkdownText.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4C0BBF856B022EA51606492 /* MarkdownText.swift */; };
|
||||
C277F9F96197AD07FA91E3CB /* MarkdownText.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4C0BBF856B022EA51606492 /* MarkdownText.swift */; };
|
||||
56306F63B84C9E99C2C26375 /* StartupAlertHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7EF8DC9BC21B1EFB1F6CBBA5 /* StartupAlertHeaderView.swift */; };
|
||||
5D754EAE78FF1E264AD02A46 /* StartupAlertHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7EF8DC9BC21B1EFB1F6CBBA5 /* StartupAlertHeaderView.swift */; };
|
||||
95BB6DF72FB35A5A8F4E24E7 /* StartupAlertHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7EF8DC9BC21B1EFB1F6CBBA5 /* StartupAlertHeaderView.swift */; };
|
||||
B54FBBEFE5E1782AFF7C434E /* StartupAlertHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7EF8DC9BC21B1EFB1F6CBBA5 /* StartupAlertHeaderView.swift */; };
|
||||
67E571F5200F9C57CC673499 /* StartupFixCommandView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68063C53EE0F48E91ADFE92F /* StartupFixCommandView.swift */; };
|
||||
F644E222C9D096B94E363E60 /* StartupFixCommandView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68063C53EE0F48E91ADFE92F /* StartupFixCommandView.swift */; };
|
||||
632A06CFEEC7A76C34992F38 /* StartupFixCommandView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68063C53EE0F48E91ADFE92F /* StartupFixCommandView.swift */; };
|
||||
F4B7E3F9B46C5AD8B8B3FEBA /* StartupFixCommandView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68063C53EE0F48E91ADFE92F /* StartupFixCommandView.swift */; };
|
||||
CCF5FE7B49F3B0BC77BAF7D7 /* StartupOutputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3E531FA9DE31575AF518941 /* StartupOutputView.swift */; };
|
||||
2F9D926CBFE51F3B21A76536 /* StartupOutputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3E531FA9DE31575AF518941 /* StartupOutputView.swift */; };
|
||||
FF788D404D414CC970301C81 /* StartupOutputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3E531FA9DE31575AF518941 /* StartupOutputView.swift */; };
|
||||
FB95EF9B99E4ED160E751E44 /* StartupOutputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3E531FA9DE31575AF518941 /* StartupOutputView.swift */; };
|
||||
1C91CB7232F304AA7F6296CB /* StartupAlertButtonRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DBEBCF2527961D5D13F68A8 /* StartupAlertButtonRow.swift */; };
|
||||
1224231FF18A15CB876EEF50 /* StartupAlertButtonRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DBEBCF2527961D5D13F68A8 /* StartupAlertButtonRow.swift */; };
|
||||
DDAA59E032BB88D66D71F736 /* StartupAlertButtonRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DBEBCF2527961D5D13F68A8 /* StartupAlertButtonRow.swift */; };
|
||||
F6E9803FDF532887CF899ED9 /* StartupAlertButtonRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DBEBCF2527961D5D13F68A8 /* StartupAlertButtonRow.swift */; };
|
||||
03FE39E72E81682800B7B5AC /* AppIcon.icon in Resources */ = {isa = PBXBuildFile; fileRef = 03FE39E52E81682800B7B5AC /* AppIcon.icon */; };
|
||||
03FE39E82E81682800B7B5AC /* AppIconEAP.icon in Resources */ = {isa = PBXBuildFile; fileRef = 03FE39E62E81682800B7B5AC /* AppIconEAP.icon */; };
|
||||
03FE39EA2E81694500B7B5AC /* AppIconUD.icon in Resources */ = {isa = PBXBuildFile; fileRef = 03FE39E92E81694500B7B5AC /* AppIconUD.icon */; };
|
||||
@@ -1151,6 +1183,14 @@
|
||||
03D846312EB64E35006EFE3C /* CrashReporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrashReporter.swift; sourceTree = "<group>"; };
|
||||
03DAD3A52EB3B08A003417BD /* DomainListVC+Certs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DomainListVC+Certs.swift"; sourceTree = "<group>"; };
|
||||
03EC943B2F47297100231276 /* ErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorView.swift; sourceTree = "<group>"; };
|
||||
B900AAECE035041E9BE09DCF /* StartupAlertViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartupAlertViewModel.swift; sourceTree = "<group>"; };
|
||||
349EE572012BA8A554E276E4 /* StartupAlertView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartupAlertView.swift; sourceTree = "<group>"; };
|
||||
D51912F2DA8FD7D8A1578644 /* StartupAlertWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartupAlertWindowController.swift; sourceTree = "<group>"; };
|
||||
A4C0BBF856B022EA51606492 /* MarkdownText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkdownText.swift; sourceTree = "<group>"; };
|
||||
7EF8DC9BC21B1EFB1F6CBBA5 /* StartupAlertHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartupAlertHeaderView.swift; sourceTree = "<group>"; };
|
||||
68063C53EE0F48E91ADFE92F /* StartupFixCommandView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartupFixCommandView.swift; sourceTree = "<group>"; };
|
||||
E3E531FA9DE31575AF518941 /* StartupOutputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartupOutputView.swift; sourceTree = "<group>"; };
|
||||
2DBEBCF2527961D5D13F68A8 /* StartupAlertButtonRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartupAlertButtonRow.swift; sourceTree = "<group>"; };
|
||||
03FE39E52E81682800B7B5AC /* AppIcon.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; path = AppIcon.icon; sourceTree = "<group>"; };
|
||||
03FE39E62E81682800B7B5AC /* AppIconEAP.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; path = AppIconEAP.icon; sourceTree = "<group>"; };
|
||||
03FE39E92E81694500B7B5AC /* AppIconUD.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; path = AppIconUD.icon; sourceTree = "<group>"; };
|
||||
@@ -2346,6 +2386,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
03EC943B2F47297100231276 /* ErrorView.swift */,
|
||||
A4C0BBF856B022EA51606492 /* MarkdownText.swift */,
|
||||
031D747B2F46225600D4FF48 /* SimpleButton.swift */,
|
||||
C44264BD2850B86C007400F1 /* SwiftUIHelper.swift */,
|
||||
C451AFF52969E40F0078E617 /* HelpButton.swift */,
|
||||
@@ -2511,9 +2552,24 @@
|
||||
path = Onboarding;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
3169708AAEC47AE3F4C33F74 /* Startup */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B900AAECE035041E9BE09DCF /* StartupAlertViewModel.swift */,
|
||||
349EE572012BA8A554E276E4 /* StartupAlertView.swift */,
|
||||
7EF8DC9BC21B1EFB1F6CBBA5 /* StartupAlertHeaderView.swift */,
|
||||
68063C53EE0F48E91ADFE92F /* StartupFixCommandView.swift */,
|
||||
E3E531FA9DE31575AF518941 /* StartupOutputView.swift */,
|
||||
2DBEBCF2527961D5D13F68A8 /* StartupAlertButtonRow.swift */,
|
||||
D51912F2DA8FD7D8A1578644 /* StartupAlertWindowController.swift */,
|
||||
);
|
||||
path = Startup;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
C4EE55B027708BB2001DF387 /* SwiftUI */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
3169708AAEC47AE3F4C33F74 /* Startup */,
|
||||
C4B609182853AAA700C95265 /* Domains */,
|
||||
C4B609172853AA9E00C95265 /* Menu */,
|
||||
C4B609162853AA9A00C95265 /* Common */,
|
||||
@@ -3022,6 +3078,14 @@
|
||||
C4C0E8E727F88B41002D32A9 /* DomainScanner.swift in Sources */,
|
||||
C4C3ED4327834C5200AB15D8 /* CustomPrefs.swift in Sources */,
|
||||
03EC943D2F47297B00231276 /* ErrorView.swift in Sources */,
|
||||
7E19EE251E743286BC95EBA3 /* StartupAlertViewModel.swift in Sources */,
|
||||
C57636BF2B81A9D147B28D1C /* StartupAlertView.swift in Sources */,
|
||||
148796E43977CCBC22AAB5CC /* StartupAlertWindowController.swift in Sources */,
|
||||
288730259CFF37771C773EA2 /* MarkdownText.swift in Sources */,
|
||||
5D754EAE78FF1E264AD02A46 /* StartupAlertHeaderView.swift in Sources */,
|
||||
F644E222C9D096B94E363E60 /* StartupFixCommandView.swift in Sources */,
|
||||
2F9D926CBFE51F3B21A76536 /* StartupOutputView.swift in Sources */,
|
||||
1224231FF18A15CB876EEF50 /* StartupAlertButtonRow.swift in Sources */,
|
||||
54B48B5F275F66AE006D90C5 /* Application.swift in Sources */,
|
||||
C4B97B78275CF1B5003F3378 /* App+ActivationPolicy.swift in Sources */,
|
||||
C4CE3BB827B31F2E0086CA49 /* MainMenu+Switcher.swift in Sources */,
|
||||
@@ -3383,6 +3447,14 @@
|
||||
032DAC2D2E8BEB6B0018E01C /* WebApiProtocol.swift in Sources */,
|
||||
C4513F902B13E2E6001AD760 /* PhpExtensionManagerWindowController.swift in Sources */,
|
||||
03EC943F2F47297B00231276 /* ErrorView.swift in Sources */,
|
||||
FEA10615F601BEC79BBDA45B /* StartupAlertViewModel.swift in Sources */,
|
||||
9297DB3F2A8903D70E1D6426 /* StartupAlertView.swift in Sources */,
|
||||
8A8AB3D1A6DAE99C91DFBEFF /* StartupAlertWindowController.swift in Sources */,
|
||||
4AAB0E1F7037BEFAE0037AFF /* MarkdownText.swift in Sources */,
|
||||
95BB6DF72FB35A5A8F4E24E7 /* StartupAlertHeaderView.swift in Sources */,
|
||||
632A06CFEEC7A76C34992F38 /* StartupFixCommandView.swift in Sources */,
|
||||
FF788D404D414CC970301C81 /* StartupOutputView.swift in Sources */,
|
||||
DDAA59E032BB88D66D71F736 /* StartupAlertButtonRow.swift in Sources */,
|
||||
C471E81528F9BAE80021E251 /* ArrayExtension.swift in Sources */,
|
||||
C471E7DA28F9BA8F0021E251 /* TestableCommand.swift in Sources */,
|
||||
C471E7E528F9BAC20021E251 /* Events.swift in Sources */,
|
||||
@@ -3522,6 +3594,14 @@
|
||||
03C29A7A2EC88E3100FBA25E /* ValetServicesDataManager.swift in Sources */,
|
||||
C471E8D928F9BB8F0021E251 /* HotkeyPreferenceView.swift in Sources */,
|
||||
03EC943E2F47297B00231276 /* ErrorView.swift in Sources */,
|
||||
1D1173AE9AC9C8315E899D06 /* StartupAlertViewModel.swift in Sources */,
|
||||
88DD3A08057A23955913FB70 /* StartupAlertView.swift in Sources */,
|
||||
A42F1F6EF455F42769E63FF5 /* StartupAlertWindowController.swift in Sources */,
|
||||
C277F9F96197AD07FA91E3CB /* MarkdownText.swift in Sources */,
|
||||
B54FBBEFE5E1782AFF7C434E /* StartupAlertHeaderView.swift in Sources */,
|
||||
F4B7E3F9B46C5AD8B8B3FEBA /* StartupFixCommandView.swift in Sources */,
|
||||
FB95EF9B99E4ED160E751E44 /* StartupOutputView.swift in Sources */,
|
||||
F6E9803FDF532887CF899ED9 /* StartupAlertButtonRow.swift in Sources */,
|
||||
C4611E5D2AEAD2FA0010BE24 /* ConfigManagerView.swift in Sources */,
|
||||
C471E8DA28F9BB8F0021E251 /* Keys.swift in Sources */,
|
||||
C471E8DB28F9BB8F0021E251 /* TerminalProgressWindowController.swift in Sources */,
|
||||
@@ -3715,6 +3795,14 @@
|
||||
C4F319C927B034A500AFF46F /* Stats.swift in Sources */,
|
||||
C4F30B04278E16BA00755FCE /* HomebrewService.swift in Sources */,
|
||||
03EC943C2F47297B00231276 /* ErrorView.swift in Sources */,
|
||||
EE4FA5D074AD5C9D09C25A55 /* StartupAlertViewModel.swift in Sources */,
|
||||
959BE2F3A5743862AC8D1E7D /* StartupAlertView.swift in Sources */,
|
||||
E56493248D049CA2EBB82B21 /* StartupAlertWindowController.swift in Sources */,
|
||||
BFF65E73753B67381C23D65E /* MarkdownText.swift in Sources */,
|
||||
56306F63B84C9E99C2C26375 /* StartupAlertHeaderView.swift in Sources */,
|
||||
67E571F5200F9C57CC673499 /* StartupFixCommandView.swift in Sources */,
|
||||
CCF5FE7B49F3B0BC77BAF7D7 /* StartupOutputView.swift in Sources */,
|
||||
1C91CB7232F304AA7F6296CB /* StartupAlertButtonRow.swift in Sources */,
|
||||
54D9E0B527E4F51E003B9AD9 /* Key.swift in Sources */,
|
||||
C4AF9F7B2754499000D44ED0 /* Valet.swift in Sources */,
|
||||
C4C1019C27C65C6F001FACC2 /* Process.swift in Sources */,
|
||||
|
||||
@@ -19,10 +19,6 @@ class CommandTracker: ObservableObject {
|
||||
commands.filter { !$0.isCompleted }
|
||||
}
|
||||
|
||||
var isActive: Bool {
|
||||
!activeCommands.isEmpty
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func track(_ command: String, id: UUID = UUID()) -> UUID {
|
||||
let tracked = LoggedCommand(id: id, command: command, startedAt: Date())
|
||||
|
||||
@@ -14,7 +14,8 @@ import Foundation
|
||||
*/
|
||||
struct EnvironmentCheck {
|
||||
let command: (_ container: Container) async -> Bool
|
||||
let fixCommand: ((_ container: Container) async throws -> Void)?
|
||||
let fixCommand: ((_ container: Container, _ didReceiveOutput: @escaping (String, ShellStream) -> Void) async throws -> Void)?
|
||||
let fixDescription: String?
|
||||
let name: String
|
||||
let titleText: String
|
||||
let subtitleText: String
|
||||
@@ -24,7 +25,8 @@ struct EnvironmentCheck {
|
||||
|
||||
init(
|
||||
command: @escaping (_ container: Container) async -> Bool,
|
||||
fix: ((_ container: Container) async throws -> Void)? = nil,
|
||||
fix: ((_ container: Container, _ didReceiveOutput: @escaping (String, ShellStream) -> Void) async throws -> Void)? = nil,
|
||||
fixDescription: String? = nil,
|
||||
name: String,
|
||||
titleText: String,
|
||||
subtitleText: String,
|
||||
@@ -34,6 +36,7 @@ struct EnvironmentCheck {
|
||||
) {
|
||||
self.command = command
|
||||
self.fixCommand = fix
|
||||
self.fixDescription = fixDescription
|
||||
self.name = name
|
||||
self.titleText = titleText
|
||||
self.subtitleText = subtitleText
|
||||
|
||||
@@ -15,7 +15,7 @@ extension Startup {
|
||||
The potential outcome of an environment check failure alert.
|
||||
*/
|
||||
enum EnvironmentAlertOutcome {
|
||||
/** The automatic fix was requested, will try and continue if it worked. */
|
||||
/** The automatic fix ran and succeeded. Continue to the next check. */
|
||||
case shouldRunFix
|
||||
|
||||
/** No automatic fix was requested, show alert and require retry of all startup checks. */
|
||||
@@ -23,11 +23,11 @@ extension Startup {
|
||||
}
|
||||
|
||||
/**
|
||||
Displays an alert for a particular check. There are two types of alerts:
|
||||
- ones that require an app restart, which prompt the user to exit the app
|
||||
- ones that allow the app to continue, which allow the user to retry
|
||||
Displays an alert for a particular check. For checks that require an app restart,
|
||||
a simple NVAlert is shown with a quit button. For all other checks, the new
|
||||
StartupAlertWindowController is used to show the enhanced startup alert.
|
||||
*/
|
||||
@MainActor internal func showAlert(for check: EnvironmentCheck) -> EnvironmentAlertOutcome {
|
||||
@MainActor internal func showAlert(for check: EnvironmentCheck) async -> EnvironmentAlertOutcome {
|
||||
// Ensure that the timeout does not fire until we restart
|
||||
Self.startupTimer?.invalidate()
|
||||
|
||||
@@ -43,29 +43,8 @@ extension Startup {
|
||||
}).show(urgency: .bringToFront)
|
||||
}
|
||||
|
||||
// Verify if an automatic fix is available
|
||||
let hasAutomaticFix = check.fixCommand != nil
|
||||
|
||||
// Present an alert with one or two buttons (depending on fix)
|
||||
let outcome = NVAlert()
|
||||
.withInformation(
|
||||
title: check.titleText,
|
||||
subtitle: check.subtitleText,
|
||||
description: check.descriptionText
|
||||
)
|
||||
.withPrimary(text: hasAutomaticFix ? "startup.fix_for_me".localized : "startup.fix_manually".localized)
|
||||
.withSecondary(if: hasAutomaticFix, text: "startup.fix_manually".localized)
|
||||
.withTertiary(if: hasAutomaticFix, text: "", action: { _ in
|
||||
NSWorkspace.shared.open(Constants.Urls.FrequentlyAskedQuestions)
|
||||
})
|
||||
.runModal(urgency: .bringToFront)
|
||||
|
||||
// If there's an automatic fix and we chose to fix it, return outcome
|
||||
if hasAutomaticFix && outcome == .alertFirstButtonReturn {
|
||||
return .shouldRunFix
|
||||
}
|
||||
|
||||
// In any other situation, we will require a retry of the startup
|
||||
return .shouldRetryStartup
|
||||
// Create and show the enhanced startup alert window
|
||||
let controller = StartupAlertWindowController.create(for: check)
|
||||
return await controller.showModal()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,26 +48,12 @@ class Startup {
|
||||
// We will present the user with an option (potentially)
|
||||
let outcome = await showAlert(for: check)
|
||||
|
||||
// If the user requested an automatic fix, do this
|
||||
// The fix ran and succeeded — continue to the next check
|
||||
if outcome == .shouldRunFix {
|
||||
// Verify a fix actually exists
|
||||
guard let command = check.fixCommand else {
|
||||
return false
|
||||
}
|
||||
|
||||
// We will try to run the fix, it may fail!
|
||||
do {
|
||||
try await command(App.shared.container)
|
||||
guard await check.succeeds() else {
|
||||
return false
|
||||
}
|
||||
continue // continue to the next check!
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// No fix requested, this is just a failure
|
||||
// No fix requested or fix failed — requires full restart
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
@@ -113,10 +99,15 @@ class Startup {
|
||||
return await !container.shell
|
||||
.pipe("ls \(container.paths.optPath) | grep php").out.contains("php")
|
||||
},
|
||||
fix: { container in
|
||||
fix: { container, didReceiveOutput in
|
||||
let brew = container.paths.brew
|
||||
await container.shell.pipe("\(brew) tap shivammathur/php && \(brew) install shivammathur/php/php")
|
||||
try await container.shell.attach(
|
||||
"\(brew) tap shivammathur/php && \(brew) install shivammathur/php/php",
|
||||
didReceiveOutput: didReceiveOutput,
|
||||
withTimeout: 120
|
||||
)
|
||||
},
|
||||
fixDescription: "brew tap shivammathur/php && brew install shivammathur/php/php",
|
||||
name: "`ls \(App.shared.container.paths.optPath) | grep php` returned php result",
|
||||
titleText: "startup.errors.php_opt.title".localized,
|
||||
subtitleText: "startup.errors.php_opt.subtitle".localized(
|
||||
@@ -131,11 +122,15 @@ class Startup {
|
||||
command: { container in
|
||||
return !container.filesystem.fileExists(container.paths.php)
|
||||
},
|
||||
fix: { container in
|
||||
// See if we can't link PHP
|
||||
fix: { container, didReceiveOutput in
|
||||
let brew = container.paths.brew
|
||||
await container.shell.pipe("\(brew) link php")
|
||||
try await container.shell.attach(
|
||||
"\(brew) link php",
|
||||
didReceiveOutput: didReceiveOutput,
|
||||
withTimeout: 120
|
||||
)
|
||||
},
|
||||
fixDescription: "brew link php",
|
||||
name: "`\(App.shared.container.paths.php)` exists",
|
||||
titleText: "startup.errors.php_binary.title".localized,
|
||||
subtitleText: "startup.errors.php_binary.subtitle".localized,
|
||||
@@ -152,10 +147,15 @@ class Startup {
|
||||
return await container.shell.pipe("\(container.paths.binPath)/php -v").err
|
||||
.contains("Library not loaded") && container.phpEnvs.currentInstall != nil
|
||||
},
|
||||
fix: { container in
|
||||
let brew = App.shared.container.paths.brew
|
||||
await container.shell.pipe("\(brew) tap shivammathur/php && \(brew) reinstall shivammathur/php/php && \(brew) link php")
|
||||
fix: { container, didReceiveOutput in
|
||||
let brew = container.paths.brew
|
||||
try await container.shell.attach(
|
||||
"\(brew) tap shivammathur/php && \(brew) reinstall shivammathur/php/php && \(brew) link php",
|
||||
didReceiveOutput: didReceiveOutput,
|
||||
withTimeout: 120
|
||||
)
|
||||
},
|
||||
fixDescription: "brew reinstall php && brew link php",
|
||||
name: "no `dyld` issue (`Library not loaded`) detected",
|
||||
titleText: "startup.errors.dyld_library.title".localized,
|
||||
subtitleText: "startup.errors.dyld_library.subtitle".localized(
|
||||
@@ -187,10 +187,15 @@ class Startup {
|
||||
await container.phpEnvs.determinePhpAlias()
|
||||
return PhpEnvironments.brewPhpAlias == nil
|
||||
},
|
||||
fix: { container in
|
||||
fix: { container, didReceiveOutput in
|
||||
let brew = container.paths.brew
|
||||
await container.shell.pipe("\(brew) update")
|
||||
try await container.shell.attach(
|
||||
"\(brew) update",
|
||||
didReceiveOutput: didReceiveOutput,
|
||||
withTimeout: 120
|
||||
)
|
||||
},
|
||||
fixDescription: "brew update",
|
||||
name: "`brew` alias is not nil and valid",
|
||||
titleText: "startup.errors.could_not_determine_alias.title".localized,
|
||||
subtitleText: "startup.errors.could_not_determine_alias.subtitle".localized,
|
||||
@@ -224,10 +229,12 @@ class Startup {
|
||||
.pipe("cat /private/etc/sudoers.d/brew")
|
||||
.out.contains(container.paths.brew)
|
||||
},
|
||||
fix: { container in
|
||||
fix: { container, didReceiveOutput in
|
||||
let valet = container.paths.binPath.appending("/valet")
|
||||
try AppleScript.runShellAsAdmin("\(valet) trust")
|
||||
let result = try AppleScript.runShellAsAdmin("\(valet) trust")
|
||||
didReceiveOutput(result, .stdOut)
|
||||
},
|
||||
fixDescription: "valet trust",
|
||||
name: "`/private/etc/sudoers.d/brew` contains brew",
|
||||
titleText: "startup.errors.sudoers_brew.title".localized,
|
||||
subtitleText: "startup.errors.sudoers_brew.subtitle".localized,
|
||||
@@ -250,10 +257,15 @@ class Startup {
|
||||
command: { container in
|
||||
return !container.filesystem.directoryExists("~/.config/valet")
|
||||
},
|
||||
fix: { container in
|
||||
fix: { container, didReceiveOutput in
|
||||
let valet = container.paths.binPath.appending("/valet")
|
||||
await container.shell.pipe("\(valet) install")
|
||||
try await container.shell.attach(
|
||||
"\(valet) install",
|
||||
didReceiveOutput: didReceiveOutput,
|
||||
withTimeout: 120
|
||||
)
|
||||
},
|
||||
fixDescription: "valet install",
|
||||
name: "`.config/valet` not empty (Valet installed)",
|
||||
titleText: "startup.errors.valet_not_installed.title".localized,
|
||||
subtitleText: "startup.errors.valet_not_installed.subtitle".localized,
|
||||
|
||||
27
phpmon/Domain/SwiftUI/Common/MarkdownText.swift
Normal file
27
phpmon/Domain/SwiftUI/Common/MarkdownText.swift
Normal file
@@ -0,0 +1,27 @@
|
||||
//
|
||||
// MarkdownText.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 25/02/2026.
|
||||
// Copyright © 2026 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
extension Text {
|
||||
init(markdown string: String, fontSize: CGFloat? = nil) {
|
||||
if var attributed = try? AttributedString(
|
||||
markdown: string,
|
||||
options: .init(interpretedSyntax: .inlineOnlyPreservingWhitespace)
|
||||
) {
|
||||
for run in attributed.runs {
|
||||
if run.inlinePresentationIntent?.contains(.code) == true {
|
||||
attributed[run.range].backgroundColor = Color(nsColor: .quaternaryLabelColor)
|
||||
}
|
||||
}
|
||||
self.init(attributed)
|
||||
} else {
|
||||
self.init(string)
|
||||
}
|
||||
}
|
||||
}
|
||||
78
phpmon/Domain/SwiftUI/Startup/StartupAlertButtonRow.swift
Normal file
78
phpmon/Domain/SwiftUI/Startup/StartupAlertButtonRow.swift
Normal file
@@ -0,0 +1,78 @@
|
||||
//
|
||||
// StartupAlertButtonRow.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 25/02/2026.
|
||||
// Copyright © 2026 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct StartupAlertButtonRow: View {
|
||||
let state: StartupAlertViewModel.State
|
||||
let hasFix: Bool
|
||||
let onQuit: () -> Void
|
||||
let onRetry: () -> Void
|
||||
let onFix: () -> Void
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
Button("startup.alert.quit".localized) {
|
||||
onQuit()
|
||||
}
|
||||
.disabled(state == .running)
|
||||
|
||||
Spacer()
|
||||
|
||||
switch state {
|
||||
case .idle where hasFix:
|
||||
Button("startup.fix_manually".localized) {
|
||||
onRetry()
|
||||
}
|
||||
Button("startup.alert.fix_automatically".localized) {
|
||||
onFix()
|
||||
}
|
||||
.buttonStyle(.custom)
|
||||
|
||||
case .running:
|
||||
HStack(spacing: 10) {
|
||||
Text("Applying. Please wait...")
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
ProgressView()
|
||||
.controlSize(.small)
|
||||
}
|
||||
|
||||
case .idle, .completed:
|
||||
Button("startup.alert.retry".localized) {
|
||||
onRetry()
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(20)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview("Fix available") {
|
||||
StartupAlertButtonRow(
|
||||
state: .idle, hasFix: true,
|
||||
onQuit: {}, onRetry: {}, onFix: {}
|
||||
)
|
||||
.frame(width: 460)
|
||||
}
|
||||
|
||||
#Preview("Running") {
|
||||
StartupAlertButtonRow(
|
||||
state: .running, hasFix: true,
|
||||
onQuit: {}, onRetry: {}, onFix: {}
|
||||
)
|
||||
.frame(width: 460)
|
||||
}
|
||||
|
||||
#Preview("No fix") {
|
||||
StartupAlertButtonRow(
|
||||
state: .completed, hasFix: false,
|
||||
onQuit: {}, onRetry: {}, onFix: {}
|
||||
)
|
||||
.frame(width: 460)
|
||||
}
|
||||
44
phpmon/Domain/SwiftUI/Startup/StartupAlertHeaderView.swift
Normal file
44
phpmon/Domain/SwiftUI/Startup/StartupAlertHeaderView.swift
Normal file
@@ -0,0 +1,44 @@
|
||||
//
|
||||
// StartupAlertHeaderView.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 25/02/2026.
|
||||
// Copyright © 2026 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct StartupAlertHeaderView: View {
|
||||
let titleText: String
|
||||
let subtitleText: String
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 12) {
|
||||
Image(nsImage: NSApp.applicationIconImage)
|
||||
.resizable()
|
||||
.frame(width: 60, height: 60)
|
||||
|
||||
VStack(alignment: .leading, spacing: 5) {
|
||||
Text(markdown: titleText)
|
||||
.font(.system(size: 15, weight: .bold))
|
||||
.textSelection(.enabled)
|
||||
Text(markdown: subtitleText)
|
||||
.font(.system(size: 12))
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.textSelection(.enabled)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding(15)
|
||||
.padding(.top, 0)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
StartupAlertHeaderView(
|
||||
titleText: "startup.errors.php_binary.title".localized,
|
||||
subtitleText: "startup.errors.php_binary.subtitle".localized
|
||||
)
|
||||
.frame(width: 460)
|
||||
}
|
||||
145
phpmon/Domain/SwiftUI/Startup/StartupAlertView.swift
Normal file
145
phpmon/Domain/SwiftUI/Startup/StartupAlertView.swift
Normal file
@@ -0,0 +1,145 @@
|
||||
//
|
||||
// StartupAlertView.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 25/02/2026.
|
||||
// Copyright © 2026 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct StartupAlertView: View {
|
||||
@ObservedObject var viewModel: StartupAlertViewModel
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
StartupAlertHeaderView(
|
||||
titleText: viewModel.check.titleText,
|
||||
subtitleText: viewModel.check.subtitleText
|
||||
)
|
||||
|
||||
Divider()
|
||||
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
if viewModel.state == .running {
|
||||
StartupOutputView(
|
||||
lines: viewModel.outputLines,
|
||||
isRunning: true
|
||||
)
|
||||
} else if viewModel.hasFix, viewModel.state == .idle {
|
||||
StartupFixCommandView(
|
||||
command: viewModel.check.fixDescription ?? ""
|
||||
)
|
||||
}
|
||||
|
||||
if !viewModel.check.descriptionText.isEmpty, viewModel.state != .running {
|
||||
Text(markdown: viewModel.check.descriptionText, fontSize: 12)
|
||||
.font(.system(size: 12))
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.textSelection(.enabled)
|
||||
}
|
||||
|
||||
if !viewModel.outputLines.isEmpty, viewModel.state == .idle {
|
||||
StartupOutputView(
|
||||
lines: viewModel.outputLines,
|
||||
isRunning: false
|
||||
)
|
||||
}
|
||||
}
|
||||
.padding(20)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
||||
Divider()
|
||||
|
||||
StartupAlertButtonRow(
|
||||
state: viewModel.state,
|
||||
hasFix: viewModel.hasFix,
|
||||
onQuit: { viewModel.quit() },
|
||||
onRetry: { viewModel.retry() },
|
||||
onFix: { viewModel.runFix() }
|
||||
)
|
||||
}
|
||||
.frame(width: 460)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
#Preview("Fix Available — brew link php") {
|
||||
let check = EnvironmentCheck(
|
||||
command: { _ in return true },
|
||||
fix: { _, output in output("Running brew link php...", .stdOut) },
|
||||
fixDescription: "brew link php",
|
||||
name: "preview_php_binary",
|
||||
titleText: "startup.errors.php_binary.title".localized,
|
||||
subtitleText: "startup.errors.php_binary.subtitle".localized,
|
||||
descriptionText: "startup.errors.php_binary.desc".localized(
|
||||
App.shared.container.paths.php
|
||||
)
|
||||
)
|
||||
let vm = StartupAlertViewModel(check: check)
|
||||
return StartupAlertView(viewModel: vm)
|
||||
}
|
||||
|
||||
#Preview("Fix Available — valet trust") {
|
||||
let check = EnvironmentCheck(
|
||||
command: { _ in return true },
|
||||
fix: { _, output in output("Password required...", .stdOut) },
|
||||
fixDescription: "valet trust",
|
||||
name: "preview_sudoers_brew",
|
||||
titleText: "startup.errors.sudoers_brew.title".localized,
|
||||
subtitleText: "startup.errors.sudoers_brew.subtitle".localized,
|
||||
descriptionText: "startup.errors.sudoers_brew.desc".localized
|
||||
)
|
||||
let vm = StartupAlertViewModel(check: check)
|
||||
return StartupAlertView(viewModel: vm)
|
||||
}
|
||||
|
||||
#Preview("Fix Running — brew link php") {
|
||||
StartupAlertView(viewModel: StartupAlertViewModel(
|
||||
check: EnvironmentCheck(
|
||||
command: { _ in return true },
|
||||
fix: { _, output in output("Running...", .stdOut) },
|
||||
fixDescription: "brew link php",
|
||||
name: "preview_php_binary_running",
|
||||
titleText: "startup.errors.php_binary.title".localized,
|
||||
subtitleText: "startup.errors.php_binary.subtitle".localized,
|
||||
descriptionText: "startup.errors.php_binary.desc".localized(
|
||||
App.shared.container.paths.php
|
||||
)
|
||||
),
|
||||
state: .running,
|
||||
outputLines: [
|
||||
OutputLine(text: "==> Linking Binary 'php' to '/opt/homebrew/bin/php'", stream: .stdOut),
|
||||
OutputLine(text: "==> Downloading https://formulae.brew.sh/api/formula.jws.json", stream: .stdOut),
|
||||
OutputLine(text: "Already downloaded: /Users/nico/Library/Caches/Homebrew/downloads/abc123.json", stream: .stdOut),
|
||||
OutputLine(text: "Warning: php is keg-only and must be linked with --force", stream: .stdErr),
|
||||
OutputLine(text: "==> Linking php... linked 25 files", stream: .stdOut),
|
||||
]
|
||||
))
|
||||
}
|
||||
|
||||
#Preview("No Fix — Valet version unsupported") {
|
||||
let check = EnvironmentCheck(
|
||||
command: { _ in return true },
|
||||
name: "preview_valet_version",
|
||||
titleText: "startup.errors.valet_version_not_supported.title".localized,
|
||||
subtitleText: "startup.errors.valet_version_not_supported.subtitle".localized,
|
||||
descriptionText: "startup.errors.valet_version_not_supported.desc".localized
|
||||
)
|
||||
let vm = StartupAlertViewModel(check: check)
|
||||
return StartupAlertView(viewModel: vm)
|
||||
}
|
||||
|
||||
#Preview("No Fix — Herd running") {
|
||||
let check = EnvironmentCheck(
|
||||
command: { _ in return true },
|
||||
name: "preview_herd_running",
|
||||
titleText: "startup.errors.herd_running.title".localized,
|
||||
subtitleText: "startup.errors.herd_running.subtitle".localized,
|
||||
descriptionText: "startup.errors.herd_running.desc".localized
|
||||
)
|
||||
let vm = StartupAlertViewModel(check: check)
|
||||
return StartupAlertView(viewModel: vm)
|
||||
}
|
||||
93
phpmon/Domain/SwiftUI/Startup/StartupAlertViewModel.swift
Normal file
93
phpmon/Domain/SwiftUI/Startup/StartupAlertViewModel.swift
Normal file
@@ -0,0 +1,93 @@
|
||||
//
|
||||
// StartupAlertViewModel.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 25/02/2026.
|
||||
// Copyright © 2026 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct OutputLine: Identifiable {
|
||||
let id = UUID()
|
||||
let text: String
|
||||
let stream: ShellStream
|
||||
}
|
||||
|
||||
class StartupAlertViewModel: ObservableObject {
|
||||
enum State {
|
||||
case idle
|
||||
case running
|
||||
case completed
|
||||
}
|
||||
|
||||
@Published var state: State = .idle
|
||||
@Published var outputLines: [OutputLine] = []
|
||||
|
||||
let check: EnvironmentCheck
|
||||
|
||||
/// Callback to dismiss the window with a result
|
||||
var onComplete: ((Startup.EnvironmentAlertOutcome) -> Void)?
|
||||
|
||||
init(check: EnvironmentCheck) {
|
||||
self.check = check
|
||||
self.state = check.fixCommand != nil ? .idle : .completed
|
||||
}
|
||||
|
||||
init(check: EnvironmentCheck, state: State, outputLines: [OutputLine] = []) {
|
||||
self.check = check
|
||||
self.state = state
|
||||
self.outputLines = outputLines
|
||||
}
|
||||
|
||||
var hasFix: Bool {
|
||||
return check.fixCommand != nil
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func runFix() {
|
||||
guard let fixCommand = check.fixCommand else { return }
|
||||
state = .running
|
||||
outputLines = []
|
||||
|
||||
Task {
|
||||
do {
|
||||
try await fixCommand(App.shared.container) { [weak self] text, stream in
|
||||
DispatchQueue.main.async {
|
||||
self?.outputLines.append(OutputLine(text: text, stream: stream))
|
||||
}
|
||||
}
|
||||
|
||||
// Fix completed — re-run the check
|
||||
let stillFails = await check.succeeds()
|
||||
await MainActor.run {
|
||||
if !stillFails {
|
||||
// Check still fails after fix
|
||||
self.state = .idle
|
||||
self.outputLines.append(
|
||||
OutputLine(text: "\nFix did not resolve the issue.", stream: .stdErr)
|
||||
)
|
||||
} else {
|
||||
// Check passed
|
||||
self.onComplete?(.shouldRunFix)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
await MainActor.run {
|
||||
self.outputLines.append(
|
||||
OutputLine(text: "\nError: \(error.localizedDescription)", stream: .stdErr)
|
||||
)
|
||||
self.state = .idle
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func quit() {
|
||||
exit(1)
|
||||
}
|
||||
|
||||
func retry() {
|
||||
onComplete?(.shouldRetryStartup)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
//
|
||||
// StartupAlertWindowController.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 25/02/2026.
|
||||
// Copyright © 2026 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
import SwiftUI
|
||||
|
||||
class StartupAlertWindowController: PMWindowController {
|
||||
|
||||
override var windowName: String {
|
||||
return "StartupAlert"
|
||||
}
|
||||
|
||||
private var viewModel: StartupAlertViewModel?
|
||||
private var didResolve = false
|
||||
|
||||
static func create(for check: EnvironmentCheck) -> StartupAlertWindowController {
|
||||
let windowController = StartupAlertWindowController()
|
||||
let viewModel = StartupAlertViewModel(check: check)
|
||||
windowController.viewModel = viewModel
|
||||
|
||||
let window = NSWindow()
|
||||
window.title = ""
|
||||
window.styleMask = [.titled, .closable]
|
||||
window.titlebarAppearsTransparent = true
|
||||
window.delegate = windowController
|
||||
window.contentView = NSHostingView(rootView: StartupAlertView(viewModel: viewModel))
|
||||
window.setContentSize(window.contentView!.fittingSize)
|
||||
window.isReleasedWhenClosed = false
|
||||
|
||||
windowController.window = window
|
||||
return windowController
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func showModal() async -> Startup.EnvironmentAlertOutcome {
|
||||
return await withCheckedContinuation { continuation in
|
||||
guard let viewModel = self.viewModel else {
|
||||
continuation.resume(returning: .shouldRetryStartup)
|
||||
return
|
||||
}
|
||||
|
||||
viewModel.onComplete = { [weak self] outcome in
|
||||
guard let self, !self.didResolve else { return }
|
||||
self.didResolve = true
|
||||
self.close()
|
||||
continuation.resume(returning: outcome)
|
||||
}
|
||||
|
||||
self.showWindow(nil)
|
||||
self.window?.center()
|
||||
NSApp.activate(ignoringOtherApps: true)
|
||||
self.window?.orderFrontRegardless()
|
||||
}
|
||||
}
|
||||
|
||||
override func windowWillClose(_ notification: Notification) {
|
||||
super.windowWillClose(notification)
|
||||
|
||||
// If closed via the close button without resolving, quit the app
|
||||
if !didResolve {
|
||||
exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
59
phpmon/Domain/SwiftUI/Startup/StartupFixCommandView.swift
Normal file
59
phpmon/Domain/SwiftUI/Startup/StartupFixCommandView.swift
Normal file
@@ -0,0 +1,59 @@
|
||||
//
|
||||
// StartupFixCommandView.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 25/02/2026.
|
||||
// Copyright © 2026 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct StartupFixCommandView: View {
|
||||
let command: String
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("AUTOMATIC FIX")
|
||||
.foregroundStyle(Color.app)
|
||||
.font(.system(size: 10))
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: "terminal")
|
||||
.foregroundStyle(.secondary)
|
||||
Text(command)
|
||||
.font(.system(size: 12, design: .monospaced))
|
||||
.foregroundStyle(.primary)
|
||||
Spacer()
|
||||
Button {
|
||||
NSPasteboard.general.clearContents()
|
||||
NSPasteboard.general.setString(command, forType: .string)
|
||||
} label: {
|
||||
Image(systemName: "doc.on.doc")
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
.padding(10)
|
||||
.background(Color.black)
|
||||
.foregroundStyle(Color.white)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 6))
|
||||
}
|
||||
}
|
||||
|
||||
#Preview("brew link php") {
|
||||
StartupFixCommandView(command: "brew link php")
|
||||
.padding(20)
|
||||
.frame(width: 460)
|
||||
}
|
||||
|
||||
#Preview("valet trust") {
|
||||
StartupFixCommandView(command: "valet trust")
|
||||
.padding(20)
|
||||
.frame(width: 460)
|
||||
}
|
||||
|
||||
#Preview("Long command") {
|
||||
StartupFixCommandView(command: "brew tap shivammathur/php && brew install shivammathur/php/php")
|
||||
.padding(20)
|
||||
.frame(width: 460)
|
||||
}
|
||||
70
phpmon/Domain/SwiftUI/Startup/StartupOutputView.swift
Normal file
70
phpmon/Domain/SwiftUI/Startup/StartupOutputView.swift
Normal file
@@ -0,0 +1,70 @@
|
||||
//
|
||||
// StartupOutputView.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 25/02/2026.
|
||||
// Copyright © 2026 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct StartupOutputView: View {
|
||||
let lines: [OutputLine]
|
||||
let isRunning: Bool
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
ScrollViewReader { proxy in
|
||||
ScrollView {
|
||||
LazyVStack(alignment: .leading, spacing: 1) {
|
||||
ForEach(lines) { line in
|
||||
Text(line.text)
|
||||
.font(.system(size: 11, design: .monospaced))
|
||||
.foregroundStyle(line.stream == .stdErr ? Color.red : .white)
|
||||
.textSelection(.enabled)
|
||||
.id(line.id)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(8)
|
||||
}
|
||||
.frame(height: 160)
|
||||
.background(Color.black)
|
||||
.foregroundStyle(Color.white)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 6))
|
||||
.onChange(of: lines.count) { _ in
|
||||
if let last = lines.last {
|
||||
proxy.scrollTo(last.id, anchor: .bottom)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview("With output") {
|
||||
StartupOutputView(
|
||||
lines: [
|
||||
OutputLine(text: "==> Linking Binary 'php' to '/opt/homebrew/bin/php'", stream: .stdOut),
|
||||
OutputLine(text: "==> Downloading https://formulae.brew.sh/api/formula.jws.json", stream: .stdOut),
|
||||
OutputLine(text: "Already downloaded: /Users/nico/Library/Caches/Homebrew/downloads/abc123.json", stream: .stdOut),
|
||||
OutputLine(text: "Warning: php is keg-only and must be linked with --force", stream: .stdErr),
|
||||
OutputLine(text: "==> Linking php... linked 25 files", stream: .stdOut),
|
||||
],
|
||||
isRunning: true
|
||||
)
|
||||
.padding(20)
|
||||
.frame(width: 460)
|
||||
}
|
||||
|
||||
#Preview("Idle with prior output") {
|
||||
StartupOutputView(
|
||||
lines: [
|
||||
OutputLine(text: "==> Linking php... linked 25 files", stream: .stdOut),
|
||||
OutputLine(text: "\nFix did not resolve the issue.", stream: .stdErr),
|
||||
],
|
||||
isRunning: false
|
||||
)
|
||||
.padding(20)
|
||||
.frame(width: 460)
|
||||
}
|
||||
@@ -665,7 +665,7 @@ You can do this by running `composer global update` in your terminal. After that
|
||||
// PHP binary not found
|
||||
"startup.errors.php_binary.title" = "PHP is not correctly installed";
|
||||
"startup.errors.php_binary.subtitle" = "You must install PHP via Homebrew. The app will not work correctly until you resolve this issue.";
|
||||
"startup.errors.php_binary.desc" = "Usually running `brew link php` in your Terminal will resolve this issue.\n\nTo diagnose what is wrong, you can try running `which php` in your Terminal, it should return `%@`.";
|
||||
"startup.errors.php_binary.desc" = "Usually, running `brew link php` in your Terminal will resolve this issue.\n\nTo diagnose what is wrong, you can try running `which php` in your Terminal, it should return `%@`.";
|
||||
|
||||
// Invalid brew info php output
|
||||
"startup.errors.php_brew_info_invalid.title" = "Homebrew returned invalid output for `brew info php --json` which requires valid JSON as output.";
|
||||
@@ -966,6 +966,10 @@ PHP Monitor will tell Valet to unsecure and re-secure all expired domains for yo
|
||||
"startup.fix_for_me" = "Fix For Me";
|
||||
"startup.fix_manually" = "I Fixed It";
|
||||
|
||||
"startup.alert.quit" = "Quit";
|
||||
"startup.alert.fix_automatically" = "Fix Automatically";
|
||||
"startup.alert.retry" = "Retry";
|
||||
|
||||
// COMMAND HISTORY
|
||||
|
||||
"command_history.title" = "Command History";
|
||||
|
||||
Reference in New Issue
Block a user