1
0
mirror of https://github.com/nicoverbruggen/phpmon.git synced 2026-03-26 14:10:07 +01:00

♻️ Refactoring command history

This commit is contained in:
2026-02-24 15:58:31 +01:00
parent 88c02f1190
commit e7f2f8438e
8 changed files with 145 additions and 68 deletions

View File

@@ -128,6 +128,10 @@
037F44232EDB92EC002EBF75 /* HomebrewWatchManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 037F44212EDB92EC002EBF75 /* HomebrewWatchManager.swift */; };
037F44242EDB92EC002EBF75 /* HomebrewWatchManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 037F44212EDB92EC002EBF75 /* HomebrewWatchManager.swift */; };
037F44252EDB92EC002EBF75 /* HomebrewWatchManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 037F44212EDB92EC002EBF75 /* HomebrewWatchManager.swift */; };
0381313D2F4DF3BA00653177 /* CommandHistoryRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0381313C2F4DF3B400653177 /* CommandHistoryRow.swift */; };
0381313E2F4DF3BA00653177 /* CommandHistoryRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0381313C2F4DF3B400653177 /* CommandHistoryRow.swift */; };
0381313F2F4DF3BA00653177 /* CommandHistoryRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0381313C2F4DF3B400653177 /* CommandHistoryRow.swift */; };
038131402F4DF3BA00653177 /* CommandHistoryRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0381313C2F4DF3B400653177 /* CommandHistoryRow.swift */; };
0386B0B42ED36C3D00CA6795 /* Locked.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0386B0B32ED36C3D00CA6795 /* Locked.swift */; };
0386B0B52ED36C3D00CA6795 /* Locked.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0386B0B32ED36C3D00CA6795 /* Locked.swift */; };
0386B0B62ED36C3D00CA6795 /* Locked.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0386B0B32ED36C3D00CA6795 /* Locked.swift */; };
@@ -1112,6 +1116,7 @@
037F44172EDB27B7002EBF75 /* Debouncer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Debouncer.swift; sourceTree = "<group>"; };
037F441C2EDB9195002EBF75 /* ConfigWatchManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigWatchManager.swift; sourceTree = "<group>"; };
037F44212EDB92EC002EBF75 /* HomebrewWatchManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomebrewWatchManager.swift; sourceTree = "<group>"; };
0381313C2F4DF3B400653177 /* CommandHistoryRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandHistoryRow.swift; sourceTree = "<group>"; };
0386B0B32ED36C3D00CA6795 /* Locked.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Locked.swift; sourceTree = "<group>"; };
0386B0B82ED36DF800CA6795 /* LockedTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockedTests.swift; sourceTree = "<group>"; };
038A2B7D2EDDB24400173ACF /* App+UUID.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "App+UUID.swift"; sourceTree = "<group>"; };
@@ -2581,6 +2586,7 @@
children = (
E51118AFDD02AA1B7D8C9278 /* CommandHistoryWindowController.swift */,
31503E15DADA980998F0F5A2 /* CommandHistoryView.swift */,
0381313C2F4DF3B400653177 /* CommandHistoryRow.swift */,
);
path = UI;
sourceTree = "<group>";
@@ -3121,6 +3127,7 @@
C464ADB2275A87CA003FCD53 /* DomainListCellProtocol.swift in Sources */,
C4EE188422D3386B00E126E5 /* Constants.swift in Sources */,
C4DEB7D427A5D60B00834718 /* Stats.swift in Sources */,
038131402F4DF3BA00653177 /* CommandHistoryRow.swift in Sources */,
039C29182E8AA314007F5FAB /* TestableWebApi.swift in Sources */,
C47015022C46D6910069AAE7 /* NVAlertExtension.swift in Sources */,
C4E49DEA28F7643D0026AC4E /* CommandProtocol.swift in Sources */,
@@ -3355,6 +3362,7 @@
C471E7DA28F9BA8F0021E251 /* TestableCommand.swift in Sources */,
C471E7E528F9BAC20021E251 /* Events.swift in Sources */,
C471E7D628F9BA8F0021E251 /* RealFileSystem.swift in Sources */,
0381313D2F4DF3BA00653177 /* CommandHistoryRow.swift in Sources */,
C471E81728F9BAE80021E251 /* NSMenuExtension.swift in Sources */,
C40D725C2A018ACC0054A067 /* BusyStatus.swift in Sources */,
03DAD3A92EB3B08F003417BD /* DomainListVC+Certs.swift in Sources */,
@@ -3417,6 +3425,7 @@
C471E8A728F9BB8F0021E251 /* App+ActivationPolicy.swift in Sources */,
C45B914C295607F400F4EC78 /* Service.swift in Sources */,
03DAD3A82EB3B08F003417BD /* DomainListVC+Certs.swift in Sources */,
0381313F2F4DF3BA00653177 /* CommandHistoryRow.swift in Sources */,
C471E8A828F9BB8F0021E251 /* App+GlobalHotkey.swift in Sources */,
C471E8A928F9BB8F0021E251 /* InterAppHandler.swift in Sources */,
0379C4A02ED71D050035D7EA /* Startup+Launch.swift in Sources */,
@@ -3739,6 +3748,7 @@
C4513F962B13E30C001AD760 /* BrewExtensionsObservable.swift in Sources */,
C4B97B76275CF08C003F3378 /* AppDelegate+MenuOutlets.swift in Sources */,
C4AFC4AF29C4F32F00BF4E0D /* BrewPhpFormula.swift in Sources */,
0381313E2F4DF3BA00653177 /* CommandHistoryRow.swift in Sources */,
C4F780CD25D80B75000DBC97 /* Alert.swift in Sources */,
C4415E8E2B0287E90035F520 /* BrewFormulaeObservable.swift in Sources */,
C485706D28BF450900539B36 /* NSMenuItemExtension.swift in Sources */,

View File

@@ -93,7 +93,7 @@
</CommandLineArgument>
<CommandLineArgument
argument = "--ch"
isEnabled = "NO">
isEnabled = "YES">
</CommandLineArgument>
<CommandLineArgument
argument = "--cli"

View File

@@ -68,18 +68,30 @@ struct LoggedCommand: Identifiable {
func durationText(at date: Date = Date()) -> String {
if let completedAt {
let duration = completedAt.timeIntervalSince(startedAt)
if duration < 0.001 {
let micros = max(1, Int(duration * 1_000_000))
return "Completed in \(micros) us"
}
let ms = max(1, Int(duration * 1000))
return "Completed in \(ms) ms"
return Self.formattedDuration(
completedAt.timeIntervalSince(startedAt),
isCompleted: true
)
}
let ms = max(1, Int(date.timeIntervalSince(startedAt) * 1000))
return "Running for \(ms) ms"
return Self.formattedDuration(
date.timeIntervalSince(startedAt),
isCompleted: false
)
}
private static func formattedDuration(
_ duration: TimeInterval,
isCompleted: Bool
) -> String {
let prefix = isCompleted ? "Completed in" : "Running for"
if duration >= 0.3 {
let seconds = String(format: "%.2f", duration)
return "\(prefix) \(seconds) sec"
}
let ms = max(1, Int(duration * 1000))
return "\(prefix) \(ms) ms"
}
}

View File

@@ -380,7 +380,6 @@ class RealShell: ShellProtocol, @unchecked Sendable {
})
}
func reloadEnvPath() {
// Instead of replacing the entire shell instance, we simply re-fetch the PATH
self.PATH = RealShell.getPath()

View File

@@ -16,8 +16,8 @@ class Container: @unchecked Sendable {
private(set) var paths: Paths!
private(set) var shell: ShellProtocol!
private(set) var command: CommandProtocol!
private(set) var webApi: WebApiProtocol!
private(set) var commandTracker: CommandTracker!
private(set) var webApi: WebApiProtocol!
// Secondary (uses primary instances above)
private(set) var preferences: Preferences!
@@ -69,10 +69,8 @@ class Container: @unchecked Sendable {
self.filesystem = RealFileSystem(container: self)
self.paths = Paths(container: self)
self.commandTracker = CommandTracker()
let realShell = RealShell(binPath: paths.binPath)
self.shell = TrackedShell(shell: realShell, commandTracker: commandTracker)
let realCommand = RealCommand()
self.command = TrackedCommand(command: realCommand, commandTracker: commandTracker)
self.shell = TrackedShell(shell: RealShell(binPath: paths.binPath), commandTracker: commandTracker)
self.command = TrackedCommand(command: RealCommand(), commandTracker: commandTracker)
self.webApi = RealWebApi(container: self)
if coreOnly {

View File

@@ -0,0 +1,55 @@
//
// CommandHistoryRow.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 24/02/2026.
// Copyright © 2026 Nico Verbruggen. All rights reserved.
//
import SwiftUI
struct CommandHistoryRow: View {
let command: LoggedCommand
let now: Date
let isEvenRow: Bool
let onAppear: () -> Void
let onDisappear: () -> Void
let onCompleted: () -> Void
var body: some View {
HStack(alignment: .top, spacing: 10) {
VStack(alignment: .leading, spacing: 4) {
HStack(alignment: .firstTextBaseline, spacing: 8) {
if command.isCompleted {
Image(systemName: "checkmark.circle.fill")
.foregroundColor(.green)
.frame(width: 16)
} else {
ProgressView()
.controlSize(.small)
.frame(width: 16)
}
Text(command.command)
.font(.system(size: 12, design: .monospaced))
.lineLimit(2)
}
Text(command.durationText(at: now))
.font(.system(size: 11))
.foregroundColor(.secondary)
}
Spacer()
}
.padding(.vertical, 2)
.onAppear(perform: onAppear)
.onDisappear(perform: onDisappear)
.onChange(of: command.isCompleted) { isCompleted in
guard isCompleted else { return }
onCompleted()
}
.listRowSeparator(.hidden)
.listRowBackground(
Color(nsColor: NSColor.alternatingContentBackgroundColors[isEvenRow ? 0 : 1])
)
}
}

View File

@@ -5,83 +5,84 @@
// Copyright © 2025 Nico Verbruggen. All rights reserved.
//
import AppKit
import SwiftUI
struct CommandHistoryView: View {
// Provides access to the tracked command history
@ObservedObject var commandTracker: CommandTracker
// Timestamp used to compute duration labels in the list; updated when tick fires
@State private var now = Date()
init(commandTracker: CommandTracker? = nil) {
self.commandTracker = commandTracker ?? App.shared.container.commandTracker
// Tracks whether the window view is currently visible
@State private var isWindowVisible = false
// IDs for visible, active rows; used to avoid ticking when none are on-screen
@State private var visibleCommandIds: Set<UUID> = []
init(commandTracker: CommandTracker) {
self.commandTracker = commandTracker
}
var body: some View {
VStack(alignment: .leading, spacing: 6) {
Text("This window displays the last executed (shell) commands. Keep in mind that only the last 200 commands are stored and displayed.")
.font(.system(size: 11))
.foregroundColor(.secondary)
.padding(.horizontal, 12)
.padding(.top, 10)
ScrollViewReader { proxy in
List {
if commandTracker.commands.isEmpty {
HStack {
Spacer()
Text("No commands have been tracked yet.")
.font(.system(size: 13))
.foregroundColor(.secondary)
.padding(30)
Spacer()
}
} else {
ForEach(commandTracker.commands) { command in
HStack(alignment: .top, spacing: 10) {
VStack(alignment: .leading, spacing: 4) {
HStack(alignment: .firstTextBaseline, spacing: 8) {
if command.isCompleted {
Image(systemName: "checkmark.circle.fill")
.foregroundColor(.green)
.frame(width: 16)
} else {
ProgressView()
.controlSize(.small)
.frame(width: 16)
}
Text(command.command)
.font(.system(size: 12, design: .monospaced))
.lineLimit(2)
}
Text(command.durationText(at: now))
.font(.system(size: 11))
.foregroundColor(.secondary)
ForEach(commandTracker.commands.indices, id: \.self) { index in
let command = commandTracker.commands[index]
let isEvenRow = index.isMultiple(of: 2)
CommandHistoryRow(
command: command,
now: now,
isEvenRow: isEvenRow,
onAppear: {
// Track only visible, active commands to avoid unnecessary ticking
guard !command.isCompleted else { return }
visibleCommandIds.insert(command.id)
},
onDisappear: {
// Remove from visible set when the row scrolls out
visibleCommandIds.remove(command.id)
},
onCompleted: {
// Stop ticking for this row once the command completes
visibleCommandIds.remove(command.id)
}
Spacer()
}
.padding(.vertical, 2)
.id(command.id)
}
)
.id(command.id)
}
}
.listStyle(.plain)
.onChange(of: commandTracker.commands.count) { _ in
// Scroll to the bottom as new commands come in
if let last = commandTracker.commands.last {
proxy.scrollTo(last.id, anchor: .bottom)
}
}
}
.frame(minWidth: 400, minHeight: 200)
.onChange(of: commandTracker.isActive) { isActive in
guard isActive else { return }
.onAppear {
// Mark the window as visible so duration ticking can start
isWindowVisible = true
now = Date()
}
.onReceive(
Timer.publish(every: 0.1, on: .main, in: .common).autoconnect()
) { _ in
if commandTracker.isActive {
.onDisappear {
// Stop ticking when the window disappears
isWindowVisible = false
}
.onReceive(Timer.publish(every: 0.08, on: .main, in: .common).autoconnect()) { _ in
// Only update running commands if the window is visible + there's command IDs that are:
// - visible (in window, based on scroll position)
// - running (so we need to update the timestamp periodically)
if commandTracker.isActive, shouldTick {
now = Date()
}
}
}
}
private var shouldTick: Bool {
isWindowVisible && !visibleCommandIds.isEmpty
}
}

View File

@@ -26,7 +26,9 @@ class CommandHistoryWindowController: PMWindowController {
panel.isFloatingPanel = true
panel.hidesOnDeactivate = false
panel.delegate = delegate ?? windowController
panel.contentView = NSHostingView(rootView: CommandHistoryView())
panel.contentView = NSHostingView(rootView: CommandHistoryView(
commandTracker: App.shared.container.commandTracker
))
panel.setContentSize(NSSize(width: 500, height: 300))
windowController.window = panel