├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.yaml │ └── feature_request.yaml ├── contributing.md └── pull_request_template.md ├── .gitignore ├── .swiftlint.yml ├── DEVELOPER.md ├── LICENSE ├── PHP Monitor.xcodeproj ├── project.pbxproj └── xcshareddata │ └── xcschemes │ ├── PHP Monitor DEV.xcscheme │ ├── PHP Monitor EAP.xcscheme │ ├── PHP Monitor Self-Updater.xcscheme │ ├── PHP Monitor.xcscheme │ └── Unit Tests.xcscheme ├── README.md ├── SECURITY.md ├── assets ├── affinity │ ├── icon-dev.afdesign │ ├── icon-eap.afdesign │ ├── icon-updater.afdesign │ ├── icon.afdesign │ └── icons.afdesign ├── icon.svg ├── menubar.svg └── source.svg ├── docs ├── build.png ├── logo.png ├── notification.png └── screenshot.jpg ├── integrations └── phpmon.alfredworkflow ├── phpmon-updater ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── icon_128x128.png │ │ ├── icon_128x128@2x.png │ │ ├── icon_16x16.png │ │ ├── icon_16x16@2x.png │ │ ├── icon_256x256.png │ │ ├── icon_256x256@2x.png │ │ ├── icon_32x32.png │ │ ├── icon_32x32@2x.png │ │ ├── icon_512x512.png │ │ └── icon_512x512@2x.png │ └── Contents.json ├── main.swift └── phpmon-updater.entitlements ├── phpmon ├── Assets.xcassets │ ├── AppColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── icon_128x128.png │ │ ├── icon_128x128@2x.png │ │ ├── icon_16x16.png │ │ ├── icon_16x16@2x.png │ │ ├── icon_256x256.png │ │ ├── icon_256x256@2x.png │ │ ├── icon_32x32.png │ │ ├── icon_32x32@2x.png │ │ ├── icon_512x512.png │ │ └── icon_512x512@2x.png │ ├── AppIconDev.appiconset │ │ ├── Contents.json │ │ ├── icon_128x128.png │ │ ├── icon_128x128@2x.png │ │ ├── icon_16x16.png │ │ ├── icon_16x16@2x.png │ │ ├── icon_256x256.png │ │ ├── icon_256x256@2x.png │ │ ├── icon_32x32.png │ │ ├── icon_32x32@2x.png │ │ ├── icon_512x512.png │ │ └── icon_512x512@2x.png │ ├── AppIconEAP.appiconset │ │ ├── Contents.json │ │ ├── icon_128x128.png │ │ ├── icon_128x128@2x.png │ │ ├── icon_16x16.png │ │ ├── icon_16x16@2x.png │ │ ├── icon_256x256.png │ │ ├── icon_256x256@2x.png │ │ ├── icon_32x32.png │ │ ├── icon_32x32@2x.png │ │ ├── icon_512x512.png │ │ └── icon_512x512@2x.png │ ├── AppSecondary.colorset │ │ └── Contents.json │ ├── Checkmark.imageset │ │ ├── Contents.json │ │ └── check.svg │ ├── Contents.json │ ├── IconColorGreen.colorset │ │ └── Contents.json │ ├── IconColorNormal.colorset │ │ └── Contents.json │ ├── IconColorRed.colorset │ │ └── Contents.json │ ├── IconDefault.imageset │ │ ├── Contents.json │ │ ├── Default.png │ │ └── Default@2x.png │ ├── IconLinked.imageset │ │ ├── Contents.json │ │ ├── Linked.png │ │ └── Linked@2x.png │ ├── IconParked.imageset │ │ ├── Contents.json │ │ ├── Parked.png │ │ └── Parked@2x.png │ ├── IconProxy.imageset │ │ ├── Contents.json │ │ ├── Proxy.png │ │ └── Proxy@2x.png │ ├── Isolated.imageset │ │ ├── Contents.json │ │ ├── Isolated.png │ │ └── Isolated@2x.png │ ├── Lock.imageset │ │ ├── Contents.json │ │ └── Locked.svg │ ├── LockUnlocked.imageset │ │ ├── Contents.json │ │ └── Unlocked.svg │ ├── Menu Bar Icons │ │ ├── Contents.json │ │ ├── MenuBar_elephant.imageset │ │ │ ├── Contents.json │ │ │ ├── Menu Bar Elephant.png │ │ │ └── Menu Bar Elephant@2x.png │ │ └── MenuBar_php.imageset │ │ │ ├── Contents.json │ │ │ ├── Menu Bar.png │ │ │ └── Menu Bar@2x.png │ ├── SpinnerBackground.colorset │ │ └── Contents.json │ ├── StatusBarIcon.imageset │ │ ├── Contents.json │ │ ├── phpmon.png │ │ └── phpmon@2x.png │ ├── StatusBarIconStatic.imageset │ │ ├── Contents.json │ │ ├── phpmon.png │ │ └── phpmon@2x.png │ ├── StatusColorBlue.colorset │ │ └── Contents.json │ ├── StatusColorGreen.colorset │ │ └── Contents.json │ ├── StatusColorRed.colorset │ │ └── Contents.json │ ├── StatusColorYellow.colorset │ │ └── Contents.json │ ├── StatusColorYellowTranslucent.colorset │ │ └── Contents.json │ └── php.imageset │ │ ├── Contents.json │ │ └── php.svg ├── Common │ ├── Command │ │ ├── ActiveCommand.swift │ │ ├── CommandProtocol.swift │ │ └── RealCommand.swift │ ├── Core │ │ ├── Actions.swift │ │ ├── Constants.swift │ │ ├── Events.swift │ │ ├── Helpers.swift │ │ ├── Homebrew.swift │ │ ├── Logger.swift │ │ ├── Paths.swift │ │ └── Process.swift │ ├── Errors │ │ ├── AlertableError.swift │ │ └── Errors.swift │ ├── Extensions │ │ ├── ArrayExtension.swift │ │ ├── DataExtension.swift │ │ ├── DateExtension.swift │ │ ├── DictionaryExtension.swift │ │ ├── NSMenuExtension.swift │ │ ├── NSMenuItemExtension.swift │ │ ├── NSWindowExtension.swift │ │ ├── NVAlertExtension.swift │ │ ├── StringExtension.swift │ │ ├── TimeIntervalExtension.swift │ │ └── XibLoadable.swift │ ├── Filesystem │ │ ├── ActiveFileSystem.swift │ │ ├── FileSystemProtocol.swift │ │ └── RealFileSystem.swift │ ├── Helpers │ │ ├── Alert.swift │ │ ├── Application.swift │ │ ├── LocalNotification.swift │ │ ├── LoginItemManager.swift │ │ ├── Measurements.swift │ │ ├── MenuBarImageGenerator.swift │ │ ├── PMWindowController.swift │ │ ├── System.swift │ │ └── VersionExtractor.swift │ ├── PHP │ │ ├── ActivePhpInstallation.swift │ │ ├── Extensions │ │ │ └── Xdebug.swift │ │ ├── Homebrew │ │ │ ├── HomebrewDecodable.swift │ │ │ └── HomebrewService.swift │ │ ├── PHP Version │ │ │ ├── PhpEnvironments.swift │ │ │ ├── PhpHelper.swift │ │ │ ├── PhpVersionNumberCollection.swift │ │ │ └── VersionNumber.swift │ │ ├── PhpConfigurationFile.swift │ │ ├── PhpExtension.swift │ │ ├── PhpInstallation.swift │ │ └── Switcher │ │ │ ├── InternalSwitcher+Valet.swift │ │ │ ├── InternalSwitcher.swift │ │ │ └── PhpSwitcher.swift │ ├── Protocols │ │ └── CreatedFromFile.swift │ ├── Shell │ │ ├── ActiveShell.swift │ │ ├── RealShell.swift │ │ └── ShellProtocol.swift │ ├── State │ │ └── BusyStatus.swift │ └── Testables │ │ ├── TestableCommand.swift │ │ ├── TestableConfiguration.swift │ │ ├── TestableFileSystem.swift │ │ └── TestableShell.swift ├── Credits.html ├── Domain │ ├── App │ │ ├── App+ActivationPolicy.swift │ │ ├── App+GlobalHotkey.swift │ │ ├── App.swift │ │ ├── AppDelegate+InterApp.swift │ │ ├── AppDelegate+MenuOutlets.swift │ │ ├── AppDelegate+Notifications.swift │ │ ├── AppDelegate.swift │ │ ├── AppUpdater.swift │ │ ├── AppVersion.swift │ │ ├── Base.lproj │ │ │ └── Main.storyboard │ │ ├── EnvironmentCheck.swift │ │ ├── InterAppHandler.swift │ │ ├── Services │ │ │ ├── FakeServicesManager.swift │ │ │ ├── Service.swift │ │ │ ├── ServicesManager.swift │ │ │ └── ValetServicesManager.swift │ │ └── Startup.swift │ ├── Integrations │ │ ├── Common │ │ │ └── RCFile.swift │ │ ├── Composer │ │ │ ├── ComposerJson.swift │ │ │ ├── ComposerWindow.swift │ │ │ └── ProjectTypeDetection.swift │ │ ├── Homebrew │ │ │ ├── Behaviors │ │ │ │ └── BrewPermissionFixer.swift │ │ │ ├── Brew.swift │ │ │ ├── BrewDiagnostics.swift │ │ │ ├── BrewPhpExtension.swift │ │ │ ├── BrewPhpFormula.swift │ │ │ ├── BrewPhpFormulaeHandler.swift │ │ │ ├── BrewTapFormulae.swift │ │ │ ├── CaskFile.swift │ │ │ ├── Commands │ │ │ │ ├── BrewCommand.swift │ │ │ │ ├── PHP Extensions │ │ │ │ │ ├── InstallPhpExtensionCommand.swift │ │ │ │ │ └── RemovePhpExtensionCommand.swift │ │ │ │ └── PHP Versions │ │ │ │ │ ├── ModifyPhpVersionCommand.swift │ │ │ │ │ └── RemovePhpVersionCommand.swift │ │ │ └── Fake │ │ │ │ └── FakeCommand.swift │ │ ├── Nginx │ │ │ └── NginxConfigurationFile.swift │ │ └── Valet │ │ │ ├── Domains │ │ │ ├── FakeValetInteractor.swift │ │ │ ├── ValetInteractor.swift │ │ │ └── ValetListable.swift │ │ │ ├── Proxies │ │ │ ├── FakeValetProxy.swift │ │ │ └── ValetProxy.swift │ │ │ ├── Scanners │ │ │ ├── DomainScanner.swift │ │ │ ├── FakeDomainScanner.swift │ │ │ ├── ValetDomainScanner.swift │ │ │ └── ValetScanners.swift │ │ │ ├── Sites │ │ │ ├── FakeValetSite.swift │ │ │ ├── PhpVersionSource.swift │ │ │ └── ValetSite.swift │ │ │ ├── Valet+Alerts.swift │ │ │ └── Valet.swift │ ├── Menu │ │ ├── AppMenu.swift │ │ ├── MainMenu+Actions.swift │ │ ├── MainMenu+Async.swift │ │ ├── MainMenu+FixMyValet.swift │ │ ├── MainMenu+Startup.swift │ │ ├── MainMenu+Switcher.swift │ │ ├── MainMenu.swift │ │ ├── StatusMenu+Items.swift │ │ └── StatusMenu.swift │ ├── PHP │ │ └── PhpGuard.swift │ ├── Preferences │ │ ├── CustomPrefs.swift │ │ ├── Keybinds │ │ │ └── GlobalKeybindPreference.swift │ │ ├── Keys.swift │ │ ├── MenuBarIcons.swift │ │ ├── PreferenceName.swift │ │ ├── Preferences.swift │ │ ├── PreferencesTabs.swift │ │ ├── PreferencesVC.swift │ │ ├── PreferencesWindowController+Hotkey.swift │ │ ├── PreferencesWindowController.swift │ │ ├── Stats.swift │ │ └── Views │ │ │ ├── CheckboxPreferenceView.swift │ │ │ ├── CheckboxPreferenceView.xib │ │ │ ├── HotkeyPreferenceView.swift │ │ │ ├── HotkeyPreferenceView.xib │ │ │ ├── SelectPreferenceView.swift │ │ │ └── SelectPreferenceView.xib │ ├── Presets │ │ ├── Preset.swift │ │ └── PresetHelper.swift │ ├── SwiftUI │ │ ├── Common │ │ │ ├── BlockingOverlayView.swift │ │ │ ├── CustomButtonStyles.swift │ │ │ ├── HelpButton.swift │ │ │ ├── SwiftUIHelper.swift │ │ │ └── UnavailableContentView.swift │ │ ├── Domains │ │ │ └── VersionPopoverView.swift │ │ └── Menu │ │ │ ├── HeaderView.swift │ │ │ ├── SectionHeaderView.swift │ │ │ ├── ServicesView.swift │ │ │ └── StatsView.swift │ ├── Terminal Alert │ │ ├── ProgressVC.swift │ │ ├── ProgressWindow.storyboard │ │ └── TerminalProgressWindowController.swift │ ├── Terminal │ │ └── Paths.swift │ └── Watcher │ │ ├── App+BrewWatch.swift │ │ ├── App+ConfigWatch.swift │ │ ├── ConfigFSNotifier.swift │ │ ├── ConfigWatchManager.swift │ │ └── FSNotifier.swift ├── IAP │ ├── InternetAccessPolicy.plist │ └── InternetAccessPolicy.strings ├── Info.plist ├── Modules │ ├── Domain List │ │ ├── Favorites.swift │ │ └── UI │ │ │ ├── AddProxyVC.swift │ │ │ ├── AddSiteVC.swift │ │ │ ├── Cells │ │ │ ├── DomainListCellProtocol.swift │ │ │ ├── DomainListKindCell.swift │ │ │ ├── DomainListNameCell.swift │ │ │ ├── DomainListPhpCell.swift │ │ │ ├── DomainListTLSCell.swift │ │ │ └── DomainListTypeCell.swift │ │ │ ├── DomainListVC+Actions.swift │ │ │ ├── DomainListVC+ContextMenu.swift │ │ │ ├── DomainListVC.swift │ │ │ ├── DomainListWindowController.swift │ │ │ ├── SelectionVC.swift │ │ │ └── Subclass │ │ │ └── PMTableView.swift │ ├── Onboarding │ │ ├── OnboardingView.swift │ │ └── OnboardingWindowController.swift │ ├── PHP Config Editor │ │ ├── Data │ │ │ ├── BytePhpPreference.swift │ │ │ └── PhpPreference.swift │ │ └── UI │ │ │ ├── ByteLimitView.swift │ │ │ ├── ConfigManagerView.swift │ │ │ └── ConfigManagerWindowController.swift │ ├── PHP Doctor │ │ ├── Data │ │ │ ├── PhpConfigChecker.swift │ │ │ ├── Warning.swift │ │ │ └── WarningManager.swift │ │ └── UI │ │ │ ├── NoWarningsView.swift │ │ │ ├── PhpDoctorView.swift │ │ │ ├── PhpDoctorWindowController.swift │ │ │ └── WarningView.swift │ ├── PHP Extension Manager │ │ ├── Data │ │ │ └── BrewExtensionsObservable.swift │ │ └── UI │ │ │ ├── PhpExtensionManagerView+Actions.swift │ │ │ ├── PhpExtensionManagerView.swift │ │ │ └── PhpExtensionManagerWindowController.swift │ └── PHP Version Manager │ │ ├── Data │ │ ├── BrewFormula+UI.swift │ │ ├── BrewFormulaeObservable.swift │ │ └── Fake │ │ │ └── FakeBrewFormulaeHandler.swift │ │ └── UI │ │ ├── PhpVersionManagerView+Actions.swift │ │ ├── PhpVersionManagerView.swift │ │ └── PhpVersionManagerWindowController.swift ├── Vendor │ └── HotKey │ │ ├── HotKey.swift │ │ ├── HotKeysController.swift │ │ ├── Key.swift │ │ ├── KeyCombo.swift │ │ ├── LICENSE │ │ └── ModifierFlagsExtension.swift ├── de.lproj │ └── Localizable.strings ├── en.lproj │ └── Localizable.strings ├── es.lproj │ └── Localizable.strings ├── fr.lproj │ └── Localizable.strings ├── id.lproj │ └── Localizable.strings ├── nl.lproj │ └── Localizable.strings ├── phpmon.entitlements ├── pt-PT.lproj │ └── Localizable.strings ├── vi.lproj │ └── Localizable.strings └── zh-Hans.lproj │ └── Localizable.strings └── tests ├── PHP Monitor.xctestplan ├── Shared ├── TestableConfigurations.swift ├── Utility.swift └── XCPMApplication.swift ├── feature ├── FeatureTestCase.swift └── InternalSwitcherTest.swift ├── ui ├── DomainsListTest.swift ├── MainMenuTest.swift ├── StartupTest.swift ├── UITestCase.swift └── UpdateCheckTest.swift └── unit ├── Commands └── CommandTest.swift ├── Parsers ├── CaskFileParserTest.swift ├── Config │ ├── BytePhpPreferenceTest.swift │ └── PhpConfigurationFileTest.swift ├── ExtensionEnumeratorTest.swift ├── HomebrewPackageTest.swift ├── HomebrewUpgradableTest.swift ├── NginxConfigurationTest.swift ├── PhpExtensionTest.swift ├── ValetConfigurationTest.swift └── ValetRcTest.swift ├── Test Files ├── brew │ ├── brew-formula.json │ ├── brew-outdated.json │ ├── brew-services-normal.json │ ├── brew-services-sudo.json │ ├── brew-services.json │ └── phpmon-dev.rb ├── nginx │ ├── nginx-proxy.test │ ├── nginx-secure-proxy-custom-tld.test │ ├── nginx-secure-proxy.test │ ├── nginx-site-isolated.test │ └── nginx-site.test ├── php │ └── php.ini ├── phpmon │ └── phpmon-config.json └── valet │ ├── valet-config.json │ ├── valetrc.broken │ └── valetrc.valid ├── Testables ├── Filesystem │ ├── RealFileSystemTest.swift │ └── TestableFileSystemTest.swift ├── Shell │ ├── RealShellTest.swift │ └── TestableShellTest.swift └── TestableConfigurationTest.swift └── Versions ├── AppVersionTest.swift ├── PhpVersionDetectionTest.swift ├── PhpVersionNumberTest.swift ├── ValetVersionExtractorTest.swift └── VersionExtractorTest.swift /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: nicoverbruggen 2 | custom: ['https://nicoverbruggen.be/sponsor'] -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yaml: -------------------------------------------------------------------------------- 1 | name: 🐞 Bug report 2 | description: Something going wrong? File a bug report! 3 | title: "[Bug] " 4 | labels: [bug] 5 | assignees: nicoverbruggen 6 | body: 7 | - type: checkboxes 8 | attributes: 9 | label: Is there an existing issue for this? 10 | description: Please search to see if an issue already exists for the bug you encountered. 11 | options: 12 | - label: I have searched the existing issues 13 | required: true 14 | - type: textarea 15 | attributes: 16 | label: Current Behavior 17 | description: A concise description of what you're experiencing. 18 | validations: 19 | required: false 20 | - type: textarea 21 | attributes: 22 | label: Expected Behavior 23 | description: A concise description of what you expected to happen. 24 | validations: 25 | required: false 26 | - type: textarea 27 | attributes: 28 | label: Steps To Reproduce 29 | description: Steps to reproduce the behavior. 30 | placeholder: | 31 | 1. Open this menu... 32 | 2. Click here... 33 | 3. Scroll to... 34 | 4. See error... 35 | validations: 36 | required: false 37 | - type: textarea 38 | attributes: 39 | label: Environment 40 | description: | 41 | examples: 42 | - **macOS**: (e.g. Ventura 13.3) 43 | - **Valet**: (e.g. 4.0) 44 | - **PHP Monitor**: (e.g. 5.8) 45 | value: | 46 | - macOS: 47 | - Valet: 48 | - PHP Monitor: 49 | render: markdown 50 | validations: 51 | required: false 52 | - type: textarea 53 | attributes: 54 | label: Do you have a log file (or a screenshot) or any additional information? 55 | description: | 56 | You can start extra verbose logging by running: `touch ~/.config/phpmon/verbose` and restarting PHP Monitor. 57 | 58 | You can find the latest log in: `~/.config/phpmon/last_session.log`. Please attach it here! 59 | 60 | (You can attach images or log files by clicking this area to highlight it and then dragging files in.) 61 | validations: 62 | required: false -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yaml: -------------------------------------------------------------------------------- 1 | name: 😎 Feature request 2 | description: Do you have a great idea for an enhancement that could improve PHP Monitor? 3 | title: "[Feature] <title>" 4 | labels: [enhancement] 5 | assignees: nicoverbruggen 6 | body: 7 | - type: checkboxes 8 | attributes: 9 | label: Is there an existing issue for this? 10 | description: Please search to see if an issue already exists for the bug you encountered. Please make sure you've checked the discussions tab as well. Enhancement requests that are not immediately approved will be moved to a discussion instead, so you will find some there. 11 | options: 12 | - label: I have searched the existing issues and discussions 13 | required: true 14 | - type: textarea 15 | attributes: 16 | label: Is this feature request related to a problem? 17 | description: "A clear and concise description of what the problem is. For example: 'I am always frustrated when...'" 18 | validations: 19 | required: true 20 | - type: textarea 21 | attributes: 22 | label: Describe the solution you'd like to see 23 | description: What would be a user-friendly way of resolving this particular issue? 24 | validations: 25 | required: true 26 | - type: textarea 27 | attributes: 28 | label: Additional information or context 29 | description: Add any other context or screenshots about the feature request here. 30 | validations: 31 | required: false -------------------------------------------------------------------------------- /.github/contributing.md: -------------------------------------------------------------------------------- 1 | # Contribution Guidelines 2 | 3 | Thank you for your interest in contributing to PHP Monitor. 4 | 5 | I consider this project a bit of a nice side-project to my daily gig, so it is very much a personal affair where I love to tinker around. 6 | 7 | **While the code of the latest PHP Monitor release is public, many things are constantly in flux that may not be pushed to this repository yet.** 8 | 9 | I don't mean to be rude, but I don't want other people involved with the project beyond simply contributing a few small things here and there, as has been the case in the past. 10 | 11 | The extra mental overhead of having additional contributors to report to, whose code will need to be reviewed... it's a lot and it makes working on PHP Monitor less enjoyable for me. 12 | 13 | Plus, at this point, the majority of PHP Monitor's main functionality is also done. 14 | 15 | As a result, I may refer you to this file at some point. Again, I don't wish to be rude, but this general rule stands: 16 | 17 | **Making any changes in a fork and opening a pull request without opening an issue first will most likely result in your PR being closed without mercy.** 18 | 19 | To repeat, I am **not opposed** to small contributions and fixes, if they are **meaningful or insightful**. 20 | 21 | To learn more, please check out the [pull request template](/.github/pull_request_template.md) which contains more information about my contribution requirements. (This will also show up when you open a new PR.) 22 | 23 | Thank you for respecting this! -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | Hello there! Thank you for considering a pull request for PHP Monitor. 2 | 3 | Please read the text below first before you submit your PR. 4 | 5 | ## Do not PR unless... 6 | 7 | In order to make development and maintenance of PHP Monitor easier, I ask that you _avoid_ making a pull request in the following situations: 8 | 9 | * No issue has been associated with the changes you‘d like to merge 10 | * You have not announced you will be addressing a particular issue 11 | * The PR is a low effort change: e.g. commits that only fix typos or phrasing may not be accepted 12 | 13 | (If you believe the phrasing of particular text in the app is unclear or incorrect, please open an issue first.) 14 | 15 | In short: It is usually best to *get in touch first* if you are making substantial changes. 16 | 17 | ## About destination branches 18 | 19 | Please keep in mind that `main` is reserved for the current code state of the latest release and should *never* be the destination branch unless a new release is happening. **Pull requests that target `main` will be closed without mercy.** 20 | 21 | Usually, the best target is the stable `dev/x.x` branch that corresponds with the latest major version that is released. 22 | 23 | There may be a newer branch available, which is an appropriate place for bigger changes, but please keep in mind that it is usually best to announce you‘ll be working on such a change before you spend the time, since as the lead contributor I might not even want said change in the app. Thank you. 24 | 25 | ## Your changes 26 | 27 | (feel free to remove the disclaimer above) 28 | 29 | * Affected parts of the app: shared code / UI code / CLI (remove what does not apply) 30 | * Estimated impact on performance: none / low / high (remove what does not apply) 31 | * Made a new build with Xcode and tested this: yes / no (remove what does not apply) 32 | * Tested on macOS version + architecture: (e.g. "Monterey on M1" or "Big Sur on Intel") 33 | * References issue(s): (please reference the issue here, using # and the number of the issue) 34 | 35 | (please describe what you have changed here) -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | PHP Monitor.xcodeproj/project.xcworkspace 2 | PHP Monitor.xcodeproj/xcuserdata 3 | 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: 2 | - todo 3 | - identifier_name 4 | - force_try 5 | - force_cast 6 | 7 | opt_in_rules: 8 | - empty_count 9 | 10 | included: 11 | - phpmon 12 | - phpmon-tests 13 | 14 | excluded: 15 | - phpmon/Vendor 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019-2023 Nico Verbruggen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /PHP Monitor.xcodeproj/xcshareddata/xcschemes/Unit Tests.xcscheme: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8"?> 2 | <Scheme 3 | LastUpgradeVersion = "1620" 4 | version = "1.3"> 5 | <BuildAction 6 | parallelizeBuildables = "YES" 7 | buildImplicitDependencies = "YES"> 8 | </BuildAction> 9 | <TestAction 10 | buildConfiguration = "Debug" 11 | selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" 12 | selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" 13 | shouldUseLaunchSchemeArgsEnv = "YES"> 14 | <Testables> 15 | <TestableReference 16 | skipped = "NO"> 17 | <BuildableReference 18 | BuildableIdentifier = "primary" 19 | BlueprintIdentifier = "C4F7807825D7F84B000DBC97" 20 | BuildableName = "Unit Tests.xctest" 21 | BlueprintName = "Unit Tests" 22 | ReferencedContainer = "container:PHP Monitor.xcodeproj"> 23 | </BuildableReference> 24 | </TestableReference> 25 | </Testables> 26 | </TestAction> 27 | <LaunchAction 28 | buildConfiguration = "Debug" 29 | selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" 30 | selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" 31 | launchStyle = "0" 32 | useCustomWorkingDirectory = "NO" 33 | ignoresPersistentStateOnLaunch = "NO" 34 | debugDocumentVersioning = "YES" 35 | debugServiceExtension = "internal" 36 | allowLocationSimulation = "YES"> 37 | </LaunchAction> 38 | <ProfileAction 39 | buildConfiguration = "Release" 40 | shouldUseLaunchSchemeArgsEnv = "YES" 41 | savedToolIdentifier = "" 42 | useCustomWorkingDirectory = "NO" 43 | debugDocumentVersioning = "YES"> 44 | </ProfileAction> 45 | <AnalyzeAction 46 | buildConfiguration = "Debug"> 47 | </AnalyzeAction> 48 | <ArchiveAction 49 | buildConfiguration = "Release" 50 | revealArchiveInOrganizer = "YES"> 51 | </ArchiveAction> 52 | </Scheme> 53 | -------------------------------------------------------------------------------- /assets/affinity/icon-dev.afdesign: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicoverbruggen/phpmon/4827c4a44b9f7dc1a9f561ea2537713976c3d68c/assets/affinity/icon-dev.afdesign -------------------------------------------------------------------------------- /assets/affinity/icon-eap.afdesign: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicoverbruggen/phpmon/4827c4a44b9f7dc1a9f561ea2537713976c3d68c/assets/affinity/icon-eap.afdesign -------------------------------------------------------------------------------- /assets/affinity/icon-updater.afdesign: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicoverbruggen/phpmon/4827c4a44b9f7dc1a9f561ea2537713976c3d68c/assets/affinity/icon-updater.afdesign -------------------------------------------------------------------------------- /assets/affinity/icon.afdesign: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicoverbruggen/phpmon/4827c4a44b9f7dc1a9f561ea2537713976c3d68c/assets/affinity/icon.afdesign -------------------------------------------------------------------------------- /assets/affinity/icons.afdesign: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicoverbruggen/phpmon/4827c4a44b9f7dc1a9f561ea2537713976c3d68c/assets/affinity/icons.afdesign -------------------------------------------------------------------------------- /assets/menubar.svg: -------------------------------------------------------------------------------- 1 | <svg width="22" height="22" xmlns="http://www.w3.org/2000/svg"><path d="M8.975 8.375H8.3V6.844a.223.223 0 00-.225-.219h-.45a.223.223 0 00-.225.219V9.03c0 .12.101.219.225.219h1.35a.223.223 0 00.225-.219v-.437a.223.223 0 00-.225-.219zm10.575 5.25h-.45v-2.956c0-.347-.143-.68-.397-.927l-2.81-2.731a1.37 1.37 0 00-.953-.386H13.7V5.312C13.7 4.588 13.095 4 12.35 4h-9C2.605 4 2 4.588 2 5.313v8.75c0 .724.605 1.312 1.35 1.312h.45C3.8 16.825 5.01 18 6.5 18c1.49 0 2.7-1.176 2.7-2.625h3.6c0 1.45 1.21 2.625 2.7 2.625 1.49 0 2.7-1.176 2.7-2.625h1.35c.247 0 .45-.197.45-.438v-.874a.445.445 0 00-.45-.438zM6.5 16.688c-.745 0-1.35-.588-1.35-1.313s.605-1.313 1.35-1.313c.745 0 1.35.588 1.35 1.313s-.605 1.313-1.35 1.313zm1.35-4.813c-1.74 0-3.15-1.37-3.15-3.063C4.7 7.12 6.11 5.75 7.85 5.75S11 7.12 11 8.813c0 1.692-1.41 3.062-3.15 3.062zm7.65 4.813c-.745 0-1.35-.588-1.35-1.313s.605-1.313 1.35-1.313c.745 0 1.35.588 1.35 1.313s-.605 1.313-1.35 1.313zM17.75 11H13.7V7.937h1.24l2.81 2.732V11z" fill="#000" fill-rule="nonzero"/></svg> -------------------------------------------------------------------------------- /docs/build.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicoverbruggen/phpmon/4827c4a44b9f7dc1a9f561ea2537713976c3d68c/docs/build.png -------------------------------------------------------------------------------- /docs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicoverbruggen/phpmon/4827c4a44b9f7dc1a9f561ea2537713976c3d68c/docs/logo.png -------------------------------------------------------------------------------- /docs/notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicoverbruggen/phpmon/4827c4a44b9f7dc1a9f561ea2537713976c3d68c/docs/notification.png -------------------------------------------------------------------------------- /docs/screenshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicoverbruggen/phpmon/4827c4a44b9f7dc1a9f561ea2537713976c3d68c/docs/screenshot.jpg -------------------------------------------------------------------------------- /integrations/phpmon.alfredworkflow: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicoverbruggen/phpmon/4827c4a44b9f7dc1a9f561ea2537713976c3d68c/integrations/phpmon.alfredworkflow -------------------------------------------------------------------------------- /phpmon-updater/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /phpmon-updater/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "icon_16x16.png", 5 | "idiom" : "mac", 6 | "scale" : "1x", 7 | "size" : "16x16" 8 | }, 9 | { 10 | "filename" : "icon_16x16@2x.png", 11 | "idiom" : "mac", 12 | "scale" : "2x", 13 | "size" : "16x16" 14 | }, 15 | { 16 | "filename" : "icon_32x32.png", 17 | "idiom" : "mac", 18 | "scale" : "1x", 19 | "size" : "32x32" 20 | }, 21 | { 22 | "filename" : "icon_32x32@2x.png", 23 | "idiom" : "mac", 24 | "scale" : "2x", 25 | "size" : "32x32" 26 | }, 27 | { 28 | "filename" : "icon_128x128.png", 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "filename" : "icon_128x128@2x.png", 35 | "idiom" : "mac", 36 | "scale" : "2x", 37 | "size" : "128x128" 38 | }, 39 | { 40 | "filename" : "icon_256x256.png", 41 | "idiom" : "mac", 42 | "scale" : "1x", 43 | "size" : "256x256" 44 | }, 45 | { 46 | "filename" : "icon_256x256@2x.png", 47 | "idiom" : "mac", 48 | "scale" : "2x", 49 | "size" : "256x256" 50 | }, 51 | { 52 | "filename" : "icon_512x512.png", 53 | "idiom" : "mac", 54 | "scale" : "1x", 55 | "size" : "512x512" 56 | }, 57 | { 58 | "filename" : "icon_512x512@2x.png", 59 | "idiom" : "mac", 60 | "scale" : "2x", 61 | "size" : "512x512" 62 | } 63 | ], 64 | "info" : { 65 | "author" : "xcode", 66 | "version" : 1 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /phpmon-updater/Assets.xcassets/AppIcon.appiconset/icon_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicoverbruggen/phpmon/4827c4a44b9f7dc1a9f561ea2537713976c3d68c/phpmon-updater/Assets.xcassets/AppIcon.appiconset/icon_128x128.png -------------------------------------------------------------------------------- /phpmon-updater/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicoverbruggen/phpmon/4827c4a44b9f7dc1a9f561ea2537713976c3d68c/phpmon-updater/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png -------------------------------------------------------------------------------- /phpmon-updater/Assets.xcassets/AppIcon.appiconset/icon_16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicoverbruggen/phpmon/4827c4a44b9f7dc1a9f561ea2537713976c3d68c/phpmon-updater/Assets.xcassets/AppIcon.appiconset/icon_16x16.png -------------------------------------------------------------------------------- /phpmon-updater/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicoverbruggen/phpmon/4827c4a44b9f7dc1a9f561ea2537713976c3d68c/phpmon-updater/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png -------------------------------------------------------------------------------- /phpmon-updater/Assets.xcassets/AppIcon.appiconset/icon_256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicoverbruggen/phpmon/4827c4a44b9f7dc1a9f561ea2537713976c3d68c/phpmon-updater/Assets.xcassets/AppIcon.appiconset/icon_256x256.png -------------------------------------------------------------------------------- /phpmon-updater/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicoverbruggen/phpmon/4827c4a44b9f7dc1a9f561ea2537713976c3d68c/phpmon-updater/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png -------------------------------------------------------------------------------- /phpmon-updater/Assets.xcassets/AppIcon.appiconset/icon_32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicoverbruggen/phpmon/4827c4a44b9f7dc1a9f561ea2537713976c3d68c/phpmon-updater/Assets.xcassets/AppIcon.appiconset/icon_32x32.png -------------------------------------------------------------------------------- /phpmon-updater/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicoverbruggen/phpmon/4827c4a44b9f7dc1a9f561ea2537713976c3d68c/phpmon-updater/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png -------------------------------------------------------------------------------- /phpmon-updater/Assets.xcassets/AppIcon.appiconset/icon_512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicoverbruggen/phpmon/4827c4a44b9f7dc1a9f561ea2537713976c3d68c/phpmon-updater/Assets.xcassets/AppIcon.appiconset/icon_512x512.png -------------------------------------------------------------------------------- /phpmon-updater/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicoverbruggen/phpmon/4827c4a44b9f7dc1a9f561ea2537713976c3d68c/phpmon-updater/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png -------------------------------------------------------------------------------- /phpmon-updater/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /phpmon-updater/main.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // PHP Monitor Self-Updater 4 | // 5 | // Created by Nico Verbruggen on 01/02/2023. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import NVAppUpdater 11 | 12 | let delegate = SelfUpdater( 13 | appName: "PHP Monitor", 14 | bundleIdentifiers: [ 15 | "com.nicoverbruggen.phpmon.eap", 16 | "com.nicoverbruggen.phpmon.dev", 17 | "com.nicoverbruggen.phpmon" 18 | ], 19 | selfUpdaterPath: "~/.config/phpmon/updater" 20 | ) 21 | 22 | NSApplication.shared.delegate = delegate 23 | _ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv) 24 | -------------------------------------------------------------------------------- /phpmon-updater/phpmon-updater.entitlements: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8"?> 2 | <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> 3 | <plist version="1.0"> 4 | <dict/> 5 | </plist> 6 | -------------------------------------------------------------------------------- /phpmon/Assets.xcassets/AppColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.988", 9 | "green" : "0.580", 10 | "red" : "0.278" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.988", 27 | "green" : "0.723", 28 | "red" : "0.277" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /phpmon/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "icon_16x16.png", 5 | "idiom" : "mac", 6 | "scale" : "1x", 7 | "size" : "16x16" 8 | }, 9 | { 10 | "filename" : "icon_16x16@2x.png", 11 | "idiom" : "mac", 12 | "scale" : "2x", 13 | "size" : "16x16" 14 | }, 15 | { 16 | "filename" : "icon_32x32.png", 17 | "idiom" : "mac", 18 | "scale" : "1x", 19 | "size" : "32x32" 20 | }, 21 | { 22 | "filename" : "icon_32x32@2x.png", 23 | "idiom" : "mac", 24 | "scale" : "2x", 25 | "size" : "32x32" 26 | }, 27 | { 28 | "filename" : "icon_128x128.png", 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "filename" : "icon_128x128@2x.png", 35 | "idiom" : "mac", 36 | "scale" : "2x", 37 | "size" : "128x128" 38 | }, 39 | { 40 | "filename" : "icon_256x256.png", 41 | "idiom" : "mac", 42 | "scale" : "1x", 43 | "size" : "256x256" 44 | }, 45 | { 46 | "filename" : "icon_256x256@2x.png", 47 | "idiom" : "mac", 48 | "scale" : "2x", 49 | "size" : "256x256" 50 | }, 51 | { 52 | "filename" : "icon_512x512.png", 53 | "idiom" : "mac", 54 | "scale" : "1x", 55 | "size" : "512x512" 56 | }, 57 | { 58 | "filename" : "icon_512x512@2x.png", 59 | "idiom" : "mac", 60 | "scale" : "2x", 61 | "size" : "512x512" 62 | } 63 | ], 64 | "info" : { 65 | "author" : "xcode", 66 | "version" : 1 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /phpmon/Assets.xcassets/AppIcon.appiconset/icon_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicoverbruggen/phpmon/4827c4a44b9f7dc1a9f561ea2537713976c3d68c/phpmon/Assets.xcassets/AppIcon.appiconset/icon_128x128.png -------------------------------------------------------------------------------- /phpmon/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicoverbruggen/phpmon/4827c4a44b9f7dc1a9f561ea2537713976c3d68c/phpmon/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png -------------------------------------------------------------------------------- /phpmon/Assets.xcassets/AppIcon.appiconset/icon_16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicoverbruggen/phpmon/4827c4a44b9f7dc1a9f561ea2537713976c3d68c/phpmon/Assets.xcassets/AppIcon.appiconset/icon_16x16.png -------------------------------------------------------------------------------- /phpmon/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicoverbruggen/phpmon/4827c4a44b9f7dc1a9f561ea2537713976c3d68c/phpmon/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png -------------------------------------------------------------------------------- /phpmon/Assets.xcassets/AppIcon.appiconset/icon_256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicoverbruggen/phpmon/4827c4a44b9f7dc1a9f561ea2537713976c3d68c/phpmon/Assets.xcassets/AppIcon.appiconset/icon_256x256.png -------------------------------------------------------------------------------- /phpmon/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicoverbruggen/phpmon/4827c4a44b9f7dc1a9f561ea2537713976c3d68c/phpmon/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png -------------------------------------------------------------------------------- /phpmon/Assets.xcassets/AppIcon.appiconset/icon_32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicoverbruggen/phpmon/4827c4a44b9f7dc1a9f561ea2537713976c3d68c/phpmon/Assets.xcassets/AppIcon.appiconset/icon_32x32.png -------------------------------------------------------------------------------- /phpmon/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicoverbruggen/phpmon/4827c4a44b9f7dc1a9f561ea2537713976c3d68c/phpmon/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png -------------------------------------------------------------------------------- /phpmon/Assets.xcassets/AppIcon.appiconset/icon_512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicoverbruggen/phpmon/4827c4a44b9f7dc1a9f561ea2537713976c3d68c/phpmon/Assets.xcassets/AppIcon.appiconset/icon_512x512.png -------------------------------------------------------------------------------- /phpmon/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicoverbruggen/phpmon/4827c4a44b9f7dc1a9f561ea2537713976c3d68c/phpmon/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png -------------------------------------------------------------------------------- /phpmon/Assets.xcassets/AppIconDev.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "icon_16x16.png", 5 | "idiom" : "mac", 6 | "scale" : "1x", 7 | "size" : "16x16" 8 | }, 9 | { 10 | "filename" : "icon_16x16@2x.png", 11 | "idiom" : "mac", 12 | "scale" : "2x", 13 | "size" : "16x16" 14 | }, 15 | { 16 | "filename" : "icon_32x32.png", 17 | "idiom" : "mac", 18 | "scale" : "1x", 19 | "size" : "32x32" 20 | }, 21 | { 22 | "filename" : "icon_32x32@2x.png", 23 | "idiom" : "mac", 24 | "scale" : "2x", 25 | "size" : "32x32" 26 | }, 27 | { 28 | "filename" : "icon_128x128.png", 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "filename" : "icon_128x128@2x.png", 35 | "idiom" : "mac", 36 | "scale" : "2x", 37 | "size" : "128x128" 38 | }, 39 | { 40 | "filename" : "icon_256x256.png", 41 | "idiom" : "mac", 42 | "scale" : "1x", 43 | "size" : "256x256" 44 | }, 45 | { 46 | "filename" : "icon_256x256@2x.png", 47 | "idiom" : "mac", 48 | "scale" : "2x", 49 | "size" : "256x256" 50 | }, 51 | { 52 | "filename" : "icon_512x512.png", 53 | "idiom" : "mac", 54 | "scale" : "1x", 55 | "size" : "512x512" 56 | }, 57 | { 58 | "filename" : "icon_512x512@2x.png", 59 | "idiom" : "mac", 60 | "scale" : "2x", 61 | "size" : "512x512" 62 | } 63 | ], 64 | "info" : { 65 | "author" : "xcode", 66 | "version" : 1 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /phpmon/Assets.xcassets/AppIconDev.appiconset/icon_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicoverbruggen/phpmon/4827c4a44b9f7dc1a9f561ea2537713976c3d68c/phpmon/Assets.xcassets/AppIconDev.appiconset/icon_128x128.png -------------------------------------------------------------------------------- /phpmon/Assets.xcassets/AppIconDev.appiconset/icon_128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicoverbruggen/phpmon/4827c4a44b9f7dc1a9f561ea2537713976c3d68c/phpmon/Assets.xcassets/AppIconDev.appiconset/icon_128x128@2x.png -------------------------------------------------------------------------------- /phpmon/Assets.xcassets/AppIconDev.appiconset/icon_16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicoverbruggen/phpmon/4827c4a44b9f7dc1a9f561ea2537713976c3d68c/phpmon/Assets.xcassets/AppIconDev.appiconset/icon_16x16.png -------------------------------------------------------------------------------- /phpmon/Assets.xcassets/AppIconDev.appiconset/icon_16x16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicoverbruggen/phpmon/4827c4a44b9f7dc1a9f561ea2537713976c3d68c/phpmon/Assets.xcassets/AppIconDev.appiconset/icon_16x16@2x.png -------------------------------------------------------------------------------- /phpmon/Assets.xcassets/AppIconDev.appiconset/icon_256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicoverbruggen/phpmon/4827c4a44b9f7dc1a9f561ea2537713976c3d68c/phpmon/Assets.xcassets/AppIconDev.appiconset/icon_256x256.png -------------------------------------------------------------------------------- /phpmon/Assets.xcassets/AppIconDev.appiconset/icon_256x256@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicoverbruggen/phpmon/4827c4a44b9f7dc1a9f561ea2537713976c3d68c/phpmon/Assets.xcassets/AppIconDev.appiconset/icon_256x256@2x.png -------------------------------------------------------------------------------- /phpmon/Assets.xcassets/AppIconDev.appiconset/icon_32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicoverbruggen/phpmon/4827c4a44b9f7dc1a9f561ea2537713976c3d68c/phpmon/Assets.xcassets/AppIconDev.appiconset/icon_32x32.png -------------------------------------------------------------------------------- /phpmon/Assets.xcassets/AppIconDev.appiconset/icon_32x32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicoverbruggen/phpmon/4827c4a44b9f7dc1a9f561ea2537713976c3d68c/phpmon/Assets.xcassets/AppIconDev.appiconset/icon_32x32@2x.png -------------------------------------------------------------------------------- /phpmon/Assets.xcassets/AppIconDev.appiconset/icon_512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicoverbruggen/phpmon/4827c4a44b9f7dc1a9f561ea2537713976c3d68c/phpmon/Assets.xcassets/AppIconDev.appiconset/icon_512x512.png -------------------------------------------------------------------------------- /phpmon/Assets.xcassets/AppIconDev.appiconset/icon_512x512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicoverbruggen/phpmon/4827c4a44b9f7dc1a9f561ea2537713976c3d68c/phpmon/Assets.xcassets/AppIconDev.appiconset/icon_512x512@2x.png -------------------------------------------------------------------------------- /phpmon/Assets.xcassets/AppIconEAP.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "icon_16x16.png", 5 | "idiom" : "mac", 6 | "scale" : "1x", 7 | "size" : "16x16" 8 | }, 9 | { 10 | "filename" : "icon_16x16@2x.png", 11 | "idiom" : "mac", 12 | "scale" : "2x", 13 | "size" : "16x16" 14 | }, 15 | { 16 | "filename" : "icon_32x32.png", 17 | "idiom" : "mac", 18 | "scale" : "1x", 19 | "size" : "32x32" 20 | }, 21 | { 22 | "filename" : "icon_32x32@2x.png", 23 | "idiom" : "mac", 24 | "scale" : "2x", 25 | "size" : "32x32" 26 | }, 27 | { 28 | "filename" : "icon_128x128.png", 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "filename" : "icon_128x128@2x.png", 35 | "idiom" : "mac", 36 | "scale" : "2x", 37 | "size" : "128x128" 38 | }, 39 | { 40 | "filename" : "icon_256x256.png", 41 | "idiom" : "mac", 42 | "scale" : "1x", 43 | "size" : "256x256" 44 | }, 45 | { 46 | "filename" : "icon_256x256@2x.png", 47 | "idiom" : "mac", 48 | "scale" : "2x", 49 | "size" : "256x256" 50 | }, 51 | { 52 | "filename" : "icon_512x512.png", 53 | "idiom" : "mac", 54 | "scale" : "1x", 55 | "size" : "512x512" 56 | }, 57 | { 58 | "filename" : "icon_512x512@2x.png", 59 | "idiom" : "mac", 60 | "scale" : "2x", 61 | "size" : "512x512" 62 | } 63 | ], 64 | "info" : { 65 | "author" : "xcode", 66 | "version" : 1 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /phpmon/Assets.xcassets/AppIconEAP.appiconset/icon_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicoverbruggen/phpmon/4827c4a44b9f7dc1a9f561ea2537713976c3d68c/phpmon/Assets.xcassets/AppIconEAP.appiconset/icon_128x128.png -------------------------------------------------------------------------------- /phpmon/Assets.xcassets/AppIconEAP.appiconset/icon_128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicoverbruggen/phpmon/4827c4a44b9f7dc1a9f561ea2537713976c3d68c/phpmon/Assets.xcassets/AppIconEAP.appiconset/icon_128x128@2x.png -------------------------------------------------------------------------------- /phpmon/Assets.xcassets/AppIconEAP.appiconset/icon_16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicoverbruggen/phpmon/4827c4a44b9f7dc1a9f561ea2537713976c3d68c/phpmon/Assets.xcassets/AppIconEAP.appiconset/icon_16x16.png -------------------------------------------------------------------------------- /phpmon/Assets.xcassets/AppIconEAP.appiconset/icon_16x16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicoverbruggen/phpmon/4827c4a44b9f7dc1a9f561ea2537713976c3d68c/phpmon/Assets.xcassets/AppIconEAP.appiconset/icon_16x16@2x.png -------------------------------------------------------------------------------- /phpmon/Assets.xcassets/AppIconEAP.appiconset/icon_256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicoverbruggen/phpmon/4827c4a44b9f7dc1a9f561ea2537713976c3d68c/phpmon/Assets.xcassets/AppIconEAP.appiconset/icon_256x256.png -------------------------------------------------------------------------------- /phpmon/Assets.xcassets/AppIconEAP.appiconset/icon_256x256@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicoverbruggen/phpmon/4827c4a44b9f7dc1a9f561ea2537713976c3d68c/phpmon/Assets.xcassets/AppIconEAP.appiconset/icon_256x256@2x.png -------------------------------------------------------------------------------- /phpmon/Assets.xcassets/AppIconEAP.appiconset/icon_32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicoverbruggen/phpmon/4827c4a44b9f7dc1a9f561ea2537713976c3d68c/phpmon/Assets.xcassets/AppIconEAP.appiconset/icon_32x32.png -------------------------------------------------------------------------------- /phpmon/Assets.xcassets/AppIconEAP.appiconset/icon_32x32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicoverbruggen/phpmon/4827c4a44b9f7dc1a9f561ea2537713976c3d68c/phpmon/Assets.xcassets/AppIconEAP.appiconset/icon_32x32@2x.png -------------------------------------------------------------------------------- /phpmon/Assets.xcassets/AppIconEAP.appiconset/icon_512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicoverbruggen/phpmon/4827c4a44b9f7dc1a9f561ea2537713976c3d68c/phpmon/Assets.xcassets/AppIconEAP.appiconset/icon_512x512.png -------------------------------------------------------------------------------- /phpmon/Assets.xcassets/AppIconEAP.appiconset/icon_512x512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicoverbruggen/phpmon/4827c4a44b9f7dc1a9f561ea2537713976c3d68c/phpmon/Assets.xcassets/AppIconEAP.appiconset/icon_512x512@2x.png -------------------------------------------------------------------------------- /phpmon/Assets.xcassets/AppSecondary.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.250", 9 | "green" : "0.250", 10 | "red" : "0.250" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.750", 27 | "green" : "0.750", 28 | "red" : "0.750" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /phpmon/Assets.xcassets/Checkmark.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "check.svg", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | }, 21 | "properties" : { 22 | "template-rendering-intent" : "template" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /phpmon/Assets.xcassets/Checkmark.imageset/check.svg: -------------------------------------------------------------------------------- 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!-- Font Awesome Pro 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) --><path d="M435.848 83.466L172.804 346.51l-96.652-96.652c-4.686-4.686-12.284-4.686-16.971 0l-28.284 28.284c-4.686 4.686-4.686 12.284 0 16.971l133.421 133.421c4.686 4.686 12.284 4.686 16.971 0l299.813-299.813c4.686-4.686 4.686-12.284 0-16.971l-28.284-28.284c-4.686-4.686-12.284-4.686-16.97 0z"/></svg> -------------------------------------------------------------------------------- /phpmon/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /phpmon/Assets.xcassets/IconColorGreen.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.501", 9 | "green" : "0.697", 10 | "red" : "0.247" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.501", 27 | "green" : "0.765", 28 | "red" : "0.247" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /phpmon/Assets.xcassets/IconColorNormal.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.000", 9 | "green" : "0.000", 10 | "red" : "0.000" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "1.000", 27 | "green" : "1.000", 28 | "red" : "1.000" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /phpmon/Assets.xcassets/IconColorRed.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.180", 9 | "green" : "0.000", 10 | "red" : "1.000" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.426", 27 | "green" : "0.363", 28 | "red" : "1.000" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /phpmon/Assets.xcassets/IconDefault.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Default.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "Default@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | }, 22 | "properties" : { 23 | "template-rendering-intent" : "template" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /phpmon/Assets.xcassets/IconDefault.imageset/Default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicoverbruggen/phpmon/4827c4a44b9f7dc1a9f561ea2537713976c3d68c/phpmon/Assets.xcassets/IconDefault.imageset/Default.png -------------------------------------------------------------------------------- /phpmon/Assets.xcassets/IconDefault.imageset/Default@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicoverbruggen/phpmon/4827c4a44b9f7dc1a9f561ea2537713976c3d68c/phpmon/Assets.xcassets/IconDefault.imageset/Default@2x.png -------------------------------------------------------------------------------- /phpmon/Assets.xcassets/IconLinked.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Linked.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "Linked@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | }, 22 | "properties" : { 23 | "template-rendering-intent" : "template" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /phpmon/Assets.xcassets/IconLinked.imageset/Linked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicoverbruggen/phpmon/4827c4a44b9f7dc1a9f561ea2537713976c3d68c/phpmon/Assets.xcassets/IconLinked.imageset/Linked.png -------------------------------------------------------------------------------- /phpmon/Assets.xcassets/IconLinked.imageset/Linked@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicoverbruggen/phpmon/4827c4a44b9f7dc1a9f561ea2537713976c3d68c/phpmon/Assets.xcassets/IconLinked.imageset/Linked@2x.png -------------------------------------------------------------------------------- /phpmon/Assets.xcassets/IconParked.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Parked.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "Parked@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | }, 22 | "properties" : { 23 | "template-rendering-intent" : "template" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /phpmon/Assets.xcassets/IconParked.imageset/Parked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicoverbruggen/phpmon/4827c4a44b9f7dc1a9f561ea2537713976c3d68c/phpmon/Assets.xcassets/IconParked.imageset/Parked.png -------------------------------------------------------------------------------- /phpmon/Assets.xcassets/IconParked.imageset/Parked@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicoverbruggen/phpmon/4827c4a44b9f7dc1a9f561ea2537713976c3d68c/phpmon/Assets.xcassets/IconParked.imageset/Parked@2x.png -------------------------------------------------------------------------------- /phpmon/Assets.xcassets/IconProxy.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Proxy.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "Proxy@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | }, 22 | "properties" : { 23 | "template-rendering-intent" : "template" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /phpmon/Assets.xcassets/IconProxy.imageset/Proxy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicoverbruggen/phpmon/4827c4a44b9f7dc1a9f561ea2537713976c3d68c/phpmon/Assets.xcassets/IconProxy.imageset/Proxy.png -------------------------------------------------------------------------------- /phpmon/Assets.xcassets/IconProxy.imageset/Proxy@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicoverbruggen/phpmon/4827c4a44b9f7dc1a9f561ea2537713976c3d68c/phpmon/Assets.xcassets/IconProxy.imageset/Proxy@2x.png -------------------------------------------------------------------------------- /phpmon/Assets.xcassets/Isolated.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Isolated.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "Isolated@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | }, 22 | "properties" : { 23 | "template-rendering-intent" : "template" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /phpmon/Assets.xcassets/Isolated.imageset/Isolated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicoverbruggen/phpmon/4827c4a44b9f7dc1a9f561ea2537713976c3d68c/phpmon/Assets.xcassets/Isolated.imageset/Isolated.png -------------------------------------------------------------------------------- /phpmon/Assets.xcassets/Isolated.imageset/Isolated@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicoverbruggen/phpmon/4827c4a44b9f7dc1a9f561ea2537713976c3d68c/phpmon/Assets.xcassets/Isolated.imageset/Isolated@2x.png -------------------------------------------------------------------------------- /phpmon/Assets.xcassets/Lock.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "Locked.svg", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | }, 21 | "properties" : { 22 | "template-rendering-intent" : "template" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /phpmon/Assets.xcassets/Lock.imageset/Locked.svg: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8" standalone="no"?> 2 | <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> 3 | <svg width="100%" height="100%" viewBox="0 0 30 30" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"> 4 | <rect id="Locked" x="0" y="0" width="30" height="30" style="fill:none;"/> 5 | <g id="Locked1" serif:id="Locked"> 6 | <g transform="matrix(0.0468317,0,0,0.0468317,4.50971,3.01112)"> 7 | <path d="M400,256L152,256L152,152.9C152,113.3 183.7,80.4 223.3,80C263.3,79.6 296,112.1 296,152L296,266.079C296,279.379 376,279.137 376,265.837L376,152C376,68 307.5,-0.3 223.5,0C139.5,0.3 72,69.5 72,153.5L72,256L48,256C21.5,256 0,277.5 0,304L0,464C0,490.5 21.5,512 48,512L400,512C426.5,512 448,490.5 448,464L448,304C448,277.5 426.5,256 400,256Z" style="fill-rule:nonzero;"/> 8 | </g> 9 | </g> 10 | </svg> 11 | -------------------------------------------------------------------------------- /phpmon/Assets.xcassets/LockUnlocked.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "Unlocked.svg", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | }, 21 | "properties" : { 22 | "template-rendering-intent" : "template" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /phpmon/Assets.xcassets/LockUnlocked.imageset/Unlocked.svg: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8" standalone="no"?> 2 | <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> 3 | <svg width="100%" height="100%" viewBox="0 0 30 30" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"> 4 | <rect id="Locked" x="0" y="0" width="30" height="30" style="fill:none;"/> 5 | <g id="Locked1" serif:id="Locked"> 6 | <g transform="matrix(0.0468317,0,0,0.0468317,4.50971,3.01112)"> 7 | <path d="M400,256L152,256L152,152.9C152,113.3 183.7,80.4 223.3,80C263.3,79.6 296,112.1 296,152L296,168C296,181.3 322.386,192 322.386,192L352,192C365.3,192 376,181.3 376,168L376,152C376,68 307.5,-0.3 223.5,0C139.5,0.3 72,69.5 72,153.5L72,256L48,256C21.5,256 0,277.5 0,304L0,464C0,490.5 21.5,512 48,512L400,512C426.5,512 448,490.5 448,464L448,304C448,277.5 426.5,256 400,256Z" style="fill-rule:nonzero;"/> 8 | </g> 9 | </g> 10 | </svg> 11 | -------------------------------------------------------------------------------- /phpmon/Assets.xcassets/Menu Bar Icons/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /phpmon/Assets.xcassets/Menu Bar Icons/MenuBar_elephant.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Menu Bar Elephant.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "Menu Bar Elephant@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /phpmon/Assets.xcassets/Menu Bar Icons/MenuBar_elephant.imageset/Menu Bar Elephant.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicoverbruggen/phpmon/4827c4a44b9f7dc1a9f561ea2537713976c3d68c/phpmon/Assets.xcassets/Menu Bar Icons/MenuBar_elephant.imageset/Menu Bar Elephant.png -------------------------------------------------------------------------------- /phpmon/Assets.xcassets/Menu Bar Icons/MenuBar_elephant.imageset/Menu Bar Elephant@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicoverbruggen/phpmon/4827c4a44b9f7dc1a9f561ea2537713976c3d68c/phpmon/Assets.xcassets/Menu Bar Icons/MenuBar_elephant.imageset/Menu Bar Elephant@2x.png -------------------------------------------------------------------------------- /phpmon/Assets.xcassets/Menu Bar Icons/MenuBar_php.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Menu Bar.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "Menu Bar@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /phpmon/Assets.xcassets/Menu Bar Icons/MenuBar_php.imageset/Menu Bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicoverbruggen/phpmon/4827c4a44b9f7dc1a9f561ea2537713976c3d68c/phpmon/Assets.xcassets/Menu Bar Icons/MenuBar_php.imageset/Menu Bar.png -------------------------------------------------------------------------------- /phpmon/Assets.xcassets/Menu Bar Icons/MenuBar_php.imageset/Menu Bar@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicoverbruggen/phpmon/4827c4a44b9f7dc1a9f561ea2537713976c3d68c/phpmon/Assets.xcassets/Menu Bar Icons/MenuBar_php.imageset/Menu Bar@2x.png -------------------------------------------------------------------------------- /phpmon/Assets.xcassets/SpinnerBackground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "1.000", 9 | "green" : "1.000", 10 | "red" : "1.000" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.172", 27 | "green" : "0.182", 28 | "red" : "0.182" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /phpmon/Assets.xcassets/StatusBarIcon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "phpmon.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "phpmon@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /phpmon/Assets.xcassets/StatusBarIcon.imageset/phpmon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicoverbruggen/phpmon/4827c4a44b9f7dc1a9f561ea2537713976c3d68c/phpmon/Assets.xcassets/StatusBarIcon.imageset/phpmon.png -------------------------------------------------------------------------------- /phpmon/Assets.xcassets/StatusBarIcon.imageset/phpmon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicoverbruggen/phpmon/4827c4a44b9f7dc1a9f561ea2537713976c3d68c/phpmon/Assets.xcassets/StatusBarIcon.imageset/phpmon@2x.png -------------------------------------------------------------------------------- /phpmon/Assets.xcassets/StatusBarIconStatic.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "phpmon.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "phpmon@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /phpmon/Assets.xcassets/StatusBarIconStatic.imageset/phpmon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicoverbruggen/phpmon/4827c4a44b9f7dc1a9f561ea2537713976c3d68c/phpmon/Assets.xcassets/StatusBarIconStatic.imageset/phpmon.png -------------------------------------------------------------------------------- /phpmon/Assets.xcassets/StatusBarIconStatic.imageset/phpmon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicoverbruggen/phpmon/4827c4a44b9f7dc1a9f561ea2537713976c3d68c/phpmon/Assets.xcassets/StatusBarIconStatic.imageset/phpmon@2x.png -------------------------------------------------------------------------------- /phpmon/Assets.xcassets/StatusColorBlue.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.988", 9 | "green" : "0.580", 10 | "red" : "0.278" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.988", 27 | "green" : "0.444", 28 | "red" : "0.277" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /phpmon/Assets.xcassets/StatusColorGreen.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.501", 9 | "green" : "0.697", 10 | "red" : "0.247" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.501", 27 | "green" : "0.765", 28 | "red" : "0.247" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /phpmon/Assets.xcassets/StatusColorRed.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.180", 9 | "green" : "0.000", 10 | "red" : "1.000" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.426", 27 | "green" : "0.363", 28 | "red" : "1.000" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /phpmon/Assets.xcassets/StatusColorYellow.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.180", 9 | "green" : "0.841", 10 | "red" : "1.000" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.426", 27 | "green" : "0.809", 28 | "red" : "1.000" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /phpmon/Assets.xcassets/StatusColorYellowTranslucent.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "0.300", 8 | "blue" : "0.180", 9 | "green" : "0.841", 10 | "red" : "1.000" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "0.300", 26 | "blue" : "0.426", 27 | "green" : "0.809", 28 | "red" : "1.000" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /phpmon/Assets.xcassets/php.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "php.svg", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | }, 21 | "properties" : { 22 | "preserves-vector-representation" : true, 23 | "template-rendering-intent" : "template" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /phpmon/Assets.xcassets/php.imageset/php.svg: -------------------------------------------------------------------------------- 1 | <svg xmlns="http://www.w3.org/2000/svg" width="800" height="800" viewBox="0 0 24 24"><path d="M7.01 10.207h-.944l-.515 2.648h.838c.556 0 .97-.105 1.242-.314.272-.21.455-.559.55-1.049.092-.47.05-.802-.124-.995-.175-.193-.523-.29-1.047-.29zM12 5.688C5.373 5.688 0 8.514 0 12s5.373 6.313 12 6.313S24 15.486 24 12c0-3.486-5.373-6.312-12-6.312zm-3.26 7.451c-.261.25-.575.438-.917.551-.336.108-.765.164-1.285.164H5.357l-.327 1.681H3.652l1.23-6.326h2.65c.797 0 1.378.209 1.744.628.366.418.476 1.002.33 1.752a2.836 2.836 0 0 1-.305.847c-.143.255-.33.49-.561.703zm4.024.715.543-2.799c.063-.318.039-.536-.068-.651-.107-.116-.336-.174-.687-.174H11.46l-.704 3.625H9.388l1.23-6.327h1.367l-.327 1.682h1.218c.767 0 1.295.134 1.586.401s.378.7.263 1.299l-.572 2.944h-1.389zm7.597-2.265a2.782 2.782 0 0 1-.305.847c-.143.255-.33.49-.561.703a2.44 2.44 0 0 1-.917.551c-.336.108-.765.164-1.286.164h-1.18l-.327 1.682h-1.378l1.23-6.326h2.649c.797 0 1.378.209 1.744.628.366.417.477 1.001.331 1.751zm-2.595-1.382h-.943l-.516 2.648h.838c.557 0 .971-.105 1.242-.314.272-.21.455-.559.551-1.049.092-.47.049-.802-.125-.995s-.524-.29-1.047-.29z"/></svg> -------------------------------------------------------------------------------- /phpmon/Common/Command/ActiveCommand.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ActiveCommand.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 12/10/2022. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | var Command: CommandProtocol { 12 | return ActiveCommand.shared 13 | } 14 | 15 | class ActiveCommand { 16 | static var shared: CommandProtocol = RealCommand() 17 | 18 | public static func useTestable(_ output: [String: String]) { 19 | Self.shared = TestableCommand(commands: output) 20 | } 21 | 22 | public static func useSystem() { 23 | Self.shared = RealCommand() 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /phpmon/Common/Command/CommandProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CommandProtocol.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 12/10/2022. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol CommandProtocol { 12 | 13 | /** 14 | Immediately executes a command. 15 | 16 | - Parameter path: The path of the command or program to invoke. 17 | - Parameter arguments: A list of arguments that are passed on. 18 | - Parameter trimNewlines: Removes empty new line output. 19 | - Parameter withStandardError: Outputs standard error output to the same string output as well. 20 | */ 21 | func execute( 22 | path: String, 23 | arguments: [String], 24 | trimNewlines: Bool, 25 | withStandardError: Bool 26 | ) -> String 27 | 28 | /** 29 | Immediately executes a command. 30 | 31 | - Parameter path: The path of the command or program to invoke. 32 | - Parameter arguments: A list of arguments that are passed on. 33 | - Parameter trimNewlines: Removes empty new line output. 34 | */ 35 | func execute( 36 | path: String, 37 | arguments: [String], 38 | trimNewlines: Bool 39 | ) -> String 40 | 41 | } 42 | -------------------------------------------------------------------------------- /phpmon/Common/Command/RealCommand.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Command.swift 3 | // PHP Monitor 4 | // 5 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 6 | // 7 | 8 | import Cocoa 9 | 10 | public class RealCommand: CommandProtocol { 11 | 12 | public func execute( 13 | path: String, 14 | arguments: [String], 15 | trimNewlines: Bool, 16 | withStandardError: Bool 17 | ) -> String { 18 | let task = Process() 19 | task.launchPath = path 20 | task.arguments = arguments 21 | 22 | let pipe = Pipe() 23 | task.standardOutput = pipe 24 | 25 | if withStandardError { 26 | task.standardError = pipe 27 | } 28 | 29 | task.launch() 30 | 31 | let data = pipe.fileHandleForReading.readDataToEndOfFile() 32 | let output: String = String.init(data: data, encoding: String.Encoding.utf8)! 33 | 34 | if trimNewlines { 35 | return output.components(separatedBy: .newlines) 36 | .filter({ !$0.isEmpty }) 37 | .joined(separator: "\n") 38 | } 39 | 40 | return output 41 | } 42 | 43 | public func execute( 44 | path: String, 45 | arguments: [String], 46 | trimNewlines: Bool = false 47 | ) -> String { 48 | self.execute( 49 | path: path, 50 | arguments: arguments, 51 | trimNewlines: trimNewlines, 52 | withStandardError: false 53 | ) 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /phpmon/Common/Core/Events.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Events.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 23/01/2022. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class Events { 12 | 13 | static let ServicesUpdated = Notification.Name("ServicesUpdated") 14 | 15 | } 16 | -------------------------------------------------------------------------------- /phpmon/Common/Core/Helpers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Helpers.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 24/12/2021. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | // MARK: Common Shell Commands 10 | 11 | /** 12 | Runs a `brew` command. Can run as superuser. 13 | */ 14 | func brew(_ command: String, sudo: Bool = false) async { 15 | await Shell.quiet("\(sudo ? "sudo " : "")" + "\(Paths.brew) \(command)") 16 | } 17 | 18 | /** 19 | Runs `sed` in order to replace all occurrences of a string in a specific file with another. 20 | */ 21 | func sed(file: String, original: String, replacement: String) async { 22 | // Escape slashes (or `sed` won't work) 23 | let e_original = original.replacingOccurrences(of: "/", with: "\\/") 24 | let e_replacement = replacement.replacingOccurrences(of: "/", with: "\\/") 25 | 26 | // Check if gsed exists; it is able to follow symlinks, 27 | // which we want to do to toggle the extension 28 | if FileSystem.fileExists("\(Paths.binPath)/gsed") { 29 | await Shell.quiet("\(Paths.binPath)/gsed -i --follow-symlinks 's/\(e_original)/\(e_replacement)/g' \(file)") 30 | } else { 31 | await Shell.quiet("sed -i '' 's/\(e_original)/\(e_replacement)/g' \(file)") 32 | } 33 | } 34 | 35 | /** 36 | Uses `grep` to determine whether a particular query string can be found in a particular file. 37 | */ 38 | func grepContains(file: String, query: String) async -> Bool { 39 | return await Shell.pipe(""" 40 | grep -q '\(query)' \(file); [ $? -eq 0 ] && echo "YES" || echo "NO" 41 | """).out 42 | .trimmingCharacters(in: .whitespacesAndNewlines) 43 | .contains("YES") 44 | } 45 | 46 | /** 47 | Attempts to introduce sleep for a particular duration. Use with caution. 48 | */ 49 | func delay(seconds: Double) async { 50 | try! await Task.sleep(nanoseconds: UInt64(seconds * 1_000_000_000)) 51 | } 52 | -------------------------------------------------------------------------------- /phpmon/Common/Core/Homebrew.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Homebrew.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 21/11/2022. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct HomebrewFormulae { 12 | static var php: HomebrewFormula { 13 | if PhpEnvironments.shared.homebrewPackage == nil { 14 | return HomebrewFormula("php", elevated: true) 15 | } 16 | 17 | guard let install = PhpEnvironments.phpInstall else { 18 | return HomebrewFormula("php", elevated: true) 19 | } 20 | 21 | return HomebrewFormula(install.formula, elevated: true) 22 | } 23 | 24 | static var nginx: HomebrewFormula { 25 | return BrewDiagnostics.usesNginxFullFormula 26 | ? HomebrewFormula("nginx-full", elevated: true) 27 | : HomebrewFormula("nginx", elevated: true) 28 | } 29 | 30 | static var dnsmasq: HomebrewFormula { 31 | return HomebrewFormula("dnsmasq", elevated: true) 32 | } 33 | } 34 | 35 | class HomebrewFormula: Equatable, Hashable, CustomStringConvertible { 36 | let name: String 37 | let elevated: Bool 38 | 39 | var description: String { 40 | return name 41 | } 42 | 43 | init(_ name: String, elevated: Bool = true) { 44 | self.name = name 45 | self.elevated = elevated 46 | } 47 | 48 | static func == (lhs: HomebrewFormula, rhs: HomebrewFormula) -> Bool { 49 | return lhs.elevated == rhs.elevated && lhs.name == rhs.name 50 | } 51 | 52 | public func hash(into hasher: inout Hasher) { 53 | hasher.combine(name) 54 | hasher.combine(elevated) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /phpmon/Common/Errors/AlertableError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Errors.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 06/02/2022. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public protocol AlertableError { 12 | func getErrorMessageKey() -> String 13 | } 14 | -------------------------------------------------------------------------------- /phpmon/Common/Errors/Errors.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VersionParseError.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 08/02/2022. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // MARK: - Alertable Errors 12 | // These errors must be resolved by the user. 13 | 14 | struct HomebrewPermissionError: Error, AlertableError { 15 | enum Kind: String { 16 | case applescriptNilError = "homebrew_permissions.applescript_returned_nil" 17 | } 18 | 19 | let kind: Kind 20 | 21 | func getErrorMessageKey() -> String { 22 | return "alert.errors.\(self.kind.rawValue)" 23 | } 24 | } 25 | 26 | // MARK: - Errors that do not have an associated alert message 27 | // The errors must be resolved by the developer. 28 | 29 | struct VersionParseError: Error {} 30 | -------------------------------------------------------------------------------- /phpmon/Common/Extensions/ArrayExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArrayExtension.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 11/06/2022. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Array { 12 | /** 13 | Sourced from Stack Overflow 14 | https://stackoverflow.com/a/33540708 15 | */ 16 | func chunked(by distance: Int) -> [[Element]] { 17 | let indicesSequence = stride(from: startIndex, to: endIndex, by: distance) 18 | let array: [[Element]] = indicesSequence.map { 19 | let newIndex = $0.advanced(by: distance) > endIndex ? endIndex : $0.advanced(by: distance) 20 | return Array(self[$0 ..< newIndex]) 21 | } 22 | return array 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /phpmon/Common/Extensions/DataExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DataExtension.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 16/10/2022. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Data { 12 | var prettyPrintedJSONString: NSString? { 13 | guard let object = try? JSONSerialization.jsonObject(with: self, options: []), 14 | let data = try? JSONSerialization.data(withJSONObject: object, options: [.prettyPrinted]), 15 | let prettyPrintedString = NSString(data: data, encoding: String.Encoding.utf8.rawValue) else { 16 | return nil 17 | } 18 | 19 | return prettyPrintedString 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /phpmon/Common/Extensions/DateExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Date.swift 3 | // PHP Monitor 4 | // 5 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 6 | // 7 | 8 | import Cocoa 9 | 10 | extension Date { 11 | 12 | func toString() -> String { 13 | let dateFormatter = DateFormatter() 14 | dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" 15 | return dateFormatter.string(from: self) 16 | } 17 | 18 | static func fromString(_ string: String) -> Date? { 19 | let dateFormatter = DateFormatter() 20 | dateFormatter.dateFormat = "yyyy-MM-dd" 21 | return dateFormatter.date(from: string) 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /phpmon/Common/Extensions/DictionaryExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DictionaryExtension.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 01/11/2022. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Dictionary { 12 | mutating func renameKey(fromKey: Key, toKey: Key) { 13 | if let entry = removeValue(forKey: fromKey) { 14 | self[toKey] = entry 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /phpmon/Common/Extensions/NSMenuExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSMenuExtension.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 14/04/2021. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | extension NSMenu { 12 | convenience init(items: [NSMenuItem], target: NSObject? = nil) { 13 | self.init() 14 | self.addItems(items, target: target) 15 | } 16 | 17 | public func addItems(_ items: [NSMenuItem], target: NSObject? = nil) { 18 | for item in items { 19 | self.addItem(item) 20 | if target != nil { 21 | item.target = target 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /phpmon/Common/Extensions/NSWindowExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSWindowExtension.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 17/02/2022. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Cocoa 11 | 12 | extension NSWindow { 13 | 14 | /** 15 | Centers a window. Taken from: https://stackoverflow.com/a/66140320 16 | */ 17 | public func setCenterPosition(offsetY: CGFloat = 0) { 18 | if let screenSize = screen?.visibleFrame.size { 19 | self.setFrameOrigin( 20 | NSPoint( 21 | x: (screenSize.width - frame.size.width) / 2, 22 | y: (screenSize.height - frame.size.height) / 2 + offsetY 23 | ) 24 | ) 25 | } 26 | } 27 | 28 | /** 29 | Shakes a window. Inspired by: http://blog.ericd.net/2016/09/30/shaking-a-macos-window/ 30 | */ 31 | func shake() { 32 | let numberOfShakes = 3, durationOfShake = 0.2, vigourOfShake: CGFloat = 0.03 33 | 34 | let frame: CGRect = self.frame 35 | let shakeAnimation: CAKeyframeAnimation = CAKeyframeAnimation() 36 | 37 | let shakePath = CGMutablePath() 38 | shakePath.move( to: CGPoint(x: frame.minX, y: frame.minY)) 39 | 40 | for _ in 0...numberOfShakes-1 { 41 | shakePath.addLine(to: CGPoint(x: frame.minX - frame.size.width * vigourOfShake, y: frame.minY)) 42 | shakePath.addLine(to: CGPoint(x: frame.minX + frame.size.width * vigourOfShake, y: frame.minY)) 43 | } 44 | 45 | shakePath.closeSubpath() 46 | shakeAnimation.path = shakePath 47 | shakeAnimation.duration = durationOfShake 48 | 49 | self.animations = ["frameOrigin": shakeAnimation] 50 | self.animator().setFrameOrigin(self.frame.origin) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /phpmon/Common/Extensions/NVAlertExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NVAlertExtension.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 16/07/2024. 6 | // Copyright © 2024 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import NVAlert 11 | 12 | extension NVAlert { 13 | /** 14 | Shows the modal for a particular error. 15 | */ 16 | @MainActor public static func show(for error: Error & AlertableError) { 17 | let key = error.getErrorMessageKey() 18 | return NVAlert().withInformation( 19 | title: "\(key).title".localized, 20 | subtitle: "\(key).description".localized 21 | ).withPrimary(text: "generic.ok".localized).show() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /phpmon/Common/Extensions/TimeIntervalExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TimeExtension.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 29/09/2022. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension TimeInterval { 12 | public static func minutes(_ amount: Int) -> TimeInterval { 13 | return Double(amount * 60) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /phpmon/Common/Extensions/XibLoadable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NibLoadable.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 04/02/2021. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Cocoa 11 | 12 | // Adapted from: https://stackoverflow.com/a/46268778 13 | 14 | protocol XibLoadable { 15 | 16 | static var xibName: String? { get } 17 | static func createFromXib(in bundle: Bundle) -> Self? 18 | 19 | } 20 | 21 | extension XibLoadable where Self: NSView { 22 | 23 | static var xibName: String? { 24 | return String(describing: Self.self) 25 | } 26 | 27 | static func createFromXib(in bundle: Bundle = Bundle.main) -> Self? { 28 | guard let xibName = xibName else { return nil } 29 | var topLevelArray: NSArray? 30 | bundle.loadNibNamed(NSNib.Name(xibName), owner: self, topLevelObjects: &topLevelArray) 31 | guard let results = topLevelArray else { return nil } 32 | let views = [Any](results).filter { $0 is Self } 33 | return views.last as? Self 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /phpmon/Common/Filesystem/ActiveFileSystem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FS.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 08/10/2022. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | var FileSystem: FileSystemProtocol { 12 | return ActiveFileSystem.shared 13 | } 14 | 15 | class ActiveFileSystem { 16 | static var shared: FileSystemProtocol = RealFileSystem() 17 | 18 | /** Note: Intermediate directories are not automatically inferred and have to be manually declared. */ 19 | public static func useTestable(_ files: [String: FakeFile]) { 20 | Self.shared = TestableFileSystem(files: files) 21 | } 22 | 23 | public static func useSystem() { 24 | Self.shared = RealFileSystem() 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /phpmon/Common/Filesystem/FileSystemProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FileSystemProtocol.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 08/10/2022. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol FileSystemProtocol { 12 | 13 | // MARK: - Basics 14 | 15 | func createDirectory(_ path: String, withIntermediateDirectories: Bool) throws 16 | 17 | func writeAtomicallyToFile(_ path: String, content: String) throws 18 | 19 | func getStringFromFile(_ path: String) throws -> String 20 | 21 | func getShallowContentsOfDirectory(_ path: String) throws -> [String] 22 | 23 | func getDestinationOfSymlink(_ path: String) throws -> String 24 | 25 | // MARK: - Move & Delete Files 26 | 27 | func move(from path: String, to newPath: String) throws 28 | 29 | func remove(_ path: String) throws 30 | 31 | // MARK: — Attributes 32 | 33 | func makeExecutable(_ path: String) throws 34 | 35 | // MARK: - Checks 36 | 37 | func isExecutableFile(_ path: String) -> Bool 38 | 39 | func isWriteableFile(_ path: String) -> Bool 40 | 41 | func anyExists(_ path: String) -> Bool 42 | 43 | func fileExists(_ path: String) -> Bool 44 | 45 | func directoryExists(_ path: String) -> Bool 46 | 47 | func isSymlink(_ path: String) -> Bool 48 | 49 | func isDirectory(_ path: String) -> Bool 50 | } 51 | -------------------------------------------------------------------------------- /phpmon/Common/Helpers/Alert.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Alert.swift 3 | // PHP Monitor 4 | // 5 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 6 | // 7 | 8 | import Cocoa 9 | 10 | class Alert { 11 | 12 | public static func confirm( 13 | onWindow window: NSWindow, 14 | messageText: String, 15 | informativeText: String, 16 | buttonTitle: String = "generic.ok".localized, 17 | buttonIsDestructive: Bool = false, 18 | secondButtonTitle: String = "generic.cancel".localized, 19 | style: NSAlert.Style = .warning, 20 | onFirstButtonPressed: @escaping (() -> Void) 21 | ) { 22 | if !Thread.isMainThread { 23 | fatalError("You should always present alerts on the main thread!") 24 | } 25 | 26 | let alert = NSAlert.init() 27 | alert.alertStyle = style 28 | alert.messageText = messageText 29 | alert.informativeText = informativeText 30 | alert.addButton(withTitle: buttonTitle) 31 | alert.buttons.first?.hasDestructiveAction = buttonIsDestructive 32 | if !secondButtonTitle.isEmpty { 33 | alert.addButton(withTitle: secondButtonTitle) 34 | } 35 | alert.beginSheetModal(for: window) { response in 36 | if response == .alertFirstButtonReturn { 37 | onFirstButtonPressed() 38 | } 39 | } 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /phpmon/Common/Helpers/LocalNotification.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LocalNotification.swift 3 | // PHP Monitor 4 | // 5 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 6 | // 7 | 8 | import Foundation 9 | import UserNotifications 10 | 11 | class LocalNotification { 12 | 13 | @MainActor public static func send(title: String, subtitle: String, preference: PreferenceName?) { 14 | if preference != nil && !Preferences.isEnabled(preference!) { 15 | return 16 | } 17 | 18 | let content = UNMutableNotificationContent() 19 | content.title = title 20 | content.body = subtitle 21 | 22 | let uuidString = UUID().uuidString 23 | let request = UNNotificationRequest( 24 | identifier: uuidString, 25 | content: content, 26 | trigger: nil 27 | ) 28 | 29 | let notificationCenter = UNUserNotificationCenter.current() 30 | notificationCenter.add(request) { (error) in 31 | if error != nil { 32 | Log.err(error!) 33 | } 34 | } 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /phpmon/Common/Helpers/LoginItemManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoginItemManager.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 15/02/2023. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import AppKit 10 | import ServiceManagement 11 | 12 | @available(macOS 13.0, *) 13 | class LoginItemManager { 14 | func loginItemIsEnabled() -> Bool { 15 | return SMAppService.mainApp.status == .enabled 16 | } 17 | 18 | func disableLoginItem() { 19 | try? SMAppService.mainApp.unregister() 20 | } 21 | 22 | func enableLoginItem() { 23 | try? SMAppService.mainApp.register() 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /phpmon/Common/Helpers/Measurements.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Measurements.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 02/03/2023. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct Measurement { 12 | let started = Date() 13 | 14 | var milliseconds: Double { 15 | return round(Date().timeIntervalSince(started) * 1000 * 1000) / 1000 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /phpmon/Common/Helpers/PMWindowController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PMWindowController.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 05/12/2021. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | /** 12 | This window class keeps track of which windows are currently visible, and reports this info back to the App class. 13 | For more information, check the `windows` property on `App`. 14 | 15 | - Note: This class does make a simple assumption: each window controller corresponds to a single view. 16 | */ 17 | class PMWindowController: NSWindowController, NSWindowDelegate { 18 | 19 | public var windowName: String { 20 | fatalError("Please specify a window name") 21 | } 22 | 23 | override func showWindow(_ sender: Any?) { 24 | super.showWindow(sender) 25 | App.shared.register(window: windowName) 26 | } 27 | 28 | func windowWillClose(_ notification: Notification) { 29 | App.shared.remove(window: windowName) 30 | } 31 | 32 | func windowDidResize(_ notification: Notification) {} 33 | 34 | deinit { 35 | Log.perf("deinit: \(String(describing: self)).\(#function)") 36 | } 37 | 38 | } 39 | 40 | extension NSWindowController { 41 | 42 | public func positionWindowInTopRightCorner(offsetY: CGFloat = 0, offsetX: CGFloat = 0) { 43 | guard let frame = NSScreen.main?.frame else { return } 44 | guard let window = self.window else { return } 45 | 46 | window.setFrame(NSRect( 47 | x: frame.size.width - window.frame.size.width - 20 + offsetX, 48 | y: frame.size.height - window.frame.size.height - 40 + offsetY, 49 | width: window.frame.width, 50 | height: window.frame.height 51 | ), display: true) 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /phpmon/Common/Helpers/System.swift: -------------------------------------------------------------------------------- 1 | // 2 | // System.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 01/11/2022. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /** 12 | Run a simple blocking Shell command on the user's own system. 13 | */ 14 | public func system(_ command: String) -> String { 15 | let task = Process() 16 | task.launchPath = "/bin/sh" 17 | task.arguments = ["-c", command] 18 | 19 | let pipe = Pipe() 20 | task.standardOutput = pipe 21 | task.launch() 22 | 23 | let data = pipe.fileHandleForReading.readDataToEndOfFile() 24 | let output: String = NSString(data: data, encoding: String.Encoding.utf8.rawValue)! as String 25 | 26 | return output 27 | } 28 | 29 | /** 30 | Same as the `system` command, but does not return the output. 31 | */ 32 | public func system_quiet(_ command: String) { 33 | let task = Process() 34 | task.launchPath = "/bin/sh" 35 | task.arguments = ["-c", command] 36 | 37 | let pipe = Pipe() 38 | task.standardOutput = pipe 39 | task.launch() 40 | 41 | _ = pipe.fileHandleForReading.readDataToEndOfFile() 42 | return 43 | } 44 | 45 | /** 46 | Retrieves the username for the currently signed in user via `/usr/bin/id`. 47 | This cannot fail or the application will crash. 48 | */ 49 | public func identity() -> String { 50 | let task = Process() 51 | task.launchPath = "/usr/bin/id" 52 | task.arguments = ["-un"] 53 | 54 | let pipe = Pipe() 55 | task.standardOutput = pipe 56 | task.launch() 57 | 58 | guard let output = String( 59 | data: pipe.fileHandleForReading.readDataToEndOfFile(), 60 | encoding: String.Encoding.utf8 61 | ) else { 62 | fatalError("Could not retrieve username via `id -un`!") 63 | } 64 | 65 | return output.trimmingCharacters(in: .whitespacesAndNewlines) 66 | } 67 | 68 | /** 69 | Retrieves the user's preferred shell. 70 | */ 71 | public func preferred_shell() -> String { 72 | return system("dscl . -read ~/ UserShell | sed 's/UserShell: //'") 73 | .trimmingCharacters(in: .whitespacesAndNewlines) 74 | } 75 | -------------------------------------------------------------------------------- /phpmon/Common/Helpers/VersionExtractor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VersionExtractor.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 16/12/2021. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class VersionExtractor { 12 | 13 | /** 14 | This attempts to extract the version number from any given string. 15 | */ 16 | public static func from(_ string: String) -> String? { 17 | do { 18 | let regex = try NSRegularExpression( 19 | pattern: #"(?<version>(\d+)(.)(\d+)((.)(\d+))?)"#, 20 | options: [] 21 | ) 22 | 23 | let match = regex.matches( 24 | in: string, 25 | options: [], 26 | range: NSRange(location: 0, length: string.count) 27 | ).first 28 | 29 | guard let match = match else { 30 | return nil 31 | } 32 | 33 | let range = Range( 34 | match.range(withName: "version"), 35 | in: string 36 | )! 37 | 38 | return String(string[range]) 39 | } catch { 40 | return nil 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /phpmon/Common/PHP/Extensions/Xdebug.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Xdebug.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 01/05/2022. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Cocoa 11 | 12 | class Xdebug { 13 | 14 | public static var enabled: Bool { 15 | return PhpEnvironments.shared.getConfigFile(forKey: "xdebug.mode") != nil 16 | } 17 | 18 | public static var activeModes: [String] { 19 | guard let file = PhpEnvironments.shared.getConfigFile(forKey: "xdebug.mode") else { 20 | return [] 21 | } 22 | 23 | guard let value = file.get(for: "xdebug.mode") else { 24 | return [] 25 | } 26 | 27 | return value.components(separatedBy: ",").filter { self.modes.contains($0) } 28 | } 29 | 30 | public static func asMenuItems() -> [NSMenuItem] { 31 | var items: [NSMenuItem] = [] 32 | 33 | let activeModes = Self.activeModes 34 | 35 | for mode in Self.modes { 36 | let item = XdebugMenuItem( 37 | title: mode, 38 | action: #selector(MainMenu.toggleXdebugMode(sender:)), 39 | keyEquivalent: "" 40 | ) 41 | 42 | item.state = activeModes.contains(mode) ? .on : .off 43 | item.mode = mode 44 | items.append(item) 45 | } 46 | 47 | return items 48 | } 49 | 50 | public static var modes: [String] { 51 | return [ 52 | "develop", 53 | "coverage", 54 | "debug", 55 | "gcstats", 56 | "profile", 57 | "trace" 58 | ] 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /phpmon/Common/PHP/Homebrew/HomebrewDecodable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HomebrewDecodable.swift 3 | // PHP Monitor 4 | // 5 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 6 | // 7 | 8 | import Foundation 9 | 10 | struct HomebrewPackage: Decodable { 11 | let full_name: String 12 | let aliases: [String] 13 | let installed: [HomebrewInstalled] 14 | let linked_keg: String? 15 | 16 | public var version: String { 17 | return aliases.first! 18 | .replacingOccurrences(of: "php@", with: "") 19 | } 20 | } 21 | 22 | struct HomebrewInstalled: Decodable { 23 | let version: String 24 | let built_as_bottle: Bool 25 | let installed_as_dependency: Bool 26 | let installed_on_request: Bool 27 | } 28 | 29 | struct OutdatedFormulae: Decodable { 30 | let formulae: [OutdatedFormula] 31 | } 32 | 33 | struct OutdatedFormula: Decodable { 34 | let name: String 35 | let installed_versions: [String] 36 | let current_version: String 37 | let pinned: Bool 38 | let pinned_version: String? 39 | } 40 | -------------------------------------------------------------------------------- /phpmon/Common/PHP/Homebrew/HomebrewService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HomebrewService.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 11/01/2022. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | final class HomebrewService: Sendable, Decodable { 12 | let name: String 13 | let service_name: String 14 | let running: Bool 15 | let loaded: Bool 16 | let pid: Int? 17 | let user: String? 18 | let status: String? 19 | let log_path: String? 20 | let error_log_path: String? 21 | 22 | init( 23 | name: String, 24 | service_name: String, 25 | running: Bool, 26 | loaded: Bool, 27 | pid: Int? = nil, 28 | user: String? = nil, 29 | status: String? = nil, 30 | log_path: String? = nil, 31 | error_log_path: String? = nil 32 | ) { 33 | self.name = name 34 | self.service_name = service_name 35 | self.running = running 36 | self.loaded = loaded 37 | self.pid = pid 38 | self.user = user 39 | self.status = status 40 | self.log_path = log_path 41 | self.error_log_path = error_log_path 42 | } 43 | 44 | /** 45 | Dummy data for preview purposes. 46 | */ 47 | public static func dummy(named service: String, enabled: Bool, status: String? = nil) -> HomebrewService { 48 | return HomebrewService( 49 | name: service, 50 | service_name: service, 51 | running: enabled, 52 | loaded: enabled, 53 | pid: nil, 54 | user: nil, 55 | status: status, 56 | log_path: nil, 57 | error_log_path: nil 58 | ) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /phpmon/Common/PHP/Switcher/PhpSwitcher.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PhpVersionSwitchContract.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 24/12/2021. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol PhpSwitcherDelegate: AnyObject { 12 | 13 | func switcherDidStartSwitching(to version: String) 14 | 15 | func switcherDidCompleteSwitch(to version: String) 16 | 17 | } 18 | 19 | protocol PhpSwitcher { 20 | 21 | func performSwitch(to version: String) async 22 | 23 | } 24 | -------------------------------------------------------------------------------- /phpmon/Common/Protocols/CreatedFromFile.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CreatedFromFile.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 15/05/2022. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol CreatedFromFile { 12 | 13 | static func from(filePath: String) -> Self? 14 | 15 | } 16 | -------------------------------------------------------------------------------- /phpmon/Common/Shell/ActiveShell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Shell.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 20/09/2022. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | var Shell: ShellProtocol { 12 | return ActiveShell.shared 13 | } 14 | 15 | class ActiveShell { 16 | static var shared: ShellProtocol = RealShell() 17 | 18 | public static func useTestable(_ expectations: [String: BatchFakeShellOutput]) { 19 | Self.shared = TestableShell(expectations: expectations) 20 | } 21 | 22 | public static func useSystem() { 23 | Self.shared = RealShell() 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /phpmon/Common/State/BusyStatus.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BusyStatus.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 02/05/2023. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class BusyStatus: ObservableObject { 12 | @Published var busy: Bool 13 | @Published var title: String 14 | @Published var description: String 15 | 16 | init(busy: Bool, title: String, description: String) { 17 | self.busy = busy 18 | self.title = title 19 | self.description = description 20 | } 21 | 22 | public static func notBusy() -> BusyStatus { 23 | return BusyStatus(busy: false, title: "", description: "") 24 | } 25 | 26 | public static func busy() -> BusyStatus { 27 | return BusyStatus(busy: false, title: "", description: "") 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /phpmon/Common/Testables/TestableCommand.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestableCommand.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 12/10/2022. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class TestableCommand: CommandProtocol { 12 | init(commands: [String: String]) { 13 | self.commands = commands 14 | } 15 | 16 | var commands: [String: String] 17 | 18 | func execute(path: String, arguments: [String]) -> String { 19 | self.execute(path: path, arguments: arguments, trimNewlines: false) 20 | } 21 | 22 | public func execute(path: String, arguments: [String], trimNewlines: Bool, withStandardError: Bool) -> String { 23 | self.execute(path: path, arguments: arguments, trimNewlines: trimNewlines) 24 | } 25 | 26 | public func execute(path: String, arguments: [String], trimNewlines: Bool) -> String { 27 | let concatenatedCommand = "\(path) \(arguments.joined(separator: " "))" 28 | assert(commands.keys.contains(concatenatedCommand), "Command `\(concatenatedCommand)` not found") 29 | return self.commands[concatenatedCommand]! 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /phpmon/Domain/App/App+ActivationPolicy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // App+ActivationPolicy.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 05/12/2021. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import Foundation 11 | 12 | extension App { 13 | 14 | // MARK: - Application State 15 | 16 | /** 17 | Registers a window as currently open. 18 | */ 19 | public func register(window name: String) { 20 | if !openWindows.contains(name) { 21 | openWindows.append(name) 22 | } 23 | updateActivationPolicy() 24 | } 25 | 26 | /** 27 | Removes a window, assuming it was closed. 28 | */ 29 | public func remove(window name: String) { 30 | openWindows.removeAll { window in 31 | window == name 32 | } 33 | updateActivationPolicy() 34 | } 35 | 36 | /** 37 | If there are any open windows, the app will be a regular app. 38 | If there are no windows open, the app will be an accessory (toolbar) app. 39 | */ 40 | public func updateActivationPolicy() { 41 | NSApp.setActivationPolicy(!openWindows.isEmpty ? .regular : .accessory) 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /phpmon/Domain/App/App+GlobalHotkey.swift: -------------------------------------------------------------------------------- 1 | // 2 | // App+GlobalHotkey.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 05/12/2021. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | extension App { 12 | 13 | // MARK: - Methods 14 | 15 | /** 16 | On startup, the preferences should be loaded from the .plist, 17 | and we'll enable the shortcut if it is set. 18 | */ 19 | func loadGlobalHotkey() { 20 | // Make sure we can retrieve the hotkey from preferences 21 | guard let hotkey = Preferences.preferences[.globalHotkey] as? String else { 22 | Log.info("No global hotkey was saved in preferences. None set.") 23 | return 24 | } 25 | 26 | // Make sure we can parse the JSON into the desired format 27 | guard let keybindPref = GlobalKeybindPreference.fromJson(hotkey) else { 28 | Log.err("No global hotkey loaded, could not be parsed!") 29 | shortcutHotkey = nil 30 | return 31 | } 32 | 33 | shortcutHotkey = HotKey(keyCombo: KeyCombo( 34 | carbonKeyCode: keybindPref.keyCode, 35 | carbonModifiers: keybindPref.carbonFlags 36 | )) 37 | } 38 | 39 | /** 40 | Sets up the action that needs to occur when the shortcut key is pressed 41 | (opens the menu). 42 | */ 43 | func setupGlobalHotkeyListener() { 44 | guard let hotkey = shortcutHotkey else { 45 | return 46 | } 47 | 48 | hotkey.keyDownHandler = { 49 | Task { @MainActor in 50 | MainMenu.shared.statusItem.button?.performClick(nil) 51 | NSApplication.shared.activate(ignoringOtherApps: true) 52 | } 53 | } 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /phpmon/Domain/App/AppDelegate+InterApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate+InterApp.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 20/12/2021. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import Foundation 11 | 12 | extension AppDelegate { 13 | 14 | /** 15 | This is an entry point for future development for integrating with the PHP Monitor 16 | application URL. You can use the `phpmon://` protocol to communicate with the app. 17 | 18 | At this time you can trigger the site list using Alfred (or some other application) 19 | by opening the following URL: `phpmon://list`. 20 | 21 | Please note that PHP Monitor needs to be running in the background for this to work. 22 | */ 23 | @MainActor func application(_ application: NSApplication, open urls: [URL]) { 24 | if !Preferences.isEnabled(.allowProtocolForIntegrations) { 25 | Log.info("Acting on commands via phpmon:// has been disabled.") 26 | return 27 | } 28 | 29 | guard let url = urls.first else { return } 30 | 31 | self.interpretCommand( 32 | url.absoluteString.replacingOccurrences(of: "phpmon://", with: ""), 33 | commands: InterApp.getCommands() 34 | ) 35 | } 36 | 37 | private func interpretCommand(_ command: String, commands: [InterApp.Action]) { 38 | commands.forEach { action in 39 | if command.starts(with: action.command) { 40 | let lastElement = String(command.split(separator: "/").last!) 41 | action.action(lastElement) 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /phpmon/Domain/App/AppDelegate+MenuOutlets.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate+MenuOutlets.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 05/12/2021. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import AppKit 11 | 12 | /** 13 | Any outlets connected to the app's main menu (not the menu that shows when the icon in 14 | the menu bar is clicked, but the regular app's main menu) are configured here. 15 | 16 | Default interactions like copy/paste, select all, close window etc. are wired up by 17 | default in the storyboard and do not need to be manually added. 18 | 19 | Extra functionality (like the menu item to reload the list of sites) does, however. 20 | 21 | - Note: This menu is only displayed when the app is NOT running in accessory mode. 22 | For more information about this, please see the ActivationPolicy-related extension. 23 | */ 24 | extension AppDelegate { 25 | 26 | // MARK: - Menu Interactions 27 | 28 | @IBAction func addSiteLinkPressed(_ sender: Any) { 29 | DomainListVC.show() 30 | 31 | guard let windowController = App.shared.domainListWindowController else { return } 32 | windowController.pressedAddLink(nil) 33 | } 34 | 35 | @IBAction func reloadDomainListPressed(_ sender: Any) { 36 | Task { // Reload domains 37 | let vc = App.shared.domainListWindowController? 38 | .window?.contentViewController as? DomainListVC 39 | 40 | if vc != nil { 41 | // If the view exists, directly reload the list of sites. 42 | await vc!.reloadDomains() 43 | } else { 44 | // If the view does not exist, reload the cached data that was populated when the app launched. 45 | await Valet.shared.reloadSites() 46 | } 47 | } 48 | } 49 | 50 | @IBAction func focusSearchField(_ sender: Any) { 51 | DomainListVC.show() 52 | 53 | guard let windowController = App.shared.domainListWindowController else { return } 54 | windowController.searchToolbarItem.searchField.becomeFirstResponder() 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /phpmon/Domain/App/AppDelegate+Notifications.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate+Notifications.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 06/12/2021. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UserNotifications 11 | 12 | extension AppDelegate { 13 | 14 | // MARK: - Notifications 15 | 16 | /** 17 | Sets up notifications. That does mean we need to ask for permission first. 18 | If we cannot get permission, we should log this. 19 | */ 20 | public func setupNotifications() { 21 | let notificationCenter = UNUserNotificationCenter.current() 22 | notificationCenter.delegate = self 23 | notificationCenter.requestAuthorization(options: [.alert], completionHandler: { granted, error in 24 | if !granted { 25 | Log.warn("PHP Monitor does not have permission to show notifications.") 26 | } 27 | if let error = error { 28 | Log.err("PHP Monitor encounted an error determining notification permissions:") 29 | Log.err(error) 30 | } 31 | }) 32 | } 33 | 34 | /** 35 | Ensure that the application displays notifications even when the app is active. 36 | */ 37 | func userNotificationCenter( 38 | _ center: UNUserNotificationCenter, 39 | willPresent notification: UNNotification, 40 | withCompletionHandler completionHandler: 41 | @escaping (UNNotificationPresentationOptions) -> Void 42 | ) { 43 | completionHandler([.banner]) 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /phpmon/Domain/App/EnvironmentCheck.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EnvironmentCheck.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 10/08/2022. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /** 12 | The `EnvironmentCheck` is used to defer the execution of all of these commands until necessary. 13 | Checks that require an app restart will always lead to an alert and app termination shortly after. 14 | */ 15 | struct EnvironmentCheck { 16 | let command: () async -> Bool 17 | let name: String 18 | let titleText: String 19 | let subtitleText: String 20 | let descriptionText: String 21 | let buttonText: String 22 | let requiresAppRestart: Bool 23 | 24 | init( 25 | command: @escaping () async -> Bool, 26 | name: String, 27 | titleText: String, 28 | subtitleText: String, 29 | descriptionText: String = "", 30 | buttonText: String = "OK", 31 | requiresAppRestart: Bool = false 32 | ) { 33 | self.command = command 34 | self.name = name 35 | self.titleText = titleText 36 | self.subtitleText = subtitleText 37 | self.descriptionText = descriptionText 38 | self.buttonText = buttonText 39 | self.requiresAppRestart = requiresAppRestart 40 | } 41 | 42 | public func succeeds() async -> Bool { 43 | return await !self.command() 44 | } 45 | } 46 | 47 | struct EnvironmentCheckGroup { 48 | let name: String 49 | let condition: () -> Bool 50 | let checks: [EnvironmentCheck] 51 | } 52 | -------------------------------------------------------------------------------- /phpmon/Domain/App/Services/Service.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ServiceWrapper.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 23/12/2022. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /** Service linked to a Homebrew formula and whether it is currently (in)active or missing. */ 12 | public struct Service: Hashable { 13 | var formula: HomebrewFormula 14 | var status: Status = .missing 15 | 16 | public var name: String { 17 | return formula.name 18 | } 19 | 20 | init(formula: HomebrewFormula, service: HomebrewService? = nil) { 21 | self.formula = formula 22 | 23 | guard let service else { return } 24 | 25 | self.status = service.running ? .active : .inactive 26 | 27 | if service.status == "error" { 28 | self.status = .error 29 | } 30 | } 31 | 32 | // MARK: - Protocols 33 | 34 | public static func == (lhs: Service, rhs: Service) -> Bool { 35 | return lhs.hashValue == rhs.hashValue 36 | } 37 | 38 | public func hash(into hasher: inout Hasher) { 39 | hasher.combine(formula) 40 | hasher.combine(status) 41 | } 42 | 43 | // MARK: - Status 44 | 45 | public enum Status: String { 46 | case active 47 | case inactive 48 | case error 49 | case missing 50 | 51 | var asBool: Bool { 52 | return self == .active 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /phpmon/Domain/Integrations/Common/RCFile.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RCFile.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 24/01/2023. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct RCFile { 12 | let path: String? 13 | let fields: [String: String] 14 | 15 | static func fromPath(_ path: String) -> RCFile? { 16 | do { 17 | let text = try String(contentsOf: URL(fileURLWithPath: path), encoding: .utf8) 18 | return RCFile(path: path, contents: text) 19 | } catch { 20 | return nil 21 | } 22 | } 23 | 24 | init(path: String? = nil, contents: String) { 25 | var fields: [String: String] = [:] 26 | 27 | contents 28 | .split(separator: "\n") 29 | .forEach({ line in 30 | if line.contains("=") { 31 | let content = line.split(separator: "=") 32 | let key = String(content[0]) 33 | .trimmingCharacters(in: .whitespaces) 34 | .replacingOccurrences(of: "\"", with: "") 35 | if key.starts(with: "#") { 36 | return 37 | } 38 | let value = String(content[1]) 39 | .trimmingCharacters(in: .whitespaces) 40 | .replacingOccurrences(of: "\"", with: "") 41 | fields[key] = value 42 | } 43 | }) 44 | 45 | self.path = path 46 | self.fields = fields 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /phpmon/Domain/Integrations/Homebrew/Brew.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Homebrew.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 17/03/2023. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class Brew { 12 | static let shared = Brew() 13 | 14 | /// Formulae that can be observed. 15 | var formulae = BrewFormulaeObservable() 16 | 17 | /// The version of Homebrew that was detected. 18 | var version: VersionNumber? 19 | 20 | /// Determine which version of Homebrew is installed. 21 | public func determineVersion() async { 22 | let output = await Shell.pipe("\(Paths.brew) --version") 23 | self.version = try? VersionNumber.parse(output.out) 24 | 25 | if let version = version { 26 | Log.info("The user has Homebrew \(version.text) installed.") 27 | 28 | if version.major < 4 { 29 | Log.warn("Managing PHP versions is only officially supported with Homebrew 4 or newer!") 30 | } 31 | } else { 32 | Log.warn("The Homebrew version could not be determined.") 33 | } 34 | } 35 | 36 | /// Each formula for each PHP version that can be installed. 37 | public static let phpVersionFormulae = [ 38 | "8.5": "shivammathur/php/php@8.5", 39 | "8.4": "shivammathur/php/php@8.4", 40 | "8.3": "shivammathur/php/php@8.3", 41 | "8.2": "shivammathur/php/php@8.2", 42 | "8.1": "shivammathur/php/php@8.1", 43 | "8.0": "shivammathur/php/php@8.0", 44 | "7.4": "shivammathur/php/php@7.4", 45 | "7.3": "shivammathur/php/php@7.3", 46 | "7.2": "shivammathur/php/php@7.2", 47 | "7.1": "shivammathur/php/php@7.1", 48 | "7.0": "shivammathur/php/php@7.0", 49 | "5.6": "shivammathur/php/php@5.6" 50 | ] 51 | } 52 | -------------------------------------------------------------------------------- /phpmon/Domain/Integrations/Homebrew/BrewTapFormulae.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BrewTapFormulae.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 01/11/2023. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class BrewTapFormulae { 12 | public static func from(tap: String) -> [String: [BrewPhpExtension]] { 13 | let directory = "\(Paths.tapPath)/\(tap)/Formula" 14 | 15 | let files = try? FileSystem.getShallowContentsOfDirectory(directory) 16 | 17 | var availableExtensions = [String: [BrewPhpExtension]]() 18 | 19 | guard let files = files else { 20 | return availableExtensions 21 | } 22 | 23 | let regex = try! NSRegularExpression(pattern: "(\\w+)@(\\d+\\.\\d+)\\.rb") 24 | 25 | for file in files { 26 | let matches = regex.matches(in: file, range: NSRange(file.startIndex..., in: file)) 27 | if let match = matches.first { 28 | if let phpExtensionRange = Range(match.range(at: 1), in: file), 29 | let versionRange = Range(match.range(at: 2), in: file) { 30 | // Determine what the extension's name is 31 | let phpExtensionName = String(file[phpExtensionRange]) 32 | 33 | // Determine what PHP version this is for 34 | let phpVersion = String(file[versionRange]) 35 | 36 | // Create a new BrewPhpExtension object (determines if installed) 37 | let phpExtension = BrewPhpExtension( 38 | path: "\(Paths.tapPath)/\(tap)/Formula/\(file)", 39 | name: phpExtensionName, 40 | phpVersion: phpVersion 41 | ) 42 | 43 | // Append the extension to the list 44 | var extensions = availableExtensions[phpVersion, default: []] 45 | extensions.append(phpExtension) 46 | availableExtensions[phpVersion] = extensions.sorted() 47 | } 48 | } 49 | } 50 | 51 | return availableExtensions 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /phpmon/Domain/Integrations/Homebrew/Fake/FakeCommand.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FakeCommand.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 21/03/2023. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class FakeCommand: BrewCommand { 12 | func getCommandTitle() -> String { 13 | return "Hello" 14 | } 15 | 16 | let version: String 17 | 18 | init(version: String) { 19 | self.version = version 20 | } 21 | 22 | func execute(onProgress: @escaping (BrewCommandProgress) -> Void) async throws { 23 | onProgress(.create(value: 0.2, title: "Hello", description: "Doing the work")) 24 | await delay(seconds: 2) 25 | onProgress(.create(value: 0.5, title: "Hello", description: "Doing some more work")) 26 | await delay(seconds: 1) 27 | onProgress(.create(value: 1, title: "Hello", description: "Job's done")) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /phpmon/Domain/Integrations/Valet/Domains/ValetListable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ValetListable.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 12/04/2022. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol ValetListable { 12 | 13 | func getListableName() -> String 14 | 15 | func getListableSecured() -> Bool 16 | 17 | func getListableAbsolutePath() -> String 18 | 19 | func getListablePhpVersion() -> String 20 | 21 | func getListableKind() -> String 22 | 23 | func getListableType() -> String 24 | 25 | func getListableUrl() -> URL? 26 | 27 | func getListableFavorited() -> Bool 28 | 29 | } 30 | -------------------------------------------------------------------------------- /phpmon/Domain/Integrations/Valet/Proxies/FakeValetProxy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FakeValetProxy.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 16/12/2022. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class FakeValetProxy: ValetProxy { 12 | override func determineSecured() { 13 | return 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /phpmon/Domain/Integrations/Valet/Scanners/DomainScanner.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DomainScanner.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 02/04/2022. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol DomainScanner { 12 | 13 | // MARK: - Sites 14 | 15 | func resolveSiteCount(paths: [String]) -> Int 16 | 17 | func resolveSitesFrom(paths: [String]) -> [ValetSite] 18 | 19 | func resolveSite(path: String) -> ValetSite? 20 | 21 | // MARK: - Proxies 22 | 23 | func resolveProxies(directoryPath: String) -> [ValetProxy] 24 | 25 | } 26 | -------------------------------------------------------------------------------- /phpmon/Domain/Integrations/Valet/Scanners/FakeDomainScanner.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FakeDomainScanner.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 02/04/2022. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | class FakeDomainScanner: DomainScanner { 10 | 11 | var sites: [ValetSite] = [ 12 | FakeValetSite(fakeWithName: "laravel", tld: "test", secure: true, 13 | path: "~/Code/laravel/framework", linked: true), 14 | 15 | FakeValetSite(fakeWithName: "tailwind", tld: "test", secure: true, 16 | path: "~/Code/tailwind/site", linked: true, constraint: "8.0"), 17 | 18 | FakeValetSite(fakeWithName: "forge", tld: "test", secure: true, 19 | path: "~/Code/laravel/forge", linked: true), 20 | 21 | FakeValetSite(fakeWithName: "concord", tld: "test", secure: false, 22 | path: "~/Code/concord", linked: true, driver: "Laravel (^8.0)", constraint: "^7.4", isolated: "7.4"), 23 | 24 | FakeValetSite(fakeWithName: "drupal", tld: "test", secure: false, 25 | path: "~/Sites/drupal", linked: false, driver: "Drupal", constraint: "^7.4", isolated: "7.4"), 26 | 27 | FakeValetSite(fakeWithName: "wordpress", tld: "test", secure: false, 28 | path: "~/Sites/wordpress", linked: false, driver: "WordPress", constraint: "^7.4", isolated: "7.4") 29 | ] 30 | 31 | var proxies: [ValetProxy] = [ 32 | FakeValetProxy(domain: "mailgun", target: "http://127.0.0.1:9999", secure: true, tld: "test") 33 | ] 34 | 35 | // MARK: - Sites 36 | 37 | func resolveSiteCount(paths: [String]) -> Int { 38 | return sites.count 39 | } 40 | 41 | func resolveSitesFrom(paths: [String]) -> [ValetSite] { 42 | return sites 43 | } 44 | 45 | func resolveSite(path: String) -> ValetSite? { 46 | return nil 47 | } 48 | 49 | // MARK: - Proxies 50 | 51 | func resolveProxies(directoryPath: String) -> [ValetProxy] { 52 | return proxies 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /phpmon/Domain/Integrations/Valet/Scanners/ValetScanners.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Valet+Scanners.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 01/11/2022. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class ValetScanner { 12 | 13 | static var active: DomainScanner = ValetDomainScanner() 14 | 15 | public static func useFake() { 16 | ValetScanner.active = FakeDomainScanner() 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /phpmon/Domain/Integrations/Valet/Sites/FakeValetSite.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ValetSite+Fake.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 19/03/2022. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class FakeValetSite: ValetSite { 12 | convenience init( 13 | fakeWithName name: String, 14 | tld: String, 15 | secure: Bool, 16 | path: String, 17 | linked: Bool, 18 | driver: String = "Laravel (^9.0)", 19 | constraint: String = "^8.1", 20 | isolated: String? = nil 21 | ) { 22 | self.init( 23 | name: name, 24 | tld: tld, 25 | absolutePath: path, 26 | aliasPath: nil, 27 | makeDeterminations: false 28 | ) 29 | 30 | self.secured = secure 31 | self.preferredPhpVersion = constraint 32 | self.preferredPhpVersionSource = constraint != "" ? .require : .unknown 33 | 34 | self.driver = driver 35 | self.driverDeterminedByComposer = true 36 | 37 | if linked { 38 | self.aliasPath = self.absolutePath 39 | } 40 | 41 | if let isolated = isolated { 42 | self.isolatedPhpVersion = PhpInstallation(isolated) 43 | } 44 | 45 | if PhpEnvironments.shared.currentInstall != nil { 46 | self.evaluateCompatibility() 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /phpmon/Domain/Integrations/Valet/Sites/PhpVersionSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VersionSource.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 25/01/2023. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | enum PhpVersionSource: String { 12 | case unknown 13 | case require 14 | case platform 15 | case valetphprc 16 | case valetrc 17 | } 18 | -------------------------------------------------------------------------------- /phpmon/Domain/Menu/AppMenu.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppMenu.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 27/06/2024. 6 | // Copyright © 2024 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class AppMenu { 12 | 13 | // MARK: - Main Menu 14 | 15 | static var appMenu: NSMenu? { 16 | return NSApplication.shared.mainMenu?.items[0].submenu 17 | } 18 | 19 | static var sitesMenu: NSMenu? { 20 | return NSApplication.shared.mainMenu?.items[1].submenu 21 | } 22 | 23 | static var editMenu: NSMenu? { 24 | return NSApplication.shared.mainMenu?.items[2].submenu 25 | } 26 | 27 | static var windowMenu: NSMenu? { 28 | return NSApplication.shared.mainMenu?.items[3].submenu 29 | } 30 | 31 | static var helpMenu: NSMenu? { 32 | return NSApplication.shared.mainMenu?.items[4].submenu 33 | } 34 | 35 | // MARK: - Submenu 36 | 37 | static var actionsMenu: NSMenuItem? { 38 | return sitesMenu?.items.last 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /phpmon/Domain/Preferences/Keybinds/GlobalKeybindPreference.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GlobalKeybindPreference.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 15/04/2021. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct GlobalKeybindPreference: Codable, CustomStringConvertible { 12 | 13 | // MARK: - Internal variables 14 | 15 | let function: Bool 16 | let control: Bool 17 | let command: Bool 18 | let shift: Bool 19 | let option: Bool 20 | let capsLock: Bool 21 | let carbonFlags: UInt32 22 | let characters: String? 23 | let keyCode: UInt32 24 | 25 | // MARK: - How the keybind is display in Preferences 26 | 27 | var description: String { 28 | var stringBuilder = "" 29 | if self.function { 30 | stringBuilder += "Fn" 31 | } 32 | if self.control { 33 | stringBuilder += "⌃" 34 | } 35 | if self.option { 36 | stringBuilder += "⌥" 37 | } 38 | if self.command { 39 | stringBuilder += "⌘" 40 | } 41 | if self.shift { 42 | stringBuilder += "⇧" 43 | } 44 | if self.capsLock { 45 | stringBuilder += "⇪" 46 | } 47 | if let characters = self.characters { 48 | stringBuilder += characters.uppercased() 49 | } 50 | return "\(stringBuilder)" 51 | } 52 | 53 | // MARK: - Persisting data to UserDefaults (as JSON) 54 | 55 | public func toJson() -> String { 56 | let jsonData = try! JSONEncoder().encode(self) 57 | return String(data: jsonData, encoding: .utf8)! 58 | } 59 | 60 | public static func fromJson(_ string: String?) -> GlobalKeybindPreference? { 61 | if string == nil { 62 | return nil 63 | } 64 | 65 | if let jsonData = string!.data(using: .utf8) { 66 | let decoder = JSONDecoder() 67 | do { 68 | return try decoder.decode(GlobalKeybindPreference.self, from: jsonData) 69 | } catch { 70 | return nil 71 | } 72 | } 73 | return nil 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /phpmon/Domain/Preferences/Keys.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Keys.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 25/07/2022. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct Keys { 12 | static let Escape = 53 13 | static let Space = 49 14 | } 15 | -------------------------------------------------------------------------------- /phpmon/Domain/Preferences/MenuBarIcons.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MenuBarIcons.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 06/02/2022. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Cocoa 11 | 12 | enum MenuBarIcon: String, CaseIterable { 13 | case iconPhp = "php" 14 | case iconElephant = "elephant" 15 | case noIcon = "none" 16 | } 17 | -------------------------------------------------------------------------------- /phpmon/Domain/Preferences/PreferencesWindowController+Hotkey.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PreferencesWindowController+Hotkey.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 25/07/2022. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | extension PreferencesWindowController { 12 | 13 | // MARK: - Key Interaction 14 | 15 | override func keyDown(with event: NSEvent) { 16 | super.keyDown(with: event) 17 | 18 | guard let tabVC = self.contentViewController as? NSTabViewController else { 19 | return 20 | } 21 | 22 | guard let vc = tabVC.tabViewItems[tabVC.selectedTabViewItemIndex].viewController as? GenericPreferenceVC else { 23 | return 24 | } 25 | 26 | if vc.listeningForHotkeyView == nil { 27 | return 28 | } 29 | 30 | if event.keyCode == Keys.Escape || event.keyCode == Keys.Space { 31 | Log.info("A blacklisted key was pressed, canceling listen!") 32 | vc.listeningForHotkeyView!.unregister(nil) 33 | } else { 34 | vc.listeningForHotkeyView!.updateShortcut(event) 35 | } 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /phpmon/Domain/Presets/PresetHelper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PresetHelper.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 02/06/2022. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class PresetHelper { 12 | 13 | static var rollbackPreset: Preset? 14 | 15 | // MARK: - Reloading Configuration 16 | 17 | public static func loadRollbackPresetFromFile() { 18 | guard let revert = try? String( 19 | contentsOfFile: "\(Paths.homePath)/.config/phpmon/preset_revert.json", 20 | encoding: .utf8 21 | ) else { 22 | PresetHelper.rollbackPreset = nil 23 | return 24 | } 25 | 26 | guard let preset = try? JSONDecoder().decode( 27 | Preset.self, 28 | from: revert.data(using: .utf8)! 29 | ) else { 30 | PresetHelper.rollbackPreset = nil 31 | return 32 | } 33 | 34 | PresetHelper.rollbackPreset = preset 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /phpmon/Domain/SwiftUI/Common/BlockingOverlayView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BlockingOverlayView.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 19/03/2023. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftUI 11 | 12 | struct BlockingOverlayView<Content: View>: View { 13 | var isBlocking: Bool 14 | var titleText: String 15 | var detailText: String 16 | var content: () -> Content 17 | 18 | init( 19 | busy: Bool, 20 | title: String, 21 | text: String, 22 | @ViewBuilder content: @escaping () -> Content 23 | ) { 24 | self.isBlocking = busy 25 | self.titleText = title 26 | self.detailText = text 27 | self.content = content 28 | } 29 | 30 | var body: some View { 31 | ZStack(alignment: .center) { 32 | content().opacity(isBlocking ? 0 : 1) 33 | if isBlocking { 34 | VStack { 35 | ActivityIndicator() 36 | Text(titleText) 37 | .font(.system(size: 14)) 38 | .bold() 39 | .foregroundColor(.primary) 40 | .padding(.top, 8) 41 | .multilineTextAlignment(.center) 42 | Text(detailText) 43 | .font(.system(size: 11)) 44 | .foregroundColor(.primary) 45 | .padding(.top, -4) 46 | .multilineTextAlignment(.center) 47 | }.padding(60) 48 | } 49 | } 50 | .background(Color.spinnerBackground) 51 | .disabled(isBlocking) 52 | } 53 | } 54 | 55 | struct ActivityIndicator: NSViewRepresentable { 56 | func makeNSView(context: Context) -> NSProgressIndicator { 57 | let nsView = NSProgressIndicator() 58 | nsView.style = .spinning 59 | nsView.startAnimation(nil) 60 | return nsView 61 | } 62 | 63 | func updateNSView(_ nsView: NSProgressIndicator, context: Context) { 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /phpmon/Domain/SwiftUI/Common/CustomButtonStyles.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomButtonStyles.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 15/03/2024. 6 | // Copyright © 2024 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | public struct CustomButtonStyle: ButtonStyle { 12 | @Environment(\.isEnabled) var isEnabled 13 | 14 | public func makeBody(configuration: Self.Configuration) -> some View { 15 | configuration.label 16 | .padding(.vertical, 4) 17 | .padding(.horizontal, 8) 18 | .foregroundStyle(.white) 19 | .background(.statusColorBlue, in: .rect(cornerRadius: 8, style: .continuous)) 20 | .opacity({ 21 | if configuration.isPressed { 22 | return 0.4 23 | } 24 | 25 | if !isEnabled { 26 | return 0.2 27 | } 28 | 29 | return 1.0 30 | }()) 31 | } 32 | } 33 | 34 | extension ButtonStyle where Self == CustomButtonStyle { 35 | static var custom: CustomButtonStyle { .init() } 36 | } 37 | -------------------------------------------------------------------------------- /phpmon/Domain/SwiftUI/Common/HelpButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HelpButton.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 07/01/2023. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftUI 11 | 12 | struct HelpButton: View { 13 | @State var frameSize: CGFloat = 14 14 | @State var textSize: CGFloat = 12 15 | @State var shadowOpacity: CGFloat = 0.3 16 | @State var shadowRadius: CGFloat = 1 17 | 18 | var action: () -> Void 19 | 20 | var body: some View { 21 | Button(action: action, label: { 22 | ZStack { 23 | Circle() 24 | .strokeBorder(Color(NSColor.separatorColor), lineWidth: 0.5) 25 | .background(Circle().foregroundColor(Color(NSColor.controlColor)).opacity(0.7)) 26 | .shadow(color: Color(NSColor.separatorColor) 27 | .opacity(shadowOpacity), radius: shadowRadius) 28 | .frame(width: frameSize, height: frameSize) 29 | Text("?").font(.system(size: textSize, weight: .medium)) 30 | .foregroundColor(Color(NSColor.labelColor)) 31 | } 32 | }) 33 | .buttonStyle(BorderlessButtonStyle()) 34 | .focusable(false) 35 | } 36 | } 37 | 38 | #Preview("Light Mode") { 39 | HelpButton(action: {}) 40 | .padding(100) 41 | } 42 | 43 | #Preview("Dark Mode") { 44 | HelpButton(action: {}) 45 | .padding(100) 46 | .preferredColorScheme(.dark) 47 | } 48 | -------------------------------------------------------------------------------- /phpmon/Domain/SwiftUI/Common/SwiftUIHelper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftUIHelper.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 08/06/2022. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftUI 11 | 12 | var isRunningTests: Bool { 13 | let environment = ProcessInfo.processInfo.environment 14 | return environment["TEST_MODE"] != nil 15 | || environment["XCTestConfigurationFilePath"] != nil 16 | } 17 | 18 | var isRunningSwiftUIPreview: Bool { 19 | #if DEBUG 20 | // If running SwiftUI *and* when debugging 21 | return ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] != nil 22 | #else 23 | // Release builds should always return false here 24 | return false 25 | #endif 26 | } 27 | 28 | extension Color { 29 | public static var appPrimary: Color = Color("AppColor") 30 | 31 | // This next one is generated automatically via asset catalogs now 32 | // public static var appSecondary: Color = Color("AppSecondary") 33 | 34 | public static var debug: Color = { 35 | if ProcessInfo.processInfo.environment["PAINT_PHPMON_SWIFTUI_VIEWS"] != nil { 36 | return Color.yellow 37 | } 38 | return Color.clear 39 | }() 40 | } 41 | -------------------------------------------------------------------------------- /phpmon/Domain/SwiftUI/Common/UnavailableContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NoDomainsView.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 19/03/2024. 6 | // Copyright © 2024 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct UnavailableContentView: View { 12 | var title: String 13 | var description: String 14 | var icon: String 15 | var button: String? 16 | var action: (() -> Void)? 17 | 18 | init( 19 | title: String, 20 | description: String, 21 | icon: String, 22 | button: String? = nil, 23 | action: (() -> Void)? = nil 24 | ) { 25 | self.title = title 26 | self.description = description 27 | self.icon = icon 28 | self.button = button 29 | self.action = action 30 | } 31 | 32 | var body: some View { 33 | Group { 34 | VStack(spacing: 15) { 35 | Image(systemName: self.icon) 36 | .resizable() 37 | .frame(width: 48, height: 48) 38 | .foregroundColor(Color.appPrimary) 39 | .padding(.bottom, 10) 40 | Text(self.title) 41 | .font(.system(size: 18, weight: .bold)) 42 | 43 | Text(self.description) 44 | .foregroundStyle(Color.secondary) 45 | .multilineTextAlignment(.center) 46 | 47 | if self.button != nil { 48 | Button(self.button!) { 49 | self.action!() 50 | }.buttonStyle(.custom) 51 | } 52 | } 53 | } 54 | .padding(30) 55 | .frame(maxWidth: 400) 56 | } 57 | } 58 | 59 | #Preview { 60 | UnavailableContentView( 61 | title: "domain_list.domains_empty.title".localized, 62 | description: "domain_list.domains_empty.desc".localized, 63 | icon: "globe", 64 | button: "domain_list.domains_empty.button".localized, 65 | action: {} 66 | ) 67 | } 68 | -------------------------------------------------------------------------------- /phpmon/Domain/SwiftUI/Menu/HeaderView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MiniHeaderView.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 10/06/2022. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct HeaderView: View { 12 | @State var text: String 13 | 14 | var body: some View { 15 | Text(text.uppercased()) 16 | .font(.system(size: 12)) 17 | .fontWeight(.bold) 18 | .foregroundColor(.appSecondary) 19 | .multilineTextAlignment(.leading) 20 | .padding(.leading, 14.0) 21 | .frame(maxWidth: .infinity, alignment: .leading) 22 | .background(Color.debug) 23 | } 24 | 25 | // MARK: - NSMenuItem 26 | 27 | static func asMenuItem( 28 | text: String, 29 | minimumWidth: CGFloat? = nil 30 | ) -> NSMenuItem { 31 | let view = NSHostingView(rootView: Self(text: text)) 32 | view.autoresizingMask = [.width, .height] 33 | 34 | var finalWidth = view.frame.width 35 | if minimumWidth != nil && minimumWidth! > finalWidth { 36 | finalWidth = minimumWidth! 37 | } 38 | 39 | view.setFrameSize(CGSize(width: finalWidth, height: 24)) 40 | 41 | let item = NSMenuItem() 42 | item.view = view 43 | 44 | return item 45 | } 46 | } 47 | 48 | #Preview { 49 | HeaderView(text: "Hello world") 50 | .frame(width: 330.0) 51 | } 52 | -------------------------------------------------------------------------------- /phpmon/Domain/SwiftUI/Menu/SectionHeaderView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MiniHeaderView.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 10/06/2022. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct SectionHeaderView: View { 12 | 13 | @State var text: String 14 | 15 | var body: some View { 16 | Text(text) 17 | .font(.system(size: 11)) 18 | .fontWeight(.medium) 19 | .foregroundColor(.appSecondary) 20 | .background(Color.debug) 21 | .minimumScaleFactor(0.8) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /phpmon/Domain/Terminal Alert/ProgressVC.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProgressVC.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 26/07/2022. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import AppKit 11 | 12 | class ProgressViewController: NSViewController { 13 | 14 | @IBOutlet weak var labelTitle: NSTextField! 15 | @IBOutlet weak var labelDescription: NSTextField! 16 | 17 | @IBOutlet var textView: NSTextView! 18 | @IBOutlet weak var imageViewType: NSImageView! 19 | 20 | deinit { 21 | Log.perf("deinit: \(String(describing: self)).\(#function)") 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /phpmon/Domain/Terminal Alert/TerminalProgressWindowController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TerminalProgressWindowController.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 18/12/2021. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import AppKit 11 | 12 | class TerminalProgressWindowController: NSWindowController, NSWindowDelegate { 13 | 14 | static func display(title: String, description: String) -> TerminalProgressWindowController { 15 | let storyboard = NSStoryboard(name: "ProgressWindow", bundle: nil) 16 | 17 | let windowController = storyboard.instantiateController( 18 | withIdentifier: "progressWindow" 19 | ) as! TerminalProgressWindowController 20 | 21 | windowController.showWindow(windowController) 22 | windowController.window?.makeKeyAndOrderFront(nil) 23 | windowController.positionWindowInTopRightCorner() 24 | 25 | windowController.progressView?.labelTitle.stringValue = title 26 | windowController.progressView?.labelDescription.stringValue = description 27 | 28 | NSApp.activate(ignoringOtherApps: true) 29 | 30 | return windowController 31 | } 32 | 33 | var progressView: ProgressViewController? { 34 | return self.contentViewController as? ProgressViewController 35 | } 36 | 37 | public func addToConsole(_ string: String) { 38 | Task { @MainActor in 39 | guard let textView = self.progressView?.textView else { 40 | return 41 | } 42 | 43 | textView.string += string 44 | textView.scrollToEndOfDocument(nil) 45 | } 46 | } 47 | 48 | public func setType(info: Bool = true) { 49 | guard let imageView = self.progressView?.imageViewType else { 50 | return 51 | } 52 | 53 | imageView.image = NSImage(named: info ? "NSInfo" : "NSCaution") 54 | } 55 | 56 | func windowWillClose(_ notification: Notification) { 57 | self.contentViewController = nil 58 | } 59 | 60 | deinit { 61 | Log.perf("deinit: \(String(describing: self)).\(#function)") 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /phpmon/Domain/Terminal/Paths.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Paths.swift 3 | // PHP Monitor 4 | // 5 | // Copyright © 2021 Nico Verbruggen. All rights reserved. 6 | // 7 | 8 | import Foundation 9 | 10 | enum HomebrewDir: String { 11 | case opt = "/opt/homebrew" 12 | case usr = "/usr/local" 13 | } 14 | 15 | class Paths { 16 | 17 | static let shared = Paths() 18 | var baseDir: HomebrewDir 19 | var userName = String(Shell.pipe("whoami").split(separator: "\n")[0]) 20 | 21 | init() { 22 | let optBrewFound = Shell.fileExists("\(HomebrewDir.opt.rawValue)/bin/brew") 23 | let usrBrewFound = Shell.fileExists("\(HomebrewDir.usr.rawValue)/bin/brew") 24 | 25 | if optBrewFound { 26 | // This is usually the case with Homebrew installed on Apple Silicon 27 | baseDir = .opt 28 | } else if usrBrewFound { 29 | // This is usually the case with Homebrew installed on Intel (or Rosetta 2) 30 | baseDir = .usr 31 | } else { 32 | // Falling back to default "legacy" Homebrew location (for Intel) 33 | print("Seems like we couldn't determine the Homebrew directory.") 34 | print("This usually means we're in trouble... (no Homebrew?)") 35 | baseDir = .usr 36 | } 37 | } 38 | 39 | // - MARK: Binaries 40 | 41 | public static var valet: String { 42 | return "\(binPath)/valet" 43 | } 44 | 45 | public static var brew: String { 46 | return "\(binPath)/brew" 47 | } 48 | 49 | public static var php: String { 50 | return "\(binPath)/php" 51 | } 52 | 53 | public static var phpConfig: String { 54 | return "\(binPath)/php-config" 55 | } 56 | 57 | // - MARK: Paths 58 | 59 | public static var whoami: String { 60 | return shared.userName 61 | } 62 | 63 | public static var binPath: String { 64 | return "\(shared.baseDir.rawValue)/bin" 65 | } 66 | 67 | public static var optPath: String { 68 | return "\(shared.baseDir.rawValue)/opt" 69 | } 70 | 71 | public static var etcPath: String { 72 | return "\(shared.baseDir.rawValue)/etc" 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /phpmon/Domain/Watcher/App+BrewWatch.swift: -------------------------------------------------------------------------------- 1 | // 2 | // App+BrewWatch.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 03/03/2023. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension App { 12 | 13 | public func prepareHomebrewWatchers() { 14 | let notifier = FSNotifier( 15 | for: URL(fileURLWithPath: Paths.binPath), 16 | eventMask: .all, 17 | onChange: { Task { await self.onHomebrewPhpModification() } } 18 | ) 19 | 20 | App.shared.watchers["homebrewBinaries"] = notifier 21 | } 22 | 23 | public func destroyHomebrewWatchers() { 24 | // Removing requires termination and then removing reference 25 | self.watchers["homebrewBinaries"]?.terminate() 26 | self.watchers["homebrewBinaries"] = nil 27 | } 28 | 29 | public func onHomebrewPhpModification() async { 30 | // let previous = PhpEnvironments.shared.currentInstall?.version.text 31 | Log.info("Something changed in the Homebrew binary directory...") 32 | await PhpEnvironments.detectPhpVersions() 33 | await MainMenu.shared.refreshActiveInstallation() 34 | 35 | // 36 | // TODO: PHP Guard 2.0 37 | // Check if the new and previous version of PHP are different 38 | // if so, we can show a notification if needed or alert the user 39 | // 40 | // let new = PhpEnvironments.shared.currentInstall?.version.text 41 | // 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /phpmon/Domain/Watcher/FSNotifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FSNotifier.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 13/01/2023. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class FSNotifier { 12 | 13 | public static var shared: FSNotifier! = nil 14 | 15 | let queue = DispatchQueue(label: "FSWatch2Queue", attributes: .concurrent) 16 | var lastUpdate: TimeInterval? 17 | 18 | private var fileDescriptor: CInt = -1 19 | private var dispatchSource: DispatchSourceFileSystemObject? 20 | 21 | internal let url: URL 22 | 23 | init(for url: URL, eventMask: DispatchSource.FileSystemEvent, onChange: @escaping () -> Void) { 24 | self.url = url 25 | 26 | fileDescriptor = open(url.path, O_EVTONLY) 27 | 28 | dispatchSource = DispatchSource.makeFileSystemObjectSource( 29 | fileDescriptor: fileDescriptor, 30 | eventMask: eventMask, 31 | queue: self.queue 32 | ) 33 | 34 | dispatchSource?.setEventHandler(handler: { 35 | let distance = self.lastUpdate?.distance(to: Date().timeIntervalSince1970) 36 | 37 | if distance == nil || distance != nil && distance! > 1.00 { 38 | // FS event fired, checking in 1s, no duplicate FS events will be acted upon 39 | self.lastUpdate = Date().timeIntervalSince1970 40 | 41 | Task { 42 | await delay(seconds: 1) 43 | onChange() 44 | } 45 | } 46 | }) 47 | 48 | dispatchSource?.setCancelHandler(handler: { [weak self] in 49 | guard let self = self else { return } 50 | 51 | close(self.fileDescriptor) 52 | self.fileDescriptor = -1 53 | self.dispatchSource = nil 54 | }) 55 | 56 | dispatchSource?.resume() 57 | } 58 | 59 | func terminate() { 60 | dispatchSource?.cancel() 61 | } 62 | 63 | deinit { 64 | Log.perf("FSNotifier for \(self.url) will be deinitialized.") 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /phpmon/IAP/InternetAccessPolicy.strings: -------------------------------------------------------------------------------- 1 | // Top-level, general application description: 2 | "ApplicationDescription" = "PHP Monitor is a tool that shows the active PHP version in your menu bar and gives you easy access to certain PHP service actions and config files."; 3 | -------------------------------------------------------------------------------- /phpmon/Info.plist: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8"?> 2 | <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> 3 | <plist version="1.0"> 4 | <dict> 5 | <key>CFBundleDevelopmentRegion</key> 6 | <string>$(DEVELOPMENT_LANGUAGE)</string> 7 | <key>CFBundleExecutable</key> 8 | <string>$(EXECUTABLE_NAME)</string> 9 | <key>CFBundleIconFile</key> 10 | <string></string> 11 | <key>CFBundleIdentifier</key> 12 | <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string> 13 | <key>CFBundleInfoDictionaryVersion</key> 14 | <string>6.0</string> 15 | <key>CFBundleName</key> 16 | <string>$(PRODUCT_NAME)</string> 17 | <key>CFBundlePackageType</key> 18 | <string>APPL</string> 19 | <key>CFBundleShortVersionString</key> 20 | <string>$(MARKETING_VERSION)</string> 21 | <key>CFBundleURLTypes</key> 22 | <array> 23 | <dict> 24 | <key>CFBundleTypeRole</key> 25 | <string>Viewer</string> 26 | <key>CFBundleURLName</key> 27 | <string>com.nicoverbruggen.phpmon</string> 28 | <key>CFBundleURLSchemes</key> 29 | <array> 30 | <string>phpmon</string> 31 | </array> 32 | </dict> 33 | </array> 34 | <key>CFBundleVersion</key> 35 | <string>$(CURRENT_PROJECT_VERSION)</string> 36 | <key>LSApplicationCategoryType</key> 37 | <string>public.app-category.utilities</string> 38 | <key>LSMinimumSystemVersion</key> 39 | <string>$(MACOSX_DEPLOYMENT_TARGET)</string> 40 | <key>LSUIElement</key> 41 | <true/> 42 | <key>NSHumanReadableCopyright</key> 43 | <string>Copyright © 2019-2025 Nico Verbruggen. All rights reserved.</string> 44 | <key>NSMainStoryboardFile</key> 45 | <string>Main</string> 46 | <key>NSPrincipalClass</key> 47 | <string>NSApplication</string> 48 | </dict> 49 | </plist> 50 | -------------------------------------------------------------------------------- /phpmon/Modules/Domain List/Favorites.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Favorites.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 25/08/2024. 6 | // Copyright © 2024 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class Favorites { 12 | static var shared: Favorites = Favorites() 13 | 14 | var items: [String] 15 | 16 | init() { 17 | if let items = UserDefaults.standard.array(forKey: "user_favorites") as? [String] { 18 | self.items = items 19 | } else { 20 | self.items = [] 21 | } 22 | } 23 | 24 | public func contains(domain: String) -> Bool { 25 | return self.items.contains(domain) 26 | } 27 | 28 | public func toggle(domain: String) { 29 | if let index = items.firstIndex(of: domain) { 30 | items.remove(at: index) 31 | } else { 32 | items.append(domain) 33 | } 34 | 35 | UserDefaults.standard.setValue(items, forKey: "user_favorites") 36 | UserDefaults.standard.synchronize() 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /phpmon/Modules/Domain List/UI/Cells/DomainListCellProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DomainListCellProtocol.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 03/12/2021. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import AppKit 11 | 12 | protocol DomainListCellProtocol { 13 | static func getCellIdentifier(for domain: ValetListable) -> String 14 | func populateCell(with site: ValetSite) 15 | func populateCell(with proxy: ValetProxy) 16 | } 17 | -------------------------------------------------------------------------------- /phpmon/Modules/Domain List/UI/Cells/DomainListKindCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DomainListTypeCell.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 16/03/2022. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import AppKit 11 | 12 | class DomainListKindCell: NSTableCellView, DomainListCellProtocol { 13 | @IBOutlet weak var imageViewType: NSImageView! 14 | 15 | static func getCellIdentifier(for domain: ValetListable) -> String { 16 | return "domainListKindCell" 17 | } 18 | 19 | func populateCell(with site: ValetSite) { 20 | // If the `aliasPath` is nil, we're dealing with a parked site (otherwise: linked). 21 | imageViewType.image = site.aliasPath == nil 22 | ? NSImage.iconParked 23 | : NSImage.iconLinked 24 | 25 | // Unless, of course, this is a default site 26 | if site.absolutePath == Valet.shared.config.defaultSite { 27 | imageViewType.image = NSImage.iconDefault 28 | } 29 | 30 | imageViewType.contentTintColor = NSColor.tertiaryLabelColor 31 | } 32 | 33 | func populateCell(with proxy: ValetProxy) { 34 | imageViewType.image = NSImage.iconProxy 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /phpmon/Modules/Domain List/UI/Cells/DomainListNameCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DomainListNameCell.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 16/03/2022. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import AppKit 11 | 12 | class DomainListNameCell: NSTableCellView, DomainListCellProtocol { 13 | @IBOutlet weak var labelSiteName: NSTextField! 14 | @IBOutlet weak var labelPathName: NSTextField! 15 | 16 | static func getCellIdentifier(for domain: ValetListable) -> String { 17 | return domain.getListableFavorited() ? "domainListNameCellFavorited" : "domainListNameCell" 18 | } 19 | 20 | func populateCell(with site: ValetSite) { 21 | labelSiteName.stringValue = "\(site.name).\(site.tld)" 22 | labelPathName.stringValue = site.absolutePathRelative 23 | } 24 | 25 | func populateCell(with proxy: ValetProxy) { 26 | labelSiteName.stringValue = "\(proxy.domain).\(proxy.tld)" 27 | labelPathName.stringValue = proxy.target 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /phpmon/Modules/Domain List/UI/Cells/DomainListTLSCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DomainListNameCell.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 16/03/2022. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import AppKit 11 | 12 | class DomainListTLSCell: NSTableCellView, DomainListCellProtocol { 13 | @IBOutlet weak var imageViewLock: NSImageView! 14 | 15 | static func getCellIdentifier(for domain: ValetListable) -> String { 16 | return "domainListTLSCell" 17 | } 18 | 19 | func populateCell(with site: ValetSite) { 20 | imageViewLock.contentTintColor = site.secured 21 | ? NSColor(named: "IconColorGreen") 22 | : NSColor(named: "IconColorRed") 23 | } 24 | 25 | func populateCell(with proxy: ValetProxy) { 26 | imageViewLock.contentTintColor = proxy.secured 27 | ? NSColor(named: "IconColorGreen") 28 | : NSColor(named: "IconColorRed") 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /phpmon/Modules/Domain List/UI/Cells/DomainListTypeCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DomainListTypeCell.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 16/03/2022. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import AppKit 11 | 12 | class DomainListTypeCell: NSTableCellView, DomainListCellProtocol { 13 | @IBOutlet weak var labelDriver: NSTextField! 14 | @IBOutlet weak var labelPhpVersion: NSTextField! 15 | 16 | static func getCellIdentifier(for domain: ValetListable) -> String { 17 | return "domainListTypeCell" 18 | } 19 | 20 | func populateCell(with site: ValetSite) { 21 | labelDriver.stringValue = site.driver ?? "driver.not_detected".localized 22 | 23 | // Determine the Laravel version 24 | if site.driver == "Laravel" && site.notableComposerDependencies.keys.contains("laravel/framework") { 25 | let constraint = site.notableComposerDependencies["laravel/framework"]! 26 | labelDriver.stringValue = "Laravel (\(constraint))" 27 | } 28 | 29 | // PHP version 30 | labelPhpVersion.stringValue = site.preferredPhpVersion == "???" ? "PHP" : "PHP \(site.preferredPhpVersion)" 31 | } 32 | 33 | func populateCell(with proxy: ValetProxy) { 34 | labelDriver.stringValue = "Proxy" 35 | labelPhpVersion.stringValue = "Active" 36 | return 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /phpmon/Modules/Domain List/UI/SelectionVC.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SelectionVC.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 14/04/2022. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Cocoa 11 | 12 | class SelectionVC: NSViewController { 13 | 14 | weak var domainListWC: DomainListWindowController? 15 | 16 | @IBOutlet weak var textFieldTitle: NSTextField! 17 | @IBOutlet weak var textFieldDescription: NSTextField! 18 | @IBOutlet weak var buttonCreateLink: NSButton! 19 | @IBOutlet weak var buttonCreateProxy: NSButton! 20 | @IBOutlet weak var buttonCancel: NSButton! 21 | 22 | override func viewDidLoad() { 23 | super.viewDidLoad() 24 | loadStaticLocalisedStrings() 25 | } 26 | 27 | override func viewDidAppear() { 28 | view.window?.makeFirstResponder(buttonCreateLink) 29 | } 30 | 31 | private func dismissView(outcome: NSApplication.ModalResponse) { 32 | guard let window = self.view.window, let parent = window.sheetParent else { return } 33 | parent.endSheet(window, returnCode: outcome) 34 | } 35 | 36 | // MARK: - Localisation 37 | 38 | func loadStaticLocalisedStrings() { 39 | textFieldTitle.stringValue = "selection.title".localized 40 | textFieldDescription.stringValue = "selection.description".localized 41 | buttonCancel.title = "selection.cancel".localized 42 | buttonCreateLink.title = "selection.create_link".localized 43 | buttonCreateProxy.title = "selection.create_proxy".localized 44 | } 45 | 46 | // MARK: - Outlet Interactions 47 | 48 | @IBAction func pressedCreateLink(_ sender: Any) { 49 | self.dismissView(outcome: .continue) 50 | domainListWC?.startCreateLinkFlow() 51 | } 52 | 53 | @IBAction func pressedCreateProxy(_ sender: Any) { 54 | self.dismissView(outcome: .continue) 55 | domainListWC?.startCreateProxyFlow() 56 | } 57 | 58 | @IBAction func pressedCancel(_ sender: Any) { 59 | self.dismissView(outcome: .cancel) 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /phpmon/Modules/Domain List/UI/Subclass/PMTableView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PMTableView.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 05/09/2022. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | /** 12 | This subclassed version of NSTableView selects a row upon right-clicking, 13 | thus making the domain list behave more like you'd expect. 14 | */ 15 | public class PMTableView: NSTableView { 16 | 17 | override open func menu(for event: NSEvent) -> NSMenu? { 18 | let row = self.row(at: self.convert(event.locationInWindow, from: nil)) 19 | 20 | if row >= 0 { 21 | self.selectRowIndexes(IndexSet(integer: row), byExtendingSelection: false) 22 | } 23 | 24 | return super.menu(for: event) 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /phpmon/Modules/Onboarding/OnboardingWindowController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OnboardingWindowController.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 25/06/2022. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import SwiftUI 11 | 12 | class OnboardingWindowController: PMWindowController { 13 | 14 | // MARK: - Window Identifier 15 | 16 | override var windowName: String { 17 | return "Onboarding" 18 | } 19 | 20 | public static func create(delegate: NSWindowDelegate?) { 21 | let windowController = Self() 22 | windowController.window = NSWindow() 23 | 24 | guard let window = windowController.window else { return } 25 | window.title = "" 26 | window.styleMask = [.titled, .closable, .miniaturizable] 27 | window.titlebarAppearsTransparent = true 28 | window.delegate = delegate ?? windowController 29 | window.contentView = NSHostingView(rootView: OnboardingView()) 30 | window.setContentSize(window.contentView!.fittingSize) 31 | 32 | App.shared.onboardingWindowController = windowController 33 | } 34 | 35 | public static func show(delegate: NSWindowDelegate? = nil) { 36 | if App.shared.onboardingWindowController == nil { 37 | Self.create(delegate: delegate) 38 | } 39 | 40 | App.shared.onboardingWindowController?.showWindow(self) 41 | App.shared.onboardingWindowController?.window?.setCenterPosition(offsetY: 70) 42 | 43 | NSApp.activate(ignoringOtherApps: true) 44 | } 45 | 46 | override func close() { 47 | super.close() 48 | 49 | // Search for updates after closing the window 50 | if Stats.successfulLaunchCount == 1 { 51 | Task { await AppUpdater().checkForUpdates(userInitiated: false) } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /phpmon/Modules/PHP Config Editor/Data/PhpPreference.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PhpPreference.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 04/09/2023. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftUI 11 | 12 | class PhpPreference { 13 | let key: String 14 | 15 | init(key: String) { 16 | self.key = key 17 | } 18 | 19 | internal static func persistToIniFile(key: String, value: String) throws { 20 | if let file = PhpEnvironments.shared.getConfigFile(forKey: key) { 21 | return try file.replace(key: key, value: value) 22 | } 23 | 24 | throw PhpConfigurationFile.ReplacementErrors.missingFile 25 | } 26 | } 27 | 28 | class BoolPhpPreference: PhpPreference { 29 | @State var value: Bool = true 30 | } 31 | 32 | class StringPhpPreference: PhpPreference { 33 | @State var value: String = "" 34 | } 35 | -------------------------------------------------------------------------------- /phpmon/Modules/PHP Config Editor/UI/ConfigManagerWindowController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConfigManagerWindowController.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 12/09/2023. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import SwiftUI 11 | 12 | class PhpConfigManagerWindowController: PMWindowController { 13 | 14 | // MARK: - Window Identifier 15 | 16 | override var windowName: String { 17 | return "ConfigManager" 18 | } 19 | 20 | public static func create(delegate: NSWindowDelegate?) { 21 | let windowController = Self() 22 | windowController.window = NSWindow() 23 | 24 | guard let window = windowController.window else { return } 25 | window.title = "" 26 | window.styleMask = [.titled, .closable, .miniaturizable] 27 | window.titlebarAppearsTransparent = true 28 | window.delegate = delegate ?? windowController 29 | window.contentView = NSHostingView(rootView: ConfigManagerView()) 30 | window.setContentSize(NSSize(width: 600, height: 480)) 31 | 32 | App.shared.phpConfigManagerWindowController = windowController 33 | } 34 | 35 | public static func show(delegate: NSWindowDelegate? = nil) { 36 | if App.shared.phpConfigManagerWindowController == nil { 37 | Self.create(delegate: delegate) 38 | } 39 | 40 | App.shared.phpConfigManagerWindowController?.showWindow(self) 41 | App.shared.phpConfigManagerWindowController?.positionWindowInTopRightCorner() 42 | 43 | NSApp.activate(ignoringOtherApps: true) 44 | App.shared.phpConfigManagerWindowController?.window?.orderFrontRegardless() 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /phpmon/Modules/PHP Doctor/Data/PhpConfigChecker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PhpConfigChecker.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 24/02/2023. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct FileExistenceCheck { 12 | let condition: (() -> Bool)? 13 | let path: String 14 | } 15 | 16 | class PhpConfigChecker { 17 | public static var shared = PhpConfigChecker() 18 | 19 | var missing: [String] = [] 20 | 21 | public func check() { 22 | missing = [] 23 | 24 | let shouldExist: [FileExistenceCheck] = [ 25 | FileExistenceCheck(condition: nil, path: "php.ini"), 26 | FileExistenceCheck(condition: nil, path: "php-fpm.conf"), 27 | FileExistenceCheck(condition: { Valet.installed }, path: "php-fpm.d/valet-fpm.conf") 28 | ] 29 | 30 | for version in PhpEnvironments.shared.availablePhpVersions { 31 | for file in shouldExist { 32 | // Early exit in case our condition is not met 33 | if file.condition != nil && file.condition!() == false { 34 | continue 35 | } 36 | 37 | // Do the check 38 | let fullFilePath = Paths.etcPath.appending("/php/\(version)/\(file.path)") 39 | if !FileSystem.fileExists(fullFilePath) { 40 | missing.append(fullFilePath) 41 | } 42 | } 43 | } 44 | 45 | if !missing.isEmpty { 46 | Log.warn("The following config file(s) were missing: \(missing)") 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /phpmon/Modules/PHP Doctor/Data/Warning.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Warning.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 09/08/2022. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct Warning: Identifiable, Hashable { 12 | var id = UUID() 13 | let command: () async -> Bool 14 | let name: String 15 | let title: String 16 | let paragraphs: () -> [String] 17 | let url: String? 18 | 19 | /** 20 | - Parameters: 21 | - command: The command that, if it returns true, means that a warning applies 22 | - name: The internal name or description for this warning 23 | - title: The title displayed for the user 24 | - paragraphs: The main body of text displayed for the user 25 | - url: The URL that one can navigate to for more information (if applicable) 26 | */ 27 | init( 28 | command: @escaping () async -> Bool, 29 | name: String, 30 | title: String, 31 | paragraphs: @escaping () -> [String], 32 | url: String? 33 | ) { 34 | self.command = command 35 | self.name = name 36 | self.title = title 37 | self.paragraphs = paragraphs 38 | self.url = url 39 | } 40 | 41 | public func applies() async -> Bool { 42 | return await self.command() 43 | } 44 | 45 | public static func == (lhs: Warning, rhs: Warning) -> Bool { 46 | return lhs.hashValue == rhs.hashValue 47 | } 48 | 49 | public func hash(into hasher: inout Hasher) { 50 | hasher.combine(self.id) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /phpmon/Modules/PHP Doctor/UI/NoWarningsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NoWarningsView.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 15/08/2022. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct NoWarningsView: View { 12 | var body: some View { 13 | VStack(alignment: .center, spacing: 15) { 14 | Image(systemName: "checkmark.circle.fill") 15 | .resizable() 16 | .renderingMode(.template) 17 | .foregroundColor(Color.green) 18 | .frame(width: 24, height: 24) 19 | VStack(alignment: .center) { 20 | Text("warnings.none".localizedForSwiftUI) 21 | } 22 | } 23 | .frame(minWidth: 0, maxWidth: .infinity) 24 | .padding(25) 25 | } 26 | } 27 | 28 | #Preview { 29 | NoWarningsView().padding() 30 | } 31 | -------------------------------------------------------------------------------- /phpmon/Modules/PHP Doctor/UI/PhpDoctorWindowController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PhpDoctorWindowController.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 09/08/2022. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import SwiftUI 11 | 12 | class PhpDoctorWindowController: PMWindowController { 13 | 14 | // MARK: - Window Identifier 15 | 16 | override var windowName: String { 17 | return "Warnings" 18 | } 19 | 20 | public static func create(delegate: NSWindowDelegate?) { 21 | let windowController = Self() 22 | windowController.window = NSWindow() 23 | 24 | guard let window = windowController.window else { return } 25 | window.title = "" 26 | window.styleMask = [.titled, .closable, .miniaturizable] 27 | window.titlebarAppearsTransparent = true 28 | window.delegate = delegate ?? windowController 29 | window.contentView = NSHostingView(rootView: PhpDoctorView()) 30 | window.setContentSize(window.contentView!.fittingSize) 31 | 32 | App.shared.phpDoctorWindowController = windowController 33 | } 34 | 35 | public static func show(delegate: NSWindowDelegate? = nil) { 36 | if App.shared.phpDoctorWindowController == nil { 37 | Self.create(delegate: delegate) 38 | } 39 | 40 | App.shared.phpDoctorWindowController?.showWindow(self) 41 | App.shared.phpDoctorWindowController?.window?.setCenterPosition(offsetY: 70) 42 | 43 | NSApp.activate(ignoringOtherApps: true) 44 | App.shared.phpDoctorWindowController?.window?.orderFrontRegardless() 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /phpmon/Modules/PHP Extension Manager/Data/BrewExtensionsObservable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BrewExtensionsObservable.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 21/11/2023. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class BrewExtensionsObservable: ObservableObject { 12 | @Published var phpVersion: String { 13 | didSet { 14 | self.loadExtensionData(for: phpVersion) 15 | } 16 | } 17 | 18 | @Published var extensions: [BrewPhpExtension] = [] 19 | 20 | init(phpVersion: String) { 21 | self.phpVersion = phpVersion 22 | self.loadExtensionData(for: phpVersion) 23 | } 24 | 25 | public func loadExtensionData(for version: String) { 26 | let tapFormulae = BrewTapFormulae.from(tap: "shivammathur/homebrew-extensions") 27 | 28 | if let filteredTapFormulae = tapFormulae[version] { 29 | self.extensions = filteredTapFormulae 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /phpmon/Modules/PHP Extension Manager/UI/PhpExtensionManagerWindowController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PhpExtensionManagerWindowController.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 13/11/2023. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Cocoa 11 | import SwiftUI 12 | 13 | class PhpExtensionManagerWindowController: PMWindowController { 14 | 15 | // MARK: - Window Identifier 16 | 17 | var view: PhpExtensionManagerView! 18 | 19 | override var windowName: String { 20 | return "PhpExtensionManager" 21 | } 22 | 23 | public static func create(delegate: NSWindowDelegate?) { 24 | let windowController = Self() 25 | 26 | windowController.window = NSWindow() 27 | windowController.view = PhpExtensionManagerView() 28 | 29 | guard let window = windowController.window else { return } 30 | window.title = "phpextman.window.title".localized 31 | window.styleMask = [.titled, .closable, .miniaturizable, .resizable] 32 | window.titlebarAppearsTransparent = false 33 | window.delegate = delegate ?? windowController 34 | window.contentView = NSHostingView(rootView: windowController.view) 35 | window.setContentSize(NSSize(width: 600, height: 800)) 36 | 37 | App.shared.phpExtensionManagerWindowController = windowController 38 | } 39 | 40 | public static func show(delegate: NSWindowDelegate? = nil) { 41 | if App.shared.phpExtensionManagerWindowController == nil { 42 | Self.create(delegate: delegate) 43 | } 44 | 45 | App.shared.phpExtensionManagerWindowController?.showWindow(self) 46 | App.shared.phpExtensionManagerWindowController?.positionWindowInTopRightCorner() 47 | 48 | NSApp.activate(ignoringOtherApps: true) 49 | 50 | App.shared.phpExtensionManagerWindowController?.window?.orderFrontRegardless() 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /phpmon/Modules/PHP Version Manager/Data/BrewFormula+UI.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BrewPhpFormula+UI.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 02/05/2023. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftUI 11 | 12 | extension BrewPhpFormula { 13 | var icon: String { 14 | if self.hasUpgrade { 15 | return "arrow.up.square.fill" 16 | } else if self.isInstalled { 17 | return "checkmark.square.fill" 18 | } 19 | return "square.dashed" 20 | } 21 | 22 | var iconColor: Color { 23 | if self.hasUpgrade { 24 | return Color("StatusColorBlue") 25 | } else if self.isInstalled { 26 | return Color("StatusColorGreen") 27 | } 28 | return Color.gray.opacity(0.3) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /phpmon/Modules/PHP Version Manager/Data/BrewFormulaeObservable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BrewPhpFormula.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 13/11/2023. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class BrewFormulaeObservable: ObservableObject { 12 | @Published var phpVersions: [BrewPhpFormula] = [] 13 | 14 | var upgradeable: [BrewPhpFormula] { 15 | return phpVersions.filter { formula in 16 | formula.hasUpgrade 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /phpmon/Modules/PHP Version Manager/UI/PhpVersionManagerWindowController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PhpVersionManagerWindowController.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 19/03/2023. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Cocoa 11 | import SwiftUI 12 | 13 | class PhpVersionManagerWindowController: PMWindowController { 14 | 15 | // MARK: - Window Identifier 16 | 17 | var view: PhpVersionManagerView! 18 | 19 | override var windowName: String { 20 | return "PhpVersionManager" 21 | } 22 | 23 | public static func create(delegate: NSWindowDelegate?) { 24 | let windowController = Self() 25 | windowController.window = NSWindow() 26 | windowController.view = PhpVersionManagerView( 27 | formulae: Brew.shared.formulae, 28 | handler: BrewPhpFormulaeHandler() 29 | ) 30 | 31 | guard let window = windowController.window else { return } 32 | window.title = "" 33 | window.styleMask = [.titled, .closable, .miniaturizable, .resizable] 34 | window.titlebarAppearsTransparent = true 35 | window.delegate = delegate ?? windowController 36 | window.contentView = NSHostingView(rootView: windowController.view) 37 | window.setContentSize(NSSize(width: 600, height: 800)) 38 | 39 | App.shared.phpVersionManagerWindowController = windowController 40 | } 41 | 42 | public static func show(delegate: NSWindowDelegate? = nil) { 43 | if App.shared.phpVersionManagerWindowController == nil { 44 | Self.create(delegate: delegate) 45 | } 46 | 47 | App.shared.phpVersionManagerWindowController?.showWindow(self) 48 | App.shared.phpVersionManagerWindowController?.positionWindowInTopRightCorner() 49 | 50 | NSApp.activate(ignoringOtherApps: true) 51 | 52 | App.shared.phpVersionManagerWindowController?.window?.orderFrontRegardless() 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /phpmon/Vendor/HotKey/HotKey.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | import Carbon 3 | 4 | public final class HotKey { 5 | 6 | // MARK: - Types 7 | 8 | public typealias Handler = () -> Void 9 | 10 | // MARK: - Properties 11 | 12 | let identifier = UUID() 13 | 14 | public let keyCombo: KeyCombo 15 | public var keyDownHandler: Handler? 16 | public var keyUpHandler: Handler? 17 | public var isPaused = false { 18 | didSet { 19 | if isPaused { 20 | HotKeysController.unregister(self) 21 | } else { 22 | HotKeysController.register(self) 23 | } 24 | } 25 | } 26 | 27 | // MARK: - Initializers 28 | 29 | public init(keyCombo: KeyCombo, keyDownHandler: Handler? = nil, keyUpHandler: Handler? = nil) { 30 | self.keyCombo = keyCombo 31 | self.keyDownHandler = keyDownHandler 32 | self.keyUpHandler = keyUpHandler 33 | 34 | HotKeysController.register(self) 35 | } 36 | 37 | public convenience init(carbonKeyCode: UInt32, carbonModifiers: UInt32, keyDownHandler: Handler? = nil, keyUpHandler: Handler? = nil) { 38 | let keyCombo = KeyCombo(carbonKeyCode: carbonKeyCode, carbonModifiers: carbonModifiers) 39 | self.init(keyCombo: keyCombo, keyDownHandler: keyDownHandler, keyUpHandler: keyUpHandler) 40 | } 41 | 42 | public convenience init(key: Key, modifiers: NSEvent.ModifierFlags, keyDownHandler: Handler? = nil, keyUpHandler: Handler? = nil) { 43 | let keyCombo = KeyCombo(key: key, modifiers: modifiers) 44 | self.init(keyCombo: keyCombo, keyDownHandler: keyDownHandler, keyUpHandler: keyUpHandler) 45 | } 46 | 47 | deinit { 48 | HotKeysController.unregister(self) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /phpmon/Vendor/HotKey/KeyCombo.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | 3 | public struct KeyCombo: Equatable { 4 | 5 | // MARK: - Properties 6 | 7 | public var carbonKeyCode: UInt32 8 | public var carbonModifiers: UInt32 9 | 10 | public var key: Key? { 11 | get { 12 | return Key(carbonKeyCode: carbonKeyCode) 13 | } 14 | 15 | set { 16 | carbonKeyCode = newValue?.carbonKeyCode ?? 0 17 | } 18 | } 19 | 20 | public var modifiers: NSEvent.ModifierFlags { 21 | get { 22 | return NSEvent.ModifierFlags(carbonFlags: carbonModifiers) 23 | } 24 | 25 | set { 26 | carbonModifiers = newValue.carbonFlags 27 | } 28 | } 29 | 30 | public var isValid: Bool { 31 | return carbonKeyCode >= 0 32 | } 33 | 34 | // MARK: - Initializers 35 | 36 | public init(carbonKeyCode: UInt32, carbonModifiers: UInt32 = 0) { 37 | self.carbonKeyCode = carbonKeyCode 38 | self.carbonModifiers = carbonModifiers 39 | } 40 | 41 | public init(key: Key, modifiers: NSEvent.ModifierFlags = []) { 42 | self.carbonKeyCode = key.carbonKeyCode 43 | self.carbonModifiers = modifiers.carbonFlags 44 | } 45 | 46 | // MARK: - Converting Keys 47 | 48 | public static func carbonKeyCodeToString(_ carbonKeyCode: UInt32) -> String? { 49 | return nil 50 | } 51 | } 52 | 53 | extension KeyCombo { 54 | public var dictionary: [String: Any] { 55 | return [ 56 | "keyCode": Int(carbonKeyCode), 57 | "modifiers": Int(carbonModifiers) 58 | ] 59 | } 60 | 61 | public init?(dictionary: [String: Any]) { 62 | guard let keyCode = dictionary["keyCode"] as? Int, 63 | let modifiers = dictionary["modifiers"] as? Int 64 | else { 65 | return nil 66 | } 67 | 68 | self.init(carbonKeyCode: UInt32(keyCode), carbonModifiers: UInt32(modifiers)) 69 | } 70 | } 71 | 72 | extension KeyCombo: CustomStringConvertible { 73 | public var description: String { 74 | var output = modifiers.description 75 | 76 | if let keyDescription = key?.description { 77 | output += keyDescription 78 | } 79 | 80 | return output 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /phpmon/Vendor/HotKey/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017–2019 Sam Soffes, http://soff.es 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /phpmon/Vendor/HotKey/ModifierFlagsExtension.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | import Carbon 3 | 4 | extension NSEvent.ModifierFlags { 5 | public var carbonFlags: UInt32 { 6 | var carbonFlags: UInt32 = 0 7 | 8 | if contains(.command) { 9 | carbonFlags |= UInt32(cmdKey) 10 | } 11 | 12 | if contains(.option) { 13 | carbonFlags |= UInt32(optionKey) 14 | } 15 | 16 | if contains(.control) { 17 | carbonFlags |= UInt32(controlKey) 18 | } 19 | 20 | if contains(.shift) { 21 | carbonFlags |= UInt32(shiftKey) 22 | } 23 | 24 | return carbonFlags 25 | } 26 | 27 | public init(carbonFlags: UInt32) { 28 | self.init() 29 | 30 | if carbonFlags & UInt32(cmdKey) == UInt32(cmdKey) { 31 | insert(.command) 32 | } 33 | 34 | if carbonFlags & UInt32(optionKey) == UInt32(optionKey) { 35 | insert(.option) 36 | } 37 | 38 | if carbonFlags & UInt32(controlKey) == UInt32(controlKey) { 39 | insert(.control) 40 | } 41 | 42 | if carbonFlags & UInt32(shiftKey) == UInt32(shiftKey) { 43 | insert(.shift) 44 | } 45 | } 46 | } 47 | 48 | extension NSEvent.ModifierFlags: @retroactive CustomStringConvertible { 49 | public var description: String { 50 | var output = "" 51 | 52 | if contains(.control) { 53 | output += Key.control.description 54 | } 55 | 56 | if contains(.option) { 57 | output += Key.option.description 58 | } 59 | 60 | if contains(.shift) { 61 | output += Key.shift.description 62 | } 63 | 64 | if contains(.command) { 65 | output += Key.command.description 66 | } 67 | 68 | return output 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /phpmon/phpmon.entitlements: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8"?> 2 | <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> 3 | <plist version="1.0"> 4 | <dict> 5 | <key>com.apple.security.app-sandbox</key> 6 | <false/> 7 | <key>com.apple.security.files.user-selected.read-only</key> 8 | <false/> 9 | </dict> 10 | </plist> 11 | -------------------------------------------------------------------------------- /tests/PHP Monitor.xctestplan: -------------------------------------------------------------------------------- 1 | { 2 | "configurations" : [ 3 | { 4 | "id" : "98F42C11-E6D2-4AD9-A5CA-40EFE44F384A", 5 | "name" : "Configuration 1", 6 | "options" : { 7 | 8 | } 9 | } 10 | ], 11 | "defaultOptions" : { 12 | "codeCoverage" : false, 13 | "commandLineArgumentEntries" : [ 14 | { 15 | "argument" : "--v", 16 | "enabled" : false 17 | } 18 | ], 19 | "environmentVariableEntries" : [ 20 | { 21 | "enabled" : false, 22 | "key" : "EXTREME_DOCTOR_MODE", 23 | "value" : "" 24 | }, 25 | { 26 | "enabled" : false, 27 | "key" : "SLOW_SHELL_MODE", 28 | "value" : "" 29 | }, 30 | { 31 | "enabled" : false, 32 | "key" : "PAINT_PHPMON_SWIFTUI_VIEWS", 33 | "value" : "" 34 | } 35 | ], 36 | "targetForVariableExpansion" : { 37 | "containerPath" : "container:PHP Monitor.xcodeproj", 38 | "identifier" : "C41C1B3222B0097F00E7CF16", 39 | "name" : "PHP Monitor" 40 | } 41 | }, 42 | "testTargets" : [ 43 | { 44 | "parallelizable" : true, 45 | "target" : { 46 | "containerPath" : "container:PHP Monitor.xcodeproj", 47 | "identifier" : "C4F7807825D7F84B000DBC97", 48 | "name" : "Unit Tests" 49 | } 50 | }, 51 | { 52 | "target" : { 53 | "containerPath" : "container:PHP Monitor.xcodeproj", 54 | "identifier" : "C471E7AC28F9B4940021E251", 55 | "name" : "Feature Tests" 56 | } 57 | }, 58 | { 59 | "target" : { 60 | "containerPath" : "container:PHP Monitor.xcodeproj", 61 | "identifier" : "C471E7BB28F9B90F0021E251", 62 | "name" : "UI Tests" 63 | } 64 | } 65 | ], 66 | "version" : 1 67 | } 68 | -------------------------------------------------------------------------------- /tests/Shared/Utility.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Utility.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 14/02/2021. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class Utility { 12 | 13 | public static func copyToTemporaryFile(resourceName: String, fileExtension: String) -> URL? { 14 | if let bundleURL = Bundle(for: Self.self).url(forResource: resourceName, withExtension: fileExtension) { 15 | let tempDirectoryURL = NSURL.fileURL(withPath: NSTemporaryDirectory(), isDirectory: true) 16 | let targetURL = tempDirectoryURL.appendingPathComponent("\(UUID().uuidString).\(fileExtension)") 17 | 18 | do { 19 | try FileManager.default.copyItem(at: bundleURL, to: targetURL) 20 | return targetURL 21 | } catch let error { 22 | Log.err("Unable to copy file: \(error)") 23 | } 24 | } 25 | 26 | return nil 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/Shared/XCPMApplication.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XCPMApplication.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 16/10/2022. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | class XCPMApplication: XCUIApplication { 12 | public func withConfiguration(_ configuration: TestableConfiguration) { 13 | let path = persistTestable(configuration) 14 | self.launchArguments = ["--configuration:\(path)"] 15 | } 16 | 17 | private func persistTestable(_ configuration: TestableConfiguration) -> String { 18 | let tempDirectoryURL = NSURL.fileURL(withPath: NSTemporaryDirectory(), isDirectory: true) 19 | let targetURL = tempDirectoryURL.appendingPathComponent("\(UUID().uuidString).json") 20 | try! configuration.toJson().write(toFile: targetURL.path, atomically: true, encoding: .utf8) 21 | return targetURL.path 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/feature/FeatureTestCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FeatureTestCase.swift 3 | // Feature Tests 4 | // 5 | // Created by Nico Verbruggen on 07/11/2022. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | class FeatureTestCase: XCTestCase { 12 | 13 | var fakeFileSystem: TestableFileSystem { 14 | let fs = ActiveFileSystem.shared 15 | 16 | if fs is TestableFileSystem { 17 | return fs as! TestableFileSystem 18 | } 19 | 20 | fatalError("The active filesystem is not a TestableFileSystem. Please use `ActiveFileSystem` to use the fake filesystem.") 21 | } 22 | 23 | public func assertFileSystemHas( 24 | _ path: String, 25 | file: StaticString = #filePath, 26 | line: UInt = #line 27 | ) { 28 | XCTAssertTrue(fakeFileSystem.files.keys.contains(path), file: file, line: line) 29 | } 30 | 31 | public func assertFileSystemDoesNotHave( 32 | _ path: String, 33 | file: StaticString = #filePath, 34 | line: UInt = #line 35 | ) { 36 | XCTAssertFalse(fakeFileSystem.files.keys.contains(path), file: file, line: line) 37 | } 38 | 39 | public func assertFileHasContents( 40 | _ path: String, 41 | contents: String, 42 | file: StaticString = #filePath, 43 | line: UInt = #line 44 | ) { 45 | XCTAssertEqual(contents, fakeFileSystem.files[path]?.content, file: file, line: line) 46 | } 47 | 48 | } 49 | 50 | -------------------------------------------------------------------------------- /tests/feature/InternalSwitcherTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Feature_Tests.swift 3 | // Feature Tests 4 | // 5 | // Created by Nico Verbruggen on 14/10/2022. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | final class InternalSwitcherTest: FeatureTestCase { 12 | 13 | public func testDefaultPhpFpmPoolIsMoved() async { 14 | ActiveFileSystem.useTestable([ 15 | "/opt/homebrew/etc/php/8.1/php-fpm.d/www.conf": .fake(.text) 16 | ]) 17 | 18 | let outcome = await InternalSwitcher().disableDefaultPhpFpmPool("8.1") 19 | XCTAssertTrue(outcome) 20 | 21 | assertFileSystemHas("/opt/homebrew/etc/php/8.1/php-fpm.d/www.conf.disabled-by-phpmon") 22 | assertFileSystemDoesNotHave("/opt/homebrew/etc/php/8.1/php-fpm.d/www.conf") 23 | } 24 | 25 | public func testExistingDisabledByPhpMonFileIsRemoved() async { 26 | ActiveFileSystem.useTestable([ 27 | "/opt/homebrew/etc/php/8.1/php-fpm.d/www.conf": .fake(.text, "system generated"), 28 | "/opt/homebrew/etc/php/8.1/php-fpm.d/www.conf.disabled-by-phpmon": .fake(.text, "phpmon generated") 29 | ]) 30 | 31 | assertFileHasContents( 32 | "/opt/homebrew/etc/php/8.1/php-fpm.d/www.conf.disabled-by-phpmon", 33 | contents: "phpmon generated" 34 | ) 35 | 36 | let outcome = await InternalSwitcher().disableDefaultPhpFpmPool("8.1") 37 | XCTAssertTrue(outcome) 38 | 39 | assertFileSystemHas("/opt/homebrew/etc/php/8.1/php-fpm.d/www.conf.disabled-by-phpmon") 40 | assertFileSystemDoesNotHave("/opt/homebrew/etc/php/8.1/php-fpm.d/www.conf") 41 | 42 | assertFileHasContents( 43 | "/opt/homebrew/etc/php/8.1/php-fpm.d/www.conf.disabled-by-phpmon", 44 | contents: "system generated" 45 | ) 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /tests/ui/DomainsListTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StartupTest.swift 3 | // UI Tests 4 | // 5 | // Created by Nico Verbruggen on 14/10/2022. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | final class DomainsListTest: UITestCase { 12 | 13 | override func setUpWithError() throws { 14 | continueAfterFailure = false 15 | } 16 | 17 | override func tearDownWithError() throws {} 18 | 19 | final func test_can_always_open_domains_list() throws { 20 | let app = launch(openMenu: true) 21 | 22 | app.menuItems["mi_domain_list".localized].click() 23 | } 24 | 25 | final func test_can_filter_domains_list() throws { 26 | let app = launch(openMenu: true) 27 | 28 | app.menuItems["mi_domain_list".localized].click() 29 | 30 | let window = app.windows.element(boundBy: 0) 31 | XCTAssertEqual(window.title, "domain_list.title".localized) 32 | 33 | let searchField = window.searchFields.element(boundBy: 0) 34 | 35 | searchField.click() 36 | searchField.typeText("non-existent thing") 37 | Thread.sleep(forTimeInterval: 0.2) 38 | XCTAssertTrue(window.tables.tableRows.count == 0) 39 | 40 | searchField.clearText() 41 | searchField.click() 42 | searchField.typeText("concord") 43 | Thread.sleep(forTimeInterval: 0.2) 44 | XCTAssertTrue(window.tables.tableRows.count == 1) 45 | 46 | sleep(1) 47 | } 48 | 49 | final func test_can_tap_add_domain_button() throws { 50 | let app = launch(openMenu: true) 51 | 52 | app.menuItems["mi_domain_list".localized].click() 53 | 54 | let window = app.windows.element(boundBy: 0) 55 | XCTAssertEqual(window.title, "domain_list.title".localized) 56 | 57 | window.buttons["Add Link"].click() 58 | 59 | assertExists(app.staticTexts["selection.title".localized]) 60 | assertExists(app.buttons["selection.create_link".localized]) 61 | assertExists(app.buttons["selection.create_proxy".localized]) 62 | assertExists(app.buttons["selection.cancel".localized]) 63 | 64 | sleep(1) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /tests/unit/Commands/CommandTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CommandTest.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 13/02/2021. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | class CommandTest: XCTestCase { 12 | 13 | func test_determine_php_version() { 14 | let version = Command.execute( 15 | path: Paths.php, 16 | arguments: ["-v"], 17 | trimNewlines: false 18 | ) 19 | 20 | XCTAssert(version.contains("(cli)")) 21 | XCTAssert(version.contains("NTS")) 22 | XCTAssert(version.contains("built")) 23 | XCTAssert(version.contains("Zend")) 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /tests/unit/Parsers/CaskFileParserTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CaskFileParserTest.swift 3 | // Unit Tests 4 | // 5 | // Created by Nico Verbruggen on 04/02/2023. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | class CaskFileParserTest: XCTestCase { 12 | 13 | // MARK: - Test Files 14 | static var exampleFilePath: URL { 15 | return Bundle(for: Self.self) 16 | .url(forResource: "phpmon-dev", withExtension: "rb")! 17 | } 18 | 19 | func test_can_extract_fields_from_cask_file() async throws { 20 | guard let caskFile = await CaskFile.from(url: CaskFileParserTest.exampleFilePath) else { 21 | return XCTFail("The CaskFile could not be parsed, check the log for more info") 22 | } 23 | 24 | XCTAssertEqual( 25 | caskFile.version, 26 | "5.7.2_1035" 27 | ) 28 | XCTAssertEqual( 29 | caskFile.sha256, 30 | "1cb147bd1b1fbd52971d90dff577465b644aee7c878f15ede57f46e8f217067a" 31 | ) 32 | XCTAssertEqual( 33 | caskFile.name, 34 | "PHP Monitor DEV" 35 | ) 36 | XCTAssertEqual( 37 | caskFile.url, 38 | "https://github.com/nicoverbruggen/phpmon/releases/download/v5.7.2/phpmon-dev.zip" 39 | ) 40 | } 41 | 42 | func test_can_extract_fields_from_remote_cask_file() async throws { 43 | guard let caskFile = await CaskFile.from(url: Constants.Urls.StableBuildCaskFile) else { 44 | return XCTFail("The remote CaskFile could not be parsed, check the log for more info") 45 | } 46 | 47 | XCTAssertTrue(caskFile.properties.keys.contains("version")) 48 | XCTAssertTrue(caskFile.properties.keys.contains("homepage")) 49 | XCTAssertTrue(caskFile.properties.keys.contains("url")) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tests/unit/Parsers/Config/BytePhpPreferenceTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BytePhpPreferenceTest.swift 3 | // Unit Tests 4 | // 5 | // Created by Nico Verbruggen on 04/09/2023. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | class BytePhpPreferenceTest: XCTestCase { 12 | 13 | func test_can_extract_memory_value() throws { 14 | let pref = BytePhpPreference(key: "memory_limit") 15 | 16 | XCTAssertEqual(pref.internalValue, "512M") 17 | XCTAssertEqual(pref.unit, .megabyte) 18 | XCTAssertEqual(pref.value, 512) 19 | } 20 | 21 | func test_can_parse_all_kinds_of_values() throws { 22 | var (unit, value) = BytePhpPreference.readFrom(internalValue: "1G")! 23 | XCTAssertEqual(unit, .gigabyte) 24 | XCTAssertEqual(value, 1) 25 | 26 | (unit, value) = BytePhpPreference.readFrom(internalValue: "256M")! 27 | XCTAssertEqual(unit, .megabyte) 28 | XCTAssertEqual(value, 256) 29 | 30 | (unit, value) = BytePhpPreference.readFrom(internalValue: "512K")! 31 | XCTAssertEqual(unit, .kilobyte) 32 | XCTAssertEqual(value, 512) 33 | 34 | (unit, value) = BytePhpPreference.readFrom(internalValue: "1024")! 35 | XCTAssertEqual(unit, .kilobyte) 36 | XCTAssertEqual(value, 1024) 37 | 38 | (unit, value) = BytePhpPreference.readFrom(internalValue: "-1")! 39 | XCTAssertEqual(unit, .kilobyte) 40 | XCTAssertEqual(value, -1) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tests/unit/Parsers/ExtensionEnumeratorTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExtensionEnumeratorTest.swift 3 | // Unit Tests 4 | // 5 | // Created by Nico Verbruggen on 30/10/2023. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | final class ExtensionEnumeratorTest: XCTestCase { 12 | 13 | override func setUp() async throws { 14 | ActiveFileSystem.useTestable([ 15 | "\(Paths.tapPath)/shivammathur/homebrew-extensions/Formula/xdebug@8.1.rb": .fake(.text, "<test>"), 16 | "\(Paths.tapPath)/shivammathur/homebrew-extensions/Formula/xdebug@8.2.rb": .fake(.text, "<test>"), 17 | "\(Paths.tapPath)/shivammathur/homebrew-extensions/Formula/xdebug@8.3.rb": .fake(.text, "<test>"), 18 | "\(Paths.tapPath)/shivammathur/homebrew-extensions/Formula/xdebug@8.4.rb": .fake(.text, "<test>"), 19 | ]) 20 | } 21 | 22 | func testCanReadFormulae() throws { 23 | let directory = "\(Paths.tapPath)/shivammathur/homebrew-extensions/Formula" 24 | let files = try FileSystem.getShallowContentsOfDirectory(directory) 25 | 26 | XCTAssertEqual( 27 | Set(["xdebug@8.1.rb", "xdebug@8.2.rb", "xdebug@8.3.rb", "xdebug@8.4.rb"]), 28 | Set(files) 29 | ) 30 | } 31 | 32 | func testCanParseFormulaeBasedOnSyntax() throws { 33 | let formulae = BrewTapFormulae.from(tap: "shivammathur/homebrew-extensions") 34 | 35 | XCTAssertEqual(formulae["8.1"], [BrewPhpExtension(path: "/", name: "xdebug", phpVersion: "8.1")]) 36 | XCTAssertEqual(formulae["8.2"], [BrewPhpExtension(path: "/", name: "xdebug", phpVersion: "8.2")]) 37 | XCTAssertEqual(formulae["8.3"], [BrewPhpExtension(path: "/", name: "xdebug", phpVersion: "8.3")]) 38 | XCTAssertEqual(formulae["8.4"], [BrewPhpExtension(path: "/", name: "xdebug", phpVersion: "8.4")]) 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /tests/unit/Parsers/ValetConfigurationTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ValetConfigParserTest.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 29/11/2021. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | class ValetConfigurationTest: XCTestCase { 12 | 13 | static var jsonConfigFileUrl: URL { 14 | return Bundle(for: Self.self).url( 15 | forResource: "valet-config", 16 | withExtension: "json" 17 | )! 18 | } 19 | 20 | func test_can_load_config_file() throws { 21 | let json = try? String( 22 | contentsOf: Self.jsonConfigFileUrl, 23 | encoding: .utf8 24 | ) 25 | let config = try! JSONDecoder().decode( 26 | Valet.Configuration.self, 27 | from: json!.data(using: .utf8)! 28 | ) 29 | 30 | XCTAssertEqual(config.tld, "test") 31 | XCTAssertEqual(config.paths, [ 32 | "/Users/username/.config/valet/Sites", 33 | "/Users/username/Sites" 34 | ]) 35 | XCTAssertEqual(config.defaultSite, "/Users/username/default-site") 36 | XCTAssertEqual(config.loopback, "127.0.0.1") 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /tests/unit/Parsers/ValetRcTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ValetRcTest.swift 3 | // Unit Tests 4 | // 5 | // Created by Nico Verbruggen on 20/01/2023. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | class ValetRcTest: XCTestCase { 12 | 13 | // MARK: - Test Files 14 | 15 | static var validPath: URL { 16 | return Bundle(for: Self.self) 17 | .url(forResource: "valetrc", withExtension: "valid")! 18 | } 19 | 20 | static var brokenPath: URL { 21 | return Bundle(for: Self.self) 22 | .url(forResource: "valetrc", withExtension: "broken")! 23 | } 24 | 25 | 26 | // MARK: - Tests 27 | 28 | func test_can_extract_fields_from_valetrc_file() throws { 29 | let fakeFile = RCFile.fromPath("/Users/fake/file.rc") 30 | XCTAssertNil(fakeFile) 31 | 32 | // Can parse the file 33 | let validFile = RCFile.fromPath(ValetRcTest.validPath.path) 34 | XCTAssertNotNil(validFile) 35 | 36 | let fields = validFile!.fields 37 | 38 | // Correctly parses and trims (and omits double quotes) per line 39 | XCTAssertEqual(fields["PHP"], "php@8.2") 40 | XCTAssertEqual(fields["OTHER"], "thing") 41 | XCTAssertEqual(fields["PHPMON_WATCH"], "true") 42 | XCTAssertEqual(fields["SYNTAX"], "variable") 43 | 44 | // Ignores entries prefixed with # 45 | XCTAssertTrue(!fields.keys.contains("#PHP")) 46 | 47 | // Ignores invalid lines 48 | XCTAssertTrue(!fields.keys.contains("OOF")) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /tests/unit/Test Files/brew/phpmon-dev.rb: -------------------------------------------------------------------------------- 1 | cask 'phpmon-dev' do 2 | depends_on formula: 'gnu-sed' 3 | 4 | version '5.7.2_1035' 5 | sha256 '1cb147bd1b1fbd52971d90dff577465b644aee7c878f15ede57f46e8f217067a' 6 | 7 | url 'https://github.com/nicoverbruggen/phpmon/releases/download/v5.7.2/phpmon-dev.zip' 8 | name 'PHP Monitor DEV' 9 | homepage 'https://phpmon.app' 10 | 11 | app 'PHP Monitor DEV.app', target: "PHP Monitor DEV.app" 12 | end 13 | 14 | -------------------------------------------------------------------------------- /tests/unit/Test Files/nginx/nginx-secure-proxy-custom-tld.test: -------------------------------------------------------------------------------- 1 | # valet stub: secure.proxy.valet.conf 2 | 3 | server { 4 | listen 127.0.0.1:80; 5 | #listen 127.0.0.1:80; # valet loopback 6 | server_name live.whatagraph.dev.com www.live.whatagraph.dev.com *.live.whatagraph.dev.com; 7 | return 301 https://$host$request_uri; 8 | } 9 | 10 | server { 11 | listen 127.0.0.1:443 ssl http2; 12 | #listen 127.0.0.1:443 ssl http2; # valet loopback 13 | server_name live.whatagraph.dev.com www.live.whatagraph.dev.com *.live.whatagraph.dev.com; 14 | root /; 15 | charset utf-8; 16 | client_max_body_size 128M; 17 | http2_push_preload on; 18 | 19 | location /41c270e4-5535-4daa-b23e-c269744c2f45/ { 20 | internal; 21 | alias /; 22 | try_files $uri $uri/; 23 | } 24 | 25 | ssl_certificate "/Users/phpmon/.config/valet/Certificates/live.whatagraph.dev.com.crt"; 26 | ssl_certificate_key "/Users/phpmon/.config/valet/Certificates/live.whatagraph.dev.com.key"; 27 | 28 | access_log off; 29 | error_log "/Users/phpmon/.config/valet/Log/live.whatagraph.dev.com-error.log"; 30 | 31 | error_page 404 "/Users/phpmon/.composer/vendor/laravel/valet/server.php"; 32 | 33 | location / { 34 | proxy_pass http://localhost:8080/; 35 | proxy_set_header Host $host; 36 | proxy_set_header X-Real-IP $remote_addr; 37 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 38 | proxy_set_header X-Forwarded-Proto $scheme; 39 | proxy_set_header X-Client-Verify SUCCESS; 40 | proxy_set_header X-Client-DN $ssl_client_s_dn; 41 | proxy_set_header X-SSL-Subject $ssl_client_s_dn; 42 | proxy_set_header X-SSL-Issuer $ssl_client_i_dn; 43 | proxy_set_header X-NginX-Proxy true; 44 | proxy_set_header Upgrade $http_upgrade; 45 | proxy_set_header Connection "upgrade"; 46 | proxy_http_version 1.1; 47 | proxy_read_timeout 1800; 48 | proxy_connect_timeout 1800; 49 | chunked_transfer_encoding on; 50 | proxy_redirect off; 51 | proxy_buffering off; 52 | } 53 | 54 | location ~ /\.ht { 55 | deny all; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tests/unit/Test Files/nginx/nginx-secure-proxy.test: -------------------------------------------------------------------------------- 1 | # valet stub: secure.proxy.valet.conf 2 | 3 | server { 4 | listen 127.0.0.1:80; 5 | #listen 127.0.0.1:80; # valet loopback 6 | server_name my-proxy.test www.my-proxy.test *.my-proxy.test; 7 | return 301 https://$host$request_uri; 8 | } 9 | 10 | server { 11 | listen 127.0.0.1:443 ssl http2; 12 | #listen 127.0.0.1:443 ssl http2; # valet loopback 13 | server_name my-proxy.test www.my-proxy.test *.my-proxy.test; 14 | root /; 15 | charset utf-8; 16 | client_max_body_size 128M; 17 | http2_push_preload on; 18 | 19 | location /41c270e4-5535-4daa-b23e-c269744c2f45/ { 20 | internal; 21 | alias /; 22 | try_files $uri $uri/; 23 | } 24 | 25 | ssl_certificate "/Users/nicoverbruggen/.config/valet/Certificates/my-proxy.test.crt"; 26 | ssl_certificate_key "/Users/nicoverbruggen/.config/valet/Certificates/my-proxy.test.key"; 27 | 28 | access_log off; 29 | error_log "/Users/nicoverbruggen/.config/valet/Log/my-proxy.test-error.log"; 30 | 31 | error_page 404 "/Users/nicoverbruggen/.composer/vendor/laravel/valet/server.php"; 32 | 33 | location / { 34 | proxy_pass http://127.0.0.1:90; 35 | proxy_set_header Host $host; 36 | proxy_set_header X-Real-IP $remote_addr; 37 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 38 | proxy_set_header X-Forwarded-Proto $scheme; 39 | proxy_set_header X-Client-Verify SUCCESS; 40 | proxy_set_header X-Client-DN $ssl_client_s_dn; 41 | proxy_set_header X-SSL-Subject $ssl_client_s_dn; 42 | proxy_set_header X-SSL-Issuer $ssl_client_i_dn; 43 | proxy_set_header X-NginX-Proxy true; 44 | proxy_set_header Upgrade $http_upgrade; 45 | proxy_set_header Connection "upgrade"; 46 | proxy_http_version 1.1; 47 | proxy_read_timeout 1800; 48 | proxy_connect_timeout 1800; 49 | chunked_transfer_encoding on; 50 | proxy_redirect off; 51 | proxy_buffering off; 52 | } 53 | 54 | location ~ /\.ht { 55 | deny all; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tests/unit/Test Files/phpmon/phpmon-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "scan_apps": [], 3 | "presets": [ 4 | { 5 | "name": "Default PHP", 6 | "extensions": { 7 | "xdebug": false 8 | }, 9 | "configuration": { 10 | "memory_limit": "128M" 11 | } 12 | }, 13 | { 14 | "name": "Personal Site", 15 | "extensions": { 16 | "xdebug": true 17 | }, 18 | "configuration": { 19 | "xdebug.mode": "coverage", 20 | "memory_limit": "512M" 21 | } 22 | }, 23 | { 24 | "name": "PHP Monitor", 25 | "extensions": { 26 | "xdebug": true 27 | }, 28 | "configuration": { 29 | "xdebug.mode": "coverage", 30 | "memory_limit": "512M" 31 | } 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /tests/unit/Test Files/valet/valet-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "tld": "test", 3 | "paths": [ 4 | "/Users/username/.config/valet/Sites", 5 | "/Users/username/Sites" 6 | ], 7 | "loopback": "127.0.0.1", 8 | "default": "/Users/username/default-site" 9 | } 10 | -------------------------------------------------------------------------------- /tests/unit/Test Files/valet/valetrc.broken: -------------------------------------------------------------------------------- 1 | fdsgdfg 2 | dgdfg 3 | 4 | PHP=fsdfs 5 | 6 | ;PHP=8.2 7 | -------------------------------------------------------------------------------- /tests/unit/Test Files/valet/valetrc.valid: -------------------------------------------------------------------------------- 1 | PHP=php@8.2 2 | #PHP=php 3 | OTHER=thing 4 | PHPMON_WATCH=true 5 | SYNTAX = "variable" 6 | OOF:NICE 7 | -------------------------------------------------------------------------------- /tests/unit/Testables/TestableConfigurationTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestableConfigurationTest.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 16/10/2022. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | class TestableConfigurationTest: XCTestCase { 12 | func test_configuration_can_be_saved_as_json() async { 13 | // WORKING 14 | var configuration = TestableConfigurations.working 15 | 16 | try! configuration.toJson().write( 17 | toFile: NSHomeDirectory() + "/.phpmon_fconf_working.json", 18 | atomically: true, 19 | encoding: .utf8 20 | ) 21 | 22 | // WORKING (WITHOUT VALET) 23 | let valetFreeConfiguration = TestableConfigurations.workingWithoutValet 24 | 25 | try! valetFreeConfiguration.toJson().write( 26 | toFile: NSHomeDirectory() + "/.phpmon_fconf_working_no_valet.json", 27 | atomically: true, 28 | encoding: .utf8 29 | ) 30 | 31 | // NOT WORKING 32 | configuration.filesystem["/opt/homebrew/bin/php"] = nil 33 | 34 | try! configuration.toJson().write( 35 | toFile: NSHomeDirectory() + "/.phpmon_fconf_broken.json", 36 | atomically: true, 37 | encoding: .utf8 38 | ) 39 | } 40 | } 41 | 42 | -------------------------------------------------------------------------------- /tests/unit/Versions/PhpVersionDetectionTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PhpVersionDetectionTest.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 01/04/2021. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | class PhpVersionDetectionTest: XCTestCase { 12 | 13 | func test_can_detect_valid_php_versions() async throws { 14 | let outcome = await PhpEnvironments.shared.extractPhpVersions( 15 | from: [ 16 | "", // empty lines should be omitted 17 | "php@8.0", 18 | "php@8.0", // should only be detected once 19 | "meta-php@8.0", // should be omitted, invalid 20 | "php@8.0-coolio", // should be omitted, invalid 21 | "php@7.0", 22 | "", 23 | "unrelatedphp@1.0", // should be omitted, invalid 24 | "php@5.6", // should be omitted, not supported 25 | "php@5.4" // should be omitted, not supported 26 | ], 27 | checkBinaries: false, 28 | generateHelpers: false 29 | ) 30 | 31 | XCTAssertEqual(outcome, ["8.0", "7.0", "5.6"]) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/unit/Versions/VersionExtractorTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VersionExtractorTest.swift 3 | // PHP Monitor 4 | // 5 | // Created by Nico Verbruggen on 16/12/2021. 6 | // Copyright © 2023 Nico Verbruggen. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | class VersionExtractorTest: XCTestCase { 12 | 13 | func test_extract_version() { 14 | XCTAssertEqual(VersionExtractor.from("Laravel Valet 2.17.1"), "2.17.1") 15 | XCTAssertEqual(VersionExtractor.from("Laravel Valet 2.0"), "2.0") 16 | } 17 | 18 | func test_version_comparison() { 19 | XCTAssertEqual("2.0".versionCompare("2.1"), .orderedAscending) 20 | XCTAssertEqual("2.1".versionCompare("2.0"), .orderedDescending) 21 | XCTAssertEqual("2.0".versionCompare("2.0"), .orderedSame) 22 | XCTAssertEqual("2.17.0".versionCompare("2.17.1"), .orderedAscending) 23 | } 24 | 25 | } 26 | --------------------------------------------------------------------------------