1
0
mirror of https://github.com/nicoverbruggen/phpmon.git synced 2026-03-27 22:40:08 +01:00

Add MarkdownTextView, alert improvements

This commit is contained in:
2026-02-27 13:30:24 +01:00
parent d227723d50
commit da0289c4da
15 changed files with 319 additions and 82 deletions

View File

@@ -182,6 +182,18 @@
03BC24A92F51A5F70051292B /* OutputLine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03BC24A72F51A5F60051292B /* OutputLine.swift */; };
03BC24AA2F51A5F70051292B /* OutputLine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03BC24A72F51A5F60051292B /* OutputLine.swift */; };
03BC24AB2F51A5F70051292B /* OutputLine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03BC24A72F51A5F60051292B /* OutputLine.swift */; };
03BC24AD2F51B33B0051292B /* MarkdownTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03BC24AC2F51B33B0051292B /* MarkdownTextView.swift */; };
03BC24AE2F51B33B0051292B /* MarkdownTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03BC24AC2F51B33B0051292B /* MarkdownTextView.swift */; };
03BC24AF2F51B33B0051292B /* MarkdownTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03BC24AC2F51B33B0051292B /* MarkdownTextView.swift */; };
03BC24B02F51B33B0051292B /* MarkdownTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03BC24AC2F51B33B0051292B /* MarkdownTextView.swift */; };
03BC24B62F51C5230051292B /* MarkdownTextViewRepresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03BC24B52F51C51F0051292B /* MarkdownTextViewRepresentable.swift */; };
03BC24B72F51C5230051292B /* MarkdownTextViewRepresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03BC24B52F51C51F0051292B /* MarkdownTextViewRepresentable.swift */; };
03BC24B82F51C5230051292B /* MarkdownTextViewRepresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03BC24B52F51C51F0051292B /* MarkdownTextViewRepresentable.swift */; };
03BC24B92F51C5230051292B /* MarkdownTextViewRepresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03BC24B52F51C51F0051292B /* MarkdownTextViewRepresentable.swift */; };
03BC24BB2F51C55E0051292B /* CodeBlockTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03BC24BA2F51C5560051292B /* CodeBlockTextView.swift */; };
03BC24BC2F51C55E0051292B /* CodeBlockTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03BC24BA2F51C5560051292B /* CodeBlockTextView.swift */; };
03BC24BD2F51C55E0051292B /* CodeBlockTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03BC24BA2F51C5560051292B /* CodeBlockTextView.swift */; };
03BC24BE2F51C55E0051292B /* CodeBlockTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03BC24BA2F51C5560051292B /* CodeBlockTextView.swift */; };
03BFF5272E312C3D007F96FA /* Startup+Timers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03BFF5262E312C39007F96FA /* Startup+Timers.swift */; };
03BFF5282E312C3D007F96FA /* Startup+Timers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03BFF5262E312C39007F96FA /* Startup+Timers.swift */; };
03BFF5292E312C3D007F96FA /* Startup+Timers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03BFF5262E312C39007F96FA /* Startup+Timers.swift */; };
@@ -234,10 +246,8 @@
1C91CB7232F304AA7F6296CB /* StartupAlertButtonRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DBEBCF2527961D5D13F68A8 /* StartupAlertButtonRow.swift */; };
1D1173AE9AC9C8315E899D06 /* StartupAlertViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B900AAECE035041E9BE09DCF /* StartupAlertViewModel.swift */; };
25250C95E34D20ED4C91C320 /* CommandHistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31503E15DADA980998F0F5A2 /* CommandHistoryView.swift */; };
288730259CFF37771C773EA2 /* MarkdownText.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4C0BBF856B022EA51606492 /* MarkdownText.swift */; };
2F9D926CBFE51F3B21A76536 /* StartupOutputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3E531FA9DE31575AF518941 /* StartupOutputView.swift */; };
4181B8F1C0ED930B2C5E5532 /* CommandHistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31503E15DADA980998F0F5A2 /* CommandHistoryView.swift */; };
4AAB0E1F7037BEFAE0037AFF /* MarkdownText.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4C0BBF856B022EA51606492 /* MarkdownText.swift */; };
5420395926135DC100FB00FA /* PreferencesVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5420395826135DC100FB00FA /* PreferencesVC.swift */; };
5420395F2613607600FB00FA /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5420395E2613607600FB00FA /* Preferences.swift */; };
5489625828312FAD004F647A /* CreatedFromFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5489625728312FAD004F647A /* CreatedFromFile.swift */; };
@@ -280,8 +290,6 @@
A42F1F6EF455F42769E63FF5 /* StartupAlertWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D51912F2DA8FD7D8A1578644 /* StartupAlertWindowController.swift */; };
B54FBBEFE5E1782AFF7C434E /* StartupAlertHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7EF8DC9BC21B1EFB1F6CBBA5 /* StartupAlertHeaderView.swift */; };
BB3AF45BED2C82AE27E86107 /* CommandHistoryWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E51118AFDD02AA1B7D8C9278 /* CommandHistoryWindowController.swift */; };
BFF65E73753B67381C23D65E /* MarkdownText.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4C0BBF856B022EA51606492 /* MarkdownText.swift */; };
C277F9F96197AD07FA91E3CB /* MarkdownText.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4C0BBF856B022EA51606492 /* MarkdownText.swift */; };
C40175B82903108900763A68 /* ValetInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40175B72903108900763A68 /* ValetInteractor.swift */; };
C40175B92903108900763A68 /* ValetInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40175B72903108900763A68 /* ValetInteractor.swift */; };
C40175BA2903108900763A68 /* ValetInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40175B72903108900763A68 /* ValetInteractor.swift */; };
@@ -1178,6 +1186,9 @@
03B947DD2F43691D00B6F899 /* TestURL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestURL.swift; sourceTree = "<group>"; };
03B947E02F436A6700B6F899 /* Binaries.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Binaries.swift; sourceTree = "<group>"; };
03BC24A72F51A5F60051292B /* OutputLine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutputLine.swift; sourceTree = "<group>"; };
03BC24AC2F51B33B0051292B /* MarkdownTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkdownTextView.swift; sourceTree = "<group>"; };
03BC24B52F51C51F0051292B /* MarkdownTextViewRepresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkdownTextViewRepresentable.swift; sourceTree = "<group>"; };
03BC24BA2F51C5560051292B /* CodeBlockTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodeBlockTextView.swift; sourceTree = "<group>"; };
03BFF5262E312C39007F96FA /* Startup+Timers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Startup+Timers.swift"; sourceTree = "<group>"; };
03BFF52B2E313240007F96FA /* StatusMenu+Driver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatusMenu+Driver.swift"; sourceTree = "<group>"; };
03C099432EA15C8B00B76D43 /* Container+Real.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Container+Real.swift"; sourceTree = "<group>"; };
@@ -1212,7 +1223,6 @@
54FCFD2F276C8DA4004CE748 /* HotkeyPreferenceView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HotkeyPreferenceView.swift; sourceTree = "<group>"; };
68063C53EE0F48E91ADFE92F /* StartupFixCommandView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartupFixCommandView.swift; sourceTree = "<group>"; };
7EF8DC9BC21B1EFB1F6CBBA5 /* StartupAlertHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartupAlertHeaderView.swift; sourceTree = "<group>"; };
A4C0BBF856B022EA51606492 /* MarkdownText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkdownText.swift; sourceTree = "<group>"; };
B900AAECE035041E9BE09DCF /* StartupAlertViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartupAlertViewModel.swift; sourceTree = "<group>"; };
C40175B72903108900763A68 /* ValetInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValetInteractor.swift; sourceTree = "<group>"; };
C40508B028ADAB44008FAC1F /* NSMenuItemExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSMenuItemExtension.swift; sourceTree = "<group>"; };
@@ -1633,6 +1643,36 @@
path = Conditions;
sourceTree = "<group>";
};
03BC24B12F51B4550051292B /* Buttons */ = {
isa = PBXGroup;
children = (
031D747B2F46225600D4FF48 /* SimpleButton.swift */,
C451AFF52969E40F0078E617 /* HelpButton.swift */,
C4EA3C462BA4F947007B0BA7 /* CustomButtonStyles.swift */,
);
path = Buttons;
sourceTree = "<group>";
};
03BC24B22F51B4690051292B /* Markdown */ = {
isa = PBXGroup;
children = (
03BC24AC2F51B33B0051292B /* MarkdownTextView.swift */,
03BC24B52F51C51F0051292B /* MarkdownTextViewRepresentable.swift */,
03BC24BA2F51C5560051292B /* CodeBlockTextView.swift */,
);
path = Markdown;
sourceTree = "<group>";
};
03BC24B32F51B4790051292B /* Views */ = {
isa = PBXGroup;
children = (
03EC943B2F47297100231276 /* ErrorView.swift */,
C43B8FD42BA9BAD3000C02BE /* UnavailableContentView.swift */,
C48DDD0C29C75C9E00D032D9 /* BlockingOverlayView.swift */,
);
path = Views;
sourceTree = "<group>";
};
03BFF1D12E3CF4F2004C56A9 /* Provision */ = {
isa = PBXGroup;
children = (
@@ -2405,14 +2445,10 @@
C4B609162853AA9A00C95265 /* Common */ = {
isa = PBXGroup;
children = (
03EC943B2F47297100231276 /* ErrorView.swift */,
A4C0BBF856B022EA51606492 /* MarkdownText.swift */,
031D747B2F46225600D4FF48 /* SimpleButton.swift */,
C44264BD2850B86C007400F1 /* SwiftUIHelper.swift */,
C451AFF52969E40F0078E617 /* HelpButton.swift */,
C4EA3C462BA4F947007B0BA7 /* CustomButtonStyles.swift */,
C48DDD0C29C75C9E00D032D9 /* BlockingOverlayView.swift */,
C43B8FD42BA9BAD3000C02BE /* UnavailableContentView.swift */,
03BC24B22F51B4690051292B /* Markdown */,
03BC24B12F51B4550051292B /* Buttons */,
03BC24B32F51B4790051292B /* Views */,
);
path = Common;
sourceTree = "<group>";
@@ -3004,6 +3040,7 @@
C4998F0A2617633900B2526E /* PreferencesWindowController.swift in Sources */,
C46FA9882822EFDC00D78807 /* PhpConfigurationFile.swift in Sources */,
C4F8C0A422D4F12C002EFE61 /* DateExtension.swift in Sources */,
03BC24AF2F51B33B0051292B /* MarkdownTextView.swift in Sources */,
C4AF9F7A2754499000D44ED0 /* Valet.swift in Sources */,
C4EB53E728553117006F9937 /* ArrayExtension.swift in Sources */,
5420395926135DC100FB00FA /* PreferencesVC.swift in Sources */,
@@ -3088,7 +3125,6 @@
7E19EE251E743286BC95EBA3 /* StartupAlertViewModel.swift in Sources */,
C57636BF2B81A9D147B28D1C /* StartupAlertView.swift in Sources */,
148796E43977CCBC22AAB5CC /* StartupAlertWindowController.swift in Sources */,
288730259CFF37771C773EA2 /* MarkdownText.swift in Sources */,
5D754EAE78FF1E264AD02A46 /* StartupAlertHeaderView.swift in Sources */,
F644E222C9D096B94E363E60 /* StartupFixCommandView.swift in Sources */,
2F9D926CBFE51F3B21A76536 /* StartupOutputView.swift in Sources */,
@@ -3104,6 +3140,7 @@
C495F5AF28A42E080087F70A /* EnvironmentCheck.swift in Sources */,
C46EBC4428DB95F0007ACC74 /* ShellProtocol.swift in Sources */,
C46EBC4C28DB95F0007ACC74 /* TrackedShell.swift in Sources */,
03BC24BB2F51C55E0051292B /* CodeBlockTextView.swift in Sources */,
C41C1B4922B00A9800E7CF16 /* MenuBarImageGenerator.swift in Sources */,
C4F30B03278E16BA00755FCE /* HomebrewService.swift in Sources */,
54D9E0B427E4F51E003B9AD9 /* Key.swift in Sources */,
@@ -3179,6 +3216,7 @@
039E1D7C2E5F0F300072D13D /* ValetUpgrader.swift in Sources */,
C474B00624C0E98C00066A22 /* LocalNotification.swift in Sources */,
036C390D2E5C8CC5008DAEDF /* PackagistP2Response.swift in Sources */,
03BC24B62F51C5230051292B /* MarkdownTextViewRepresentable.swift in Sources */,
031D74842F46225C00D4FF48 /* SelectDomainTypeView.swift in Sources */,
C4D5CFCA27E0F9CD00035329 /* NginxConfigurationFile.swift in Sources */,
C485707028BF452300539B36 /* PhpDoctorWindowController.swift in Sources */,
@@ -3420,6 +3458,7 @@
C471E7DB28F9BA8F0021E251 /* RealShell.swift in Sources */,
C471E7FA28F9BA8F0021E251 /* TrackedShell.swift in Sources */,
C471E7FF28F9BAD10021E251 /* Xdebug.swift in Sources */,
03BC24BD2F51C55E0051292B /* CodeBlockTextView.swift in Sources */,
037F441D2EDB9195002EBF75 /* ConfigWatchManager.swift in Sources */,
C409349F298EE8E900D25014 /* AppUpdater.swift in Sources */,
03BFF5292E312C3D007F96FA /* Startup+Timers.swift in Sources */,
@@ -3454,11 +3493,11 @@
C471E82028F9BB290021E251 /* NginxConfigurationFile.swift in Sources */,
032DAC2D2E8BEB6B0018E01C /* WebApiProtocol.swift in Sources */,
C4513F902B13E2E6001AD760 /* PhpExtensionManagerWindowController.swift in Sources */,
03BC24B82F51C5230051292B /* MarkdownTextViewRepresentable.swift in Sources */,
03EC943F2F47297B00231276 /* ErrorView.swift in Sources */,
FEA10615F601BEC79BBDA45B /* StartupAlertViewModel.swift in Sources */,
9297DB3F2A8903D70E1D6426 /* StartupAlertView.swift in Sources */,
8A8AB3D1A6DAE99C91DFBEFF /* StartupAlertWindowController.swift in Sources */,
4AAB0E1F7037BEFAE0037AFF /* MarkdownText.swift in Sources */,
95BB6DF72FB35A5A8F4E24E7 /* StartupAlertHeaderView.swift in Sources */,
632A06CFEEC7A76C34992F38 /* StartupFixCommandView.swift in Sources */,
FF788D404D414CC970301C81 /* StartupOutputView.swift in Sources */,
@@ -3474,6 +3513,7 @@
C4821C5C2C2DEDE200357A68 /* AppMenu.swift in Sources */,
0386B0B42ED36C3D00CA6795 /* Locked.swift in Sources */,
0392CDED2EB25371009176DA /* SecurePopoverView.swift in Sources */,
03BC24B02F51B33B0051292B /* MarkdownTextView.swift in Sources */,
C471E81328F9BAE80021E251 /* XibLoadable.swift in Sources */,
C4D3661C291173EA006BD146 /* DictionaryExtension.swift in Sources */,
C4B79ECD29CA475900A483EE /* RemovePhpVersionCommand.swift in Sources */,
@@ -3606,7 +3646,6 @@
03BC24A82F51A5F70051292B /* OutputLine.swift in Sources */,
88DD3A08057A23955913FB70 /* StartupAlertView.swift in Sources */,
A42F1F6EF455F42769E63FF5 /* StartupAlertWindowController.swift in Sources */,
C277F9F96197AD07FA91E3CB /* MarkdownText.swift in Sources */,
B54FBBEFE5E1782AFF7C434E /* StartupAlertHeaderView.swift in Sources */,
F4B7E3F9B46C5AD8B8B3FEBA /* StartupFixCommandView.swift in Sources */,
FB95EF9B99E4ED160E751E44 /* StartupOutputView.swift in Sources */,
@@ -3624,10 +3663,12 @@
C471E8E128F9BB8F0021E251 /* PresetHelper.swift in Sources */,
038131492F4E00D500653177 /* TrackedTestableCommand.swift in Sources */,
031D747D2F46225600D4FF48 /* SimpleButton.swift in Sources */,
03BC24AD2F51B33B0051292B /* MarkdownTextView.swift in Sources */,
038131442F4E00C300653177 /* TrackedTestableShell.swift in Sources */,
C471E8E228F9BB8F0021E251 /* WarningView.swift in Sources */,
C471E8E328F9BB8F0021E251 /* PhpDoctorView.swift in Sources */,
0A1A6208D3DD2495FBD8569B /* CommandHistoryView.swift in Sources */,
03BC24B92F51C5230051292B /* MarkdownTextViewRepresentable.swift in Sources */,
C471E8E428F9BB8F0021E251 /* NoWarningsView.swift in Sources */,
C471E8E528F9BB8F0021E251 /* OnboardingView.swift in Sources */,
C4B79EBF29CA38DB00A483EE /* BrewCommand.swift in Sources */,
@@ -3708,6 +3749,7 @@
C471E7CA28F9BA480021E251 /* TestableFileSystem.swift in Sources */,
C471E7DD28F9BAA30021E251 /* CommandProtocol.swift in Sources */,
C471E7FC28F9BAA30021E251 /* TrackedCommand.swift in Sources */,
03BC24BC2F51C55E0051292B /* CodeBlockTextView.swift in Sources */,
C471E7D128F9BA630021E251 /* RealFileSystem.swift in Sources */,
033D459B2B0D4EC600070080 /* InstallPhpExtensionCommand.swift in Sources */,
C471E82B28F9BB340021E251 /* Valet.swift in Sources */,
@@ -3746,6 +3788,7 @@
buildActionMask = 2147483647;
files = (
C449B4F427EE7FC800C47E8A /* DomainListKindCell.swift in Sources */,
03BC24B72F51C5230051292B /* MarkdownTextViewRepresentable.swift in Sources */,
54EAC806262F212B0092D14E /* GlobalKeybindPreference.swift in Sources */,
C485707128BF452E00539B36 /* WarningManager.swift in Sources */,
C41CA5EE2774F8EE00A2C80E /* DomainListVC+Actions.swift in Sources */,
@@ -3807,7 +3850,6 @@
EE4FA5D074AD5C9D09C25A55 /* StartupAlertViewModel.swift in Sources */,
959BE2F3A5743862AC8D1E7D /* StartupAlertView.swift in Sources */,
E56493248D049CA2EBB82B21 /* StartupAlertWindowController.swift in Sources */,
BFF65E73753B67381C23D65E /* MarkdownText.swift in Sources */,
56306F63B84C9E99C2C26375 /* StartupAlertHeaderView.swift in Sources */,
67E571F5200F9C57CC673499 /* StartupFixCommandView.swift in Sources */,
CCF5FE7B49F3B0BC77BAF7D7 /* StartupOutputView.swift in Sources */,
@@ -3910,6 +3952,7 @@
C4E2E86528FC2F1B003B070C /* XCPMApplication.swift in Sources */,
C489E0BC2A220A4200323F5E /* FakeBrewFormulaeHandler.swift in Sources */,
036C390B2E5C8CC5008DAEDF /* PackagistP2Response.swift in Sources */,
03BC24AE2F51B33B0051292B /* MarkdownTextView.swift in Sources */,
C4CE3BBB27B324230086CA49 /* MainMenu+Switcher.swift in Sources */,
03DAD3A62EB3B08F003417BD /* DomainListVC+Certs.swift in Sources */,
C4B79ECC29CA475900A483EE /* RemovePhpVersionCommand.swift in Sources */,
@@ -4012,6 +4055,7 @@
0381314A2F4E00D500653177 /* TrackedTestableCommand.swift in Sources */,
C4551657297AED18009B8466 /* ValetRcTest.swift in Sources */,
C464ADAD275A7A3F003FCD53 /* DomainListWindowController.swift in Sources */,
03BC24BE2F51C55E0051292B /* CodeBlockTextView.swift in Sources */,
C40C7F1F2772136000DDDCDC /* PhpEnvironments.swift in Sources */,
C464ADB0275A7A6A003FCD53 /* DomainListVC.swift in Sources */,
C43A8A1A25D9CD1000591B77 /* Utility.swift in Sources */,

View File

@@ -0,0 +1,55 @@
//
// CodeBlockTextView.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 27/02/2026.
// Copyright © 2026 Nico Verbruggen. All rights reserved.
//
import SwiftUI
import AppKit
/// Note: Written with the help of an LLM.
class CodeBlockTextView: NSTextView {
private let codePaddingX: CGFloat = 4
private let codePaddingY: CGFloat = 1
private let codeCornerRadius: CGFloat = 4
override func draw(_ dirtyRect: NSRect) {
drawCodeBackgrounds()
super.draw(dirtyRect)
}
override var intrinsicContentSize: NSSize {
guard let textContainer, let layoutManager else {
return super.intrinsicContentSize
}
layoutManager.ensureLayout(for: textContainer)
let rect = layoutManager.usedRect(for: textContainer)
return NSSize(width: NSView.noIntrinsicMetric, height: ceil(rect.height))
}
private func drawCodeBackgrounds() {
guard let textStorage, let layoutManager, let textContainer else { return }
let codeSpanKey = MarkdownTextViewRepresentable.codeSpanKey
let appColor = NSColor(named: "AppColor") ?? .systemBlue
textStorage.enumerateAttribute(codeSpanKey, in: NSRange(location: 0, length: textStorage.length)) { value, range, _ in
guard value != nil else { return }
// Get the glyph range and bounding rect for this code span
let glyphRange = layoutManager.glyphRange(forCharacterRange: range, actualCharacterRange: nil)
let textRect = layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer)
// Offset by text container inset
let rect = textRect.offsetBy(dx: textContainerInset.width, dy: textContainerInset.height)
let paddedRect = rect.insetBy(dx: -codePaddingX, dy: -codePaddingY)
let path = NSBezierPath(roundedRect: paddedRect, xRadius: codeCornerRadius, yRadius: codeCornerRadius)
// Fill
appColor.withAlphaComponent(0.15).setFill()
path.fill()
}
}
}

View File

@@ -0,0 +1,39 @@
//
// MarkdownTextView.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 27/02/2026.
// Copyright © 2026 Nico Verbruggen. All rights reserved.
//
import SwiftUI
struct MarkdownTextView: View {
let string: String
let fontSize: CGFloat
init(_ string: String, fontSize: CGFloat = 12) {
self.string = string
self.fontSize = fontSize
}
var body: some View {
MarkdownTextViewRepresentable(string: string, fontSize: fontSize)
}
}
// MARK: - Previews
#Preview("Inline code") {
MarkdownTextView("startup.errors.php_binary.desc".localized(
"/opt/homebrew/bin/php"
))
.frame(width: 460)
.padding()
}
#Preview("No code") {
MarkdownTextView("startup.errors.valet_version_not_supported.desc".localized)
.frame(width: 460)
.padding()
}

View File

@@ -0,0 +1,107 @@
//
// MarkdownTextViewRepresentable.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 27/02/2026.
// Copyright © 2026 Nico Verbruggen. All rights reserved.
//
import SwiftUI
import AppKit
/// Note: Written with the help of an LLM.
struct MarkdownTextViewRepresentable: NSViewRepresentable {
let string: String
let fontSize: CGFloat
func makeNSView(context: Context) -> CodeBlockTextView {
let textView = CodeBlockTextView()
textView.isEditable = false
textView.isSelectable = true
textView.drawsBackground = false
textView.textContainerInset = .zero
textView.textContainer?.lineFragmentPadding = 0
textView.setContentHuggingPriority(.defaultHigh, for: .vertical)
textView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
textView.setContentCompressionResistancePriority(.required, for: .vertical)
configure(textView)
return textView
}
func updateNSView(_ textView: CodeBlockTextView, context: Context) {
configure(textView)
}
private func configure(_ textView: CodeBlockTextView) {
let attributed = Self.buildAttributedString(from: string, fontSize: fontSize)
textView.textStorage?.setAttributedString(attributed)
textView.invalidateIntrinsicContentSize()
}
// MARK: - Attributed String Builder
static func buildAttributedString(from string: String, fontSize: CGFloat) -> NSAttributedString {
let result = NSMutableAttributedString()
let font = NSFont.systemFont(ofSize: fontSize)
let codeFont = NSFont.monospacedSystemFont(ofSize: fontSize - 1, weight: .regular)
// Add additional spacing for code blocks w/ thin spaces
let thinSpace = "\u{2009}\u{2009}\u{2009}"
let defaultAttributes: [NSAttributedString.Key: Any] = [
.font: font,
.foregroundColor: NSColor.labelColor
]
var current = string.startIndex
while let backtickStart = string[current...].firstIndex(of: "`") {
if current < backtickStart {
result.append(NSAttributedString(
string: String(string[current..<backtickStart]),
attributes: defaultAttributes
))
}
let afterBacktick = string.index(after: backtickStart)
if afterBacktick < string.endIndex,
let backtickEnd = string[afterBacktick...].firstIndex(of: "`") {
// Thin space before
result.append(NSAttributedString(string: thinSpace, attributes: defaultAttributes))
// Code span with marker attribute
let codeAttributes: [NSAttributedString.Key: Any] = [
.font: codeFont,
.foregroundColor: NSColor.labelColor,
Self.codeSpanKey: true
]
result.append(NSAttributedString(
string: String(string[afterBacktick..<backtickEnd]),
attributes: codeAttributes
))
// Thin space after
result.append(NSAttributedString(string: thinSpace, attributes: defaultAttributes))
current = string.index(after: backtickEnd)
} else {
result.append(NSAttributedString(
string: String(string[backtickStart...]),
attributes: defaultAttributes
))
current = string.endIndex
}
}
if current < string.endIndex {
result.append(NSAttributedString(
string: String(string[current...]),
attributes: defaultAttributes
))
}
return result
}
static let codeSpanKey = NSAttributedString.Key("PHPMonitorCodeSpan")
}

View File

@@ -1,25 +0,0 @@
//
// MarkdownText.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 25/02/2026.
// Copyright © 2026 Nico Verbruggen. All rights reserved.
//
import SwiftUI
extension Text {
init(markdown string: String, fontSize: CGFloat? = nil) {
if var attributed = try? AttributedString(
markdown: string,
options: .init(interpretedSyntax: .inlineOnlyPreservingWhitespace)
) {
for run in attributed.runs where ((run.inlinePresentationIntent?.contains(.code)) != nil) {
attributed[run.range].backgroundColor = Color(nsColor: .quaternaryLabelColor)
}
self.init(attributed)
} else {
self.init(string)
}
}
}

View File

@@ -19,13 +19,12 @@ struct StartupAlertHeaderView: View {
.frame(width: 60, height: 60)
VStack(alignment: .leading, spacing: 5) {
Text(markdown: titleText)
Text(titleText)
.font(.system(size: 15, weight: .bold))
.textSelection(.enabled)
Text(markdown: subtitleText)
.font(.system(size: 12))
MarkdownTextView(subtitleText, fontSize: 13)
.fixedSize(horizontal: false, vertical: true)
.textSelection(.enabled)
}
Spacer()

View File

@@ -11,6 +11,18 @@ import SwiftUI
struct StartupAlertView: View {
@ObservedObject var viewModel: StartupAlertViewModel
/// Whether the bottom section (description text and/or past output) has content to display.
/// This is used to conditionally show the section and its divider,
/// avoiding empty padded sections and double dividers.
private var hasBottomContent: Bool {
let hasDescription = !viewModel.check.descriptionText.isEmpty && viewModel.state == .idle
let hasOutput = !viewModel.outputLines.isEmpty
&& (viewModel.state == .idle || viewModel.state == .completed)
return hasDescription || hasOutput
}
var body: some View {
VStack(spacing: 0) {
StartupAlertHeaderView(
@@ -18,39 +30,45 @@ struct StartupAlertView: View {
subtitleText: viewModel.check.subtitleText
)
Divider()
if viewModel.state == .running || (viewModel.hasFix && viewModel.state == .idle) {
Divider()
VStack(alignment: .leading, spacing: 12) {
if viewModel.state == .running {
StartupOutputView(
lines: viewModel.outputLines,
isRunning: true
)
} else if viewModel.hasFix, viewModel.state == .idle {
StartupFixCommandView(
command: viewModel.check.fixDescription ?? ""
)
}
if !viewModel.check.descriptionText.isEmpty,
viewModel.state == .idle {
Text(markdown: viewModel.check.descriptionText, fontSize: 12)
.font(.system(size: 12))
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
.textSelection(.enabled)
}
if !viewModel.outputLines.isEmpty,
viewModel.state == .idle || viewModel.state == .completed {
StartupOutputView(
lines: viewModel.outputLines,
isRunning: false
)
VStack(alignment: .leading, spacing: 12) {
if viewModel.state == .running {
StartupOutputView(
lines: viewModel.outputLines,
isRunning: true
)
} else {
StartupFixCommandView(
command: viewModel.check.fixDescription ?? ""
)
}
}
.padding(15)
.frame(maxWidth: .infinity, alignment: .leading)
}
if hasBottomContent {
Divider()
VStack(alignment: .leading, spacing: 12) {
if !viewModel.check.descriptionText.isEmpty,
viewModel.state == .idle {
MarkdownTextView(viewModel.check.descriptionText, fontSize: 12)
}
if !viewModel.outputLines.isEmpty,
viewModel.state == .idle || viewModel.state == .completed {
StartupOutputView(
lines: viewModel.outputLines,
isRunning: false
)
}
}
.padding(15)
.frame(maxWidth: .infinity, alignment: .leading)
}
.padding(20)
.frame(maxWidth: .infinity, alignment: .leading)
Divider()

View File

@@ -93,17 +93,17 @@ class StartupAlertViewModel: ObservableObject {
@MainActor private func pass() {
self.state = .completed
self.outputLines.append(OutputLine(text: "\nFix applied successfully! Continuing...", stream: .stdOut))
self.outputLines.append(OutputLine(text: "---\nFix applied successfully! Continuing...", stream: .stdOut))
}
@MainActor private func fail() {
self.state = .idle
self.outputLines.append(OutputLine(text: "\nFix did not resolve the issue.", stream: .stdErr))
self.outputLines.append(OutputLine(text: "---\nFix did not resolve the issue.", stream: .stdErr))
}
@MainActor private func errorAndIdle(_ error: Error) {
self.state = .idle
self.outputLines.append(OutputLine(text: "\nError: \(error.localizedDescription)", stream: .stdErr))
self.outputLines.append(OutputLine(text: "---\nError: \(error.localizedDescription)", stream: .stdErr))
}
// MARK: - Alert Outcomes

View File

@@ -663,8 +663,8 @@ You can do this by running `composer global update` in your terminal. After that
"alert.homebrew_missing.quit" = "Quit";
// PHP binary not found
"startup.errors.php_binary.title" = "PHP is not correctly installed";
"startup.errors.php_binary.subtitle" = "You must install PHP via Homebrew. The app will not work correctly until you resolve this issue.";
"startup.errors.php_binary.title" = "PHP is not correctly linked";
"startup.errors.php_binary.subtitle" = "A valid PHP installation was found, but it is not linked. The app will not work correctly until you resolve this issue.";
"startup.errors.php_binary.desc" = "Usually, running `brew link php` in your Terminal will resolve this issue.\n\nTo diagnose what is wrong, you can try running `which php` in your Terminal, it should return `%@`.";
// Invalid brew info php output
@@ -716,12 +716,12 @@ If you are seeing this message but are confused why this folder has gone missing
// Brew & sudoers
"startup.errors.sudoers_brew.title" = "Brew has not been added to sudoers.d";
"startup.errors.sudoers_brew.subtitle" = "You must run `sudo valet trust` to ensure Valet can start and stop services without having to use sudo every time. The app will not work correctly until you resolve this issue.";
"startup.errors.sudoers_brew.subtitle" = "You must run `valet trust` to ensure Valet can start and stop services without having to use sudo every time. The app will not work correctly until you resolve this issue.";
"startup.errors.sudoers_brew.desc" = "If you keep seeing this error, it is possible that there is a permission issue where PHP Monitor cannot validate the file, which can usually be resolved by running: `sudo chmod +r /private/etc/sudoers.d/brew`";
// Valet & sudoers
"startup.errors.sudoers_valet.title" = "Valet has not been added to sudoers.d";
"startup.errors.sudoers_valet.subtitle" = "You must run `sudo valet trust` to ensure Valet can start and stop services without having to use sudo every time. The app will not work correctly until you resolve this issue. If you did this before, please run `sudo valet trust` again.";
"startup.errors.sudoers_valet.subtitle" = "You must run `valet trust` to ensure Valet can start and stop services without having to use sudo every time. The app will not work correctly until you resolve this issue.";
"startup.errors.sudoers_valet.desc" = "If you keep seeing this error, it is possible that there is a permission issue where PHP Monitor cannot validate the file, which can usually be resolved by running: `sudo chmod +r /private/etc/sudoers.d/valet`";
// Platform issue detected