1
0
mirror of https://github.com/nicoverbruggen/phpmon.git synced 2025-08-07 03:50:08 +02:00

♻️ Rework Shell as singleton, logging

This commit is contained in:
2019-07-09 18:33:48 +02:00
parent 1bfcdd546b
commit a71c6afc0d
9 changed files with 160 additions and 42 deletions

View File

@ -18,6 +18,7 @@
C476FF9822B0DD830098105B /* Alert.swift in Sources */ = {isa = PBXBuildFile; fileRef = C476FF9722B0DD830098105B /* Alert.swift */; }; C476FF9822B0DD830098105B /* Alert.swift in Sources */ = {isa = PBXBuildFile; fileRef = C476FF9722B0DD830098105B /* Alert.swift */; };
C4D8016622B1584700C6DA1B /* BootChecks.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D8016522B1584700C6DA1B /* BootChecks.swift */; }; C4D8016622B1584700C6DA1B /* BootChecks.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D8016522B1584700C6DA1B /* BootChecks.swift */; };
C4EE188422D3386B00E126E5 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EE188322D3386B00E126E5 /* Constants.swift */; }; C4EE188422D3386B00E126E5 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EE188322D3386B00E126E5 /* Constants.swift */; };
C4F8C0A422D4F12C002EFE61 /* DateExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F8C0A322D4F12C002EFE61 /* DateExtension.swift */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
@ -35,6 +36,7 @@
C476FF9722B0DD830098105B /* Alert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Alert.swift; sourceTree = "<group>"; }; C476FF9722B0DD830098105B /* Alert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Alert.swift; sourceTree = "<group>"; };
C4D8016522B1584700C6DA1B /* BootChecks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BootChecks.swift; sourceTree = "<group>"; }; C4D8016522B1584700C6DA1B /* BootChecks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BootChecks.swift; sourceTree = "<group>"; };
C4EE188322D3386B00E126E5 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = "<group>"; }; C4EE188322D3386B00E126E5 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = "<group>"; };
C4F8C0A322D4F12C002EFE61 /* DateExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateExtension.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
@ -73,6 +75,7 @@
C41E181522CB614C0072CF09 /* Terminal */, C41E181522CB614C0072CF09 /* Terminal */,
C41C1B4E22B024F100E7CF16 /* Helpers */, C41C1B4E22B024F100E7CF16 /* Helpers */,
C41E181822CB62200072CF09 /* View Controllers */, C41E181822CB62200072CF09 /* View Controllers */,
C4F8C0A222D4F100002EFE61 /* Extensions */,
C41C1B3622B0097F00E7CF16 /* AppDelegate.swift */, C41C1B3622B0097F00E7CF16 /* AppDelegate.swift */,
C41C1B3F22B0098000E7CF16 /* Info.plist */, C41C1B3F22B0098000E7CF16 /* Info.plist */,
C41C1B4022B0098000E7CF16 /* phpmon.entitlements */, C41C1B4022B0098000E7CF16 /* phpmon.entitlements */,
@ -124,6 +127,14 @@
path = "View Controllers"; path = "View Controllers";
sourceTree = "<group>"; sourceTree = "<group>";
}; };
C4F8C0A222D4F100002EFE61 /* Extensions */ = {
isa = PBXGroup;
children = (
C4F8C0A322D4F12C002EFE61 /* DateExtension.swift */,
);
path = Extensions;
sourceTree = "<group>";
};
/* End PBXGroup section */ /* End PBXGroup section */
/* Begin PBXNativeTarget section */ /* Begin PBXNativeTarget section */
@ -195,6 +206,7 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
C4D8016622B1584700C6DA1B /* BootChecks.swift in Sources */, C4D8016622B1584700C6DA1B /* BootChecks.swift in Sources */,
C4F8C0A422D4F12C002EFE61 /* DateExtension.swift in Sources */,
C41C1B4722B009A400E7CF16 /* Shell.swift in Sources */, C41C1B4722B009A400E7CF16 /* Shell.swift in Sources */,
C41C1B4D22B0215A00E7CF16 /* Services.swift in Sources */, C41C1B4D22B0215A00E7CF16 /* Services.swift in Sources */,
C41C1B4922B00A9800E7CF16 /* ImageGenerator.swift in Sources */, C41C1B4922B00A9800E7CF16 /* ImageGenerator.swift in Sources */,

View File

@ -13,6 +13,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
// MARK: - Variables // MARK: - Variables
let sharedShell : Shell
let statusItem = NSStatusBar.system.statusItem(withLength: 32) let statusItem = NSStatusBar.system.statusItem(withLength: 32)
var timer: Timer? var timer: Timer?
var version: PhpVersion? = nil var version: PhpVersion? = nil
@ -21,6 +22,11 @@ class AppDelegate: NSObject, NSApplicationDelegate {
var log: String = "" var log: String = ""
var windowController: NSWindowController? = nil var windowController: NSWindowController? = nil
override init() {
self.sharedShell = Shell.shared
super.init()
}
// MARK: - Lifecycle // MARK: - Lifecycle
func applicationDidFinishLaunching(_ aNotification: Notification) { func applicationDidFinishLaunching(_ aNotification: Notification) {
@ -30,12 +36,11 @@ class AppDelegate: NSObject, NSApplicationDelegate {
DispatchQueue.global(qos: .userInitiated).async { [unowned self] in DispatchQueue.global(qos: .userInitiated).async { [unowned self] in
BootChecks.perform() BootChecks.perform()
self.availablePhpVersions = Services.detectPhpVersions() self.availablePhpVersions = Services.detectPhpVersions()
print("The following PHP versions were detected:")
print(self.availablePhpVersions)
self.updatePhpVersionInStatusBar() self.updatePhpVersionInStatusBar()
// Schedule a request to fetch the PHP version every 15 seconds // Schedule a request to fetch the PHP version every 60 seconds
Timer.scheduledTimer( DispatchQueue.main.async {
timeInterval: 15, self.timer = Timer.scheduledTimer(
timeInterval: 60,
target: self, target: self,
selector: #selector(self.updatePhpVersionInStatusBar), selector: #selector(self.updatePhpVersionInStatusBar),
userInfo: nil, userInfo: nil,
@ -43,6 +48,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
) )
} }
} }
}
func applicationWillTerminate(_ aNotification: Notification) { func applicationWillTerminate(_ aNotification: Notification) {
// Insert code here to tear down your application // Insert code here to tear down your application
@ -94,7 +100,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
menu.addItem(NSMenuItem.separator()) menu.addItem(NSMenuItem.separator())
} }
// TODO: Enable when implementation is complete // TODO: Enable when implementation is complete
// menu.addItem(NSMenuItem(title: "View terminal output", action: #selector(self.openOutput), keyEquivalent: "")) menu.addItem(NSMenuItem(title: "View terminal output", action: #selector(self.openOutput), keyEquivalent: ""))
menu.addItem(NSMenuItem(title: "About phpmon", action: #selector(self.openAbout), keyEquivalent: "")) menu.addItem(NSMenuItem(title: "About phpmon", action: #selector(self.openAbout), keyEquivalent: ""))
menu.addItem(NSMenuItem(title: "Quit phpmon", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q")) menu.addItem(NSMenuItem(title: "Quit phpmon", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q"))
DispatchQueue.main.async { DispatchQueue.main.async {
@ -108,16 +114,22 @@ class AppDelegate: NSObject, NSApplicationDelegate {
@objc func openOutput() { @objc func openOutput() {
if (self.windowController == nil) { if (self.windowController == nil) {
let vc = NSStoryboard(name: "Main", bundle: nil).instantiateController(withIdentifier: "logWindow") as! LogViewController let vc = NSStoryboard(name: "Main", bundle: nil).instantiateController(withIdentifier: "logWindow") as! LogViewController
Shell.shared.delegate = vc
let window = NSWindow(contentViewController: vc) let window = NSWindow(contentViewController: vc)
window.title = "Terminal Output" window.title = "Terminal Output"
self.windowController = NSWindowController(window: window) self.windowController = NSWindowController(window: window)
} }
self.windowController!.showWindow(self) self.windowController!.showWindow(self)
NSApp.activate(ignoringOtherApps: true)
// TODO: Send window to front (if possible) // TODO: Send window to front (if possible)
} }
@objc func updatePhpVersionInStatusBar() { @objc func updatePhpVersionInStatusBar() {
self.version = PhpVersion() self.version = PhpVersion()
if (Shell.shared.history.count > 0) {
_ = Shell.shared.history.popLast()
}
if (self.busy) { if (self.busy) {
DispatchQueue.main.async { DispatchQueue.main.async {
self.setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIcon"))!) self.setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIcon"))!)

View File

@ -14,11 +14,9 @@ class PhpVersion {
var long : String = "???" var long : String = "???"
init() { init() {
let version = Shell let version = Shell.shared
// Get the version directly from PHP // Get the version directly from PHP
.execute(command: "php -r 'print phpversion();'") .pipe("php -r 'print phpversion();'")
// also remove any colors
.replacingOccurrences(of: "\u{1b}(B\u{1b}[m", with: "")
// That's the long version // That's the long version
self.long = version self.long = version

View File

@ -0,0 +1,19 @@
//
// Date.swift
// phpmon
//
// Created by Nico Verbruggen on 09/07/2019.
// Copyright © 2019 Nico Verbruggen. All rights reserved.
//
import Cocoa
extension Date
{
func toString() -> String
{
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
return dateFormatter.string(from: self)
}
}

View File

@ -13,31 +13,31 @@ class BootChecks {
public static func perform() public static func perform()
{ {
self.presentAlertOnMainThreadIf( self.presentAlertOnMainThreadIf(
!Shell.execute(command: "which php").contains("/usr/local/bin/php"), !Shell.shared.pipe("which php").contains("/usr/local/bin/php"),
messageText: "PHP is not correctly installed", messageText: "PHP is not correctly installed",
informativeText: "You must install PHP via brew. Try running `which php` in Terminal, it should return `/usr/local/bin/php`. The app will not work correctly until you resolve this issue." informativeText: "You must install PHP via brew. Try running `which php` in Terminal, it should return `/usr/local/bin/php`. The app will not work correctly until you resolve this issue."
) )
self.presentAlertOnMainThreadIf( self.presentAlertOnMainThreadIf(
!Shell.execute(command: "ls /usr/local/opt | grep php@7.3").contains("php@7.3"), !Shell.shared.pipe("ls /usr/local/opt | grep php@7.3").contains("php@7.3"),
messageText: "PHP 7.3 is not correctly installed", messageText: "PHP 7.3 is not correctly installed",
informativeText: "PHP 7.3 alias was not found in `/usr/local/opt`. The app will not work correctly until you resolve this issue." informativeText: "PHP 7.3 alias was not found in `/usr/local/opt`. The app will not work correctly until you resolve this issue."
) )
self.presentAlertOnMainThreadIf( self.presentAlertOnMainThreadIf(
!Shell.execute(command: "which valet").contains("/usr/local/bin/valet"), !Shell.shared.pipe("which valet").contains("/usr/local/bin/valet"),
messageText: "Laravel Valet is not correctly installed", messageText: "Laravel Valet is not correctly installed",
informativeText: "You must install Valet via brew. Try running `which valet` in Terminal, it should return `/usr/local/bin/valet`. The app will not work correctly until you resolve this issue." informativeText: "You must install Valet via brew. Try running `which valet` in Terminal, it should return `/usr/local/bin/valet`. The app will not work correctly until you resolve this issue."
) )
self.presentAlertOnMainThreadIf( self.presentAlertOnMainThreadIf(
!Shell.execute(command: "cat /private/etc/sudoers.d/brew").contains("/usr/local/bin/brew"), !Shell.shared.pipe("cat /private/etc/sudoers.d/brew").contains("/usr/local/bin/brew"),
messageText: "Brew has not been added to sudoers.d", messageText: "Brew has not been added to sudoers.d",
informativeText: "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." informativeText: "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."
) )
self.presentAlertOnMainThreadIf( self.presentAlertOnMainThreadIf(
!Shell.execute(command: "cat /private/etc/sudoers.d/valet").contains("/usr/local/bin/valet"), !Shell.shared.pipe("cat /private/etc/sudoers.d/valet").contains("/usr/local/bin/valet"),
messageText: "Valet has not been added to sudoers.d", messageText: "Valet has not been added to sudoers.d",
informativeText: "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." informativeText: "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."
) )

View File

@ -11,17 +11,17 @@ import AppKit
class Services { class Services {
public static func mysqlIsRunning() -> Bool { public static func mysqlIsRunning() -> Bool {
let running = Shell.execute(command: "launchctl list | grep homebrew.mxcl.mysql") let running = Shell.shared.pipe( "launchctl list | grep homebrew.mxcl.mysql")
return (running != "") return (running != "")
} }
public static func nginxIsRunning() -> Bool { public static func nginxIsRunning() -> Bool {
let running = Shell.execute(command: "launchctl list | grep homebrew.mxcl.nginx") let running = Shell.shared.pipe( "launchctl list | grep homebrew.mxcl.nginx")
return (running != "") return (running != "")
} }
public static func detectPhpVersions() -> [String] { public static func detectPhpVersions() -> [String] {
let files = Shell.execute(command: "ls /usr/local/opt | grep php@") let files = Shell.shared.pipe( "ls /usr/local/opt | grep php@")
var versions = files.components(separatedBy: "\n") var versions = files.components(separatedBy: "\n")
// Remove all empty strings // Remove all empty strings
versions.removeAll { (string) -> Bool in versions.removeAll { (string) -> Bool in
@ -37,20 +37,20 @@ class Services {
public static func switchToPhpVersion(version: String, availableVersions: [String]) { public static func switchToPhpVersion(version: String, availableVersions: [String]) {
availableVersions.forEach { (version) in availableVersions.forEach { (version) in
_ = Shell.execute(command: "brew unlink php@\(version)") Shell.shared.run( "brew unlink php@\(version)")
} }
if (availableVersions.contains("7.3")) { if (availableVersions.contains("7.3")) {
_ = Shell.execute(command: "brew link php@7.3") Shell.shared.run( "brew link php@7.3")
if (version == Constants.LatestPhpVersion) { if (version == Constants.LatestPhpVersion) {
_ = Shell.execute(command: "valet use php") Shell.shared.run( "valet use php")
} else { } else {
_ = Shell.execute(command: "valet use php@\(version)") Shell.shared.run( "valet use php@\(version)")
} }
} }
} }
public static func restartPhp(version: String) { public static func restartPhp(version: String) {
_ = Shell.execute(command: "brew services restart php@\(version)") Shell.shared.run( "brew services restart php@\(version)")
} }
public static func openPhpConfigFolder(version: String) { public static func openPhpConfigFolder(version: String) {

View File

@ -8,9 +8,38 @@
import Cocoa import Cocoa
protocol ShellDelegate: class {
func didCompleteCommand(historyItem: ShellHistoryItem)
}
class ShellHistoryItem {
var command: String
var output: String
var date: Date
init(command: String, output: String) {
self.command = command
self.output = output
self.date = Date()
}
}
class Shell { class Shell {
public static func execute(command: String) -> String { static let shared = Shell()
var history : [ShellHistoryItem] = []
var delegate : ShellDelegate?
/// Runs a shell command without using the description.
public func run(_ command: String) {
// Equivalent of piping to /dev/null; don't do anything with the string
_ = self.pipe(command)
}
/// Runs a shell command and returns the output.
public func pipe(_ command: String) -> String {
let task = Process() let task = Process()
task.launchPath = "/bin/bash" task.launchPath = "/bin/bash"
task.arguments = ["--login", "-c", command] task.arguments = ["--login", "-c", command]
@ -24,7 +53,14 @@ class Shell {
let output: String = NSString( let output: String = NSString(
data: data, data: data,
encoding: String.Encoding.utf8.rawValue encoding: String.Encoding.utf8.rawValue
)! as String )!.replacingOccurrences(
of: "\u{1B}(B\u{1B}[m",
with: ""
) as String
let historyItem = ShellHistoryItem(command: command, output: output)
history.append(historyItem)
delegate?.didCompleteCommand(historyItem: historyItem)
return output return output
} }

View File

@ -59,11 +59,11 @@
<objects> <objects>
<viewController storyboardIdentifier="logWindow" id="XfG-lQ-9wD" customClass="LogViewController" customModule="phpmon" customModuleProvider="target" sceneMemberID="viewController"> <viewController storyboardIdentifier="logWindow" id="XfG-lQ-9wD" customClass="LogViewController" customModule="phpmon" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" identifier="main" id="m2S-Jp-Qdl"> <view key="view" identifier="main" id="m2S-Jp-Qdl">
<rect key="frame" x="0.0" y="0.0" width="578" height="308"/> <rect key="frame" x="0.0" y="0.0" width="662" height="475"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="ICa-gx-jgq"> <button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="ICa-gx-jgq">
<rect key="frame" x="489" y="13" width="75" height="32"/> <rect key="frame" x="573" y="8" width="75" height="32"/>
<buttonCell key="cell" type="push" title="Close" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="3md-FI-EWa"> <buttonCell key="cell" type="push" title="Close" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="3md-FI-EWa">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/> <behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/> <font key="font" metaFont="system"/>
@ -73,18 +73,18 @@
</connections> </connections>
</button> </button>
<scrollView borderType="line" horizontalLineScroll="10" horizontalPageScroll="10" verticalLineScroll="10" verticalPageScroll="10" hasHorizontalScroller="NO" translatesAutoresizingMaskIntoConstraints="NO" id="vZy-5S-021"> <scrollView borderType="line" horizontalLineScroll="10" horizontalPageScroll="10" verticalLineScroll="10" verticalPageScroll="10" hasHorizontalScroller="NO" translatesAutoresizingMaskIntoConstraints="NO" id="vZy-5S-021">
<rect key="frame" x="20" y="51" width="538" height="237"/> <rect key="frame" x="20" y="46" width="622" height="409"/>
<clipView key="contentView" drawsBackground="NO" copiesOnScroll="NO" id="s5L-AU-0fw"> <clipView key="contentView" drawsBackground="NO" copiesOnScroll="NO" id="s5L-AU-0fw">
<rect key="frame" x="1" y="1" width="536" height="235"/> <rect key="frame" x="1" y="1" width="620" height="407"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews> <subviews>
<textView importsGraphics="NO" richText="NO" verticallyResizable="YES" smartInsertDelete="YES" id="tN6-Y9-1pA"> <textView importsGraphics="NO" richText="NO" verticallyResizable="YES" smartInsertDelete="YES" id="tN6-Y9-1pA">
<rect key="frame" x="0.0" y="0.0" width="536" height="235"/> <rect key="frame" x="0.0" y="0.0" width="620" height="407"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/> <color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/> <color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
<size key="minSize" width="536" height="235"/> <size key="minSize" width="620" height="407"/>
<size key="maxSize" width="538" height="10000000"/> <size key="maxSize" width="620" height="10000000"/>
<color key="insertionPointColor" name="textColor" catalog="System" colorSpace="catalog"/> <color key="insertionPointColor" name="textColor" catalog="System" colorSpace="catalog"/>
</textView> </textView>
</subviews> </subviews>
@ -94,7 +94,7 @@
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
</scroller> </scroller>
<scroller key="verticalScroller" wantsLayer="YES" verticalHuggingPriority="750" horizontal="NO" id="qp7-7R-gTO"> <scroller key="verticalScroller" wantsLayer="YES" verticalHuggingPriority="750" horizontal="NO" id="qp7-7R-gTO">
<rect key="frame" x="521" y="1" width="16" height="235"/> <rect key="frame" x="605" y="1" width="16" height="407"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
</scroller> </scroller>
</scrollView> </scrollView>
@ -104,14 +104,17 @@
<constraint firstAttribute="trailing" secondItem="ICa-gx-jgq" secondAttribute="trailing" constant="20" id="LFS-0E-Ibw"/> <constraint firstAttribute="trailing" secondItem="ICa-gx-jgq" secondAttribute="trailing" constant="20" id="LFS-0E-Ibw"/>
<constraint firstItem="vZy-5S-021" firstAttribute="top" secondItem="m2S-Jp-Qdl" secondAttribute="top" constant="20" id="Nec-oI-CjE"/> <constraint firstItem="vZy-5S-021" firstAttribute="top" secondItem="m2S-Jp-Qdl" secondAttribute="top" constant="20" id="Nec-oI-CjE"/>
<constraint firstAttribute="trailing" secondItem="vZy-5S-021" secondAttribute="trailing" constant="20" id="kBJ-O5-eYI"/> <constraint firstAttribute="trailing" secondItem="vZy-5S-021" secondAttribute="trailing" constant="20" id="kBJ-O5-eYI"/>
<constraint firstAttribute="bottom" secondItem="ICa-gx-jgq" secondAttribute="bottom" constant="20" id="kYB-Fn-DSA"/> <constraint firstAttribute="bottom" secondItem="ICa-gx-jgq" secondAttribute="bottom" constant="15" id="kYB-Fn-DSA"/>
<constraint firstItem="ICa-gx-jgq" firstAttribute="top" secondItem="vZy-5S-021" secondAttribute="bottom" constant="10" id="xdn-yU-LVb"/> <constraint firstItem="ICa-gx-jgq" firstAttribute="top" secondItem="vZy-5S-021" secondAttribute="bottom" constant="10" id="xdn-yU-LVb"/>
</constraints> </constraints>
</view> </view>
<connections>
<outlet property="textView" destination="tN6-Y9-1pA" id="z77-me-Od6"/>
</connections>
</viewController> </viewController>
<customObject id="rPt-NT-nkU" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/> <customObject id="rPt-NT-nkU" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects> </objects>
<point key="canvasLocation" x="-147" y="294"/> <point key="canvasLocation" x="-105" y="377.5"/>
</scene> </scene>
</scenes> </scenes>
</document> </document>

View File

@ -8,10 +8,48 @@
import Cocoa import Cocoa
class LogViewController: NSViewController { class LogViewController: NSViewController, ShellDelegate {
@IBOutlet var textView: NSTextView!
public func appendHistoryItem(_ historyItem: ShellHistoryItem) {
self.append(
"""
======
@ \(historyItem.date.toString())
------
$ \(historyItem.command)
------
> \(historyItem.output)
"""
)
}
public func append(_ text : String) {
self.textView.textStorage?.append(
NSAttributedString(
string: text,
attributes: [
NSAttributedString.Key.font: NSFont(name: "Menlo", size: 12.0)!
]
)
)
self.textView.scrollToEndOfDocument(nil)
}
override func viewDidLoad() {
self.textView.isEditable = false
for entry in Shell.shared.history {
self.appendHistoryItem(entry)
}
}
func didCompleteCommand(historyItem: ShellHistoryItem) {
self.appendHistoryItem(historyItem)
}
@IBAction func pressed(_ sender: Any) { @IBAction func pressed(_ sender: Any) {
self.view.window?.windowController?.close() self.view.window?.windowController?.close()
} }
} }