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:
@@ -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 */,
|
||||
|
||||
@@ -93,7 +93,7 @@
|
||||
</CommandLineArgument>
|
||||
<CommandLineArgument
|
||||
argument = "--ch"
|
||||
isEnabled = "NO">
|
||||
isEnabled = "YES">
|
||||
</CommandLineArgument>
|
||||
<CommandLineArgument
|
||||
argument = "--cli"
|
||||
|
||||
@@ -68,18 +68,30 @@ struct LoggedCommand: Identifiable {
|
||||
|
||||
func durationText(at date: Date = Date()) -> String {
|
||||
if let completedAt {
|
||||
let duration = completedAt.timeIntervalSince(startedAt)
|
||||
return Self.formattedDuration(
|
||||
completedAt.timeIntervalSince(startedAt),
|
||||
isCompleted: true
|
||||
)
|
||||
}
|
||||
|
||||
if duration < 0.001 {
|
||||
let micros = max(1, Int(duration * 1_000_000))
|
||||
return "Completed in \(micros) us"
|
||||
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 "Completed in \(ms) ms"
|
||||
}
|
||||
|
||||
let ms = max(1, Int(date.timeIntervalSince(startedAt) * 1000))
|
||||
return "Running for \(ms) ms"
|
||||
return "\(prefix) \(ms) ms"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 {
|
||||
|
||||
55
phpmon/Modules/Command History/UI/CommandHistoryRow.swift
Normal file
55
phpmon/Modules/Command History/UI/CommandHistoryRow.swift
Normal 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])
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
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)
|
||||
}
|
||||
} 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)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.padding(.vertical, 2)
|
||||
)
|
||||
.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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user