diff --git a/App/Controllers/AppDelegate.swift b/App/Controllers/AppDelegate.swift index a23bfda..60611d3 100644 --- a/App/Controllers/AppDelegate.swift +++ b/App/Controllers/AppDelegate.swift @@ -42,17 +42,30 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } } + @objc func addWindow() { + // No windows exist. Make the first one. + if app.supportsMultipleScenes { + app.activateScene(userActivity: .terminalScene, asSingleton: false) + } + } + + @objc func newTab() { + // No windows exist. Pass through to addWindow(). + addWindow() + } + // MARK: - UISceneSession Lifecycle func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { - if let userActivity = options.userActivities.first { - if userActivity.activityType == SettingsSceneDelegate.activityType { - return UISceneConfiguration(name: "Settings", sessionRole: .windowApplication) - } else if userActivity.activityType == AboutSceneDelegate.activityType { - return UISceneConfiguration(name: "About", sessionRole: .windowApplication) - } + let userActivity = options.userActivities.first + switch userActivity?.activityType { + case SettingsSceneDelegate.activityType: + return UISceneConfiguration(name: "Settings", sessionRole: .windowApplication) + case AboutSceneDelegate.activityType: + return UISceneConfiguration(name: "About", sessionRole: .windowApplication) + default: + return UISceneConfiguration(name: "Terminal", sessionRole: .windowApplication) } - return UISceneConfiguration(name: "Terminal", sessionRole: .windowApplication) } // MARK: - Catalyst @@ -60,85 +73,104 @@ class AppDelegate: UIResponder, UIApplicationDelegate { override func buildMenu(with builder: UIMenuBuilder) { super.buildMenu(with: builder) - // Remove Edit menu text editing items - builder.remove(menu: .spelling) - builder.remove(menu: .substitutions) - builder.remove(menu: .transformations) - builder.remove(menu: .speech) - - // Remove Format menu - builder.remove(menu: .format) - - // Remove View menu toolbar items - builder.remove(menu: .toolbar) - - // Application menu - builder.insertSibling(UIMenu(options: .displayInline, + switch builder.system { + case .main: + // Remove Edit menu text editing items + builder.remove(menu: .spelling) + builder.remove(menu: .substitutions) + builder.remove(menu: .transformations) + builder.remove(menu: .speech) + + // Remove Format menu + builder.remove(menu: .format) + + // Remove View menu toolbar items + builder.remove(menu: .toolbar) + + // Application menu + builder.insertSibling(UIMenu(options: .displayInline, + children: [ + UIKeyCommand(title: .localize("SETTINGS_MAC", comment: "Title of Settings page on macOS (where Settings is usually named Preferences)."), + action: #selector(RootViewController.openSettings), + input: ",", + modifierFlags: .command) + ]), + afterMenu: .about) + builder.replace(menu: .about, + with: UIMenu(options: .displayInline, + children: [ + UICommand(title: .localize("ABOUT", comment: "Title of About page."), + action: #selector(self.openAbout)) + ])) + + // File menu + builder.replace(menu: .newScene, + with: UIMenu(options: .displayInline, + children: [ + UIKeyCommand(title: .localize("NEW_WINDOW", comment: "VoiceOver label for the new window button."), + action: #selector(RootViewController.addWindow), + input: "n", + modifierFlags: .command), + UIKeyCommand(title: .localize("NEW_TAB", comment: "VoiceOver label for the new tab button."), + action: #selector(RootViewController.newTab), + input: "t", + modifierFlags: .command) + ])) + + builder.replace(menu: .close, + with: UIMenu(options: .displayInline, + children: [ + // TODO: Disabling for now, needs research. + // Probably need to directly access the NSWindow to do this. +// UIKeyCommand(title: .localize("CLOSE_WINDOW", comment: "VoiceOver label for the close window button."), +// action: #selector(RootViewController.closeCurrentWindow), +// input: "w", +// modifierFlags: [ .command, .shift ]), + UIKeyCommand(title: .localize("CLOSE_TAB", comment: "VoiceOver label for the close tab button."), + action: #selector(RootViewController.removeCurrentTerminal), + input: "w", + modifierFlags: .command) + ])) + + builder.insertChild(UIMenu(options: .displayInline, children: [ - UIKeyCommand(title: .localize("SETTINGS_MAC", comment: "Title of Settings page on macOS (where Settings is usually named Preferences)."), - action: #selector(RootViewController.openSettings), - input: ",", + UIKeyCommand(title: .localize("SPLIT_HORIZONTALLY"), + action: #selector(RootViewController.splitHorizontally), + input: "d", + modifierFlags: [.command, .shift]), + UIKeyCommand(title: .localize("SPLIT_VERTICALLY"), + action: #selector(RootViewController.splitVertically), + input: "d", modifierFlags: .command) ]), - afterMenu: .about) - builder.replace(menu: .about, - with: UIMenu(options: .displayInline, - children: [ - UICommand(title: .localize("ABOUT", comment: "Title of About page."), - action: #selector(self.openAbout)) - ])) - - // File menu - builder.replace(menu: .newScene, - with: UIMenu(options: .displayInline, - children: [ - UIKeyCommand(title: .localize("NEW_WINDOW", comment: "VoiceOver label for the new window button."), - action: #selector(RootViewController.addWindow), - input: "n", - modifierFlags: .command), - UIKeyCommand(title: .localize("NEW_TAB", comment: "VoiceOver label for the new tab button."), - action: #selector(RootViewController.newTab), - input: "t", - modifierFlags: .command) - ])) - - builder.replace(menu: .close, - with: UIMenu(options: .displayInline, - children: [ - // TODO: Disabling for now, needs research. - // Probably need to directly access the NSWindow to do this. -// UIKeyCommand(title: .localize("CLOSE_WINDOW", comment: "VoiceOver label for the close window button."), -// action: #selector(RootViewController.closeCurrentWindow), -// input: "w", -// modifierFlags: [ .command, .shift ]), - UIKeyCommand(title: .localize("CLOSE_TAB", comment: "VoiceOver label for the close tab button."), - action: #selector(RootViewController.removeCurrentTerminal), - input: "w", - modifierFlags: .command) - ])) - - builder.insertChild(UIMenu(options: .displayInline, - children: [ - UIKeyCommand(title: .localize("SPLIT_HORIZONTALLY"), - action: #selector(RootViewController.splitHorizontally), - input: "d", - modifierFlags: [.command, .shift]), - UIKeyCommand(title: .localize("SPLIT_VERTICALLY"), - action: #selector(RootViewController.splitVertically), - input: "d", - modifierFlags: .command) - ]), - atEndOfMenu: .file) - - // Edit menu - builder.insertSibling(UIMenu(options: .displayInline, - children: [ - UIKeyCommand(title: .localize("CLEAR_TERMINAL", comment: "VoiceOver label for a button that clears the terminal."), - action: #selector(TerminalSessionViewController.clearTerminal), - input: "k", - modifierFlags: .command) - ]), - afterMenu: .standardEdit) + atEndOfMenu: .file) + + // Edit menu + builder.insertSibling(UIMenu(options: .displayInline, + children: [ + UIKeyCommand(title: .localize("CLEAR_TERMINAL", comment: "VoiceOver label for a button that clears the terminal."), + action: #selector(TerminalSessionViewController.clearTerminal), + input: "k", + modifierFlags: .command) + ]), + afterMenu: .standardEdit) + + case .context: + // Remove Speech menu + builder.remove(menu: .speech) + + // Add Clear Terminal + builder.insertSibling(UIMenu(options: .displayInline, + children: [ + UIKeyCommand(title: .localize("CLEAR_TERMINAL", comment: "VoiceOver label for a button that clears the terminal."), + action: #selector(TerminalSessionViewController.clearTerminal), + input: "k", + modifierFlags: .command) + ]), + afterMenu: .standardEdit) + + default: break + } } } diff --git a/App/Controllers/TerminalSceneDelegate.swift b/App/Controllers/TerminalSceneDelegate.swift index 1698dee..9adae57 100644 --- a/App/Controllers/TerminalSceneDelegate.swift +++ b/App/Controllers/TerminalSceneDelegate.swift @@ -9,7 +9,13 @@ import UIKit import NewTermCommon -class TerminalSceneDelegate: UIResponder, UIWindowSceneDelegate { +extension NSUserActivity { + static let terminalScene = NSUserActivity(activityType: TerminalSceneDelegate.activityType) +} + +class TerminalSceneDelegate: UIResponder, UIWindowSceneDelegate, IdentifiableSceneDelegate { + + static let activityType = "ws.hbang.Terminal.TerminalSceneActivity" var window: UIWindow? diff --git a/App/UI/Keyboard/TerminalKeyInput.swift b/App/UI/Keyboard/TerminalKeyInput.swift index a8db955..f9ef6ba 100644 --- a/App/UI/Keyboard/TerminalKeyInput.swift +++ b/App/UI/Keyboard/TerminalKeyInput.swift @@ -45,6 +45,27 @@ class TerminalKeyInput: TextInputBase { // Should be [UIKey], but I can’t use @available(iOS 13.4, *) on a property private var pressedKeys = [Any]() + private lazy var keyValues: [KeyboardButton: [UInt8]] = [ + metaKey: EscapeSequences.meta, + tabKey: EscapeSequences.tab, + upKey: EscapeSequences.up, + downKey: EscapeSequences.down, + leftKey: EscapeSequences.left, + rightKey: EscapeSequences.right, + moreToolbar.homeKey: EscapeSequences.home, + moreToolbar.endKey: EscapeSequences.end, + moreToolbar.pageUpKey: EscapeSequences.pageUp, + moreToolbar.pageDownKey: EscapeSequences.pageDown, + moreToolbar.deleteKey: EscapeSequences.delete, + ] + + private lazy var keyAppValues: [KeyboardButton: [UInt8]] = [ + upKey: EscapeSequences.upApp, + downKey: EscapeSequences.downApp, + leftKey: EscapeSequences.leftApp, + rightKey: EscapeSequences.rightApp + ] + override init(frame: CGRect) { super.init(frame: frame) @@ -212,33 +233,12 @@ class TerminalKeyInput: TextInputBase { return } - let keyValues: [KeyboardButton: Data] = [ - metaKey: EscapeSequences.meta, - tabKey: EscapeSequences.tab, - moreToolbar.homeKey: EscapeSequences.home, - moreToolbar.endKey: EscapeSequences.end, - moreToolbar.pageUpKey: EscapeSequences.pageUp, - moreToolbar.pageDownKey: EscapeSequences.pageDown, - moreToolbar.deleteKey: EscapeSequences.delete - ] if let data = keyValues[sender] { terminalInputDelegate!.receiveKeyboardInput(data: data) } } @objc private func arrowKeyPressed(_ sender: KeyboardButton) { - let keyValues: [KeyboardButton: Data] = [ - upKey: EscapeSequences.up, - downKey: EscapeSequences.down, - leftKey: EscapeSequences.left, - rightKey: EscapeSequences.right - ] - let keyAppValues: [KeyboardButton: Data] = [ - upKey: EscapeSequences.upApp, - downKey: EscapeSequences.downApp, - leftKey: EscapeSequences.leftApp, - rightKey: EscapeSequences.rightApp - ] let values = terminalInputDelegate!.applicationCursor ? keyAppValues : keyValues if let data = values[sender] { terminalInputDelegate!.receiveKeyboardInput(data: data) @@ -334,30 +334,15 @@ class TerminalKeyInput: TextInputBase { override func insertText(_ text: String) { // Used by the software keyboard only. See pressesBegan(_:with:) below for hardware keyboard. - let input = text.data(using: .utf8)! - var data = Data() - - for character in input { - var newCharacter = character - - if ctrlDown { - // Translate capital to lowercase - if character >= 0x41 && character <= 0x5A { // >= 'A' <= 'Z' - newCharacter += 0x61 - 0x41 // 'a' - 'A' - } - - // Convert to the matching control character - if character >= 0x61 && character <= 0x7A { // >= 'a' <= 'z' - newCharacter -= 0x61 - 1 // 'a' - 1 - } - } - + let data = text.utf8.map { character -> UInt8 in // Convert newline to carriage return if character == 0x0A { - newCharacter = 0x0D + return 0x0D } - - data.append(contentsOf: [ newCharacter ]) + if ctrlDown { + return EscapeSequences.asciiToControl(character) + } + return character } terminalInputDelegate!.receiveKeyboardInput(data: data) @@ -444,7 +429,8 @@ class TerminalKeyInput: TextInputBase { } override func paste(_ sender: Any?) { - if let data = UIPasteboard.general.string?.data(using: .utf8) { + if let string = UIPasteboard.general.string { + let data = [UInt8](string.utf8) terminalInputDelegate!.receiveKeyboardInput(data: data) } } @@ -459,7 +445,7 @@ class TerminalKeyInput: TextInputBase { return false } - var keyData: Data + var keyData: [UInt8] switch key.keyCode { case .keyboardReturnOrEnter: keyData = EscapeSequences.return case .keyboardEscape: keyData = EscapeSequences.meta @@ -499,20 +485,11 @@ class TerminalKeyInput: TextInputBase { case .keyboardPageUp: keyData = EscapeSequences.pageUp case .keyboardPageDown: keyData = EscapeSequences.pageDown - case .keyboardF1: keyData = EscapeSequences.fn[0] - case .keyboardF2: keyData = EscapeSequences.fn[1] - case .keyboardF3: keyData = EscapeSequences.fn[2] - case .keyboardF4: keyData = EscapeSequences.fn[3] - case .keyboardF5: keyData = EscapeSequences.fn[4] - case .keyboardF6: keyData = EscapeSequences.fn[5] - case .keyboardF7: keyData = EscapeSequences.fn[6] - case .keyboardF8: keyData = EscapeSequences.fn[7] - case .keyboardF9: keyData = EscapeSequences.fn[8] - case .keyboardF10: keyData = EscapeSequences.fn[9] - case .keyboardF11: keyData = EscapeSequences.fn[10] - case .keyboardF12: keyData = EscapeSequences.fn[11] + case .keyboardF1, .keyboardF2, .keyboardF3, .keyboardF4, .keyboardF5, .keyboardF6, .keyboardF7, + .keyboardF8, .keyboardF9, .keyboardF10, .keyboardF11, .keyboardF12: + keyData = EscapeSequences.fn[key.keyCode.rawValue - UIKeyboardHIDUsage.keyboardF1.rawValue] - default: keyData = key.characters.data(using: .utf8) ?? Data() + default: keyData = [UInt8](key.characters.utf8) } // If we didn’t get anything to type, nothing else to do here. @@ -522,14 +499,14 @@ class TerminalKeyInput: TextInputBase { // Translate ctrl key sequences to the approriate escape. if key.modifierFlags.contains(.control) { - keyData = Data(keyData.map { character in EscapeSequences.asciiToControl(character) }) + keyData = keyData.map { character in EscapeSequences.asciiToControl(character) } } // Prepend esc before each byte if meta key is down. if key.modifierFlags.contains(.alternate) { - keyData = Data(keyData.reduce([], { result, character in + keyData = keyData.reduce([], { result, character in return result + EscapeSequences.meta + [ character ] - })) + }) } terminalInputDelegate?.receiveKeyboardInput(data: keyData) @@ -615,7 +592,8 @@ class TerminalKeyInput: TextInputBase { #if targetEnvironment(macCatalyst) let keyRepeat = (UserDefaults.standard.object(forKey: "KeyRepeat") as? TimeInterval ?? 8) * 0.012 #else - let keyRepeat = UserDefaults(suiteName: "com.apple.Accessibility")?.object(forKey: "KeyRepeatInterval") as? TimeInterval ?? 0.1 + let keyRepeat = UserDefaults(suiteName: "com.apple.Accessibility")? + .object(forKey: "KeyRepeatInterval") as? TimeInterval ?? 0.1 #endif hardwareRepeatTimer = Timer.scheduledTimer(timeInterval: keyRepeat, target: self, @@ -634,9 +612,8 @@ extension TerminalKeyInput: TerminalPasswordInputViewDelegate { // User could have typed on the keyboard while it was in password mode, rather than using the // password autofill. Send a return if it seems like a password was actually received, // otherwise just pretend it was typed like normal. - if password.count > 2, - let data = password.data(using: .utf8) { - terminalInputDelegate!.receiveKeyboardInput(data: data) + if password.count > 2 { + terminalInputDelegate!.receiveKeyboardInput(data: [UInt8](password.utf8)) terminalInputDelegate!.receiveKeyboardInput(data: EscapeSequences.return) } else { insertText(password) diff --git a/Common/Controllers/SubProcess.swift b/Common/Controllers/SubProcess.swift index 2af9423..469ac75 100644 --- a/Common/Controllers/SubProcess.swift +++ b/Common/Controllers/SubProcess.swift @@ -16,27 +16,29 @@ enum SubProcessIllegalStateError: Error { } enum SubProcessIOError: Error { - case readFailed, writeFailed + case readFailed(errno: errno_t?) + case writeFailed(errno: errno_t?) } protocol SubProcessDelegate: AnyObject { - func subProcessDidConnect() - func subProcess(didReceiveData data: Data) + func subProcess(didReceiveData data: [UInt8]) func subProcess(didDisconnectWithError error: Error?) func subProcess(didReceiveError error: Error) - } -class SubProcess: NSObject { +class SubProcess { weak var delegate: SubProcessDelegate? private var childPID: pid_t? private var fileDescriptor: Int32? - private var fileHandle: FileHandle? - var screenSize: ScreenSize = ScreenSize(cols: 80, rows: 25) { + private let queue = DispatchQueue(label: "ws.hbang.Terminal.io-queue") + private var readSource: DispatchSourceRead? + private var signalSource: DispatchSourceProcess? + + var screenSize = ScreenSize.default { didSet { updateWindowSize() } } @@ -74,25 +76,25 @@ class SubProcess: NSObject { // When opening a new tab, we can switch straight to the previous tab’s working directory. chdir(NSHomeDirectory()) -#if targetEnvironment(simulator) + #if targetEnvironment(simulator) let path = "/bin/bash" - let args = ([ "bash", "--login", "-i" ] as NSArray).cStringArray()! -#else + let args = ["bash", "--login", "-i"].cStringArray + #else let path = "/usr/bin/login" - let args = ([ "login", "-fpl\(hushLogin ? "q" : "")", NSUserName() ] as NSArray).cStringArray()! -#endif + let args = ["login", "-fpl\(hushLogin ? "q" : "")", NSUserName()].cStringArray + #endif - let env = ([ + let env = [ "TERM=xterm-256color", "COLORTERM=truecolor", "LANG=\(localeCode)", "TERM_PROGRAM=NewTerm", "LC_TERMINAL=NewTerm" - ] as NSArray).cStringArray()! + ].cStringArray defer { - free(args) - free(env) + args.deallocate() + env.deallocate() } if execve(path, args, env) == -1 { @@ -107,11 +109,17 @@ class SubProcess: NSObject { os_log("Process forked: %d", type: .debug, pid) childPID = pid - fileHandle = FileHandle(fileDescriptor: fileDescriptor!, closeOnDealloc: true) - fileHandle!.readabilityHandler = { [weak self] fileHandle in - self?.didReceiveData(fileHandle.availableData) + readSource = DispatchSource.makeReadSource(fileDescriptor: fileDescriptor!, queue: queue) + readSource?.setEventHandler { [weak self] in + self?.handleRead() + } + signalSource = DispatchSource.makeProcessSource(identifier: pid, eventMask: .signal, queue: queue) + signalSource?.setEventHandler { + os_log("received signal") } + readSource?.activate() + signalSource?.activate() delegate!.subProcessDidConnect() break } @@ -129,7 +137,10 @@ class SubProcess: NSObject { self.childPID = nil fileDescriptor = nil - fileHandle = nil + readSource?.cancel() + readSource = nil + signalSource?.cancel() + signalSource = nil if !fromError { // nil error means disconnected due to user request @@ -139,8 +150,51 @@ class SubProcess: NSObject { } } - func write(data: Data) { - fileHandle?.write(data) + private func handleRead() { + guard let fileDescriptor = fileDescriptor else { + return + } + + let buffer = UnsafeMutableRawPointer.allocate(byteCount: Int(BUFSIZ), alignment: MemoryLayout.alignment) + let bytesRead = read(fileDescriptor, buffer, Int(BUFSIZ)) + switch bytesRead { + case -1: + let code = errno + switch code { + case EAGAIN, EINTR: + // Ignore, we’ll be called again when the source is ready. + break + + default: + // Something is wrong. + DispatchQueue.main.async { + self.delegate?.subProcess(didDisconnectWithError: SubProcessIOError.readFailed(errno: code)) + } + } + + case 0: + // Zero-length data is an indicator of EOF. This can happen if the user exits the terminal by + // typing `exit` or ^D, or if there’s a catastrophic failure (e.g. /bin/login is broken). + try? stop(fromError: false) + + default: + // Read from output and notify delegate. + let bytes = buffer.bindMemory(to: UInt8.self, capacity: bytesRead) + let data = Array(UnsafeBufferPointer(start: bytes, count: bytesRead)) + delegate?.subProcess(didReceiveData: data) + } + buffer.deallocate() + } + + func write(data: [UInt8]) { + queue.async { + guard let fileDescriptor = self.fileDescriptor else { + return + } + _ = data.withUnsafeBytes { (buffer: UnsafeRawBufferPointer) in + Darwin.write(fileDescriptor, buffer.baseAddress!, buffer.count) + } + } } private var localeCode: String { @@ -170,25 +224,8 @@ class SubProcess: NSObject { return "en_US.UTF-8" } - private func didReceiveData(_ data: Data) { - if data.isEmpty { - // Zero-length data is an indicator of EOF. This can happen if the user exits the terminal by - // typing `exit` or ^D, or if there’s a catastrophic failure (e.g. /bin/login is broken). - try? self.stop(fromError: true) - } - - DispatchQueue.main.async { - // Forward to the delegate. - if data.isEmpty { - self.delegate?.subProcess(didDisconnectWithError: SubProcessIOError.readFailed) - } else { - self.delegate?.subProcess(didReceiveData: data) - } - } - } - private func updateWindowSize() { - if fileDescriptor == nil { + guard let fileDescriptor = fileDescriptor else { return } @@ -196,7 +233,7 @@ class SubProcess: NSObject { windowSize.ws_col = UInt16(screenSize.cols) windowSize.ws_row = UInt16(screenSize.rows) - if ioctl(fileDescriptor!, TIOCSWINSZ, &windowSize) == -1 { + if ioctl(fileDescriptor, TIOCSWINSZ, &windowSize) == -1 { os_log("Setting screen size failed: %{public errno}d", type: .error, errno) } } @@ -206,8 +243,7 @@ class SubProcess: NSObject { os_log("Illegal state - SubProcess deallocated while still running", type: .error) } - childPID = nil - fileHandle = nil + try? stop(fromError: true) } } diff --git a/Common/Controllers/TerminalController.swift b/Common/Controllers/TerminalController.swift index b31f4be..8c5b49a 100644 --- a/Common/Controllers/TerminalController.swift +++ b/Common/Controllers/TerminalController.swift @@ -50,7 +50,7 @@ public class TerminalController { private var updateTimer: CADisplayLink? private var refreshRate: TimeInterval = 60 private var isVisible = true - private var readBuffer = Data() + private var readBuffer = [UInt8]() internal var terminalQueue = DispatchQueue(label: "ws.hbang.Terminal.terminal-queue") @@ -195,37 +195,48 @@ public class TerminalController { try subProcess!.stop() } - internal func write(_ data: Data) { - subProcess?.write(data: data) + // MARK: - Terminal + + public func readInputStream(_ data: [UInt8]) { + terminalQueue.async { + self.readBuffer += data + } } - // MARK: - Terminal + private func readInputStream(_ data: Data) { + readInputStream([UInt8](data)) + } - public func readInputStream(_ data: Data) { - readBuffer.append(data) + public func write(_ data: [UInt8]) { + subProcess?.write(data: data) + } + + public func write(_ data: Data) { + write([UInt8](data)) } @objc private func updateTimerFired() { - if !readBuffer.isEmpty { - let bytes = Array(readBuffer) - terminalQueue.async { - self.terminal?.feed(byteArray: bytes) + terminalQueue.async { + if !self.readBuffer.isEmpty { + self.terminal?.feed(byteArray: self.readBuffer) + self.readBuffer.removeAll() } - readBuffer.removeAll() - } - guard let cursorLocation = terminal?.getCursorLocation() else { - return - } - if terminal?.getUpdateRange() == nil && cursorLocation == lastCursorLocation { - return - } - terminal?.clearUpdateRange() - lastCursorLocation = cursorLocation + guard let cursorLocation = self.terminal?.getCursorLocation() else { + return + } + if self.terminal?.getUpdateRange() == nil && cursorLocation == self.lastCursorLocation { + return + } + self.terminal?.clearUpdateRange() + self.lastCursorLocation = cursorLocation - // TODO: We should handle the scrollback separately so it only appears if the user scrolls - delegate?.refresh(attributedString: stringSupplier.attributedString(), - backgroundColor: stringSupplier.colorMap!.background) + DispatchQueue.main.async { + // TODO: We should handle the scrollback separately so it only appears if the user scrolls + self.delegate?.refresh(attributedString: self.stringSupplier.attributedString(), + backgroundColor: self.stringSupplier.colorMap!.background) + } + } } public func clearTerminal() { @@ -282,9 +293,8 @@ extension TerminalController: TerminalDelegate { public func isProcessTrusted(source: Terminal) -> Bool { isLocalhost } public func send(source: Terminal, data: ArraySlice) { - let actualData = Data(data) - DispatchQueue.main.async { - self.write(actualData) + terminalQueue.async { + self.write([UInt8](data)) } } @@ -345,7 +355,7 @@ extension TerminalController: TerminalInputProtocol { public var applicationCursor: Bool { terminal?.applicationCursor ?? false } - public func receiveKeyboardInput(data: Data) { + public func receiveKeyboardInput(data: [UInt8]) { // Forward the data from the keyboard directly to the subprocess subProcess!.write(data: data) } @@ -358,36 +368,24 @@ extension TerminalController: SubProcessDelegate { // Yay } - func subProcess(didReceiveData data: Data) { + func subProcess(didReceiveData data: [UInt8]) { // Simply forward the input stream down the VT100 processor. When it notices changes to the // screen, it should invoke our refresh delegate below. readInputStream(data) } func subProcess(didDisconnectWithError error: Error?) { - if error == nil { - // Graceful termination - return - } - - if let ioError = error as? SubProcessIOError { - switch ioError { - case .readFailed: - // This can be the user just typing an EOF (^D) to end the terminal session. However, it - // can also happen because the process crashed for some reason. If it seems like the shell - // exited gracefully, just close the tab. - if (processLaunchDate ?? Date()) < Date(timeIntervalSinceNow: -3) { - delegate?.close() - } - break - - case .writeFailed: - break + if let error = error { + delegate?.didReceiveError(error: error) + } else { + // This can be the user just typing an EOF (^D) to end the terminal session. However, it + // can also happen because the process crashed for some reason. If it seems like the shell + // exited gracefully, just close the tab. + if (processLaunchDate ?? Date()) < Date(timeIntervalSinceNow: -3) { + delegate?.close() } } - delegate?.didReceiveError(error: error!) - // Write the termination message to the terminal. let processCompleted = String.localize("PROCESS_COMPLETED_TITLE", comment: "Title displayed when the terminal’s process has ended.") let cols = Int(subProcess?.screenSize.cols ?? 0) diff --git a/Common/Extensions/Array+Additions.swift b/Common/Extensions/Array+Additions.swift new file mode 100644 index 0000000..ee3ebe5 --- /dev/null +++ b/Common/Extensions/Array+Additions.swift @@ -0,0 +1,22 @@ +// +// Array+Additions.swift +// NewTerm Common +// +// Created by Adam Demasi on 22/3/2022. +// + +import Foundation + +extension Array where Element == String { + var cStringArray: [UnsafeMutablePointer?] { + map { item in item.cString } + [nil] + } +} + +extension Array where Element == Optional> { + func deallocate() { + for item in self { + item?.deallocate() + } + } +} diff --git a/Common/Extensions/NSArray+Additions.h b/Common/Extensions/NSArray+Additions.h deleted file mode 100644 index 8eacf81..0000000 --- a/Common/Extensions/NSArray+Additions.h +++ /dev/null @@ -1,15 +0,0 @@ -// -// NSArray+Additions.h -// NewTerm -// -// Created by Adam Demasi on 23/3/18. -// Copyright © 2018 HASHBANG Productions. All rights reserved. -// - -@import Foundation; - -@interface NSArray (Additions) - -- (char **)cStringArray; - -@end diff --git a/Common/Extensions/NSArray+Additions.m b/Common/Extensions/NSArray+Additions.m deleted file mode 100644 index d23e190..0000000 --- a/Common/Extensions/NSArray+Additions.m +++ /dev/null @@ -1,27 +0,0 @@ -// -// NSArray+Additions.m -// NewTerm -// -// Created by Adam Demasi on 23/3/18. -// Copyright © 2018 HASHBANG Productions. All rights reserved. -// - -#import "NSArray+Additions.h" - -@implementation NSArray (Additions) - -- (char **)cStringArray { - // This is in objc because it’s impossibly complex to do this in Swift… - NSUInteger count = self.count + 1; - char **result = malloc(sizeof(char *) * count); - - for (NSUInteger i = 0; i < self.count; i++) { - NSString *item = [self[i] isKindOfClass:NSString.class] ? self[i] : ((NSObject *)self[i]).description; - result[i] = (char *)item.UTF8String; - } - - result[count - 1] = NULL; - return result; -} - -@end diff --git a/Common/Extensions/String+Additions.swift b/Common/Extensions/String+Additions.swift new file mode 100644 index 0000000..a063ba3 --- /dev/null +++ b/Common/Extensions/String+Additions.swift @@ -0,0 +1,14 @@ +// +// String+Additions.swift +// NewTerm Common +// +// Created by Adam Demasi on 22/3/2022. +// + +import Foundation + +extension String { + var cString: UnsafeMutablePointer? { + strdup(self) + } +} diff --git a/Common/Supporting Files/NewTermCommon.h b/Common/Supporting Files/NewTermCommon.h index cb696cc..22b7061 100644 --- a/Common/Supporting Files/NewTermCommon.h +++ b/Common/Supporting Files/NewTermCommon.h @@ -5,6 +5,5 @@ // Created by Adam Demasi on 20/6/19. // -#import "NSArray+Additions.h" #import "CrossPlatformUI.h" #import "CompactConstraint.h" diff --git a/Common/Supporting Files/PrefixHeader.pch b/Common/Supporting Files/PrefixHeader.pch index 5f4c10e..d57277c 100644 --- a/Common/Supporting Files/PrefixHeader.pch +++ b/Common/Supporting Files/PrefixHeader.pch @@ -6,4 +6,3 @@ // #import -#import diff --git a/Common/VT100/TerminalConstants.swift b/Common/VT100/TerminalConstants.swift index 394ea0f..86e7ef6 100644 --- a/Common/VT100/TerminalConstants.swift +++ b/Common/VT100/TerminalConstants.swift @@ -23,58 +23,55 @@ public struct EscapeSequences { // https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-PC-Style-Function-Keys - public static let backspace = Data([ 0x7F ]) // \x7F - public static let meta = Data([ 0x1B ]) // \e - public static let tab = Data([ 0x09 ]) // \t - public static let `return` = Data([ 0x0D ]) // \r + public static let backspace: [UInt8] = [0x7F] // \x7F + public static let meta: [UInt8] = [0x1B] // \e + public static let tab: [UInt8] = [0x09] // \t + public static let `return`: [UInt8] = [0x0D] // \r - public static let up = Data([ 0x1B, 0x5B, 0x41 ]) // \e[A - public static let upApp = Data([ 0x1B, 0x4F, 0x41 ]) // \eOA - public static let down = Data([ 0x1B, 0x5B, 0x42 ]) // \e[B - public static let downApp = Data([ 0x1B, 0x4F, 0x42 ]) // \eOB - public static let left = Data([ 0x1B, 0x5B, 0x44 ]) // \e[D - public static let leftApp = Data([ 0x1B, 0x4F, 0x44 ]) // \eOD - public static let leftMeta = Data([ 0x62 ]) // \eb (removed \e) - public static let right = Data([ 0x1B, 0x5B, 0x43 ]) // \e[C - public static let rightApp = Data([ 0x1B, 0x4F, 0x43 ]) // \eOC - public static let rightMeta = Data([ 0x66 ]) // \ef (removed \e) + public static let up: [UInt8] = [0x1B, 0x5B, 0x41] // \e[A + public static let upApp: [UInt8] = [0x1B, 0x4F, 0x41] // \eOA + public static let down: [UInt8] = [0x1B, 0x5B, 0x42] // \e[B + public static let downApp: [UInt8] = [0x1B, 0x4F, 0x42] // \eOB + public static let left: [UInt8] = [0x1B, 0x5B, 0x44] // \e[D + public static let leftApp: [UInt8] = [0x1B, 0x4F, 0x44] // \eOD + public static let leftMeta: [UInt8] = [0x62] // \eb (removed \e) + public static let right: [UInt8] = [0x1B, 0x5B, 0x43] // \e[C + public static let rightApp: [UInt8] = [0x1B, 0x4F, 0x43] // \eOC + public static let rightMeta: [UInt8] = [0x66] // \ef (removed \e) - public static let home = Data([ 0x1B, 0x5B, 0x48 ]) // \e[H - public static let homeApp = Data([ 0x1B, 0x4F, 0x48 ]) // \eOH - public static let end = Data([ 0x1B, 0x5B, 0x46 ]) // \e[F - public static let endApp = Data([ 0x1B, 0x4F, 0x46 ]) // \eOF - public static let pageUp = Data([ 0x1B, 0x5B, 0x35, 0x7E ]) // \e[5~ - public static let pageDown = Data([ 0x1B, 0x5B, 0x36, 0x7E ]) // \e[6~ - public static let delete = Data([ 0x1B, 0x5B, 0x33, 0x7E ]) // \e[3~ + public static let home: [UInt8] = [0x1B, 0x5B, 0x48] // \e[H + public static let homeApp: [UInt8] = [0x1B, 0x4F, 0x48] // \eOH + public static let end: [UInt8] = [0x1B, 0x5B, 0x46] // \e[F + public static let endApp: [UInt8] = [0x1B, 0x4F, 0x46] // \eOF + public static let pageUp: [UInt8] = [0x1B, 0x5B, 0x35, 0x7E] // \e[5~ + public static let pageDown: [UInt8] = [0x1B, 0x5B, 0x36, 0x7E] // \e[6~ + public static let delete: [UInt8] = [0x1B, 0x5B, 0x33, 0x7E] // \e[3~ - public static let fn = [ - Data([ 0x1B, 0x4F, 0x50 ]), // \eOP - Data([ 0x1B, 0x4F, 0x51 ]), // \eOQ - Data([ 0x1B, 0x4F, 0x52 ]), // \eOR - Data([ 0x1B, 0x4F, 0x53 ]), // \eOS - Data([ 0x1B, 0x5B, 0x31, 0x35, 0x7E ]), // \e[15~ - Data([ 0x1B, 0x5B, 0x31, 0x37, 0x7E ]), // \e[17~ - Data([ 0x1B, 0x5B, 0x31, 0x38, 0x7E ]), // \e[18~ - Data([ 0x1B, 0x5B, 0x31, 0x39, 0x7E ]), // \e[19~ - Data([ 0x1B, 0x5B, 0x32, 0x30, 0x7E ]), // \e[20~ - Data([ 0x1B, 0x5B, 0x32, 0x31, 0x7E ]), // \e[21~ - Data([ 0x1B, 0x5B, 0x32, 0x33, 0x7E ]), // \e[23~ - Data([ 0x1B, 0x5B, 0x32, 0x34, 0x7E ]), // \e[24~ + public static let fn: [[UInt8]] = [ + [0x1B, 0x4F, 0x50], // \eOP + [0x1B, 0x4F, 0x51], // \eOQ + [0x1B, 0x4F, 0x52], // \eOR + [0x1B, 0x4F, 0x53], // \eOS + [0x1B, 0x5B, 0x31, 0x35, 0x7E], // \e[15~ + [0x1B, 0x5B, 0x31, 0x37, 0x7E], // \e[17~ + [0x1B, 0x5B, 0x31, 0x38, 0x7E], // \e[18~ + [0x1B, 0x5B, 0x31, 0x39, 0x7E], // \e[19~ + [0x1B, 0x5B, 0x32, 0x30, 0x7E], // \e[20~ + [0x1B, 0x5B, 0x32, 0x31, 0x7E], // \e[21~ + [0x1B, 0x5B, 0x32, 0x33, 0x7E], // \e[23~ + [0x1B, 0x5B, 0x32, 0x34, 0x7E], // \e[24~ ] public static func asciiToControl(_ character: UInt8) -> UInt8 { var newCharacter = character - // Translate capital to lowercase if character >= 0x41 && character <= 0x5A { // >= 'A' <= 'Z' newCharacter += 0x61 - 0x41 // 'a' - 'A' } - // Convert to the matching control character if character >= 0x61 && character <= 0x7A { // >= 'a' <= 'z' newCharacter -= 0x61 - 1 // 'a' - 1 } - return newCharacter } diff --git a/Common/VT100/TerminalInputProtocol.swift b/Common/VT100/TerminalInputProtocol.swift index 5d72288..0c63c31 100644 --- a/Common/VT100/TerminalInputProtocol.swift +++ b/Common/VT100/TerminalInputProtocol.swift @@ -9,7 +9,7 @@ import Foundation public protocol TerminalInputProtocol: AnyObject { - func receiveKeyboardInput(data: Data) + func receiveKeyboardInput(data: [UInt8]) var applicationCursor: Bool { get } diff --git a/NewTerm.xcodeproj/project.pbxproj b/NewTerm.xcodeproj/project.pbxproj index 86c587a..d05e7bd 100644 --- a/NewTerm.xcodeproj/project.pbxproj +++ b/NewTerm.xcodeproj/project.pbxproj @@ -19,6 +19,8 @@ 4E3237A426FB530C00AEDA06 /* UIColorAdditions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E3237A326FB530C00AEDA06 /* UIColorAdditions.swift */; }; 4E3237FB26FC5FC300AEDA06 /* ColorBars.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E3237FA26FC5FC300AEDA06 /* ColorBars.swift */; }; 4E3237FE26FEB0D800AEDA06 /* UIDevice+Additions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E3237FC26FEB0D600AEDA06 /* UIDevice+Additions.swift */; }; + 4E3441D327E96EA60022C957 /* String+Additions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E3441D227E96EA60022C957 /* String+Additions.swift */; }; + 4E3441D527E96ED30022C957 /* Array+Additions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E3441D427E96ED30022C957 /* Array+Additions.swift */; }; 4E460121261EC8C0004DBCC2 /* UpdateCheckManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E460120261EC8C0004DBCC2 /* UpdateCheckManager.swift */; }; 4E460129261FF23F004DBCC2 /* SettingsSceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E460128261FF23F004DBCC2 /* SettingsSceneDelegate.swift */; }; 4E479710262D44D8003CF48C /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 4E479713262D44D8003CF48C /* Localizable.stringsdict */; }; @@ -58,11 +60,9 @@ CF71BC9922BB6E0300AFB1E1 /* TerminalController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFBB96C522B5481B00585BE6 /* TerminalController.swift */; }; CF71BC9A22BB6E0300AFB1E1 /* CrossPlatformUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF71BC6022BB691700AFB1E1 /* CrossPlatformUI.swift */; }; CF71BC9B22BB6E0300AFB1E1 /* TerminalInputProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF71BC6222BB69EF00AFB1E1 /* TerminalInputProtocol.swift */; }; - CF71BC9C22BB6E1100AFB1E1 /* NSArray+Additions.m in Sources */ = {isa = PBXBuildFile; fileRef = CFBB969422B5481B00585BE6 /* NSArray+Additions.m */; }; CF71BCA822BB6E1100AFB1E1 /* NSLayoutConstraint+CompactConstraint.m in Sources */ = {isa = PBXBuildFile; fileRef = CFBB96B922B5481B00585BE6 /* NSLayoutConstraint+CompactConstraint.m */; }; CF71BCA922BB6E1100AFB1E1 /* UIView+CompactConstraint.m in Sources */ = {isa = PBXBuildFile; fileRef = CFBB96B722B5481B00585BE6 /* UIView+CompactConstraint.m */; }; CF71BCC322BB727B00AFB1E1 /* libcurses.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = CF71BCC222BB727B00AFB1E1 /* libcurses.tbd */; }; - CF71BCC622BB737200AFB1E1 /* NSArray+Additions.h in Headers */ = {isa = PBXBuildFile; fileRef = CFBB969322B5481B00585BE6 /* NSArray+Additions.h */; settings = {ATTRIBUTES = (Public, ); }; }; CF71BCC822BB737200AFB1E1 /* CrossPlatformUI.h in Headers */ = {isa = PBXBuildFile; fileRef = CF71BC6422BB6A2B00AFB1E1 /* CrossPlatformUI.h */; settings = {ATTRIBUTES = (Public, ); }; }; CF71BCD622BB737200AFB1E1 /* CompactConstraint.h in Headers */ = {isa = PBXBuildFile; fileRef = CFBB96BC22B5481B00585BE6 /* CompactConstraint.h */; settings = {ATTRIBUTES = (Public, ); }; }; CF71BCD722BB737200AFB1E1 /* NSLayoutConstraint+CompactConstraint.h in Headers */ = {isa = PBXBuildFile; fileRef = CFBB96BB22B5481B00585BE6 /* NSLayoutConstraint+CompactConstraint.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -132,6 +132,8 @@ 4E3237A326FB530C00AEDA06 /* UIColorAdditions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIColorAdditions.swift; sourceTree = ""; }; 4E3237FA26FC5FC300AEDA06 /* ColorBars.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorBars.swift; sourceTree = ""; }; 4E3237FC26FEB0D600AEDA06 /* UIDevice+Additions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIDevice+Additions.swift"; sourceTree = ""; }; + 4E3441D227E96EA60022C957 /* String+Additions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Additions.swift"; sourceTree = ""; }; + 4E3441D427E96ED30022C957 /* Array+Additions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Additions.swift"; sourceTree = ""; }; 4E460120261EC8C0004DBCC2 /* UpdateCheckManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateCheckManager.swift; sourceTree = ""; }; 4E460128261FF23F004DBCC2 /* SettingsSceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSceneDelegate.swift; sourceTree = ""; }; 4E479712262D44D8003CF48C /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Localizable.stringsdict; sourceTree = ""; }; @@ -186,8 +188,6 @@ CFBB968B22B5481B00585BE6 /* KeyboardButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyboardButton.swift; sourceTree = ""; }; CFBB968D22B5481B00585BE6 /* Global.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Global.swift; sourceTree = ""; }; CFBB968E22B5481B00585BE6 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = ""; }; - CFBB969322B5481B00585BE6 /* NSArray+Additions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSArray+Additions.h"; sourceTree = ""; }; - CFBB969422B5481B00585BE6 /* NSArray+Additions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSArray+Additions.m"; sourceTree = ""; }; CFBB969622B5481B00585BE6 /* RootViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RootViewController.swift; sourceTree = ""; }; CFBB969722B5481B00585BE6 /* TerminalSessionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TerminalSessionViewController.swift; sourceTree = ""; }; CFBB96B722B5481B00585BE6 /* UIView+CompactConstraint.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIView+CompactConstraint.m"; sourceTree = ""; }; @@ -429,9 +429,9 @@ CFBB969222B5481B00585BE6 /* Extensions */ = { isa = PBXGroup; children = ( - CFBB969322B5481B00585BE6 /* NSArray+Additions.h */, - CFBB969422B5481B00585BE6 /* NSArray+Additions.m */, 4E9FB97126172079005AFCC8 /* Color+Additions.swift */, + 4E3441D227E96EA60022C957 /* String+Additions.swift */, + 4E3441D427E96ED30022C957 /* Array+Additions.swift */, 4E98081926184A2500E41883 /* String+Localization.swift */, 4E3237A326FB530C00AEDA06 /* UIColorAdditions.swift */, ); @@ -548,7 +548,6 @@ buildActionMask = 2147483647; files = ( CF71BCDC22BB737700AFB1E1 /* NewTermCommon.h in Headers */, - CF71BCC622BB737200AFB1E1 /* NSArray+Additions.h in Headers */, CF71BCC822BB737200AFB1E1 /* CrossPlatformUI.h in Headers */, CFD553912345E315005805D1 /* ncurses.h in Headers */, CFD553902345E315005805D1 /* ncurses_dll.h in Headers */, @@ -722,12 +721,12 @@ buildActionMask = 2147483647; files = ( 4E980826261873CE00E41883 /* AppFont.swift in Sources */, - CF71BC9C22BB6E1100AFB1E1 /* NSArray+Additions.m in Sources */, 4E2492D026230E59002C47CB /* TerminalController+ITermExtensions.swift in Sources */, 4E980833261ACA0400E41883 /* AppTheme.swift in Sources */, 4E9FB97226172079005AFCC8 /* Color+Additions.swift in Sources */, CF71BCDE22BB73C200AFB1E1 /* Global.swift in Sources */, CF71BCA822BB6E1100AFB1E1 /* NSLayoutConstraint+CompactConstraint.m in Sources */, + 4E3441D527E96ED30022C957 /* Array+Additions.swift in Sources */, 4E3237FB26FC5FC300AEDA06 /* ColorBars.swift in Sources */, CF71BCA922BB6E1100AFB1E1 /* UIView+CompactConstraint.m in Sources */, 4E3237A426FB530C00AEDA06 /* UIColorAdditions.swift in Sources */, @@ -735,6 +734,7 @@ CF71BC9722BB6E0300AFB1E1 /* Preferences.swift in Sources */, CF71BC9822BB6E0300AFB1E1 /* SubProcess.swift in Sources */, 4E9FB97526172293005AFCC8 /* StringSupplier.swift in Sources */, + 4E3441D327E96EA60022C957 /* String+Additions.swift in Sources */, CF71BC9922BB6E0300AFB1E1 /* TerminalController.swift in Sources */, 4E9FB97D26174A78005AFCC8 /* TerminalConstants.swift in Sources */, 4E9FB978261724FA005AFCC8 /* ColorMap.swift in Sources */, diff --git a/NewTerm.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/NewTerm.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 0dfa5cd..9c20738 100644 --- a/NewTerm.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/NewTerm.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -6,7 +6,7 @@ "location" : "https://github.com/migueldeicaza/SwiftTerm.git", "state" : { "branch" : "main", - "revision" : "8094937a8b9509d5ccfe43082dbc52b0d6b45e96" + "revision" : "d661230c695e343fec580b531ac71e8db300594f" } } ],