// // ViewController.swift // MacTerminal // // Created by Miguel de Icaza on 3/11/20. // Copyright © 2020 Miguel de Icaza. All rights reserved. // import Cocoa import SwiftTerm class ViewController: NSViewController, LocalProcessTerminalViewDelegate, NSUserInterfaceValidations { @IBOutlet var loggingMenuItem: NSMenuItem? var changingSize = false var logging: Bool = false var zoomGesture: NSMagnificationGestureRecognizer? var postedTitle: String = "" var postedDirectory: String? = nil func sizeChanged(source: LocalProcessTerminalView, newCols: Int, newRows: Int) { if changingSize { return } changingSize = true //var border = view.window!.frame - view.frame var newFrame = terminal.getOptimalFrameSize () let windowFrame = view.window!.frame newFrame = CGRect (x: windowFrame.minX, y: windowFrame.minY, width: newFrame.width, height: windowFrame.height - view.frame.height + newFrame.height) view.window?.setFrame(newFrame, display: true, animate: true) changingSize = false } func updateWindowTitle () { var newTitle: String if let dir = postedDirectory { if let uri = URL(string: dir) { if postedTitle == "" { newTitle = uri.path } else { newTitle = "\(postedTitle) - \(uri.path)" } } else { newTitle = postedTitle } } else { newTitle = postedTitle } view.window?.title = newTitle } func setTerminalTitle(source: LocalProcessTerminalView, title: String) { postedTitle = title updateWindowTitle () } func hostCurrentDirectoryUpdate (source: TerminalView, directory: String?) { self.postedDirectory = directory updateWindowTitle() } func processTerminated(source: TerminalView, exitCode: Int32?) { view.window?.close() if let e = exitCode { print ("Process terminated with code: \(e)") } else { print ("Process vanished") } } var terminal: LocalProcessTerminalView! static weak var lastTerminal: LocalProcessTerminalView! func getBufferAsData () -> Data { return terminal.getTerminal().getBufferAsData () } func updateLogging () { // let path = logging ? "/Users/miguel/Downloads/Logs" : nil // terminal.setHostLogging (directory: path) NSUserDefaultsController.shared.defaults.set (logging, forKey: "LogHostOutput") } // Returns the shell associated with the current account func getShell () -> String { let bufsize = sysconf(_SC_GETPW_R_SIZE_MAX) guard bufsize != -1 else { return "/bin/bash" } let buffer = UnsafeMutablePointer.allocate(capacity: bufsize) defer { buffer.deallocate() } var pwd = passwd() var result: UnsafeMutablePointer? = UnsafeMutablePointer.allocate(capacity: 1) if getpwuid_r(getuid(), &pwd, buffer, bufsize, &result) != 0 { return "/bin/bash" } return String (cString: pwd.pw_shell) } class TD: TerminalDelegate { func send(source: Terminal, data: ArraySlice) { } } func test () { let a = Terminal (delegate: TD ()) print (a) } override func viewDidLoad() { super.viewDidLoad() test () terminal = LocalProcessTerminalView(frame: view.frame) zoomGesture = NSMagnificationGestureRecognizer(target: self, action: #selector(zoomGestureHandler)) terminal.addGestureRecognizer(zoomGesture!) ViewController.lastTerminal = terminal terminal.processDelegate = self terminal.feed(text: "Welcome to SwiftTerm") let shell = getShell() let shellIdiom = "-" + NSString(string: shell).lastPathComponent FileManager.default.changeCurrentDirectoryPath (FileManager.default.homeDirectoryForCurrentUser.path) terminal.startProcess (executable: shell, execName: shellIdiom) view.addSubview(terminal) logging = NSUserDefaultsController.shared.defaults.bool(forKey: "LogHostOutput") updateLogging () } override func viewWillDisappear() { //terminal = nil } @objc func zoomGestureHandler (_ sender: NSMagnificationGestureRecognizer) { if sender.magnification > 0 { biggerFont (sender) } else { smallerFont(sender) } } override func viewDidLayout() { super.viewDidLayout() changingSize = true terminal.frame = view.frame changingSize = false terminal.needsLayout = true } @objc @IBAction func set80x25 (_ source: AnyObject) { terminal.resize(cols: 80, rows: 25) } var lowerCol = 80 var lowerRow = 25 var higherCol = 160 var higherRow = 60 func queueNextSize () { // If they requested a stop if resizificating == 0 { return } var next = terminal.getTerminal().getDims () if resizificating > 0 { if next.cols < higherCol { next.cols += 1 } if next.rows < higherRow { next.rows += 1 } } else { if next.cols > lowerCol { next.cols -= 1 } if next.rows > lowerRow { next.rows -= 1 } } terminal.resize (cols: next.cols, rows: next.rows) var direction = resizificating if next.rows == higherRow && next.cols == higherCol { direction = -1 } if next.rows == lowerRow && next.cols == lowerCol { direction = 1 } DispatchQueue.main.asyncAfter(deadline: .now() + 0.03) { self.resizificating = direction self.queueNextSize() } } var resizificating = 0 @objc @IBAction func resizificator (_ source: AnyObject) { if resizificating != 1 { resizificating = 1 queueNextSize () } else { resizificating = 0 } } @objc @IBAction func resizificatorDown (_ source: AnyObject) { if resizificating != -1 { resizificating = -1 queueNextSize () } else { resizificating = 0 } } @objc @IBAction func allowMouseReporting (_ source: AnyObject) { terminal.allowMouseReporting.toggle () } @objc @IBAction func exportBuffer (_ source: AnyObject) { saveData { self.terminal.getTerminal().getBufferAsData () } } @objc @IBAction func exportSelection (_ source: AnyObject) { saveData { if let str = self.terminal.getSelection () { return str.data (using: .utf8) ?? Data () } return Data () } } func saveData (_ getData: @escaping () -> Data) { let savePanel = NSSavePanel () savePanel.canCreateDirectories = true savePanel.allowedFileTypes = ["txt"] savePanel.title = "Export Buffer Contents As Text" savePanel.nameFieldStringValue = "TerminalCapture" savePanel.begin { (result) in if result.rawValue == NSApplication.ModalResponse.OK.rawValue { let data = getData () if let url = savePanel.url { do { try data.write(to: url) } catch let error as NSError { let alert = NSAlert (error: error) alert.runModal() } } } } } @objc @IBAction func softReset (_ source: AnyObject) { terminal.getTerminal().softReset () terminal.setNeedsDisplay(terminal.frame) } @objc @IBAction func hardReset (_ source: AnyObject) { terminal.getTerminal().resetToInitialState () terminal.setNeedsDisplay(terminal.frame) } @objc @IBAction func toggleOptionAsMetaKey (_ source: AnyObject) { terminal.optionAsMetaKey.toggle () } @objc @IBAction func biggerFont (_ source: AnyObject) { let size = terminal.font.pointSize guard size < 72 else { return } terminal.font = NSFont.monospacedSystemFont(ofSize: size+1, weight: .regular) } @objc @IBAction func smallerFont (_ source: AnyObject) { let size = terminal.font.pointSize guard size > 5 else { return } terminal.font = NSFont.monospacedSystemFont(ofSize: size-1, weight: .regular) } @objc @IBAction func defaultFontSize (_ source: AnyObject) { terminal.font = NSFont.monospacedSystemFont(ofSize: NSFont.systemFontSize, weight: .regular) } @objc @IBAction func addTab (_ source: AnyObject) { // if let win = view.window { // win.tabbingMode = .preferred // if let wc = win.windowController { // if let d = wc.document as? Document { // do { // let x = Document() // x.makeWindowControllers() // // try NSDocumentController.shared.newDocument(self) // } catch {} // print ("\(d.debugDescription)") // } // } // } // win.tabbingMode = .preferred // win.addTabbedWindow(win, ordered: .above) // // if let wc = win.windowController { // wc.newWindowForTab(self() // wc.showWindow(source) // } // } } func validateUserInterfaceItem(_ item: NSValidatedUserInterfaceItem) -> Bool { if item.action == #selector(debugToggleHostLogging(_:)) { if let m = item as? NSMenuItem { m.state = logging ? NSControl.StateValue.on : NSControl.StateValue.off } } if item.action == #selector(resizificator(_:)) { if let m = item as? NSMenuItem { m.state = resizificating == 1 ? NSControl.StateValue.on : NSControl.StateValue.off } } if item.action == #selector(resizificatorDown(_:)) { if let m = item as? NSMenuItem { m.state = resizificating == -1 ? NSControl.StateValue.on : NSControl.StateValue.off } } if item.action == #selector(allowMouseReporting(_:)) { if let m = item as? NSMenuItem { m.state = terminal.allowMouseReporting ? NSControl.StateValue.on : NSControl.StateValue.off } } if item.action == #selector(toggleOptionAsMetaKey(_:)) { if let m = item as? NSMenuItem { m.state = terminal.optionAsMetaKey ? NSControl.StateValue.on : NSControl.StateValue.off } } // Only enable "Export selection" if we have a selection if item.action == #selector(exportSelection(_:)) { return terminal.selectionActive } return true } @objc @IBAction func debugToggleHostLogging (_ source: AnyObject) { logging = !logging updateLogging() } }