ViewController.swift 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401
  1. //
  2. // ViewController.swift
  3. // MacTerminal
  4. //
  5. // Created by Miguel de Icaza on 3/11/20.
  6. // Copyright © 2020 Miguel de Icaza. All rights reserved.
  7. //
  8. import Cocoa
  9. import SwiftTerm
  10. class ViewController: NSViewController, LocalProcessTerminalViewDelegate, NSUserInterfaceValidations {
  11. @IBOutlet var loggingMenuItem: NSMenuItem?
  12. var changingSize = false
  13. var logging: Bool = false
  14. var zoomGesture: NSMagnificationGestureRecognizer?
  15. var postedTitle: String = ""
  16. var postedDirectory: String? = nil
  17. func sizeChanged(source: LocalProcessTerminalView, newCols: Int, newRows: Int) {
  18. if changingSize {
  19. return
  20. }
  21. changingSize = true
  22. //var border = view.window!.frame - view.frame
  23. var newFrame = terminal.getOptimalFrameSize ()
  24. let windowFrame = view.window!.frame
  25. newFrame = CGRect (x: windowFrame.minX, y: windowFrame.minY, width: newFrame.width, height: windowFrame.height - view.frame.height + newFrame.height)
  26. view.window?.setFrame(newFrame, display: true, animate: true)
  27. changingSize = false
  28. }
  29. func updateWindowTitle ()
  30. {
  31. var newTitle: String
  32. if let dir = postedDirectory {
  33. if let uri = URL(string: dir) {
  34. if postedTitle == "" {
  35. newTitle = uri.path
  36. } else {
  37. newTitle = "\(postedTitle) - \(uri.path)"
  38. }
  39. } else {
  40. newTitle = postedTitle
  41. }
  42. } else {
  43. newTitle = postedTitle
  44. }
  45. view.window?.title = newTitle
  46. }
  47. func setTerminalTitle(source: LocalProcessTerminalView, title: String) {
  48. postedTitle = title
  49. updateWindowTitle ()
  50. }
  51. func hostCurrentDirectoryUpdate (source: TerminalView, directory: String?) {
  52. self.postedDirectory = directory
  53. updateWindowTitle()
  54. }
  55. func processTerminated(source: TerminalView, exitCode: Int32?) {
  56. view.window?.close()
  57. if let e = exitCode {
  58. print ("Process terminated with code: \(e)")
  59. } else {
  60. print ("Process vanished")
  61. }
  62. }
  63. var terminal: LocalProcessTerminalView!
  64. static weak var lastTerminal: LocalProcessTerminalView!
  65. func getBufferAsData () -> Data
  66. {
  67. return terminal.getTerminal().getBufferAsData ()
  68. }
  69. func updateLogging ()
  70. {
  71. // let path = logging ? "/Users/miguel/Downloads/Logs" : nil
  72. // terminal.setHostLogging (directory: path)
  73. NSUserDefaultsController.shared.defaults.set (logging, forKey: "LogHostOutput")
  74. }
  75. // Returns the shell associated with the current account
  76. func getShell () -> String
  77. {
  78. let bufsize = sysconf(_SC_GETPW_R_SIZE_MAX)
  79. guard bufsize != -1 else {
  80. return "/bin/bash"
  81. }
  82. let buffer = UnsafeMutablePointer<Int8>.allocate(capacity: bufsize)
  83. defer {
  84. buffer.deallocate()
  85. }
  86. var pwd = passwd()
  87. var result: UnsafeMutablePointer<passwd>? = UnsafeMutablePointer<passwd>.allocate(capacity: 1)
  88. if getpwuid_r(getuid(), &pwd, buffer, bufsize, &result) != 0 {
  89. return "/bin/bash"
  90. }
  91. return String (cString: pwd.pw_shell)
  92. }
  93. class TD: TerminalDelegate {
  94. func send(source: Terminal, data: ArraySlice<UInt8>) {
  95. }
  96. }
  97. func test () {
  98. let a = Terminal (delegate: TD ())
  99. print (a)
  100. }
  101. override func viewDidLoad() {
  102. super.viewDidLoad()
  103. test ()
  104. terminal = LocalProcessTerminalView(frame: view.frame)
  105. zoomGesture = NSMagnificationGestureRecognizer(target: self, action: #selector(zoomGestureHandler))
  106. terminal.addGestureRecognizer(zoomGesture!)
  107. ViewController.lastTerminal = terminal
  108. terminal.processDelegate = self
  109. terminal.feed(text: "Welcome to SwiftTerm")
  110. let shell = getShell()
  111. let shellIdiom = "-" + NSString(string: shell).lastPathComponent
  112. FileManager.default.changeCurrentDirectoryPath (FileManager.default.homeDirectoryForCurrentUser.path)
  113. terminal.startProcess (executable: shell, execName: shellIdiom)
  114. view.addSubview(terminal)
  115. logging = NSUserDefaultsController.shared.defaults.bool(forKey: "LogHostOutput")
  116. updateLogging ()
  117. }
  118. override func viewWillDisappear() {
  119. //terminal = nil
  120. }
  121. @objc
  122. func zoomGestureHandler (_ sender: NSMagnificationGestureRecognizer) {
  123. if sender.magnification > 0 {
  124. biggerFont (sender)
  125. } else {
  126. smallerFont(sender)
  127. }
  128. }
  129. override func viewDidLayout() {
  130. super.viewDidLayout()
  131. changingSize = true
  132. terminal.frame = view.frame
  133. changingSize = false
  134. terminal.needsLayout = true
  135. }
  136. @objc @IBAction
  137. func set80x25 (_ source: AnyObject)
  138. {
  139. terminal.resize(cols: 80, rows: 25)
  140. }
  141. var lowerCol = 80
  142. var lowerRow = 25
  143. var higherCol = 160
  144. var higherRow = 60
  145. func queueNextSize ()
  146. {
  147. // If they requested a stop
  148. if resizificating == 0 {
  149. return
  150. }
  151. var next = terminal.getTerminal().getDims ()
  152. if resizificating > 0 {
  153. if next.cols < higherCol {
  154. next.cols += 1
  155. }
  156. if next.rows < higherRow {
  157. next.rows += 1
  158. }
  159. } else {
  160. if next.cols > lowerCol {
  161. next.cols -= 1
  162. }
  163. if next.rows > lowerRow {
  164. next.rows -= 1
  165. }
  166. }
  167. terminal.resize (cols: next.cols, rows: next.rows)
  168. var direction = resizificating
  169. if next.rows == higherRow && next.cols == higherCol {
  170. direction = -1
  171. }
  172. if next.rows == lowerRow && next.cols == lowerCol {
  173. direction = 1
  174. }
  175. DispatchQueue.main.asyncAfter(deadline: .now() + 0.03) {
  176. self.resizificating = direction
  177. self.queueNextSize()
  178. }
  179. }
  180. var resizificating = 0
  181. @objc @IBAction
  182. func resizificator (_ source: AnyObject)
  183. {
  184. if resizificating != 1 {
  185. resizificating = 1
  186. queueNextSize ()
  187. } else {
  188. resizificating = 0
  189. }
  190. }
  191. @objc @IBAction
  192. func resizificatorDown (_ source: AnyObject)
  193. {
  194. if resizificating != -1 {
  195. resizificating = -1
  196. queueNextSize ()
  197. } else {
  198. resizificating = 0
  199. }
  200. }
  201. @objc @IBAction
  202. func allowMouseReporting (_ source: AnyObject)
  203. {
  204. terminal.allowMouseReporting.toggle ()
  205. }
  206. @objc @IBAction
  207. func exportBuffer (_ source: AnyObject)
  208. {
  209. saveData { self.terminal.getTerminal().getBufferAsData () }
  210. }
  211. @objc @IBAction
  212. func exportSelection (_ source: AnyObject)
  213. {
  214. saveData {
  215. if let str = self.terminal.getSelection () {
  216. return str.data (using: .utf8) ?? Data ()
  217. }
  218. return Data ()
  219. }
  220. }
  221. func saveData (_ getData: @escaping () -> Data)
  222. {
  223. let savePanel = NSSavePanel ()
  224. savePanel.canCreateDirectories = true
  225. savePanel.allowedFileTypes = ["txt"]
  226. savePanel.title = "Export Buffer Contents As Text"
  227. savePanel.nameFieldStringValue = "TerminalCapture"
  228. savePanel.begin { (result) in
  229. if result.rawValue == NSApplication.ModalResponse.OK.rawValue {
  230. let data = getData ()
  231. if let url = savePanel.url {
  232. do {
  233. try data.write(to: url)
  234. } catch let error as NSError {
  235. let alert = NSAlert (error: error)
  236. alert.runModal()
  237. }
  238. }
  239. }
  240. }
  241. }
  242. @objc @IBAction
  243. func softReset (_ source: AnyObject)
  244. {
  245. terminal.getTerminal().softReset ()
  246. terminal.setNeedsDisplay(terminal.frame)
  247. }
  248. @objc @IBAction
  249. func hardReset (_ source: AnyObject)
  250. {
  251. terminal.getTerminal().resetToInitialState ()
  252. terminal.setNeedsDisplay(terminal.frame)
  253. }
  254. @objc @IBAction
  255. func toggleOptionAsMetaKey (_ source: AnyObject)
  256. {
  257. terminal.optionAsMetaKey.toggle ()
  258. }
  259. @objc @IBAction
  260. func biggerFont (_ source: AnyObject)
  261. {
  262. let size = terminal.font.pointSize
  263. guard size < 72 else {
  264. return
  265. }
  266. terminal.font = NSFont.monospacedSystemFont(ofSize: size+1, weight: .regular)
  267. }
  268. @objc @IBAction
  269. func smallerFont (_ source: AnyObject)
  270. {
  271. let size = terminal.font.pointSize
  272. guard size > 5 else {
  273. return
  274. }
  275. terminal.font = NSFont.monospacedSystemFont(ofSize: size-1, weight: .regular)
  276. }
  277. @objc @IBAction
  278. func defaultFontSize (_ source: AnyObject)
  279. {
  280. terminal.font = NSFont.monospacedSystemFont(ofSize: NSFont.systemFontSize, weight: .regular)
  281. }
  282. @objc @IBAction
  283. func addTab (_ source: AnyObject)
  284. {
  285. // if let win = view.window {
  286. // win.tabbingMode = .preferred
  287. // if let wc = win.windowController {
  288. // if let d = wc.document as? Document {
  289. // do {
  290. // let x = Document()
  291. // x.makeWindowControllers()
  292. //
  293. // try NSDocumentController.shared.newDocument(self)
  294. // } catch {}
  295. // print ("\(d.debugDescription)")
  296. // }
  297. // }
  298. // }
  299. // win.tabbingMode = .preferred
  300. // win.addTabbedWindow(win, ordered: .above)
  301. //
  302. // if let wc = win.windowController {
  303. // wc.newWindowForTab(self()
  304. // wc.showWindow(source)
  305. // }
  306. // }
  307. }
  308. func validateUserInterfaceItem(_ item: NSValidatedUserInterfaceItem) -> Bool
  309. {
  310. if item.action == #selector(debugToggleHostLogging(_:)) {
  311. if let m = item as? NSMenuItem {
  312. m.state = logging ? NSControl.StateValue.on : NSControl.StateValue.off
  313. }
  314. }
  315. if item.action == #selector(resizificator(_:)) {
  316. if let m = item as? NSMenuItem {
  317. m.state = resizificating == 1 ? NSControl.StateValue.on : NSControl.StateValue.off
  318. }
  319. }
  320. if item.action == #selector(resizificatorDown(_:)) {
  321. if let m = item as? NSMenuItem {
  322. m.state = resizificating == -1 ? NSControl.StateValue.on : NSControl.StateValue.off
  323. }
  324. }
  325. if item.action == #selector(allowMouseReporting(_:)) {
  326. if let m = item as? NSMenuItem {
  327. m.state = terminal.allowMouseReporting ? NSControl.StateValue.on : NSControl.StateValue.off
  328. }
  329. }
  330. if item.action == #selector(toggleOptionAsMetaKey(_:)) {
  331. if let m = item as? NSMenuItem {
  332. m.state = terminal.optionAsMetaKey ? NSControl.StateValue.on : NSControl.StateValue.off
  333. }
  334. }
  335. // Only enable "Export selection" if we have a selection
  336. if item.action == #selector(exportSelection(_:)) {
  337. return terminal.selectionActive
  338. }
  339. return true
  340. }
  341. @objc @IBAction
  342. func debugToggleHostLogging (_ source: AnyObject)
  343. {
  344. logging = !logging
  345. updateLogging()
  346. }
  347. }