├── Example ├── Cartfile ├── Cartfile.resolved ├── TerminalExample │ ├── Assets.xcassets │ │ ├── Contents.json │ │ └── AppIcon.appiconset │ │ │ ├── 100.png │ │ │ ├── 114.png │ │ │ ├── 120.png │ │ │ ├── 128.png │ │ │ ├── 144.png │ │ │ ├── 152.png │ │ │ ├── 16.png │ │ │ ├── 167.png │ │ │ ├── 172.png │ │ │ ├── 180.png │ │ │ ├── 196.png │ │ │ ├── 20.png │ │ │ ├── 216.png │ │ │ ├── 256.png │ │ │ ├── 29.png │ │ │ ├── 32.png │ │ │ ├── 40.png │ │ │ ├── 48.png │ │ │ ├── 50.png │ │ │ ├── 512.png │ │ │ ├── 55.png │ │ │ ├── 57.png │ │ │ ├── 58.png │ │ │ ├── 60.png │ │ │ ├── 64.png │ │ │ ├── 72.png │ │ │ ├── 76.png │ │ │ ├── 80.png │ │ │ ├── 87.png │ │ │ ├── 88.png │ │ │ ├── 1024.png │ │ │ └── Contents.json │ ├── AppDelegate.swift │ ├── Util │ │ ├── Number+fallback.swift │ │ └── KeyboardPicker.swift │ ├── KeyboardNotification │ │ ├── KeyboardNotificationObservable.swift │ │ ├── Notification+Keyboard.swift │ │ └── KeyboardLayoutGuide.swift │ ├── Info.plist │ ├── Base.lproj │ │ └── LaunchScreen.storyboard │ ├── InputAccessoryView │ │ ├── NibCreatable.swift │ │ ├── InputAccessoryView.swift │ │ └── InputAccessoryView.xib │ └── ViewController │ │ ├── TerminalViewController.swift │ │ ├── SSHTextViewController.swift │ │ └── SSHTerminalViewController.swift ├── bin │ └── carthage.sh ├── .swiftlint.yml └── TerminalExample.xcodeproj │ └── xcshareddata │ └── xcschemes │ └── TerminalExample.xcscheme ├── gif ├── sl.gif ├── vim.gif ├── cmatrix.gif └── nyancat.gif ├── Terminal ├── Terminal.h └── Info.plist ├── Sources └── Terminal │ ├── Public │ ├── TerminalSize.swift │ ├── TerminalEraseEvent.swift │ ├── TerminalClearEvent.swift │ ├── TerminalScrollEvent.swift │ ├── TerminalKeyDownEvent.swift │ └── TerminalView.swift │ ├── Internal │ ├── UIGestureRecognizer+Cancel.swift │ ├── debugLog.swift │ ├── UIColor+CSSString.swift │ ├── Character+Meta.swift │ ├── UIView+EdgeConstraints.swift │ ├── String+SpecialKeys.swift │ ├── Character+Control.swift │ ├── WKWebViewExtension.swift │ └── HtermWebView.swift │ └── Resources │ ├── hterm.html │ ├── hterm.css │ └── hterm_bridge.js ├── Package.swift ├── bin └── build_hterm.sh ├── LICENSE ├── .swiftlint.yml ├── Terminal.xcodeproj ├── xcshareddata │ └── xcschemes │ │ └── Terminal.xcscheme └── project.pbxproj ├── bitrise.yml ├── README.md └── .gitignore /Example/Cartfile: -------------------------------------------------------------------------------- 1 | github "NMSSH/NMSSH" 2 | 3 | -------------------------------------------------------------------------------- /Example/Cartfile.resolved: -------------------------------------------------------------------------------- 1 | github "NMSSH/NMSSH" "2.3.1" 2 | -------------------------------------------------------------------------------- /gif/sl.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnpp73/Terminal/HEAD/gif/sl.gif -------------------------------------------------------------------------------- /gif/vim.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnpp73/Terminal/HEAD/gif/vim.gif -------------------------------------------------------------------------------- /gif/cmatrix.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnpp73/Terminal/HEAD/gif/cmatrix.gif -------------------------------------------------------------------------------- /gif/nyancat.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnpp73/Terminal/HEAD/gif/nyancat.gif -------------------------------------------------------------------------------- /Example/TerminalExample/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/TerminalExample/Assets.xcassets/AppIcon.appiconset/100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnpp73/Terminal/HEAD/Example/TerminalExample/Assets.xcassets/AppIcon.appiconset/100.png -------------------------------------------------------------------------------- /Example/TerminalExample/Assets.xcassets/AppIcon.appiconset/114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnpp73/Terminal/HEAD/Example/TerminalExample/Assets.xcassets/AppIcon.appiconset/114.png -------------------------------------------------------------------------------- /Example/TerminalExample/Assets.xcassets/AppIcon.appiconset/120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnpp73/Terminal/HEAD/Example/TerminalExample/Assets.xcassets/AppIcon.appiconset/120.png -------------------------------------------------------------------------------- /Example/TerminalExample/Assets.xcassets/AppIcon.appiconset/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnpp73/Terminal/HEAD/Example/TerminalExample/Assets.xcassets/AppIcon.appiconset/128.png -------------------------------------------------------------------------------- /Example/TerminalExample/Assets.xcassets/AppIcon.appiconset/144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnpp73/Terminal/HEAD/Example/TerminalExample/Assets.xcassets/AppIcon.appiconset/144.png -------------------------------------------------------------------------------- /Example/TerminalExample/Assets.xcassets/AppIcon.appiconset/152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnpp73/Terminal/HEAD/Example/TerminalExample/Assets.xcassets/AppIcon.appiconset/152.png -------------------------------------------------------------------------------- /Example/TerminalExample/Assets.xcassets/AppIcon.appiconset/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnpp73/Terminal/HEAD/Example/TerminalExample/Assets.xcassets/AppIcon.appiconset/16.png -------------------------------------------------------------------------------- /Example/TerminalExample/Assets.xcassets/AppIcon.appiconset/167.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnpp73/Terminal/HEAD/Example/TerminalExample/Assets.xcassets/AppIcon.appiconset/167.png -------------------------------------------------------------------------------- /Example/TerminalExample/Assets.xcassets/AppIcon.appiconset/172.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnpp73/Terminal/HEAD/Example/TerminalExample/Assets.xcassets/AppIcon.appiconset/172.png -------------------------------------------------------------------------------- /Example/TerminalExample/Assets.xcassets/AppIcon.appiconset/180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnpp73/Terminal/HEAD/Example/TerminalExample/Assets.xcassets/AppIcon.appiconset/180.png -------------------------------------------------------------------------------- /Example/TerminalExample/Assets.xcassets/AppIcon.appiconset/196.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnpp73/Terminal/HEAD/Example/TerminalExample/Assets.xcassets/AppIcon.appiconset/196.png -------------------------------------------------------------------------------- /Example/TerminalExample/Assets.xcassets/AppIcon.appiconset/20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnpp73/Terminal/HEAD/Example/TerminalExample/Assets.xcassets/AppIcon.appiconset/20.png -------------------------------------------------------------------------------- /Example/TerminalExample/Assets.xcassets/AppIcon.appiconset/216.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnpp73/Terminal/HEAD/Example/TerminalExample/Assets.xcassets/AppIcon.appiconset/216.png -------------------------------------------------------------------------------- /Example/TerminalExample/Assets.xcassets/AppIcon.appiconset/256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnpp73/Terminal/HEAD/Example/TerminalExample/Assets.xcassets/AppIcon.appiconset/256.png -------------------------------------------------------------------------------- /Example/TerminalExample/Assets.xcassets/AppIcon.appiconset/29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnpp73/Terminal/HEAD/Example/TerminalExample/Assets.xcassets/AppIcon.appiconset/29.png -------------------------------------------------------------------------------- /Example/TerminalExample/Assets.xcassets/AppIcon.appiconset/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnpp73/Terminal/HEAD/Example/TerminalExample/Assets.xcassets/AppIcon.appiconset/32.png -------------------------------------------------------------------------------- /Example/TerminalExample/Assets.xcassets/AppIcon.appiconset/40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnpp73/Terminal/HEAD/Example/TerminalExample/Assets.xcassets/AppIcon.appiconset/40.png -------------------------------------------------------------------------------- /Example/TerminalExample/Assets.xcassets/AppIcon.appiconset/48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnpp73/Terminal/HEAD/Example/TerminalExample/Assets.xcassets/AppIcon.appiconset/48.png -------------------------------------------------------------------------------- /Example/TerminalExample/Assets.xcassets/AppIcon.appiconset/50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnpp73/Terminal/HEAD/Example/TerminalExample/Assets.xcassets/AppIcon.appiconset/50.png -------------------------------------------------------------------------------- /Example/TerminalExample/Assets.xcassets/AppIcon.appiconset/512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnpp73/Terminal/HEAD/Example/TerminalExample/Assets.xcassets/AppIcon.appiconset/512.png -------------------------------------------------------------------------------- /Example/TerminalExample/Assets.xcassets/AppIcon.appiconset/55.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnpp73/Terminal/HEAD/Example/TerminalExample/Assets.xcassets/AppIcon.appiconset/55.png -------------------------------------------------------------------------------- /Example/TerminalExample/Assets.xcassets/AppIcon.appiconset/57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnpp73/Terminal/HEAD/Example/TerminalExample/Assets.xcassets/AppIcon.appiconset/57.png -------------------------------------------------------------------------------- /Example/TerminalExample/Assets.xcassets/AppIcon.appiconset/58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnpp73/Terminal/HEAD/Example/TerminalExample/Assets.xcassets/AppIcon.appiconset/58.png -------------------------------------------------------------------------------- /Example/TerminalExample/Assets.xcassets/AppIcon.appiconset/60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnpp73/Terminal/HEAD/Example/TerminalExample/Assets.xcassets/AppIcon.appiconset/60.png -------------------------------------------------------------------------------- /Example/TerminalExample/Assets.xcassets/AppIcon.appiconset/64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnpp73/Terminal/HEAD/Example/TerminalExample/Assets.xcassets/AppIcon.appiconset/64.png -------------------------------------------------------------------------------- /Example/TerminalExample/Assets.xcassets/AppIcon.appiconset/72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnpp73/Terminal/HEAD/Example/TerminalExample/Assets.xcassets/AppIcon.appiconset/72.png -------------------------------------------------------------------------------- /Example/TerminalExample/Assets.xcassets/AppIcon.appiconset/76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnpp73/Terminal/HEAD/Example/TerminalExample/Assets.xcassets/AppIcon.appiconset/76.png -------------------------------------------------------------------------------- /Example/TerminalExample/Assets.xcassets/AppIcon.appiconset/80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnpp73/Terminal/HEAD/Example/TerminalExample/Assets.xcassets/AppIcon.appiconset/80.png -------------------------------------------------------------------------------- /Example/TerminalExample/Assets.xcassets/AppIcon.appiconset/87.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnpp73/Terminal/HEAD/Example/TerminalExample/Assets.xcassets/AppIcon.appiconset/87.png -------------------------------------------------------------------------------- /Example/TerminalExample/Assets.xcassets/AppIcon.appiconset/88.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnpp73/Terminal/HEAD/Example/TerminalExample/Assets.xcassets/AppIcon.appiconset/88.png -------------------------------------------------------------------------------- /Terminal/Terminal.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | FOUNDATION_EXPORT double TerminalVersionNumber; 4 | FOUNDATION_EXPORT const unsigned char TerminalVersionString[]; 5 | -------------------------------------------------------------------------------- /Example/TerminalExample/Assets.xcassets/AppIcon.appiconset/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnpp73/Terminal/HEAD/Example/TerminalExample/Assets.xcassets/AppIcon.appiconset/1024.png -------------------------------------------------------------------------------- /Sources/Terminal/Public/TerminalSize.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct TerminalSize: Equatable { 4 | public let cols: Int 5 | public let rows: Int 6 | public static let zero = TerminalSize(cols: 0, rows: 0) 7 | } 8 | -------------------------------------------------------------------------------- /Example/TerminalExample/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | let kSSHHostNameDefaultsKey = "SSHHostNameDefaultsKey" 4 | 5 | @UIApplicationMain 6 | class AppDelegate: UIResponder, UIApplicationDelegate { 7 | var window: UIWindow? 8 | } 9 | -------------------------------------------------------------------------------- /Sources/Terminal/Internal/UIGestureRecognizer+Cancel.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension UIGestureRecognizer { 4 | func cancel() { 5 | if isEnabled { 6 | isEnabled = false 7 | isEnabled = true 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Sources/Terminal/Internal/debugLog.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | func debugLog(fileName: String = #file, line: Int = #line, functionName: String = #function, _ msg: String = "") { 4 | #if DEBUG 5 | print("\(Date().description) [\(fileName.split(separator: "/").last ?? "") @L\(line) \(functionName)] " + msg) 6 | #endif 7 | } 8 | -------------------------------------------------------------------------------- /Sources/Terminal/Public/TerminalEraseEvent.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum TerminalEraseEvent { 4 | case toLeft 5 | case toRight 6 | case line 7 | case above 8 | case below 9 | 10 | internal var htermFunction: HtermEraseFunction { 11 | switch self { 12 | case .toLeft: return .toLeft 13 | case .toRight: return .toRight 14 | case .line: return .line 15 | case .above: return .above 16 | case .below: return .below 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/Terminal/Internal/UIColor+CSSString.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension UIColor { 4 | 5 | private func convert8bit(_ value: CGFloat) -> Int { 6 | lround(Double(value) * 255.0) 7 | } 8 | 9 | var cssString: String { 10 | var r: CGFloat = 0.0 11 | var g: CGFloat = 0.0 12 | var b: CGFloat = 0.0 13 | var a: CGFloat = 0.0 14 | getRed(&r, green: &g, blue: &b, alpha: &a) 15 | return String(format: "rgba(%ld, %ld, %ld, %ld)", convert8bit(r), convert8bit(g), convert8bit(b), convert8bit(a)) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/Terminal/Public/TerminalClearEvent.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum TerminalClearEvent { 4 | case clearScrollback 5 | case reset 6 | case softReset 7 | case clearHome 8 | case clear 9 | 10 | internal var htermFunction: HtermClearFunction { 11 | switch self { 12 | case .clearScrollback: return .clearScrollback 13 | case .reset: return .reset 14 | case .softReset: return .softReset 15 | case .clearHome: return .clearHome 16 | case .clear: return .clear 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/Terminal/Public/TerminalScrollEvent.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum TerminalScrollEvent { 4 | case home 5 | case end 6 | case pageUp 7 | case pageDown 8 | case lineUp 9 | case lineDown 10 | 11 | internal var htermFunction: HtermScrollFunction { 12 | switch self { 13 | case .home: return .home 14 | case .end: return .end 15 | case .pageUp: return .pageUp 16 | case .pageDown: return .pageDown 17 | case .lineUp: return .lineUp 18 | case .lineDown: return .lineDown 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/Terminal/Resources/hterm.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /Sources/Terminal/Internal/Character+Meta.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | let metaKeys = "abcdefghijklmnopqrstuvwxyz0123456789-=[]\\;',./".sorted() 4 | 5 | extension Character { 6 | fileprivate var canCombineWithMeta: Bool { 7 | metaKeys.contains(self) 8 | } 9 | } 10 | 11 | extension String { 12 | func combineWithMetaKey() -> Self? { 13 | guard count == 1 else { 14 | return nil 15 | } 16 | guard Character(self).canCombineWithMeta else { 17 | return nil 18 | } 19 | return Self.terminalEscape + self 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "Terminal", 7 | platforms: [ 8 | .iOS(.v11), 9 | ], 10 | products: [ 11 | .library(name: "Terminal", targets: ["Terminal"]), 12 | ], 13 | targets: [ 14 | .target( 15 | name: "Terminal", 16 | dependencies: [], 17 | resources: [ 18 | .copy("Resources/hterm_all.js"), 19 | .copy("Resources/hterm.html"), 20 | .copy("Resources/hterm.css"), 21 | .copy("Resources/hterm_bridge.js"), 22 | ] 23 | ), 24 | ] 25 | ) 26 | -------------------------------------------------------------------------------- /bin/build_hterm.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -Ceu 4 | 5 | HTERM_VERSION='1.89' 6 | 7 | CURRENT_DIR=$(cd "$(dirname "$0")" || exit 1; pwd) 8 | PROJECT_DIR=$(cd "${CURRENT_DIR}/.." || exit 1; pwd) 9 | TMPDIR=$(mktemp -d) 10 | trap 'rm -rf "${TMPDIR}"' EXIT 11 | 12 | cd "${TMPDIR}" || exit 1 13 | 14 | echo "CURRENT_DIR: ${CURRENT_DIR}" 15 | echo "PROJECT_DIR: ${PROJECT_DIR}" 16 | echo "TMPDIR: ${TMPDIR}" 17 | 18 | git clone 'https://chromium.googlesource.com/apps/libapps' 19 | cd ./libapps || exit 1 20 | git checkout "refs/tags/hterm-${HTERM_VERSION}" 21 | 22 | ./hterm/bin/mkdist 23 | 24 | mkdir -p "${PROJECT_DIR}/Resources" 25 | cp -f './hterm/dist/js/hterm_all.js' "${PROJECT_DIR}/Resources" 26 | -------------------------------------------------------------------------------- /Sources/Terminal/Resources/hterm.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | display: block; 3 | position: relative; 4 | width: 100%; 5 | height: 100%; 6 | margin: 0; 7 | padding: 0; 8 | overflow: hidden; 9 | background: transparent; 10 | -webkit-tap-highlight-color: transparent; 11 | font-feature-settings: "liga" 0; 12 | } 13 | 14 | ime { 15 | display: block; 16 | position: absolute; 17 | min-height: var(--hterm-charsize-height); 18 | max-width: 100%; 19 | z-index: 1000; 20 | overflow-wrap: break-word; 21 | } 22 | 23 | #terminal { 24 | display: block; 25 | position: relative; 26 | width: 100%; 27 | height: 100%; 28 | margin: 0; 29 | padding: 0; 30 | } 31 | -------------------------------------------------------------------------------- /Terminal/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | 22 | 23 | -------------------------------------------------------------------------------- /Sources/Terminal/Internal/UIView+EdgeConstraints.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension UIView { 4 | 5 | func addConstraintsToEdges(_ targetView: UIView) { 6 | // should be fail when invalid UIView hierarchy. 7 | translatesAutoresizingMaskIntoConstraints = false 8 | leftAnchor.constraint(equalTo: targetView.leftAnchor).isActive = true 9 | rightAnchor.constraint(equalTo: targetView.rightAnchor).isActive = true 10 | topAnchor.constraint(equalTo: targetView.topAnchor).isActive = true 11 | bottomAnchor.constraint(equalTo: targetView.bottomAnchor).isActive = true 12 | } 13 | 14 | func addConstraintsToSuperviewEdges() { 15 | // This is safe. 16 | guard let superview = superview else { 17 | return 18 | } 19 | addConstraintsToEdges(superview) 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /Sources/Terminal/Public/TerminalKeyDownEvent.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum TerminalKeyDownEvent { 4 | case escape 5 | case backspace 6 | case tab 7 | case pageUp 8 | case pageDown 9 | case leftArrow 10 | case upArrow 11 | case downArrow 12 | case rightArrow 13 | 14 | internal var string: String { 15 | switch self { 16 | case .escape: return .terminalEscape 17 | case .backspace: return .terminalBackspace 18 | case .tab: return .terminalTab 19 | case .pageUp: return .terminalPageUp 20 | case .pageDown: return .terminalPageDown 21 | case .leftArrow: return .terminalLeftArrow 22 | case .upArrow: return .terminalUpArrow 23 | case .downArrow: return .terminalDownArrow 24 | case .rightArrow: return .terminalRightArrow 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Sources/Terminal/Internal/String+SpecialKeys.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension String { 4 | static let terminalEscape: Self = "\u{1b}" 5 | static let terminalBackspace: Self = "\u{7f}" 6 | 7 | static let terminalTab: Self = "\t" 8 | static let terminalBackTab: Self = .terminalEscape + "[Z" // ^[[Z https://stuff.mit.edu/afs/sipb/user/daveg/Info/backtab-howto.txt 9 | 10 | static let terminalPageUp: Self = .terminalEscape + "[5~" // https://github.com/mintty/mintty/wiki/Keycodes#editing-keys 11 | static let terminalPageDown: Self = .terminalEscape + "[6~" // https://github.com/mintty/mintty/wiki/Keycodes#editing-keys 12 | 13 | static let terminalLeftArrow: Self = terminalEscape + "[D" 14 | static let terminalUpArrow: Self = terminalEscape + "[A" 15 | static let terminalDownArrow: Self = terminalEscape + "[B" 16 | static let terminalRightArrow: Self = terminalEscape + "[C" 17 | } 18 | -------------------------------------------------------------------------------- /Sources/Terminal/Internal/Character+Control.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | let controlKeys = "abcdefghijklmnopqrstuvwxyz@^-=[]\\".sorted() 4 | 5 | extension Character { 6 | 7 | private static let controlMask: UInt8 = 0x40 8 | 9 | private var canCombineWithControl: Bool { 10 | controlKeys.contains(self) 11 | } 12 | 13 | func combineWithControlKey() -> Self? { 14 | guard canCombineWithControl else { 15 | return nil 16 | } 17 | guard let asciiCode = Character(uppercased()).asciiValue else { 18 | return nil 19 | } 20 | let masked = asciiCode ^ Self.controlMask 21 | return Self(UnicodeScalar(masked)) 22 | } 23 | 24 | } 25 | 26 | extension String { 27 | func combineWithControlKey() -> Self? { 28 | guard count == 1 else { 29 | return nil 30 | } 31 | guard let controlled = Character(self).combineWithControlKey() else { 32 | return nil 33 | } 34 | return String(controlled) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Example/TerminalExample/Util/Number+fallback.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension CGFloat { 4 | init(_ string: String, fallback: FloatLiteralType = 0.0) { 5 | if let i = Int(string) { 6 | self.init(i) 7 | } else { 8 | self.init(fallback) 9 | } 10 | } 11 | } 12 | 13 | extension Double { 14 | init(_ string: String, fallback: FloatLiteralType = 0.0) { 15 | if let i = Int(string) { 16 | self.init(i) 17 | } else { 18 | self.init(fallback) 19 | } 20 | } 21 | } 22 | 23 | extension Int { 24 | init(_ string: String, fallback: IntegerLiteralType = 0) { 25 | if let i = Int(string) { 26 | self.init(i) 27 | } else { 28 | self.init(fallback) 29 | } 30 | } 31 | } 32 | 33 | extension UInt { 34 | init(_ string: String, fallback: IntegerLiteralType = 0) { 35 | if let i = UInt(string) { 36 | self.init(i) 37 | } else { 38 | self.init(fallback) 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Yusuke Sugamiya (dnpp.org) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Sources/Terminal/Internal/WKWebViewExtension.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import WebKit 3 | 4 | extension WKWebViewConfiguration { 5 | static func htermConfiguration() -> WKWebViewConfiguration { 6 | let configuration = WKWebViewConfiguration() 7 | configuration.allowsInlineMediaPlayback = false // iPhone default: false, iPad default: true 8 | configuration.allowsAirPlayForMediaPlayback = false // default: true 9 | configuration.allowsPictureInPictureMediaPlayback = false // default: true 10 | configuration.preferences.javaScriptEnabled = true 11 | configuration.preferences.javaScriptCanOpenWindowsAutomatically = false 12 | configuration.preferences.setValue(true as Bool, forKey: "allowFileAccessFromFileURLs") 13 | configuration.preferences.setValue(true as Bool, forKey: "shouldAllowUserInstalledFonts") 14 | configuration.selectionGranularity = .character 15 | return configuration 16 | } 17 | } 18 | 19 | private let jsonEncoder = JSONEncoder() 20 | 21 | extension WKWebView { 22 | func evaluateOneArgumentJavaScript(functionName: String, arg: T, completionHandler: ((Any?, Error?) -> Void)? = nil) { 23 | guard let jsonData = try? jsonEncoder.encode([arg]) else { 24 | return 25 | } 26 | guard let json = String(data: jsonData, encoding: .utf8) else { 27 | return 28 | } 29 | let javaScript = functionName + "(" + json + "[0])" 30 | evaluateJavaScript(javaScript, completionHandler: completionHandler) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Example/TerminalExample/KeyboardNotification/KeyboardNotificationObservable.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | protocol KeyboardNotificationObservable: AnyObject { 4 | var keyboardNotificationTokens: [Any] { get set } 5 | func addObserverKeyboardNotifications() 6 | func removeObserverKeyboardNotifications() 7 | func handleKeyboardNotification(_ notification: Notification) 8 | } 9 | 10 | let keyboardNotificationKeys: [Notification.Name] = [ 11 | UIResponder.keyboardWillShowNotification, 12 | UIResponder.keyboardDidShowNotification, 13 | UIResponder.keyboardWillHideNotification, 14 | UIResponder.keyboardDidHideNotification, 15 | UIResponder.keyboardWillChangeFrameNotification, 16 | UIResponder.keyboardDidChangeFrameNotification 17 | ] 18 | 19 | extension KeyboardNotificationObservable { 20 | 21 | func addObserverKeyboardNotifications() { 22 | if keyboardNotificationTokens.count > 0 { 23 | return 24 | } 25 | keyboardNotificationTokens = keyboardNotificationKeys.compactMap { (name: Notification.Name) -> Any in 26 | let token = NotificationCenter.default.addObserver(forName: name, object: nil, queue: nil) { [weak self] (notification: Notification) in 27 | self?.handleKeyboardNotification(notification) 28 | } 29 | return token // for swiftlint hack. for discarded_notification_center_observer rule 30 | } 31 | } 32 | 33 | func removeObserverKeyboardNotifications() { 34 | if keyboardNotificationTokens.count == 0 { 35 | return 36 | } 37 | keyboardNotificationTokens.forEach { 38 | NotificationCenter.default.removeObserver($0) 39 | } 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /Example/TerminalExample/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | ITSAppUsesNonExemptEncryption 22 | 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationLandscapeLeft 37 | UIInterfaceOrientationLandscapeRight 38 | 39 | UISupportedInterfaceOrientations~ipad 40 | 41 | UIInterfaceOrientationPortrait 42 | UIInterfaceOrientationPortraitUpsideDown 43 | UIInterfaceOrientationLandscapeLeft 44 | UIInterfaceOrientationLandscapeRight 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /Example/TerminalExample/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Example/TerminalExample/InputAccessoryView/NibCreatable.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension Collection { 4 | public subscript(safe index: Index) -> Element? { 5 | startIndex <= index && index < endIndex ? self[index] : nil 6 | } 7 | } 8 | 9 | protocol NibCreatable: AnyObject { 10 | static var currentBundle: Bundle { get } 11 | static var nibName: String { get } 12 | static var nibIndex: Int? { get } 13 | static var nibOptions: [UINib.OptionsKey: Any]? { get } 14 | static var nib: UINib { get } 15 | static func createFromNib(owner: Any?) -> Self? 16 | } 17 | 18 | extension NibCreatable { 19 | 20 | static var currentBundle: Bundle { Bundle(for: Self.self) } 21 | 22 | static var nibName: String { 23 | guard let className = NSStringFromClass(Self.self).components(separatedBy: ".").last else { 24 | fatalError("NibCreatable could not get valid className for \(Self.self)") 25 | } 26 | return className 27 | } 28 | 29 | static var nibIndex: Int? { nil } 30 | 31 | static var nibOptions: [UINib.OptionsKey: Any]? { nil } 32 | 33 | static var nib: UINib { 34 | UINib(nibName: nibName, bundle: currentBundle) 35 | } 36 | 37 | static func createFromNib(owner: Any? = nil) -> Self? { 38 | guard let bundleContents = currentBundle.loadNibNamed(nibName, owner: owner, options: nibOptions) else { 39 | return nil 40 | } 41 | if let nibIndex = nibIndex { 42 | guard let instance = bundleContents[safe: nibIndex] as? Self else { 43 | return nil 44 | } 45 | return instance 46 | } else { 47 | guard let instance = bundleContents.first(where: { $0 is Self }) as? Self else { 48 | return nil 49 | } 50 | return instance 51 | } 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /Example/TerminalExample/InputAccessoryView/InputAccessoryView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Terminal 3 | 4 | final class InputAccessoryView: UIView, NibCreatable { 5 | 6 | weak var terminalView: TerminalView? 7 | 8 | @IBAction private func handleTouchUpInsideTerminalInputAccessoryEscapeButton(_ sender: UIButton) { 9 | terminalView?.press(.escape) 10 | } 11 | 12 | @IBAction private func handleTouchUpInsideTerminalInputAccessoryTabButton(_ sender: UIButton) { 13 | terminalView?.press(.tab) 14 | } 15 | 16 | @IBAction private func handleTouchUpInsideTerminalInputAccessoryControlButton(_ sender: UIButton) { 17 | guard let terminalView = terminalView else { 18 | return 19 | } 20 | terminalView.isControlKeyPressed.toggle() 21 | sender.isSelected = terminalView.isControlKeyPressed 22 | } 23 | 24 | @IBAction private func handleTouchUpInsideTerminalInputAccessoryMetaButton(_ sender: UIButton) { 25 | guard let terminalView = terminalView else { 26 | return 27 | } 28 | terminalView.isMetaKeyPressed.toggle() 29 | sender.isSelected = terminalView.isMetaKeyPressed 30 | } 31 | 32 | @IBAction private func handleTouchUpInsideTerminalInputAccessoryLeftArrowButton(_ sender: UIButton) { 33 | terminalView?.press(.leftArrow) 34 | } 35 | 36 | @IBAction private func handleTouchUpInsideTerminalInputAccessoryDownArrowButton(_ sender: UIButton) { 37 | terminalView?.press(.downArrow) 38 | } 39 | 40 | @IBAction private func handleTouchUpInsideTerminalInputAccessoryUpArrowButton(_ sender: UIButton) { 41 | terminalView?.press(.upArrow) 42 | } 43 | 44 | @IBAction private func handleTouchUpInsideTerminalInputAccessoryRightArrowButton(_ sender: UIButton) { 45 | terminalView?.press(.rightArrow) 46 | } 47 | 48 | @IBAction private func handleTouchUpInsideTerminalInputAccessoryCloseButton(_ sender: UIButton) { 49 | _ = terminalView?.resignFirstResponder() 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /Example/TerminalExample/KeyboardNotification/Notification+Keyboard.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension Notification { 4 | var keyboardInfo: KeyboardInfo? { 5 | guard keyboardNotificationKeys.contains(name) else { 6 | return nil 7 | } 8 | return KeyboardInfo(userInfo: userInfo) 9 | } 10 | 11 | struct KeyboardInfo { 12 | 13 | let userInfo: [AnyHashable: Any]? 14 | 15 | var frameBegin: CGRect { 16 | guard let rect = userInfo?[UIResponder.keyboardFrameBeginUserInfoKey] as? CGRect else { 17 | fatalError("missing keyboardFrameBegin value in userInfo") 18 | } 19 | return rect 20 | } 21 | 22 | var frameEnd: CGRect { 23 | guard let rect = userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { 24 | fatalError("missing keyboardFrameEnd value in userInfo") 25 | } 26 | return rect 27 | } 28 | 29 | var animationDuration: TimeInterval { 30 | guard let duration = userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? TimeInterval else { 31 | fatalError("missing keyboardAnimationDuration in userInfo") 32 | } 33 | return duration 34 | } 35 | 36 | var animationCurve: UIView.AnimationCurve { 37 | guard let rawValue = userInfo?[UIResponder.keyboardAnimationCurveUserInfoKey] as? Int else { 38 | fatalError("missing keyboardAnimationCurve in userInfo") 39 | } 40 | guard let animationCurve = UIView.AnimationCurve(rawValue: rawValue) else { 41 | fatalError("invalid rawValue for UIView.AnimationCurve. rawValue: \(rawValue)") 42 | } 43 | return animationCurve 44 | } 45 | 46 | var isLocal: Bool { 47 | guard let keyboardIsLocal = userInfo?[UIResponder.keyboardIsLocalUserInfoKey] as? Bool else { 48 | fatalError("missing keyboardIsLocal in userInfo") 49 | } 50 | return keyboardIsLocal 51 | } 52 | 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Example/bin/carthage.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euo pipefail 4 | 5 | SRCROOT=$(cd "$(dirname "$0")/../" || exit 1; pwd) 6 | PLATFORM_NAME='iphoneos' 7 | 8 | echo "SRCROOT: ${SRCROOT}" 9 | echo "PLATFORM_NAME: ${PLATFORM_NAME}" 10 | 11 | if [ ! -x /usr/local/bin/carthage ]; then 12 | echo "error: Carthage not installed." 13 | exit 1 14 | fi 15 | 16 | # workaround for carthage 0.35.0 and 0.35.1 with Xcode 12 17 | # see https://github.com/Carthage/Carthage/issues/3019#issuecomment-693381253 18 | xcconfig=$(mktemp /tmp/static.xcconfig.XXXXXX) 19 | trap 'rm -f "${xcconfig}"' INT TERM HUP EXIT 20 | 21 | # For Xcode 12 make sure EXCLUDED_ARCHS is set to arm architectures otherwise 22 | # the build will fail on lipo due to duplicate architectures. 23 | 24 | CURRENT_XCODE_VERSION=$(xcodebuild -version | grep "Build version" | cut -d ' ' -f3) 25 | echo "CURRENT_XCODE_VERSION: ${CURRENT_XCODE_VERSION}" 26 | 27 | echo "EXCLUDED_ARCHS__EFFECTIVE_PLATFORM_SUFFIX_simulator__NATIVE_ARCH_64_BIT_x86_64__XCODE_1200__BUILD_${CURRENT_XCODE_VERSION} = arm64 arm64e armv7 armv7s armv6 armv8" >> "${xcconfig}" 28 | echo 'EXCLUDED_ARCHS__EFFECTIVE_PLATFORM_SUFFIX_simulator__NATIVE_ARCH_64_BIT_x86_64__XCODE_1200 = $(EXCLUDED_ARCHS__EFFECTIVE_PLATFORM_SUFFIX_simulator__NATIVE_ARCH_64_BIT_x86_64__XCODE_1200__BUILD_$(XCODE_PRODUCT_BUILD_VERSION))' >> "${xcconfig}" 29 | echo 'EXCLUDED_ARCHS = $(inherited) $(EXCLUDED_ARCHS__EFFECTIVE_PLATFORM_SUFFIX_$(EFFECTIVE_PLATFORM_SUFFIX)__NATIVE_ARCH_64_BIT_$(NATIVE_ARCH_64_BIT)__XCODE_$(XCODE_VERSION_MAJOR))' >> "${xcconfig}" 30 | echo 'ONLY_ACTIVE_ARCH=NO' >> "${xcconfig}" 31 | echo 'VALID_ARCHS = $(inherited) x86_64' >> "${xcconfig}" 32 | 33 | export XCODE_XCCONFIG_FILE="${xcconfig}" 34 | echo "${XCODE_XCCONFIG_FILE}" 35 | 36 | if [ $# -eq 1 ]; then 37 | /usr/local/bin/carthage "${1}" --platform "${PLATFORM_NAME}" --project-directory "${SRCROOT}" --cache-builds --no-use-binaries 38 | elif [ $# -gt 0 ]; then 39 | /usr/local/bin/carthage "$@" 40 | elif [ ! -d "${SRCROOT}/Carthage/Build" ]; then 41 | /usr/local/bin/carthage bootstrap --platform "${PLATFORM_NAME}" --project-directory "${SRCROOT}" --cache-builds --no-use-binaries 42 | else 43 | /usr/local/bin/carthage build --platform "${PLATFORM_NAME}" --project-directory "${SRCROOT}" --cache-builds --no-use-binaries 44 | fi 45 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | whitelist_rules: 2 | - todo 3 | - trailing_newline 4 | - leading_whitespace 5 | - trailing_whitespace 6 | - trailing_semicolon 7 | - private_action 8 | - private_outlet 9 | - redundant_nil_coalescing 10 | - single_test_class 11 | - trailing_closure 12 | - class_delegate_protocol 13 | - closing_brace 14 | - closure_parameter_position 15 | - colon 16 | - comma 17 | - compiler_protocol_init 18 | - control_statement 19 | - empty_parentheses_with_trailing_closure 20 | - force_cast 21 | - force_try 22 | - force_unwrapping 23 | - for_where 24 | - generic_type_name 25 | - implicit_getter 26 | - mark 27 | - opening_brace 28 | - operator_whitespace 29 | - operator_usage_whitespace 30 | - private_unit_test 31 | - redundant_discardable_let 32 | - redundant_optional_initialization 33 | - redundant_string_enum_value 34 | - redundant_void_return 35 | - return_arrow_whitespace 36 | - shorthand_operator 37 | - statement_position 38 | - type_name 39 | - vertical_parameter_alignment 40 | - vertical_whitespace 41 | - weak_delegate 42 | - closure_end_indentation 43 | - closure_spacing 44 | - conditional_returns_on_newline 45 | - explicit_init 46 | - first_where 47 | - prohibited_super_call 48 | - block_based_kvo 49 | - fatal_error_message 50 | - identifier_name 51 | - valid_ibinspectable 52 | - attributes 53 | - discarded_notification_center_observer 54 | - discouraged_direct_init 55 | - empty_string 56 | - identical_operands 57 | - implicit_return 58 | - implicitly_unwrapped_optional 59 | - inert_defer 60 | - literal_expression_end_indentation 61 | - modifier_order 62 | - multiline_parameters 63 | - multiple_closures_with_trailing_closure 64 | - redundant_set_access_control 65 | - sorted_first_last 66 | - switch_case_alignment 67 | - syntactic_sugar 68 | - toggle_bool 69 | - unneeded_break_in_switch 70 | - unused_import 71 | identifier_name: 72 | min_length: 0 73 | max_length: 74 | warning: 50 75 | warning: 60 76 | excluded: 77 | - id 78 | - URL 79 | type_name: 80 | min_length: 4 81 | max_length: 82 | warning: 50 83 | error: 60 84 | force_cast: error 85 | force_try: 86 | severity: error 87 | included: 88 | - Sources 89 | - Terminal 90 | excluded: 91 | - Carthage 92 | - Pods 93 | -------------------------------------------------------------------------------- /Example/.swiftlint.yml: -------------------------------------------------------------------------------- 1 | whitelist_rules: 2 | - todo 3 | - trailing_newline 4 | - leading_whitespace 5 | - trailing_whitespace 6 | - trailing_semicolon 7 | - private_action 8 | - private_outlet 9 | - redundant_nil_coalescing 10 | - single_test_class 11 | - trailing_closure 12 | - class_delegate_protocol 13 | - closing_brace 14 | - closure_parameter_position 15 | - colon 16 | - comma 17 | - compiler_protocol_init 18 | - control_statement 19 | - empty_parentheses_with_trailing_closure 20 | - force_cast 21 | - force_try 22 | - force_unwrapping 23 | - for_where 24 | - generic_type_name 25 | - implicit_getter 26 | - mark 27 | - opening_brace 28 | - operator_whitespace 29 | - operator_usage_whitespace 30 | - private_unit_test 31 | - redundant_discardable_let 32 | - redundant_optional_initialization 33 | - redundant_string_enum_value 34 | - redundant_void_return 35 | - return_arrow_whitespace 36 | - shorthand_operator 37 | - statement_position 38 | - type_name 39 | - vertical_parameter_alignment 40 | - vertical_whitespace 41 | - weak_delegate 42 | - closure_end_indentation 43 | - closure_spacing 44 | - conditional_returns_on_newline 45 | - explicit_init 46 | - first_where 47 | - prohibited_super_call 48 | - block_based_kvo 49 | - fatal_error_message 50 | - identifier_name 51 | - valid_ibinspectable 52 | - attributes 53 | - discarded_notification_center_observer 54 | - discouraged_direct_init 55 | - empty_string 56 | - identical_operands 57 | - implicit_return 58 | - implicitly_unwrapped_optional 59 | - inert_defer 60 | - literal_expression_end_indentation 61 | - modifier_order 62 | - multiline_parameters 63 | - multiple_closures_with_trailing_closure 64 | - redundant_set_access_control 65 | - sorted_first_last 66 | - switch_case_alignment 67 | - syntactic_sugar 68 | - toggle_bool 69 | - unneeded_break_in_switch 70 | - unused_import 71 | identifier_name: 72 | min_length: 0 73 | max_length: 74 | warning: 50 75 | warning: 60 76 | excluded: 77 | - id 78 | - URL 79 | type_name: 80 | min_length: 4 81 | max_length: 82 | warning: 50 83 | error: 60 84 | force_cast: error 85 | force_try: 86 | severity: error 87 | included: 88 | - TerminalExample 89 | excluded: 90 | - Carthage 91 | - Pods 92 | -------------------------------------------------------------------------------- /Terminal.xcodeproj/xcshareddata/xcschemes/Terminal.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 44 | 50 | 51 | 57 | 58 | 59 | 60 | 62 | 63 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /bitrise.yml: -------------------------------------------------------------------------------- 1 | --- 2 | format_version: '8' 3 | default_step_lib_source: https://github.com/bitrise-io/bitrise-steplib.git 4 | project_type: ios 5 | trigger_map: 6 | - push_branch: master 7 | workflow: deploy_testflight 8 | workflows: 9 | setup: 10 | steps: 11 | - slack@3: 12 | inputs: 13 | - pretext: "*Setup Started!*" 14 | - webhook_url: "$SLACK_HOOK_URL" 15 | - activate-ssh-key@4: 16 | run_if: '{{getenv "SSH_RSA_PRIVATE_KEY" | ne ""}}' 17 | - git-clone@4: {} 18 | - ios-auto-provision-appstoreconnect@0: 19 | inputs: 20 | - distribution_type: app-store 21 | - cache-pull@2: {} 22 | - brew-install@0: 23 | inputs: 24 | - upgrade: 'no' 25 | - packages: swiftlint carthage 26 | - carthage@3: 27 | inputs: 28 | - carthage_options: "--project-directory Example --platform ios --cache-builds 29 | --no-use-binaries" 30 | - carthage_command: bootstrap 31 | - cache-push@2: 32 | inputs: 33 | - cache_paths: | 34 | $BITRISE_CACHE_DIR 35 | ./Example/Carthage -> ./Example/Carthage/Cachefile 36 | build: 37 | steps: 38 | - set-xcode-build-number@1: 39 | inputs: 40 | - build_version_offset: '' 41 | - plist_path: Example/TerminalExample/Info.plist 42 | - xcode-archive@2: 43 | inputs: 44 | - project_path: "$BITRISE_PROJECT_PATH" 45 | - scheme: "$BITRISE_SCHEME" 46 | - export_method: app-store 47 | - slack@3: 48 | inputs: 49 | - webhook_url: "$SLACK_HOOK_URL" 50 | before_run: 51 | - setup 52 | test: 53 | steps: 54 | - xcode-analyze@2: {} 55 | - xcode-test@2: {} 56 | before_run: 57 | - setup 58 | deploy_testflight: 59 | steps: 60 | - deploy-to-bitrise-io@1: 61 | inputs: 62 | - is_enable_public_page: 'false' 63 | - deploy-to-itunesconnect-application-loader@0: 64 | inputs: 65 | - api_key_path: "$BITRISEIO_APP_STORE_CONNECT_AUTH_KEY_URL" 66 | - api_issuer: "$APP_STORE_CONNECT_ISSUER" 67 | - slack@3: 68 | inputs: 69 | - pretext: "*Upload Succeeded!*" 70 | - pretext_on_error: "*Upload Failed!*" 71 | - webhook_url: "$SLACK_HOOK_URL" 72 | before_run: 73 | - build 74 | app: 75 | envs: 76 | - opts: 77 | is_expand: false 78 | BITRISE_PROJECT_PATH: Example/TerminalExample.xcodeproj 79 | - opts: 80 | is_expand: false 81 | BITRISE_SCHEME: TerminalExample 82 | - opts: 83 | is_expand: false 84 | BITRISE_EXPORT_METHOD: app-store 85 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Terminal 2 | =========== 3 | 4 | [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat-square)](https://github.com/Carthage/Carthage) 5 | [![Swift Package Manager compatible](https://img.shields.io/badge/Swift%20Package%20Manager-compatible-4BC51D.svg?style=flat-square)](https://github.com/apple/swift-package-manager) 6 | 7 | 8 | ## What is this 9 | 10 | Terminal Emulator for iOS. based on [hterm.js](https://hterm.org) with `WKWebView` 11 | 12 | This is not ssh client. only emulate terminal output. 13 | 14 | If need ssh, you could use [`NMSSH/NMSSH`](https://github.com/NMSSH/NMSSH) library. Please see example app. 15 | 16 | 17 | ## Demo 18 | 19 | nyancat sl cmatrix vim 20 | 21 | 22 | ## Features 23 | 24 | - supports iOS 11 ~ (iOS 12 or above recommended) 25 | - supports Interface Builder creation 26 | - supports GUI selection, select all, copy, paste 27 | - supports hardware keyboard input 28 | - supports CJK IME (little buggy...) 29 | 30 | 31 | ## Technical Detail 32 | 33 | Please see my [blog post](https://blog.dnpp.org/ios_terminal_emulator) (written in Japanese). 34 | 35 | 36 | ## How to use 37 | 38 | See Example Appication. 39 | 40 | - [`TerminalViewController.swift`](/Example/TerminalExample/ViewController/TerminalViewController.swift) 41 | - [`SSHTerminalViewController.swift`](/Example/TerminalExample/ViewController/SSHTerminalViewController.swift) 42 | 43 | 44 | ## How to build Example App 45 | 46 | ### `swiftlint` and `carthage` required 47 | 48 | ```sh 49 | brew install swiftlint carthage 50 | ``` 51 | 52 | ### Clone Repository and prepare 53 | 54 | ```sh 55 | git clone git@github.com:dnpp73/Terminal.git 56 | 57 | cd Terminal/Example 58 | 59 | carthage bootstrap --platform iOS --no-use-binaries 60 | 61 | open TerminalExample.xcodeproj 62 | ``` 63 | 64 | If you want to run on a real devices, change the `Team` because of code sign problem. 65 | 66 | 67 | ### `sshd` with password authentication? 68 | 69 | I recommend using Docker. See [`Dockerize an SSH service`](https://docs.docker.com/engine/examples/running_ssh_service/) 70 | 71 | [`dnpp73/chef_cutting_board`](https://github.com/dnpp73/chef_cutting_board) is convenient. 72 | 73 | 74 | ## Carthage 75 | 76 | https://github.com/Carthage/Carthage 77 | 78 | Write your `Cartfile` 79 | 80 | ``` 81 | github "dnpp73/Terminal" 82 | ``` 83 | 84 | and run 85 | 86 | ```sh 87 | carthage bootstrap --cache-builds --no-use-binaries --platform iOS 88 | ``` 89 | 90 | or 91 | 92 | ```sh 93 | carthage update --cache-builds --no-use-binaries --platform iOS 94 | ``` 95 | 96 | 97 | ## Respect 98 | 99 | - [Blink Shell for iOS](https://blink.sh) ([GitHub](https://github.com/blinksh/blink)) 100 | - [iSH](https://ish.app) ([GitHub](https://github.com/ish-app/ish)) 101 | - [a-Shell](https://holzschu.github.io/a-Shell_iOS/) ([GitHub](https://github.com/holzschu/a-shell)) 102 | 103 | 104 | ## License 105 | 106 | [MIT](/LICENSE) 107 | -------------------------------------------------------------------------------- /Example/TerminalExample.xcodeproj/xcshareddata/xcschemes/TerminalExample.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /Example/TerminalExample/Util/KeyboardPicker.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | protocol KeyboardPickerDelegate: AnyObject { 4 | func keyboardPicker(_ keyboardPicker: KeyboardPicker, didSelect index: Int) 5 | } 6 | 7 | class KeyboardPicker: UIButton { 8 | 9 | weak var delegate: KeyboardPickerDelegate? 10 | 11 | var dataSource: [String] = [] 12 | 13 | var selectedIndex: Int { 14 | get { 15 | lastSelectedIndex 16 | } 17 | set { 18 | lastSelectedIndex = newValue 19 | pickerView?.selectRow(newValue, inComponent: 0, animated: false) 20 | } 21 | } 22 | 23 | var selectedData: String { 24 | get { 25 | dataSource[lastSelectedIndex] 26 | } 27 | set { 28 | let index = dataSource.firstIndex(of: newValue) ?? 0 29 | lastSelectedIndex = index 30 | pickerView?.selectRow(index, inComponent: 0, animated: false) 31 | } 32 | } 33 | 34 | weak var pickerView: UIPickerView? 35 | 36 | private var lastSelectedIndex: Int = 0 37 | 38 | override init(frame: CGRect) { 39 | super.init(frame: frame) 40 | addTarget(self, action: #selector(touchUpInsideSelf(_:)), for: .touchUpInside) 41 | } 42 | 43 | required init?(coder: NSCoder) { 44 | super.init(coder: coder) 45 | addTarget(self, action: #selector(touchUpInsideSelf(_:)), for: .touchUpInside) 46 | } 47 | 48 | @objc 49 | private func touchUpInsideSelf(_ sender: KeyboardPicker) { 50 | becomeFirstResponder() 51 | } 52 | 53 | override var canBecomeFirstResponder: Bool { 54 | true 55 | } 56 | 57 | override var inputView: UIView? { 58 | let pickerView: UIPickerView = UIPickerView() 59 | pickerView.delegate = self 60 | pickerView.dataSource = self 61 | pickerView.selectRow(lastSelectedIndex, inComponent: 0, animated: false) 62 | pickerView.autoresizingMask = [.flexibleHeight] 63 | self.pickerView = pickerView 64 | 65 | // SafeArea 対応をする為に UIView を挟む 66 | let view = UIView() 67 | view.autoresizingMask = [.flexibleHeight] 68 | view.addSubview(pickerView) 69 | 70 | pickerView.translatesAutoresizingMaskIntoConstraints = false 71 | pickerView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true 72 | pickerView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true 73 | pickerView.bottomAnchor.constraint(equalTo: view.layoutMarginsGuide.bottomAnchor).isActive = true 74 | 75 | return view 76 | } 77 | 78 | override var inputAccessoryView: UIView? { 79 | let view = UIVisualEffectView(effect: UIBlurEffect(style: .light)) 80 | view.frame = CGRect(x: 0, y: 0, width: frame.width, height: 44) 81 | 82 | let closeButton = UIButton(type: .custom) 83 | closeButton.setTitle("Close", for: .normal) 84 | closeButton.sizeToFit() 85 | closeButton.addTarget(self, action: #selector(touchUpInsideCloseButton(_:)), for: .touchUpInside) 86 | 87 | view.contentView.addSubview(closeButton) 88 | 89 | closeButton.translatesAutoresizingMaskIntoConstraints = false 90 | closeButton.widthAnchor.constraint(equalToConstant: closeButton.frame.size.width).isActive = true 91 | closeButton.heightAnchor.constraint(equalToConstant: closeButton.frame.size.height).isActive = true 92 | closeButton.topAnchor.constraint(equalTo: view.topAnchor, constant: 5).isActive = true 93 | closeButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16).isActive = true 94 | 95 | return view 96 | } 97 | 98 | @objc 99 | private func touchUpInsideCloseButton(_ sender: UIButton) { 100 | resignFirstResponder() 101 | } 102 | 103 | } 104 | 105 | extension KeyboardPicker: UIPickerViewDelegate, UIPickerViewDataSource { 106 | 107 | func numberOfComponents(in pickerView: UIPickerView) -> Int { 108 | 1 109 | } 110 | 111 | func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { 112 | dataSource.count 113 | } 114 | 115 | func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { 116 | dataSource[row] 117 | } 118 | 119 | func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { 120 | lastSelectedIndex = row 121 | delegate?.keyboardPicker(self, didSelect: row) 122 | } 123 | 124 | } 125 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/osx,xcode,swift,macos,windows,carthage,fastlane,cocoapods,objective-c 3 | 4 | ### Carthage ### 5 | # Carthage 6 | # 7 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 8 | # Carthage/Checkouts 9 | 10 | Carthage/ 11 | 12 | ### CocoaPods ### 13 | ## CocoaPods GitIgnore Template 14 | 15 | # CocoaPods - Only use to conserve bandwidth / Save time on Pushing 16 | # - Also handy if you have a large number of dependant pods 17 | # - AS PER https://guides.cocoapods.org/using/using-cocoapods.html NEVER IGNORE THE LOCK FILE 18 | Pods/ 19 | 20 | ### fastlane ### 21 | # fastlane - A streamlined workflow tool for Cocoa deployment 22 | # 23 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 24 | # screenshots whenever they are needed. 25 | # For more information about the recommended setup visit: 26 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 27 | 28 | # fastlane specific 29 | fastlane/report.xml 30 | 31 | # deliver temporary files 32 | fastlane/Preview.html 33 | 34 | # snapshot generated screenshots 35 | fastlane/screenshots/**/*.png 36 | fastlane/screenshots/screenshots.html 37 | 38 | # scan temporary files 39 | fastlane/test_output 40 | 41 | ### macOS ### 42 | *.DS_Store 43 | .AppleDouble 44 | .LSOverride 45 | 46 | # Icon must end with two \r 47 | Icon 48 | 49 | # Thumbnails 50 | ._* 51 | 52 | # Files that might appear in the root of a volume 53 | .DocumentRevisions-V100 54 | .fseventsd 55 | .Spotlight-V100 56 | .TemporaryItems 57 | .Trashes 58 | .VolumeIcon.icns 59 | .com.apple.timemachine.donotpresent 60 | 61 | # Directories potentially created on remote AFP share 62 | .AppleDB 63 | .AppleDesktop 64 | Network Trash Folder 65 | Temporary Items 66 | .apdisk 67 | 68 | ### Objective-C ### 69 | # Xcode 70 | # 71 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 72 | 73 | ## Build generated 74 | build/ 75 | DerivedData/ 76 | 77 | ## Various settings 78 | *.pbxuser 79 | !default.pbxuser 80 | *.mode1v3 81 | !default.mode1v3 82 | *.mode2v3 83 | !default.mode2v3 84 | *.perspectivev3 85 | !default.perspectivev3 86 | xcuserdata/ 87 | 88 | ## Other 89 | *.moved-aside 90 | *.xccheckout 91 | *.xcscmblueprint 92 | 93 | ## Obj-C/Swift specific 94 | *.hmap 95 | *.ipa 96 | *.dSYM.zip 97 | *.dSYM 98 | 99 | # CocoaPods - Refactored to standalone file 100 | 101 | 102 | # Carthage - Refactored to standalone file 103 | 104 | # fastlane 105 | # 106 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 107 | # screenshots whenever they are needed. 108 | # For more information about the recommended setup visit: 109 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 110 | 111 | fastlane/screenshots 112 | 113 | # Code Injection 114 | # 115 | # After new code Injection tools there's a generated folder /iOSInjectionProject 116 | # https://github.com/johnno1962/injectionforxcode 117 | 118 | iOSInjectionProject/ 119 | 120 | ### Objective-C Patch ### 121 | 122 | ### OSX ### 123 | 124 | # Icon must end with two \r 125 | 126 | # Thumbnails 127 | 128 | # Files that might appear in the root of a volume 129 | 130 | # Directories potentially created on remote AFP share 131 | 132 | ### Swift ### 133 | # Xcode 134 | # 135 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 136 | 137 | ## Build generated 138 | 139 | ## Various settings 140 | 141 | ## Other 142 | 143 | ## Obj-C/Swift specific 144 | 145 | ## Playgrounds 146 | timeline.xctimeline 147 | playground.xcworkspace 148 | 149 | # Swift Package Manager 150 | # 151 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 152 | # Packages/ 153 | # Package.pins 154 | .build/ 155 | 156 | # CocoaPods - Refactored to standalone file 157 | 158 | # Carthage - Refactored to standalone file 159 | 160 | # fastlane 161 | # 162 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 163 | # screenshots whenever they are needed. 164 | # For more information about the recommended setup visit: 165 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 166 | 167 | 168 | ### Windows ### 169 | # Windows thumbnail cache files 170 | Thumbs.db 171 | ehthumbs.db 172 | ehthumbs_vista.db 173 | 174 | # Folder config file 175 | Desktop.ini 176 | 177 | # Recycle Bin used on file shares 178 | $RECYCLE.BIN/ 179 | 180 | # Windows Installer files 181 | *.cab 182 | *.msi 183 | *.msm 184 | *.msp 185 | 186 | # Windows shortcuts 187 | *.lnk 188 | 189 | ### Xcode ### 190 | # Xcode 191 | # 192 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 193 | 194 | ## Build generated 195 | 196 | ## Various settings 197 | 198 | ## Other 199 | 200 | ### Xcode Patch ### 201 | *.xcodeproj/* 202 | !*.xcodeproj/project.pbxproj 203 | !*.xcodeproj/xcshareddata/ 204 | !*.xcworkspace/contents.xcworkspacedata 205 | /*.gcno 206 | 207 | 208 | # End of https://www.gitignore.io/api/osx,xcode,swift,macos,windows,carthage,fastlane,cocoapods,objective-c 209 | 210 | vendor 211 | !.gitkeep 212 | 213 | # Swift Package Manager 214 | /.build 215 | /.swiftpm 216 | 217 | .bitrise.secrets.yml 218 | -------------------------------------------------------------------------------- /Sources/Terminal/Resources/hterm_bridge.js: -------------------------------------------------------------------------------- 1 | hterm.defaultStorage = new lib.Storage.Memory(); 2 | 3 | const onTerminalReady = () => { 4 | // JavaScript -> Native 5 | const native = new Proxy({}, { 6 | get(obj, prop) { 7 | return (...args) => { 8 | if (args.length == 0) { 9 | args = null; 10 | } else if (args.length == 1) { 11 | args = args[0]; 12 | } 13 | if (window.webkit) { 14 | webkit.messageHandlers[prop].postMessage(args); 15 | } 16 | }; 17 | }, 18 | }); 19 | 20 | window.native_log = (obj) => { 21 | native.log(obj + ""); 22 | } 23 | 24 | // Native -> JavaScript 25 | window.exports = {}; 26 | 27 | const io = term.io.push(); 28 | term.reset(); 29 | 30 | exports.write = (data) => { 31 | io.writeUTF16(data); // same as `io.print(data);` 32 | // io.writeUTF8(data); // writeUTF8() is legacy in hterm. 33 | }; 34 | 35 | io.sendString = (str) => { 36 | native.htermHandleSendString(str); 37 | }; 38 | 39 | io.onVTKeyStroke = (str) => { 40 | native.htermHandleOnVTKeyStroke(str); 41 | }; 42 | 43 | io.onTerminalResize = (cols, rows) => { 44 | if (term.prompt) { 45 | term.prompt.resize(); 46 | } 47 | native.htermHandleOnTerminalResize(cols, rows); 48 | }; 49 | 50 | exports.syncTerminalSize = () => { 51 | // We don't use `hterm.Terminal.prototype.realizeSize_` because it will fire new `onTerminalResize_` notification. 52 | // first: height 53 | const rows = term.screen_.getHeight() // don't trust `this.screenSize` 54 | term.realizeHeight_(rows); 55 | 56 | // next: width 57 | const cols = term.screen_.getWidth() 58 | term.realizeWidth_(cols); 59 | 60 | return [cols, rows] 61 | }; 62 | 63 | term.scrollPort_.getScreenNode().contentEditable = false; 64 | 65 | term.scrollPort_.getScreenNode().addEventListener('focus', (e) => { 66 | native.htermDidFocusScreen(); 67 | }, {capture: false}); 68 | 69 | term.scrollPort_.getScreenNode().addEventListener('blur', (e) => { 70 | native.htermDidBlurScreen(); 71 | }, {capture: false}); 72 | 73 | term.scrollPort_.onTouch = (e) => { 74 | if (e.type == 'touchstart') { 75 | native.htermScrollPortDidTouchStart(); 76 | } else if (e.type == 'touchmove') { 77 | native.htermScrollPortDidTouchMove(); 78 | } else if (e.type == 'touchend') { 79 | native.htermScrollPortDidTouchEnd(); 80 | } else if (e.type == 'touchcancel') { 81 | native.htermScrollPortDidTouchCancel(); 82 | } 83 | }; 84 | 85 | exports.clearSelection = () => { 86 | window.getSelection().removeAllRanges(); 87 | }; 88 | 89 | // css string. Default: '"DejaVu Sans Mono", "Noto Sans Mono", "Everson Mono", FreeMono, Menlo, Terminal, monospace'. See hterm_all.js@L9932 90 | exports.setFontFamily = (fontFamily) => { 91 | term.getPrefs().set('font-family', fontFamily); 92 | // `terminal.syncFontFamily();` will automatically fire. in hterm_all.js@L14032 `this.prefs_.addObservers()` 93 | }; 94 | 95 | // true or false or null. Null to autodetect. default null 96 | exports.setEnableBold = (enabled) => { 97 | term.getPrefs().set('enable-bold', enabled); 98 | }; 99 | 100 | // true or false. default true 101 | exports.setEnableBoldAsBright = (enabled) => { 102 | term.getPrefs().set('enable-bold-as-bright', enabled); 103 | }; 104 | 105 | exports.getCharacterSize = () => { 106 | return [term.scrollPort_.characterSize.width, term.scrollPort_.characterSize.height]; 107 | }; 108 | 109 | exports.setUserGesture = () => { 110 | term.accessibilityReader_.hasUserGesture = true; 111 | }; 112 | 113 | exports.getVisibleText = () => { 114 | const start = term.scrollPort_.getTopRowIndex(); 115 | const end = term.scrollPort_.getBottomRowIndex(start) + 1; 116 | return term.getRowsText(start, end); 117 | }; 118 | 119 | exports.getAllText = () => { 120 | const end = term.getRowCount(); 121 | return term.getRowsText(0, end); 122 | }; 123 | 124 | hterm.openUrl = (url) => { 125 | native.htermDidHandleURL(url); 126 | }; 127 | 128 | term.setCursorShape(hterm.Terminal.cursorShape.BEAM); 129 | 130 | native.htermDidLoad(); 131 | }; 132 | 133 | 134 | const htermSetup = () => { 135 | window.term = new hterm.Terminal(); 136 | 137 | const css = ` 138 | x-screen { 139 | background: transparent !important; 140 | } 141 | x-screen::-webkit-scrollbar { 142 | display: none; 143 | } 144 | `; 145 | 146 | const p = term.getPrefs(); 147 | p.set('background-color', 'transparent'); 148 | p.set('foreground-color', 'transparent'); 149 | p.set('cursor-color', 'transparent'); 150 | 151 | p.set('terminal-encoding', 'raw'); // or 'iso-2022' 152 | 153 | p.set('font-family', 'Menlo'); 154 | p.set('font-size', 9); 155 | 156 | p.set('enable-resize-status', false); 157 | p.set('copy-on-select', false); 158 | p.set('enable-clipboard-notice', false); 159 | p.set('user-css-text', css); 160 | 161 | p.set('audible-bell-sound', ''); 162 | p.set('receive-encoding', 'raw'); 163 | p.set('allow-images-inline', true); 164 | 165 | p.set('scroll-wheel-may-send-arrow-keys', true); 166 | 167 | term.decorate(document.getElementById('terminal')); 168 | 169 | // term.installKeyboard(); 170 | 171 | term.onTerminalReady = onTerminalReady; 172 | }; 173 | 174 | window.onload = () => { 175 | lib.init(htermSetup); 176 | }; 177 | -------------------------------------------------------------------------------- /Example/TerminalExample/KeyboardNotification/KeyboardLayoutGuide.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyboardLayoutGuide.swift 3 | // KeyboardLayoutGuide 4 | // 5 | // Created by Sacha DSO on 14/11/2017. 6 | // Copyright © 2017 freshos. All rights reserved. 7 | // 8 | // https://github.com/freshOS/KeyboardLayoutGuide/blob/8f2f7b600fcb788fdda571b19b86a560ef2f9559/Sources/KeyboardLayoutGuide/KeyboardLayoutGuide.swift 9 | 10 | import UIKit 11 | 12 | private class Keyboard { 13 | fileprivate static let shared = Keyboard() 14 | private init() {} 15 | fileprivate var currentHeight: CGFloat = 0 16 | } 17 | 18 | extension UIView { 19 | private enum Identifiers { 20 | static var usingSafeArea = "KeyboardLayoutGuideUsingSafeArea" 21 | static var notUsingSafeArea = "KeyboardLayoutGuide" 22 | } 23 | 24 | /// A layout guide representing the inset for the keyboard. 25 | /// Use this layout guide’s top anchor to create constraints pinning to the top of the keyboard or the bottom of safe area. 26 | var keyboardLayoutGuide: UILayoutGuide { 27 | getOrCreateKeyboardLayoutGuide(identifier: Identifiers.usingSafeArea, usesSafeArea: true) 28 | } 29 | 30 | /// A layout guide representing the inset for the keyboard. 31 | /// Use this layout guide’s top anchor to create constraints pinning to the top of the keyboard or the bottom of the view. 32 | var keyboardLayoutGuideNoSafeArea: UILayoutGuide { 33 | getOrCreateKeyboardLayoutGuide(identifier: Identifiers.notUsingSafeArea, usesSafeArea: false) 34 | } 35 | 36 | private func getOrCreateKeyboardLayoutGuide(identifier: String, usesSafeArea: Bool) -> UILayoutGuide { 37 | if let existing = layoutGuides.first(where: { $0.identifier == identifier }) { 38 | return existing 39 | } 40 | let new = KeyboardLayoutGuide() 41 | new.usesSafeArea = usesSafeArea 42 | new.identifier = identifier 43 | addLayoutGuide(new) 44 | new.setUp() 45 | return new 46 | } 47 | } 48 | 49 | class KeyboardLayoutGuide: UILayoutGuide { 50 | var usesSafeArea = true { 51 | didSet { 52 | updateButtomAnchor() 53 | } 54 | } 55 | 56 | private var bottomConstraint: NSLayoutConstraint? 57 | 58 | @available(*, unavailable) 59 | required init?(coder aDecoder: NSCoder) { 60 | fatalError("init(coder:) has not been implemented") 61 | } 62 | 63 | init(notificationCenter: NotificationCenter = NotificationCenter.default) { 64 | super.init() 65 | // Observe keyboardWillChangeFrame notifications 66 | notificationCenter.addObserver(self, selector: #selector(keyboardWillChangeFrame(_:)), name: UIResponder.keyboardWillChangeFrameNotification, object: nil) 67 | } 68 | 69 | fileprivate func setUp() { 70 | guard let view = owningView else { 71 | return 72 | } 73 | NSLayoutConstraint.activate([ 74 | heightAnchor.constraint(equalToConstant: Keyboard.shared.currentHeight), 75 | leftAnchor.constraint(equalTo: view.leftAnchor), 76 | rightAnchor.constraint(equalTo: view.rightAnchor), 77 | ]) 78 | updateButtomAnchor() 79 | } 80 | 81 | func updateButtomAnchor() { 82 | if let bottomConstraint = bottomConstraint { 83 | bottomConstraint.isActive = false 84 | } 85 | 86 | guard let view = owningView else { 87 | return 88 | } 89 | 90 | let viewBottomAnchor: NSLayoutYAxisAnchor 91 | if #available(iOS 11.0, *), usesSafeArea { 92 | viewBottomAnchor = view.safeAreaLayoutGuide.bottomAnchor 93 | } else { 94 | viewBottomAnchor = view.bottomAnchor 95 | } 96 | 97 | bottomConstraint = bottomAnchor.constraint(equalTo: viewBottomAnchor) 98 | bottomConstraint?.isActive = true 99 | } 100 | 101 | @objc 102 | private func keyboardWillChangeFrame(_ notification: Notification) { 103 | guard let keyboardInfo = notification.keyboardInfo else { 104 | return 105 | } 106 | let frameEnd = keyboardInfo.frameEnd 107 | let screenHeight = UIApplication.shared.keyWindow?.bounds.height ?? UIScreen.main.bounds.height 108 | var height = screenHeight - frameEnd.minY 109 | let duration = keyboardInfo.animationDuration 110 | if #available(iOS 11.0, *), usesSafeArea, height > 0, let bottom = owningView?.safeAreaInsets.bottom { 111 | height -= bottom 112 | } 113 | heightConstraint?.constant = height 114 | if duration > 0.0 { 115 | animate(notification) 116 | } 117 | Keyboard.shared.currentHeight = height 118 | } 119 | 120 | private func animate(_ notification: Notification) { 121 | if let owningView = self.owningView, isVisible(view: owningView) { 122 | self.owningView?.layoutIfNeeded() 123 | } else { 124 | UIView.performWithoutAnimation { 125 | self.owningView?.layoutIfNeeded() 126 | } 127 | } 128 | } 129 | } 130 | 131 | // MARK: - Helpers 132 | 133 | extension UILayoutGuide { 134 | fileprivate var heightConstraint: NSLayoutConstraint? { 135 | owningView?.constraints.first { 136 | $0.firstItem as? UILayoutGuide == self && $0.firstAttribute == .height 137 | } 138 | } 139 | } 140 | 141 | /// Credits to John Gibb for this nice helper :) 142 | /// https://stackoverflow.com/questions/1536923/determine-if-uiview-is-visible-to-the-user 143 | private func isVisible(view: UIView) -> Bool { 144 | func isVisible(view: UIView, inView: UIView?) -> Bool { 145 | guard let inView = inView else { 146 | return true 147 | } 148 | let viewFrame = inView.convert(view.bounds, from: view) 149 | if viewFrame.intersects(inView.bounds) { 150 | return isVisible(view: view, inView: inView.superview) 151 | } 152 | return false 153 | } 154 | return isVisible(view: view, inView: view.superview) 155 | } 156 | -------------------------------------------------------------------------------- /Example/TerminalExample/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | {"images":[{"size":"60x60","expected-size":"180","filename":"180.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"40x40","expected-size":"80","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"40x40","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"60x60","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"57x57","expected-size":"57","filename":"57.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"29x29","expected-size":"29","filename":"29.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"87","filename":"87.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"57x57","expected-size":"114","filename":"114.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"60","filename":"60.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"1024x1024","filename":"1024.png","expected-size":"1024","idiom":"ios-marketing","folder":"Assets.xcassets/AppIcon.appiconset/","scale":"1x"},{"size":"40x40","expected-size":"80","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"72x72","expected-size":"72","filename":"72.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"76x76","expected-size":"152","filename":"152.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"50x50","expected-size":"100","filename":"100.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"76x76","expected-size":"76","filename":"76.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"29x29","expected-size":"29","filename":"29.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"50x50","expected-size":"50","filename":"50.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"72x72","expected-size":"144","filename":"144.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"40x40","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"83.5x83.5","expected-size":"167","filename":"167.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"20x20","expected-size":"20","filename":"20.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"20x20","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"idiom":"watch","filename":"172.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"38mm","scale":"2x","size":"86x86","expected-size":"172","role":"quickLook"},{"idiom":"watch","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"38mm","scale":"2x","size":"40x40","expected-size":"80","role":"appLauncher"},{"idiom":"watch","filename":"88.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"40mm","scale":"2x","size":"44x44","expected-size":"88","role":"appLauncher"},{"idiom":"watch","filename":"100.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"44mm","scale":"2x","size":"50x50","expected-size":"100","role":"appLauncher"},{"idiom":"watch","filename":"196.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"42mm","scale":"2x","size":"98x98","expected-size":"196","role":"quickLook"},{"idiom":"watch","filename":"216.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"44mm","scale":"2x","size":"108x108","expected-size":"216","role":"quickLook"},{"idiom":"watch","filename":"48.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"38mm","scale":"2x","size":"24x24","expected-size":"48","role":"notificationCenter"},{"idiom":"watch","filename":"55.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"42mm","scale":"2x","size":"27.5x27.5","expected-size":"55","role":"notificationCenter"},{"size":"29x29","expected-size":"87","filename":"87.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"watch","role":"companionSettings","scale":"3x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"watch","role":"companionSettings","scale":"2x"},{"size":"1024x1024","expected-size":"1024","filename":"1024.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"watch-marketing","scale":"1x"},{"size":"128x128","expected-size":"128","filename":"128.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"256x256","expected-size":"256","filename":"256.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"128x128","expected-size":"256","filename":"256.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"256x256","expected-size":"512","filename":"512.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"32x32","expected-size":"32","filename":"32.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"512x512","expected-size":"512","filename":"512.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"16x16","expected-size":"16","filename":"16.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"16x16","expected-size":"32","filename":"32.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"32x32","expected-size":"64","filename":"64.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"512x512","expected-size":"1024","filename":"1024.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"}]} -------------------------------------------------------------------------------- /Example/TerminalExample/ViewController/TerminalViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Terminal 3 | 4 | final class TerminalViewController: UIViewController, TerminalViewDelegate, KeyboardPickerDelegate { 5 | 6 | private static let initialFontSize = "9" 7 | private static let fontSizes: [String] = Array(1...50).map { String($0) } 8 | 9 | private static let initialCursorShape = TerminalCursorShape.beam.rawValue 10 | private static let cursorShapes: [String] = [ 11 | TerminalCursorShape.block.rawValue, 12 | TerminalCursorShape.beam.rawValue, 13 | TerminalCursorShape.underline.rawValue, 14 | ] 15 | 16 | private static let initialCursorBlink = "true" 17 | private static let cursorBlinks: [String] = ["true", "false"] 18 | 19 | private let terminalInputAccessoryView = InputAccessoryView.createFromNib() 20 | 21 | @IBOutlet private var terminalView: TerminalView! 22 | 23 | @IBOutlet private var fontSizePicker: KeyboardPicker! 24 | @IBOutlet private var cursorShapePicker: KeyboardPicker! 25 | @IBOutlet private var cursorBlinkPicker: KeyboardPicker! 26 | 27 | @IBOutlet private var terminalModeChangeButton: UIButton! 28 | 29 | override func viewDidLoad() { 30 | super.viewDidLoad() 31 | 32 | terminalInputAccessoryView?.terminalView = terminalView 33 | 34 | terminalView.delegate = self 35 | terminalView.bottomAnchor.constraint(equalTo: view.keyboardLayoutGuide.topAnchor, constant: -8.0).isActive = true // fail if write `@IBOutlet var didSet` scope 36 | 37 | fontSizePicker.dataSource = Self.fontSizes 38 | fontSizePicker.delegate = self 39 | fontSizePicker.selectedData = Self.initialFontSize 40 | 41 | cursorShapePicker.dataSource = Self.cursorShapes 42 | cursorShapePicker.delegate = self 43 | cursorShapePicker.selectedData = Self.initialCursorShape 44 | 45 | cursorBlinkPicker.dataSource = Self.cursorBlinks 46 | cursorBlinkPicker.delegate = self 47 | cursorBlinkPicker.selectedData = Self.initialCursorBlink 48 | 49 | terminalViewDidChangeMode(terminalView) 50 | } 51 | 52 | @IBAction private func handleTouchUpInsideClearScrollbackButton(_ sender: UIButton) { 53 | terminalView?.terminalClear(.clearScrollback) 54 | } 55 | 56 | @IBAction private func handleTouchUpInsideResetButton(_ sender: UIButton) { 57 | terminalView?.terminalClear(.reset) 58 | } 59 | 60 | @IBAction private func handleTouchUpInsideSoftResetButton(_ sender: UIButton) { 61 | terminalView?.terminalClear(.softReset) 62 | } 63 | 64 | @IBAction private func handleTouchUpInsideReload(_ sender: UIButton) { 65 | terminalView?.reloadTerminal() 66 | } 67 | 68 | @IBAction private func handleTouchUpInsideScrollHome(_ sender: UIButton) { 69 | terminalView?.terminalScroll(.home) 70 | } 71 | 72 | @IBAction private func handleTouchUpInsideScrollEnd(_ sender: UIButton) { 73 | terminalView?.terminalScroll(.end) 74 | } 75 | 76 | @IBAction private func handleTouchUpInsideScrollPageUp(_ sender: UIButton) { 77 | terminalView?.terminalScroll(.pageUp) 78 | } 79 | 80 | @IBAction private func handleTouchUpInsideScrollPageDown(_ sender: UIButton) { 81 | terminalView?.terminalScroll(.pageDown) 82 | } 83 | 84 | @IBAction private func handleTouchUpInsideScrollLineUp(_ sender: UIButton) { 85 | terminalView?.terminalScroll(.lineUp) 86 | } 87 | 88 | @IBAction private func handleTouchUpInsideScrollLineDown(_ sender: UIButton) { 89 | terminalView?.terminalScroll(.lineDown) 90 | } 91 | 92 | @IBAction private func handleTouchUpInsideModeButton(_ sender: UIButton) { 93 | guard let t = terminalView else { 94 | return 95 | } 96 | switch t.mode { 97 | case .input: t.enterScrollMode() 98 | case .scroll: t.enterInputMode() 99 | } 100 | } 101 | 102 | @IBAction private func handleTouchUpInsideEditableChangeButton(_ sender: UIButton) { 103 | guard let t = terminalView else { 104 | return 105 | } 106 | t.isTerminalContentEditable.toggle() 107 | sender.setTitle("Editable: \(t.isTerminalContentEditable)", for: .normal) 108 | } 109 | 110 | @IBAction private func handleTouchUpInsideGetVisibleTextButton(_ sender: UIButton) { 111 | terminalView?.getVisibleText { 112 | print($0) 113 | } 114 | } 115 | 116 | @IBAction private func handleTouchUpInsideGetAllTextButton(_ sender: UIButton) { 117 | terminalView?.getAllText { 118 | print($0) 119 | } 120 | } 121 | 122 | func terminalViewDidLoad(_ terminalView: TerminalView) { 123 | print("[TerminalViewController(TerminalViewDelegate).terminalViewDidLoad:]") 124 | } 125 | 126 | func terminalView(_ terminalView: TerminalView, didChangeTerminalSize oldSize: TerminalSize) { 127 | print("[TerminalViewController(TerminalViewDelegate).terminalView:didChangeTerminalSize:] oldSize: \(oldSize), currentSize: \(terminalView.terminalSize)") 128 | } 129 | 130 | func terminalView(_ terminalView: TerminalView, didHandleURL url: URL) { 131 | print("[TerminalViewController(TerminalViewDelegate).terminalView:didHandleURL:] url: \(url)") 132 | } 133 | 134 | func terminalView(_ terminalView: TerminalView, didHandleKeyInput string: String) { 135 | print("[TerminalViewController(TerminalViewDelegate).terminalView:didHandleKeyInput:] string: \(string)") 136 | if let data = string.data(using: .utf8) { 137 | terminalView.appendBuffer(data) 138 | } 139 | } 140 | 141 | func terminalView(_ terminalView: TerminalView, didHandleSendString string: String) { 142 | print("[TerminalViewController(TerminalViewDelegate).terminalView:didHandleSendString:] string: \(string)") 143 | if let data = string.data(using: .utf8) { 144 | terminalView.appendBuffer(data) 145 | } 146 | } 147 | 148 | func terminalViewDidHandleDeleteBackward(_ terminalView: TerminalView) { 149 | print("[TerminalViewController(TerminalViewDelegate).terminalViewDidHandleDeleteBackward:]") 150 | // terminalView.terminalErase(.toLeft) 151 | // terminalView.terminalClear(.clear) 152 | terminalView.press(.backspace) 153 | } 154 | 155 | func terminalView(_ terminalView: TerminalView, didHandleOnVTKeyStroke string: String) { 156 | print("[TerminalViewController(TerminalViewDelegate).terminalView:didHandleOnVTKeyStroke:] \(string)") 157 | if let data = string.data(using: .utf8) { 158 | terminalView.appendBuffer(data) 159 | } 160 | } 161 | 162 | func terminalViewRequestInputAccessoryView(_ terminalView: TerminalView) -> UIView? { 163 | print("[TerminalViewController(TerminalViewDelegate).terminalView:terminalViewRequestInputAccessoryView:]") 164 | return terminalInputAccessoryView 165 | } 166 | 167 | func terminalViewRequestInputAccessoryViewController(_ terminalView: TerminalView) -> UIInputViewController? { 168 | print("[TerminalViewController(TerminalViewDelegate).terminalView:terminalViewRequestInputAccessoryViewController:]") 169 | return nil 170 | } 171 | 172 | func terminalViewDidChangeMode(_ terminalView: TerminalView) { 173 | let title: String 174 | switch terminalView.mode { 175 | case .input: title = "Mode: input" 176 | case .scroll: title = "Mode: scroll" 177 | } 178 | terminalModeChangeButton.setTitle(title, for: .normal) 179 | } 180 | 181 | func keyboardPicker(_ keyboardPicker: KeyboardPicker, didSelect index: Int) { 182 | guard let terminalView = terminalView else { 183 | return 184 | } 185 | switch keyboardPicker { 186 | case fontSizePicker: 187 | let fontSize = UInt(keyboardPicker.selectedData, fallback: 9) 188 | terminalView.terminalFontSize = fontSize 189 | case cursorShapePicker: 190 | if let shape = TerminalCursorShape(rawValue: keyboardPicker.selectedData) { 191 | terminalView.terminalCursorShape = shape 192 | } 193 | case cursorBlinkPicker: 194 | let isBlink = keyboardPicker.selectedData == "true" 195 | terminalView.isTerminalCursorBlink = isBlink 196 | default: 197 | break 198 | } 199 | } 200 | 201 | } 202 | -------------------------------------------------------------------------------- /Example/TerminalExample/ViewController/SSHTextViewController.swift: -------------------------------------------------------------------------------- 1 | // respect: 2 | // https://github.com/NMSSH/NMSSH/blob/28a3b8a2c9caf5a15d37c5c6dd9d415dda6a03f4/Examples/PTYExample/PTYExample/NMTerminalViewController.m 3 | 4 | import UIKit 5 | import NMSSH 6 | 7 | final class SSHTextViewController: UIViewController { 8 | 9 | fileprivate var lastCommand: String = "" 10 | 11 | fileprivate let sshQueue = DispatchQueue(label: "NMSSH.queue.SSHTextViewController") 12 | fileprivate var session: NMSSHSession? 13 | fileprivate var channel: NMSSHChannel? { session?.channel } 14 | 15 | @IBOutlet private var userNameTextField: UITextField! 16 | @IBOutlet private var hostNameTextField: UITextField! 17 | @IBOutlet private var passwordTextField: UITextField! 18 | @IBOutlet private var textView: UITextView! 19 | @IBOutlet private var actionBarButtonItem: UIBarButtonItem! 20 | 21 | override func viewDidLoad() { 22 | super.viewDidLoad() 23 | hostNameTextField?.text = UserDefaults.standard.string(forKey: kSSHHostNameDefaultsKey) 24 | } 25 | 26 | override func viewDidDisappear(_ animated: Bool) { 27 | super.viewDidDisappear(animated) 28 | disconnect() 29 | } 30 | 31 | fileprivate func changeTextViewEditableOnMainThread(_ isEditable: Bool) { 32 | let execute: () -> Void = { [weak self] in 33 | guard let textView = self?.textView else { 34 | return 35 | } 36 | textView.isEditable = isEditable 37 | } 38 | if Thread.isMainThread { 39 | execute() 40 | } else { 41 | DispatchQueue.main.async(execute: execute) 42 | } 43 | } 44 | 45 | fileprivate func appendTextOnMainThread(_ text: String) { 46 | let execute: () -> Void = { [weak self] in 47 | guard let textView = self?.textView, let currentText = textView.text else { 48 | return 49 | } 50 | let newText = currentText + text 51 | textView.text = newText 52 | textView.scrollRangeToVisible(NSRange(location: newText.count - 1, length: 1)) 53 | textView.selectedRange = NSRange(location: newText.count, length: 0) 54 | } 55 | if Thread.isMainThread { 56 | execute() 57 | } 58 | DispatchQueue.main.async(execute: execute) 59 | } 60 | 61 | fileprivate func changeBarButtonItemTitleOnMainThread(_ title: String) { 62 | let execute: () -> Void = { [weak self] in 63 | guard let actionBarButtonItem = self?.actionBarButtonItem else { 64 | return 65 | } 66 | actionBarButtonItem.title = title 67 | } 68 | if Thread.isMainThread { 69 | execute() 70 | } else { 71 | DispatchQueue.main.async(execute: execute) 72 | } 73 | } 74 | 75 | @IBAction private func handleActionBarButtonItem(_ sender: UIBarButtonItem) { 76 | if session?.isConnected ?? false { 77 | disconnect() 78 | } else { 79 | connect() 80 | } 81 | } 82 | 83 | private func connect() { 84 | changeTextViewEditableOnMainThread(false) 85 | guard let user = userNameTextField?.text, let host = hostNameTextField?.text, let pass = passwordTextField?.text else { 86 | return 87 | } 88 | 89 | UserDefaults.standard.set(host, forKey: kSSHHostNameDefaultsKey) 90 | UserDefaults.standard.synchronize() 91 | 92 | sshQueue.async { 93 | let session = NMSSHSession.connect(toHost: host, withUsername: user) 94 | self.session = session 95 | session.delegate = self 96 | if session.isConnected == false { 97 | self.appendTextOnMainThread("[ERROR] connection failed.\n") 98 | self.changeTextViewEditableOnMainThread(false) 99 | return 100 | } 101 | 102 | self.appendTextOnMainThread("[INFO] connected: " + session.username + "@" + host + "\n") 103 | 104 | session.authenticate(byPassword: pass) 105 | if session.isAuthorized == false { 106 | self.appendTextOnMainThread("[ERROR] authentication failed.\n") 107 | self.changeTextViewEditableOnMainThread(false) 108 | return 109 | } 110 | 111 | self.appendTextOnMainThread("[INFO] authenticate success.\n") 112 | 113 | session.channel.delegate = self 114 | session.channel.requestPty = true 115 | 116 | do { 117 | try session.channel.startShell() 118 | self.changeTextViewEditableOnMainThread(true) 119 | self.changeBarButtonItemTitleOnMainThread("Disconnect") 120 | } catch let e { 121 | let message = "[ERROR] " + e.localizedDescription + "\n" 122 | self.appendTextOnMainThread(message) 123 | self.changeTextViewEditableOnMainThread(false) 124 | } 125 | } 126 | } 127 | 128 | private func disconnect() { 129 | sshQueue.async { 130 | if let session = self.session { 131 | session.disconnect() 132 | } 133 | self.session = nil // 使い回すとおかしくなるっぽい。 134 | } 135 | changeTextViewEditableOnMainThread(false) 136 | self.changeBarButtonItemTitleOnMainThread("Connect") 137 | } 138 | 139 | fileprivate func performCommand() { 140 | let command = lastCommand 141 | sshQueue.async { 142 | guard let channel = self.channel else { 143 | return 144 | } 145 | var error: NSError? 146 | channel.write(command, error: &error, timeout: NSNumber(value: 10)) 147 | } 148 | lastCommand = "" 149 | } 150 | 151 | } 152 | 153 | extension SSHTextViewController: NMSSHSessionDelegate { 154 | 155 | func session(_ session: NMSSHSession, shouldConnectToHostWithFingerprint fingerprint: String) -> Bool { 156 | self.appendTextOnMainThread("[INFO][session:shouldConnectToHostWithFingerprint:] fingerprint: \(fingerprint)\n") 157 | return true 158 | } 159 | 160 | /* 161 | func session(_ session: NMSSHSession, keyboardInteractiveRequest request: String) -> String { 162 | return password 163 | } 164 | */ 165 | 166 | func session(_ session: NMSSHSession, didDisconnectWithError error: Error) { 167 | let message = "\n[ERROR] " + error.localizedDescription + "\n" 168 | self.appendTextOnMainThread(message) 169 | self.changeTextViewEditableOnMainThread(false) 170 | self.changeBarButtonItemTitleOnMainThread("Connect") 171 | } 172 | 173 | } 174 | 175 | extension SSHTextViewController: NMSSHChannelDelegate { 176 | 177 | func channel(_ channel: NMSSHChannel, didReadData message: String) { 178 | let replace = message.replacingOccurrences(of: "\r", with: "") 179 | appendTextOnMainThread(replace) 180 | } 181 | 182 | func channel(_ channel: NMSSHChannel, didReadError error: String) { 183 | let replace = error.replacingOccurrences(of: "\r", with: "") 184 | appendTextOnMainThread("\n[ERROR] \(replace)\n") 185 | } 186 | 187 | func channelShellDidClose(_ channel: NMSSHChannel) { 188 | appendTextOnMainThread("\n[INFO] Shell Closed.\n") 189 | changeTextViewEditableOnMainThread(false) 190 | } 191 | 192 | } 193 | 194 | extension SSHTextViewController: UITextViewDelegate { 195 | 196 | func textViewDidChange(_ textView: UITextView) { 197 | textView.scrollRangeToVisible(NSRange(location: textView.text.count - 1, length: 1)) 198 | } 199 | 200 | /* 201 | func textViewDidChangeSelection(_ textView: UITextView) { 202 | // textView.selectedRange = NSRange(location: textView.text.count, length: 0) 203 | } 204 | */ 205 | 206 | func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { 207 | if range.location == textView.text.count - 1 && range.length == 1 && text.count == 0 { 208 | if lastCommand.count > 0 { 209 | lastCommand.removeLast() 210 | return true 211 | } else { 212 | return false 213 | } 214 | } else if range.location != textView.text.count { 215 | return false 216 | } 217 | 218 | lastCommand.append(text) 219 | if text == "\n" { 220 | performCommand() 221 | } 222 | return true 223 | } 224 | 225 | } 226 | -------------------------------------------------------------------------------- /Example/TerminalExample/ViewController/SSHTerminalViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import NMSSH 3 | import Terminal 4 | 5 | func debugLog(fileName: String = #file, line: Int = #line, functionName: String = #function, _ msg: String = "") { 6 | #if DEBUG 7 | print("\(Date().description) [\(fileName.split(separator: "/").last ?? "") @L\(line) \(functionName)] " + msg) 8 | #endif 9 | } 10 | 11 | final class SSHTerminalViewController: UIViewController { 12 | 13 | fileprivate let sshQueue = DispatchQueue(label: "NMSSH.queue.SSHTerminalViewController") 14 | fileprivate var session: NMSSHSession? 15 | fileprivate var channel: NMSSHChannel? { session?.channel } 16 | 17 | private static let initialFontSize = "9" 18 | private static let fontSizes: [String] = Array(1...50).map { String($0) } 19 | 20 | private static let initialCursorShape = TerminalCursorShape.beam.rawValue 21 | private static let cursorShapes: [String] = [ 22 | TerminalCursorShape.block.rawValue, 23 | TerminalCursorShape.beam.rawValue, 24 | TerminalCursorShape.underline.rawValue, 25 | ] 26 | 27 | private static let initialCursorBlink = "true" 28 | private static let cursorBlinks: [String] = ["true", "false"] 29 | 30 | private let terminalInputAccessoryView = InputAccessoryView.createFromNib() 31 | 32 | @IBOutlet private var userNameTextField: UITextField! 33 | @IBOutlet private var hostNameTextField: UITextField! 34 | @IBOutlet private var passwordTextField: UITextField! 35 | @IBOutlet private var actionBarButtonItem: UIBarButtonItem! 36 | 37 | @IBOutlet fileprivate var terminalView: TerminalView! 38 | 39 | @IBOutlet fileprivate var fontSizePicker: KeyboardPicker! 40 | @IBOutlet fileprivate var cursorShapePicker: KeyboardPicker! 41 | @IBOutlet fileprivate var cursorBlinkPicker: KeyboardPicker! 42 | 43 | @IBOutlet private var terminalModeChangeButton: UIButton! 44 | 45 | override func viewDidLoad() { 46 | super.viewDidLoad() 47 | 48 | hostNameTextField?.text = UserDefaults.standard.string(forKey: kSSHHostNameDefaultsKey) 49 | 50 | terminalInputAccessoryView?.terminalView = terminalView 51 | 52 | terminalView.delegate = self 53 | terminalView.bottomAnchor.constraint(equalTo: view.keyboardLayoutGuide.topAnchor).isActive = true // fail if write `@IBOutlet var didSet` scope 54 | 55 | fontSizePicker.dataSource = Self.fontSizes 56 | fontSizePicker.delegate = self 57 | fontSizePicker.selectedData = Self.initialFontSize 58 | 59 | cursorShapePicker.dataSource = Self.cursorShapes 60 | cursorShapePicker.delegate = self 61 | cursorShapePicker.selectedData = Self.initialCursorShape 62 | 63 | cursorBlinkPicker.dataSource = Self.cursorBlinks 64 | cursorBlinkPicker.delegate = self 65 | cursorBlinkPicker.selectedData = Self.initialCursorBlink 66 | 67 | terminalViewDidChangeMode(terminalView) 68 | } 69 | 70 | override func viewDidDisappear(_ animated: Bool) { 71 | super.viewDidDisappear(animated) 72 | disconnect() 73 | } 74 | 75 | fileprivate func changeBarButtonItemTitleOnMainThread(_ title: String) { 76 | let execute: () -> Void = { [weak self] in 77 | guard let actionBarButtonItem = self?.actionBarButtonItem else { 78 | return 79 | } 80 | actionBarButtonItem.title = title 81 | } 82 | if Thread.isMainThread { 83 | execute() 84 | } else { 85 | DispatchQueue.main.async(execute: execute) 86 | } 87 | } 88 | 89 | // MARK: - IBActions 90 | 91 | @IBAction private func handleActionBarButtonItem(_ sender: UIBarButtonItem) { 92 | if session?.isConnected ?? false { 93 | disconnect() 94 | } else { 95 | connect() 96 | } 97 | } 98 | 99 | @IBAction private func handleTouchUpInsideClearScrollbackButton(_ sender: UIButton) { 100 | terminalView?.terminalClear(.clearScrollback) 101 | } 102 | 103 | @IBAction private func handleTouchUpInsideResetButton(_ sender: UIButton) { 104 | terminalView?.terminalClear(.reset) 105 | } 106 | 107 | @IBAction private func handleTouchUpInsideSoftResetButton(_ sender: UIButton) { 108 | terminalView?.terminalClear(.softReset) 109 | } 110 | 111 | @IBAction private func handleTouchUpInsideReload(_ sender: UIButton) { 112 | actionBarButtonItem?.isEnabled = false 113 | disconnect() 114 | terminalView?.reloadTerminal() 115 | } 116 | 117 | @IBAction private func handleTouchUpInsideScrollHome(_ sender: UIButton) { 118 | terminalView?.terminalScroll(.home) 119 | } 120 | 121 | @IBAction private func handleTouchUpInsideScrollEnd(_ sender: UIButton) { 122 | terminalView?.terminalScroll(.end) 123 | } 124 | 125 | @IBAction private func handleTouchUpInsideScrollPageUp(_ sender: UIButton) { 126 | terminalView?.terminalScroll(.pageUp) 127 | } 128 | 129 | @IBAction private func handleTouchUpInsideScrollPageDown(_ sender: UIButton) { 130 | terminalView?.terminalScroll(.pageDown) 131 | } 132 | 133 | @IBAction private func handleTouchUpInsideScrollLineUp(_ sender: UIButton) { 134 | terminalView?.terminalScroll(.lineUp) 135 | } 136 | 137 | @IBAction private func handleTouchUpInsideScrollLineDown(_ sender: UIButton) { 138 | terminalView?.terminalScroll(.lineDown) 139 | } 140 | 141 | @IBAction private func handleTouchUpInsideModeButton(_ sender: UIButton) { 142 | guard let t = terminalView else { 143 | return 144 | } 145 | switch t.mode { 146 | case .input: t.enterScrollMode() 147 | case .scroll: t.enterInputMode() 148 | } 149 | } 150 | 151 | @IBAction private func handleTouchUpInsideEditableChangeButton(_ sender: UIButton) { 152 | guard let t = terminalView else { 153 | return 154 | } 155 | t.isTerminalContentEditable.toggle() 156 | sender.setTitle("Editable: \(t.isTerminalContentEditable)", for: .normal) 157 | } 158 | 159 | @IBAction private func handleTouchUpInsideGetVisibleTextButton(_ sender: UIButton) { 160 | terminalView?.getVisibleText { 161 | print($0) 162 | } 163 | } 164 | 165 | @IBAction private func handleTouchUpInsideGetAllTextButton(_ sender: UIButton) { 166 | terminalView?.getAllText { 167 | print($0) 168 | } 169 | } 170 | 171 | // MARK: - SSH Connection 172 | 173 | private func connect() { 174 | guard let user = userNameTextField?.text, let host = hostNameTextField?.text, let pass = passwordTextField?.text else { 175 | return 176 | } 177 | 178 | UserDefaults.standard.set(host, forKey: kSSHHostNameDefaultsKey) 179 | UserDefaults.standard.synchronize() 180 | 181 | let s = terminalView.terminalSize 182 | let w = UInt(s.cols) 183 | let h = UInt(s.rows) 184 | 185 | sshQueue.async { 186 | let session = NMSSHSession.connect(toHost: host, withUsername: user) 187 | self.session = session 188 | session.delegate = self 189 | if session.isConnected == false { 190 | debugLog("[ERROR] connection failed.") 191 | return 192 | } 193 | debugLog("[INFO] connected: " + session.username + "@" + host) 194 | 195 | session.authenticate(byPassword: pass) 196 | if session.isAuthorized == false { 197 | debugLog("[ERROR] authentication failed.") 198 | return 199 | } 200 | debugLog("[INFO] authenticate success.") 201 | 202 | session.channel.ptyTerminalType = .xterm 203 | session.channel.requestPty = true 204 | session.channel.delegate = self 205 | // requestSizeWidth() is not here. should be after startShell() 206 | 207 | do { 208 | try session.channel.startShell() 209 | 210 | let success = session.channel.requestSizeWidth(w, height: h) 211 | debugLog("[INFO] requestSizeWidth success: \(success)") 212 | 213 | self.changeBarButtonItemTitleOnMainThread("Disconnect") 214 | } catch let e { 215 | let message = "[ERROR] " + e.localizedDescription 216 | debugLog(message) 217 | } 218 | } 219 | } 220 | 221 | private func disconnect() { 222 | sshQueue.async { 223 | if let session = self.session { 224 | session.disconnect() 225 | } 226 | self.session = nil // need clear. should not reuse. 227 | } 228 | changeBarButtonItemTitleOnMainThread("Connect") 229 | } 230 | 231 | fileprivate func writeSSHChannel(_ string: String) { 232 | sshQueue.async { 233 | guard let channel = self.channel else { 234 | return 235 | } 236 | var error: NSError? 237 | channel.write(string, error: &error, timeout: NSNumber(value: 10)) 238 | } 239 | } 240 | 241 | fileprivate func writeSSHChannel(_ data: Data) { 242 | sshQueue.async { 243 | guard let channel = self.channel else { 244 | return 245 | } 246 | var error: NSError? 247 | channel.write(data, error: &error, timeout: NSNumber(value: 10)) 248 | } 249 | } 250 | 251 | } 252 | 253 | extension SSHTerminalViewController: NMSSHSessionDelegate { 254 | 255 | func session(_ session: NMSSHSession, shouldConnectToHostWithFingerprint fingerprint: String) -> Bool { 256 | debugLog("[INFO] fingerprint: \(fingerprint)") 257 | return true 258 | } 259 | 260 | func session(_ session: NMSSHSession, didDisconnectWithError error: Error) { 261 | let message = "[ERROR] " + error.localizedDescription 262 | debugLog(message) 263 | changeBarButtonItemTitleOnMainThread("Connect") 264 | } 265 | 266 | } 267 | 268 | extension SSHTerminalViewController: NMSSHChannelDelegate { 269 | 270 | func channel(_ channel: NMSSHChannel, didReadRawData data: Data) { 271 | terminalView.appendBuffer(data) 272 | } 273 | 274 | func channel(_ channel: NMSSHChannel, didReadRawError error: Data) { 275 | terminalView.appendBuffer(error) 276 | } 277 | 278 | func channelShellDidClose(_ channel: NMSSHChannel) { 279 | debugLog("[INFO] Shell Closed.") 280 | } 281 | 282 | } 283 | 284 | extension SSHTerminalViewController: TerminalViewDelegate { 285 | 286 | func terminalViewDidLoad(_ terminalView: TerminalView) { 287 | actionBarButtonItem.isEnabled = true 288 | } 289 | 290 | func terminalView(_ terminalView: TerminalView, didChangeTerminalSize oldSize: TerminalSize) { 291 | guard let channel = channel else { 292 | return 293 | } 294 | let s = terminalView.terminalSize 295 | let w = UInt(s.cols) 296 | let h = UInt(s.rows) 297 | let success = channel.requestSizeWidth(w, height: h) 298 | debugLog("oldSize: \(oldSize), currentSize: \(terminalView.terminalSize), success: \(success)") 299 | } 300 | 301 | func terminalView(_ terminalView: TerminalView, didHandleURL url: URL) { 302 | } 303 | 304 | func terminalView(_ terminalView: TerminalView, didHandleKeyInput string: String) { 305 | writeSSHChannel(string) 306 | } 307 | 308 | func terminalViewDidHandleDeleteBackward(_ terminalView: TerminalView) { 309 | terminalView.press(.backspace) 310 | } 311 | 312 | func terminalView(_ terminalView: TerminalView, didHandleSendString string: String) { 313 | writeSSHChannel(string) 314 | } 315 | 316 | func terminalView(_ terminalView: TerminalView, didHandleOnVTKeyStroke string: String) { 317 | writeSSHChannel(string) 318 | } 319 | 320 | func terminalViewRequestInputAccessoryView(_ terminalView: TerminalView) -> UIView? { 321 | terminalInputAccessoryView 322 | } 323 | 324 | func terminalViewRequestInputAccessoryViewController(_ terminalView: TerminalView) -> UIInputViewController? { 325 | nil 326 | } 327 | 328 | func terminalViewDidChangeMode(_ terminalView: TerminalView) { 329 | let title: String 330 | switch terminalView.mode { 331 | case .input: title = "Mode: input" 332 | case .scroll: title = "Mode: scroll" 333 | } 334 | terminalModeChangeButton.setTitle(title, for: .normal) 335 | } 336 | 337 | } 338 | 339 | extension SSHTerminalViewController: KeyboardPickerDelegate { 340 | func keyboardPicker(_ keyboardPicker: KeyboardPicker, didSelect index: Int) { 341 | guard let terminalView = terminalView else { 342 | return 343 | } 344 | switch keyboardPicker { 345 | case fontSizePicker: 346 | let fontSize = UInt(keyboardPicker.selectedData, fallback: 9) 347 | terminalView.terminalFontSize = fontSize 348 | case cursorShapePicker: 349 | if let shape = TerminalCursorShape(rawValue: keyboardPicker.selectedData) { 350 | terminalView.terminalCursorShape = shape 351 | } 352 | case cursorBlinkPicker: 353 | let isBlink = keyboardPicker.selectedData == "true" 354 | terminalView.isTerminalCursorBlink = isBlink 355 | default: 356 | break 357 | } 358 | } 359 | } 360 | -------------------------------------------------------------------------------- /Example/TerminalExample/InputAccessoryView/InputAccessoryView.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 35 | 50 | 65 | 80 | 95 | 110 | 125 | 140 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | -------------------------------------------------------------------------------- /Sources/Terminal/Internal/HtermWebView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import WebKit 3 | 4 | private func onMain(execute: @escaping (() -> Void)) { 5 | if Thread.isMainThread { 6 | execute() 7 | } else { 8 | DispatchQueue.main.async(execute: execute) 9 | } 10 | } 11 | 12 | final class HtermWebView: WKWebView { 13 | 14 | private(set) var isHtermLoaded: Bool = false { 15 | didSet { 16 | if isHtermLoaded == true { 17 | if oldValue == false { 18 | reloadHtermStyles() 19 | } 20 | if let parentTerminalView = parentTerminalView { 21 | parentTerminalView.delegate?.terminalViewDidLoad(parentTerminalView) 22 | } 23 | } 24 | } 25 | } 26 | 27 | private(set) var terminalSize: TerminalSize = .zero { 28 | didSet { 29 | if oldValue != terminalSize, let parentTerminalView = parentTerminalView { 30 | parentTerminalView.delegate?.terminalView(parentTerminalView, didChangeTerminalSize: oldValue) 31 | } 32 | } 33 | } 34 | 35 | private(set) var isHtermFocused: Bool = false 36 | 37 | var isContentEditable: Bool = false { 38 | didSet { 39 | if isHtermLoaded, oldValue != isContentEditable { 40 | evaluateJavaScript("term.scrollPort_.getScreenNode().contentEditable = \(isContentEditable)") 41 | } 42 | } 43 | } 44 | 45 | override var backgroundColor: UIColor? { 46 | didSet { 47 | if isHtermLoaded, oldValue != backgroundColor { 48 | evaluateOneArgumentJavaScript(functionName: "term.setBackgroundColor", arg: (backgroundColor ?? .clear).cssString) 49 | } 50 | } 51 | } 52 | 53 | var foregroundColor: UIColor = .clear { 54 | didSet { 55 | if isHtermLoaded, oldValue != foregroundColor { 56 | evaluateOneArgumentJavaScript(functionName: "term.setForegroundColor", arg: foregroundColor.cssString) 57 | } 58 | } 59 | } 60 | 61 | var cursorColor: UIColor = .clear { 62 | didSet { 63 | if isHtermLoaded, oldValue != cursorColor { 64 | evaluateOneArgumentJavaScript(functionName: "term.setCursorColor", arg: cursorColor.cssString) 65 | } 66 | } 67 | } 68 | 69 | var isCursorBlink: Bool = false { 70 | didSet { 71 | if isHtermLoaded, oldValue != isCursorBlink { 72 | evaluateOneArgumentJavaScript(functionName: "term.setCursorBlink", arg: isCursorBlink) 73 | } 74 | } 75 | } 76 | 77 | var cursorShape: TerminalCursorShape = .beam { 78 | didSet { 79 | if isHtermLoaded, oldValue != cursorShape { 80 | evaluateOneArgumentJavaScript(functionName: "term.setCursorShape", arg: cursorShape.rawValue) 81 | } 82 | } 83 | } 84 | 85 | var fontFamily: String = "Menlo" { 86 | didSet { 87 | if isHtermLoaded, oldValue != fontFamily { 88 | evaluateOneArgumentJavaScript(functionName: "exports.setFontFamily", arg: fontFamily) 89 | } 90 | } 91 | } 92 | 93 | var fontSize: UInt = 9 { 94 | didSet { 95 | if isHtermLoaded, oldValue != fontSize, 2 <= fontSize, fontSize <= 100 { 96 | evaluateOneArgumentJavaScript(functionName: "term.setFontSize", arg: fontSize) 97 | } 98 | } 99 | } 100 | 101 | var isEnableBold: Bool? { 102 | didSet { 103 | if isHtermLoaded, oldValue != isEnableBold { 104 | evaluateOneArgumentJavaScript(functionName: "exports.setEnableBold", arg: isEnableBold) 105 | } 106 | } 107 | } 108 | 109 | var isEnableBoldAsBright: Bool = true { 110 | didSet { 111 | if isHtermLoaded, oldValue != isEnableBoldAsBright { 112 | evaluateOneArgumentJavaScript(functionName: "exports.setEnableBoldAsBright", arg: isEnableBoldAsBright) 113 | } 114 | } 115 | } 116 | 117 | // MARK: - Private Vars 118 | 119 | weak var parentTerminalView: TerminalView? 120 | 121 | fileprivate var shouldEnterInputModeWhenJSTouchEnd: Bool = false 122 | 123 | // MARK: - Initializer 124 | 125 | deinit { 126 | for bridgingFunction in HtermBridgingFunction.allCases { 127 | let name = bridgingFunction.rawValue 128 | configuration.userContentController.removeScriptMessageHandler(forName: name) 129 | } 130 | } 131 | 132 | @available(*, unavailable) 133 | required init?(coder: NSCoder) { 134 | fatalError("init?(code: NSCode) is forbidden. use init(frame: CGRect) instead") 135 | } 136 | 137 | @available(*, unavailable) 138 | override init(frame: CGRect, configuration: WKWebViewConfiguration) { 139 | fatalError("init?(frame: CGRect, configuration: WKWebViewConfiguration) is forbidden. use init(frame: CGRect) instead") 140 | } 141 | 142 | init(frame: CGRect) { 143 | let configuration = WKWebViewConfiguration.htermConfiguration() 144 | super.init(frame: frame, configuration: configuration) 145 | 146 | isOpaque = false 147 | isUserInteractionEnabled = false 148 | scrollView.isScrollEnabled = false 149 | scrollView.delaysContentTouches = false 150 | scrollView.panGestureRecognizer.isEnabled = false 151 | backgroundColor = .clear // default 152 | scrollView.backgroundColor = .clear 153 | 154 | allowsLinkPreview = false 155 | 156 | for bridgingFunction in HtermBridgingFunction.allCases { 157 | let name = bridgingFunction.rawValue 158 | configuration.userContentController.add(self, name: name) 159 | } 160 | } 161 | 162 | // MARK: - UIResponder 163 | 164 | override var canBecomeFirstResponder: Bool { false } 165 | override var canResignFirstResponder: Bool { false } 166 | // override var canBecomeFocused: Bool { false } 167 | 168 | override public func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool { 169 | if isHtermLoaded == false { 170 | return false 171 | } 172 | switch action { 173 | case #selector(UIResponderStandardEditActions.copy(_:)): 174 | return super.canPerformAction(action, withSender: sender) 175 | case #selector(UIResponderStandardEditActions.select(_:)): 176 | return super.canPerformAction(action, withSender: sender) 177 | case #selector(UIResponderStandardEditActions.selectAll(_:)): 178 | return super.canPerformAction(action, withSender: sender) 179 | case #selector(UIResponderStandardEditActions.cut(_:)): 180 | return false 181 | case #selector(UIResponderStandardEditActions.paste(_:)): 182 | return false 183 | case #selector(UIResponderStandardEditActions.delete(_:)): 184 | return false 185 | case #selector(UIResponderStandardEditActions.increaseSize(_:)): 186 | return false 187 | case #selector(UIResponderStandardEditActions.decreaseSize(_:)): 188 | return false 189 | default: 190 | // debugLog("action: \(action.description), sender: \(sender.debugDescription )") 191 | // return super.canPerformAction(action, withSender: sender) 192 | return false 193 | } 194 | } 195 | 196 | // MARK: - UIResponderStandardEditActions 197 | 198 | override public func copy(_ sender: Any?) { 199 | guard isHtermLoaded else { 200 | return 201 | } 202 | evaluateJavaScript("term.getSelectionText()") { (result: Any?, error: Error?) -> Void in 203 | if let error = error { 204 | print("[ERROR] [HtermWebView.htermCopyAll(): term.getSelectionText()] error: \(error)") 205 | return 206 | } 207 | guard let result = result as? String else { 208 | print("[ERROR] [HtermWebView.htermCopyAll(): term.getSelectionText] result is not String") 209 | return 210 | } 211 | UIPasteboard.general.string = result 212 | } 213 | } 214 | 215 | override func select(_ sender: Any?) { 216 | guard isHtermLoaded else { 217 | return 218 | } 219 | super.select(sender) 220 | } 221 | 222 | override func selectAll(_ sender: Any?) { 223 | guard isHtermLoaded else { 224 | return 225 | } 226 | super.selectAll(sender) 227 | } 228 | 229 | // MARK: - UITraitEnvironment 230 | 231 | override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { 232 | super.traitCollectionDidChange(previousTraitCollection) 233 | if #available(iOS 13.0, *) { 234 | if traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) { 235 | reloadHtermStyles() 236 | } 237 | } 238 | } 239 | 240 | // MARK: - Custom Methods 241 | 242 | private func loadBundleHTML() { 243 | #if SWIFT_PACKAGE && swift(>=5.3) 244 | let bundle = Bundle.module 245 | #else 246 | let bundle = Bundle(for: Self.self) 247 | #endif 248 | guard let url = bundle.url(forResource: "hterm", withExtension: "html") else { 249 | fatalError("[FATAL] could not get 'hterm.html' file url in Bundle.") 250 | } 251 | loadFileURL(url, allowingReadAccessTo: url) 252 | } 253 | 254 | private func reloadHtermStyles() { 255 | guard isHtermLoaded else { 256 | return 257 | } 258 | evaluateOneArgumentJavaScript(functionName: "term.setBackgroundColor", arg: (backgroundColor ?? .clear).cssString) 259 | evaluateOneArgumentJavaScript(functionName: "term.setForegroundColor", arg: foregroundColor.cssString) 260 | evaluateOneArgumentJavaScript(functionName: "term.setCursorColor", arg: cursorColor.cssString) 261 | evaluateOneArgumentJavaScript(functionName: "term.setFontSize", arg: fontSize) 262 | evaluateOneArgumentJavaScript(functionName: "term.setCursorBlink", arg: isCursorBlink) 263 | evaluateOneArgumentJavaScript(functionName: "term.setCursorShape", arg: cursorShape.rawValue) 264 | evaluateOneArgumentJavaScript(functionName: "exports.setFontFamily", arg: fontFamily) 265 | } 266 | 267 | func reloadHterm() { 268 | terminalSize = .zero 269 | htermBlur() 270 | isHtermLoaded = false 271 | stopLoading() 272 | loadBundleHTML() 273 | // reloadHtermStyles will call isHtermLoaded's didSet 274 | } 275 | 276 | } 277 | 278 | // MARK: - JavaScript -> Native 279 | 280 | // for `webkit.messageHandlers[prop].postMessage(args);` 281 | private enum HtermBridgingFunction: String, CaseIterable { 282 | case log 283 | 284 | case htermDidLoad 285 | 286 | case htermDidFocusScreen 287 | case htermDidBlurScreen 288 | 289 | case htermScrollPortDidTouchStart 290 | case htermScrollPortDidTouchMove 291 | case htermScrollPortDidTouchEnd 292 | case htermScrollPortDidTouchCancel 293 | 294 | case htermDidHandleURL 295 | 296 | case htermHandleSendString 297 | case htermHandleOnVTKeyStroke 298 | case htermHandleOnTerminalResize 299 | } 300 | 301 | extension HtermWebView: WKScriptMessageHandler { 302 | func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { 303 | guard let bridgingFunction = HtermBridgingFunction(rawValue: message.name) else { 304 | return 305 | } 306 | onMain { 307 | switch bridgingFunction { 308 | case .log: 309 | print("\(message.body)") 310 | case .htermDidLoad: 311 | self.isHtermLoaded = true 312 | self.setNeedsLayout() 313 | case .htermDidFocusScreen: 314 | self.isHtermFocused = true 315 | case .htermDidBlurScreen: 316 | self.isHtermFocused = false 317 | case .htermScrollPortDidTouchStart: 318 | self.shouldEnterInputModeWhenJSTouchEnd = true 319 | case .htermScrollPortDidTouchMove: 320 | self.shouldEnterInputModeWhenJSTouchEnd = false 321 | case .htermScrollPortDidTouchEnd: 322 | if self.shouldEnterInputModeWhenJSTouchEnd { 323 | self.shouldEnterInputModeWhenJSTouchEnd = false 324 | if let parentTerminalView = self.parentTerminalView { 325 | parentTerminalView.enterInputMode() 326 | } 327 | } 328 | case .htermScrollPortDidTouchCancel: 329 | self.shouldEnterInputModeWhenJSTouchEnd = false 330 | case .htermDidHandleURL: 331 | if let body = message.body as? String, let url = URL(string: body), let parentTerminalView = self.parentTerminalView { 332 | parentTerminalView.delegate?.terminalView(parentTerminalView, didHandleURL: url) 333 | } 334 | case .htermHandleSendString: 335 | guard let string = message.body as? String else { 336 | break 337 | } 338 | if let parentTerminalView = self.parentTerminalView { 339 | parentTerminalView.delegate?.terminalView(parentTerminalView, didHandleSendString: string) 340 | } 341 | self.setUserGesture() 342 | case .htermHandleOnVTKeyStroke: 343 | guard let string = message.body as? String else { 344 | break 345 | } 346 | if let parentTerminalView = self.parentTerminalView { 347 | parentTerminalView.delegate?.terminalView(parentTerminalView, didHandleOnVTKeyStroke: string) 348 | } 349 | self.setUserGesture() 350 | case .htermHandleOnTerminalResize: 351 | guard let array = message.body as? [Any] else { 352 | break 353 | } 354 | guard array.count == 2 else { 355 | break 356 | } 357 | guard let cols = array[0] as? Int, let rows = array[1] as? Int else { 358 | break 359 | } 360 | self.terminalSize = TerminalSize(cols: cols, rows: rows) 361 | } 362 | } 363 | } 364 | } 365 | 366 | // MARK: - Native -> JavaScript 367 | 368 | enum HtermScrollFunction: String { 369 | case home = "term.scrollHome()" 370 | case end = "term.scrollEnd()" 371 | case pageUp = "term.scrollPageUp()" 372 | case pageDown = "term.scrollPageDown()" 373 | case lineUp = "term.scrollLineUp()" 374 | case lineDown = "term.scrollLineDown()" 375 | } 376 | 377 | enum HtermClearFunction: String { 378 | case clearScrollback = "term.clearScrollback()" 379 | case reset = "term.reset()" 380 | case softReset = "term.softReset()" 381 | case clearHome = "term.clearHome()" 382 | case clear = "term.clear()" 383 | } 384 | 385 | enum HtermEraseFunction: String { 386 | case toLeft = "term.eraseToLeft()" 387 | case toRight = "term.eraseToRight()" 388 | case line = "term.eraseLine()" 389 | case above = "term.eraseAbove()" 390 | case below = "term.eraseBelow()" 391 | } 392 | 393 | extension HtermWebView { 394 | 395 | func htermFocus() { 396 | guard isHtermLoaded && isHtermFocused == false else { 397 | return 398 | } 399 | evaluateJavaScript("term.focus()") { (_, error: Error?) -> Void in 400 | onMain { 401 | if let error = error { 402 | print("[ERROR] [HtermWebView.htermFocus()] error: \(error)") 403 | return 404 | } 405 | self.isHtermFocused = true 406 | } 407 | } 408 | } 409 | 410 | func htermBlur() { 411 | guard isHtermLoaded && isHtermFocused == true else { 412 | return 413 | } 414 | evaluateJavaScript("term.blur()") { (_, error: Error?) -> Void in 415 | onMain { 416 | if let error = error { 417 | print("[ERROR] [HtermWebView.htermBlur()] error: \(error)") 418 | return 419 | } 420 | self.isHtermFocused = false 421 | } 422 | } 423 | } 424 | 425 | func write(_ data: Data) { 426 | guard isHtermLoaded else { 427 | return 428 | } 429 | guard let str = String(data: data, encoding: .utf8) else { 430 | return 431 | } 432 | // hterm uses UTF-16 by version 1.85 or later. but I think it's JavaScript world... maybe safe `.utf8` 433 | // Do wee need `data = removeInvalidUTF8(data)` ? 434 | evaluateOneArgumentJavaScript(functionName: "exports.write", arg: str) 435 | } 436 | 437 | func syncTerminalSize() { 438 | guard isHtermLoaded else { 439 | return 440 | } 441 | evaluateJavaScript("exports.syncTerminalSize()") { (result: Any?, error: Error?) -> Void in 442 | if let error = error { 443 | print("[ERROR] [HtermWebView.syncTerminalSize()] error: \(error)") 444 | return 445 | } 446 | guard let result = result as? [Int] else { 447 | print("[ERROR] [HtermWebView.syncTerminalSize()] result is not Array") 448 | return 449 | } 450 | onMain { 451 | let cols = result[0] 452 | let rows = result[1] 453 | self.terminalSize = TerminalSize(cols: cols, rows: rows) 454 | } 455 | } 456 | } 457 | 458 | func clearSelection(completionHandler: ((Any?, Error?) -> Void)? = nil) { 459 | guard isHtermLoaded else { 460 | return 461 | } 462 | evaluateJavaScript("exports.clearSelection()", completionHandler: completionHandler) 463 | } 464 | 465 | func htermScroll(_ function: HtermScrollFunction) { 466 | guard isHtermLoaded else { 467 | return 468 | } 469 | evaluateJavaScript(function.rawValue) 470 | } 471 | 472 | func htermClear(_ function: HtermClearFunction) { 473 | guard isHtermLoaded else { 474 | return 475 | } 476 | evaluateJavaScript(function.rawValue) 477 | } 478 | 479 | func htermErase(_ function: HtermEraseFunction) { 480 | guard isHtermLoaded else { 481 | return 482 | } 483 | evaluateJavaScript(function.rawValue) 484 | } 485 | 486 | func setUserGesture() { 487 | guard isHtermLoaded else { 488 | return 489 | } 490 | evaluateJavaScript("exports.setUserGesture()") 491 | } 492 | 493 | func getVisibleText(callback: @escaping ((String) -> Void)) { 494 | guard isHtermLoaded else { 495 | callback("") 496 | return 497 | } 498 | evaluateJavaScript("exports.getVisibleText()") { (result: Any?, error: Error?) -> Void in 499 | onMain { 500 | if let error = error { 501 | print("[ERROR] [HtermWebView.getVisibleText(): exports.getVisibleText()] error: \(error)") 502 | callback("") 503 | return 504 | } 505 | guard let result = result as? String else { 506 | print("[ERROR] [HtermWebView.getVisibleText(): exports.getVisibleText()] result is not String") 507 | callback("") 508 | return 509 | } 510 | callback(result) 511 | } 512 | } 513 | } 514 | 515 | func getAllText(callback: @escaping ((String) -> Void)) { 516 | guard isHtermLoaded else { 517 | callback("") 518 | return 519 | } 520 | evaluateJavaScript("exports.getAllText()") { (result: Any?, error: Error?) -> Void in 521 | onMain { 522 | if let error = error { 523 | print("[ERROR] [HtermWebView.getAllText(): exports.getAllText()] error: \(error)") 524 | callback("") 525 | return 526 | } 527 | guard let result = result as? String else { 528 | print("[ERROR] [HtermWebView.getAllText(): exports.getAllText()] result is not String") 529 | callback("") 530 | return 531 | } 532 | callback(result) 533 | } 534 | } 535 | } 536 | 537 | } 538 | -------------------------------------------------------------------------------- /Sources/Terminal/Public/TerminalView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import WebKit 3 | 4 | public enum TerminalMode { 5 | case input 6 | case scroll 7 | } 8 | 9 | public enum TerminalCursorShape: String { 10 | case block = "BLOCK" 11 | case beam = "BEAM" 12 | case underline = "UNDERLINE" 13 | } 14 | 15 | public protocol TerminalViewDelegate: AnyObject { 16 | func terminalViewDidLoad(_ terminalView: TerminalView) 17 | 18 | func terminalView(_ terminalView: TerminalView, didChangeTerminalSize oldSize: TerminalSize) 19 | func terminalView(_ terminalView: TerminalView, didHandleURL url: URL) 20 | 21 | func terminalView(_ terminalView: TerminalView, didHandleKeyInput string: String) 22 | func terminalViewDidHandleDeleteBackward(_ terminalView: TerminalView) 23 | 24 | func terminalView(_ terminalView: TerminalView, didHandleSendString string: String) 25 | func terminalView(_ terminalView: TerminalView, didHandleOnVTKeyStroke string: String) 26 | 27 | func terminalViewRequestInputAccessoryView(_ terminalView: TerminalView) -> UIView? 28 | func terminalViewRequestInputAccessoryViewController(_ terminalView: TerminalView) -> UIInputViewController? 29 | 30 | func terminalViewDidChangeMode(_ terminalView: TerminalView) 31 | } 32 | 33 | public final class TerminalView: UIView { 34 | 35 | // MARK: - Public Vars 36 | 37 | public weak var delegate: TerminalViewDelegate? 38 | 39 | public var isTerminalLoaded: Bool { htermWebView?.isHtermLoaded ?? false } 40 | 41 | public var terminalSize: TerminalSize { htermWebView?.terminalSize ?? .zero } 42 | 43 | public private(set) var mode: TerminalMode = .input { 44 | didSet { 45 | if oldValue != mode { 46 | delegate?.terminalViewDidChangeMode(self) 47 | } 48 | } 49 | } 50 | 51 | // danger... be careful. 52 | public var isTerminalContentEditable: Bool { 53 | get { htermWebView?.isContentEditable ?? false } 54 | set { htermWebView?.isContentEditable = newValue } 55 | } 56 | 57 | public var isTerminalCursorBlink: Bool { 58 | get { htermWebView?.isCursorBlink ?? true } 59 | set { htermWebView?.isCursorBlink = newValue } 60 | } 61 | public var terminalCursorShape: TerminalCursorShape { 62 | get { htermWebView?.cursorShape ?? .beam } 63 | set { htermWebView?.cursorShape = newValue } 64 | } 65 | 66 | public var isControlKeyPressed: Bool = false 67 | public var isMetaKeyPressed: Bool = false 68 | 69 | public var useOptionKeyAsMetaKey: Bool = false { 70 | didSet { 71 | registerKeyCommands() 72 | } 73 | } 74 | 75 | @IBInspectable public var terminalBackgroundColor: UIColor? { 76 | get { htermWebView?.backgroundColor } 77 | set { htermWebView?.backgroundColor = newValue } 78 | } 79 | 80 | @IBInspectable public var terminalForegroundColor: UIColor? { 81 | get { htermWebView?.foregroundColor } 82 | set { htermWebView?.foregroundColor = (newValue ?? .clear) } 83 | } 84 | 85 | @IBInspectable public var terminalCursorColor: UIColor? { 86 | get { htermWebView?.cursorColor } 87 | set { htermWebView?.cursorColor = (newValue ?? .clear) } 88 | } 89 | 90 | @IBInspectable public var terminalFontFamily: String? { 91 | get { htermWebView?.fontFamily } 92 | set { htermWebView?.fontFamily = (newValue ?? "Menlo") } 93 | } 94 | 95 | @IBInspectable public var terminalFontSize: UInt { 96 | get { htermWebView?.fontSize ?? 9 } 97 | set { htermWebView?.fontSize = newValue } 98 | } 99 | 100 | // true or false or null. Null to autodetect. default null. 101 | // `nil` will be convert to JavaScript's `null` by JSONEncoder. This is safe. 102 | public var terminalIsEnableBold: Bool? { 103 | get { htermWebView?.isEnableBold } 104 | set { htermWebView?.isEnableBold = newValue } 105 | } 106 | 107 | public var terminalIsEnableBoldAsBright: Bool { 108 | get { htermWebView?.isEnableBoldAsBright ?? true } 109 | set { htermWebView?.isEnableBoldAsBright = newValue } 110 | } 111 | 112 | // MARK: - Public Vars for UITextInputTraits 113 | 114 | public var keyboardType: UIKeyboardType = .default 115 | public var keyboardAppearance: UIKeyboardAppearance = .default 116 | public var returnKeyType: UIReturnKeyType = .default 117 | public var isSecureTextEntry: Bool = false 118 | 119 | public var autocorrectionType: UITextAutocorrectionType = .no 120 | public var autocapitalizationType: UITextAutocapitalizationType = .none 121 | public var spellCheckingType: UITextSpellCheckingType = .no 122 | 123 | public var smartQuotesType: UITextSmartQuotesType = .no 124 | public var smartDashesType: UITextSmartDashesType = .no 125 | public var smartInsertDeleteType: UITextSmartInsertDeleteType = .no 126 | 127 | // MARK: - Public Vars for UITextInput IME Hack 128 | 129 | public weak var inputDelegate: UITextInputDelegate? 130 | 131 | // MARK: - Private Vars 132 | 133 | fileprivate var htermWebView: HtermWebView? 134 | 135 | fileprivate var buffer = Data() 136 | fileprivate let bufferQueue = DispatchQueue(label: "TerminalView.bufferQueue") 137 | 138 | fileprivate var needsTerminalScrollEnd: Bool = false 139 | 140 | private let tapGestureRecognizer = UITapGestureRecognizer() 141 | private let longPressGestureRecognizer = UILongPressGestureRecognizer() 142 | private let panGestureRecognizer = UIPanGestureRecognizer() 143 | 144 | // MARK: - Private Vars for UITextInput IME Hack 145 | 146 | fileprivate var markedTextForIMEHack: String? 147 | fileprivate let markedTextRangeForIMEHack = UITextRange() 148 | fileprivate let selectedTextRangeForIMEHack = UITextRange() 149 | fileprivate var textInputStringTokenizerForIMEHack: UITextInputStringTokenizer? 150 | 151 | // MARK: - Private Var for UIKeyCommand 152 | 153 | private var customKeyCommands: [UIKeyCommand] = [] 154 | 155 | // MARK: - Initializer 156 | 157 | deinit { 158 | bufferQueue.sync { 159 | buffer.removeAll() 160 | } 161 | } 162 | 163 | override public init(frame: CGRect) { 164 | super.init(frame: frame) 165 | commonInit() 166 | } 167 | 168 | public required init?(coder: NSCoder) { 169 | super.init(coder: coder) 170 | commonInit() 171 | } 172 | 173 | private func commonInit() { 174 | panGestureRecognizer.addTarget(self, action: #selector(handlePanGestureRecognizer(_:))) 175 | addGestureRecognizer(panGestureRecognizer) 176 | 177 | longPressGestureRecognizer.addTarget(self, action: #selector(handleLongPressGestureRecognizer(_:))) 178 | addGestureRecognizer(longPressGestureRecognizer) 179 | 180 | tapGestureRecognizer.addTarget(self, action: #selector(handleTapGestureRecognizer(_:))) 181 | addGestureRecognizer(tapGestureRecognizer) 182 | 183 | let htermWebView = HtermWebView(frame: bounds) 184 | htermWebView.parentTerminalView = self 185 | addSubview(htermWebView) 186 | htermWebView.addConstraintsToSuperviewEdges() 187 | self.htermWebView = htermWebView 188 | 189 | textInputStringTokenizerForIMEHack = UITextInputStringTokenizer(textInput: self) 190 | 191 | registerKeyCommands() 192 | 193 | htermWebView.reloadHterm() 194 | } 195 | 196 | // MARK: - UIView 197 | 198 | override public func layoutSubviews() { 199 | super.layoutSubviews() 200 | guard let hterm = htermWebView, hterm.isHtermLoaded else { 201 | return 202 | } 203 | var data: Data? 204 | bufferQueue.sync { 205 | data = buffer 206 | buffer.removeAll() 207 | } 208 | if let data = data { 209 | hterm.write(data) 210 | } 211 | hterm.syncTerminalSize() 212 | if needsTerminalScrollEnd { 213 | needsTerminalScrollEnd = false 214 | terminalScroll(.end) 215 | } 216 | } 217 | 218 | // MARK: - UIResponder 219 | 220 | override public var canBecomeFirstResponder: Bool { mode == .input } 221 | override public var canResignFirstResponder: Bool { mode == .input } 222 | 223 | override public func becomeFirstResponder() -> Bool { 224 | guard let htermWebView = htermWebView else { 225 | return false 226 | } 227 | let accepted = super.becomeFirstResponder() 228 | if accepted { 229 | htermWebView.htermFocus() 230 | } 231 | return accepted 232 | } 233 | 234 | override public func resignFirstResponder() -> Bool { 235 | guard let htermWebView = htermWebView else { 236 | return false 237 | } 238 | let accepted = super.resignFirstResponder() 239 | if accepted { 240 | htermWebView.htermBlur() 241 | } 242 | return accepted 243 | } 244 | 245 | override public func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool { 246 | // debugLog("action: \(action.description), sender: \(sender.debugDescription )") 247 | if mode != .input { 248 | return false 249 | } 250 | guard let htermWebView = htermWebView else { 251 | return false 252 | } 253 | switch action { 254 | case #selector(UIResponderStandardEditActions.cut(_:)): 255 | return false 256 | case #selector(UIResponderStandardEditActions.copy): 257 | return false 258 | case #selector(UIResponderStandardEditActions.paste(_:)): 259 | return htermWebView.isHtermLoaded 260 | case #selector(UIResponderStandardEditActions.delete(_:)): 261 | return false 262 | case #selector(UIResponderStandardEditActions.select(_:)): 263 | return htermWebView.isHtermLoaded 264 | case #selector(UIResponderStandardEditActions.selectAll(_:)): 265 | return htermWebView.isHtermLoaded 266 | case #selector(UIResponderStandardEditActions.increaseSize(_:)): 267 | return htermWebView.isHtermLoaded 268 | case #selector(UIResponderStandardEditActions.decreaseSize(_:)): 269 | return htermWebView.isHtermLoaded 270 | default: 271 | return super.canPerformAction(action, withSender: sender) 272 | } 273 | } 274 | 275 | override public var inputAccessoryView: UIView? { 276 | delegate?.terminalViewRequestInputAccessoryView(self) 277 | } 278 | 279 | override public var inputAccessoryViewController: UIInputViewController? { 280 | delegate?.terminalViewRequestInputAccessoryViewController(self) 281 | } 282 | 283 | // MARK: - UIResponderStandardEditActions 284 | 285 | override public func copy(_ sender: Any?) { 286 | // nop 287 | } 288 | 289 | override public func paste(_ sender: Any?) { 290 | if let string = UIPasteboard.general.string { 291 | handleKeyInput(string) 292 | } 293 | } 294 | 295 | override public func select(_ sender: Any?) { 296 | guard let htermWebView = htermWebView else { 297 | return 298 | } 299 | enterScrollMode() 300 | // TODO: want to remove magical dispatch_after... 301 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { 302 | htermWebView.select(sender) 303 | } 304 | } 305 | 306 | override public func selectAll(_ sender: Any?) { 307 | guard let htermWebView = htermWebView else { 308 | return 309 | } 310 | enterScrollMode() 311 | // TODO: want to remove magical dispatch_after... 312 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { 313 | htermWebView.selectAll(sender) 314 | } 315 | } 316 | 317 | override public func increaseSize(_ sender: Any?) { 318 | let size = terminalFontSize + 1 319 | terminalFontSize = min(size, 30) 320 | } 321 | 322 | override public func decreaseSize(_ sender: Any?) { 323 | let size = terminalFontSize - 1 324 | terminalFontSize = max(size, 2) 325 | } 326 | 327 | // MARK: - UIKeyCommand 328 | 329 | override public var keyCommands: [UIKeyCommand]? { 330 | customKeyCommands 331 | } 332 | 333 | private func registerKeyCommands() { 334 | customKeyCommands.removeAll() 335 | 336 | let normal: [String] = [ 337 | UIKeyCommand.inputEscape, 338 | // UIKeyCommand.inputHome, // iOS 13.4 ~ 339 | UIKeyCommand.inputPageUp, 340 | UIKeyCommand.inputPageDown, 341 | // UIKeyCommand.inputEnd, // iOS 13.4 ~ 342 | UIKeyCommand.inputLeftArrow, 343 | UIKeyCommand.inputDownArrow, 344 | UIKeyCommand.inputUpArrow, 345 | UIKeyCommand.inputRightArrow, 346 | ] 347 | normal.forEach { 348 | customKeyCommands.append(UIKeyCommand(input: $0, modifierFlags: [], action: #selector(handleKeyCommand(_:)))) 349 | } 350 | controlKeys.forEach { 351 | let string = String($0) 352 | customKeyCommands.append(UIKeyCommand(input: string, modifierFlags: [.control], action: #selector(handleKeyCommand(_:)))) 353 | } 354 | if useOptionKeyAsMetaKey { 355 | metaKeys.forEach { 356 | let string = String($0) 357 | customKeyCommands.append(UIKeyCommand(input: string, modifierFlags: [.alternate], action: #selector(handleKeyCommand(_:)))) 358 | } 359 | } 360 | 361 | // Clear 362 | customKeyCommands.append(UIKeyCommand(input: "k", modifierFlags: .command, action: #selector(handleKeyCommand(_:)))) 363 | 364 | // Shft + Tab 365 | customKeyCommands.append(UIKeyCommand(input: .terminalTab, modifierFlags: .shift, action: #selector(handleKeyCommand(_:)))) 366 | } 367 | 368 | @objc 369 | private func handleKeyCommand(_ keyCommand: UIKeyCommand) { 370 | // debugLog(keyCommand.description) 371 | guard let i = keyCommand.input else { 372 | return 373 | } 374 | if keyCommand.modifierFlags == .command, i == "k" { 375 | htermWebView?.htermClear(.reset) 376 | } else if keyCommand.modifierFlags == .shift, i == .terminalTab { 377 | handleKeyInput(.terminalBackTab) 378 | } else if keyCommand.modifierFlags == .control, let controlled = i.combineWithControlKey() { 379 | handleKeyInput(controlled) 380 | } else if useOptionKeyAsMetaKey, keyCommand.modifierFlags == .alternate, let metaPrefixed = i.combineWithMetaKey() { 381 | handleKeyInput(metaPrefixed) 382 | } else { 383 | switch i { 384 | case UIKeyCommand.inputEscape: 385 | handleKeyInput(.terminalEscape) 386 | case UIKeyCommand.inputPageUp: 387 | handleKeyInput(.terminalPageUp) 388 | case UIKeyCommand.inputPageDown: 389 | handleKeyInput(.terminalPageDown) 390 | case UIKeyCommand.inputLeftArrow: 391 | handleKeyInput(.terminalLeftArrow) 392 | case UIKeyCommand.inputUpArrow: 393 | handleKeyInput(.terminalUpArrow) 394 | case UIKeyCommand.inputDownArrow: 395 | handleKeyInput(.terminalDownArrow) 396 | case UIKeyCommand.inputRightArrow: 397 | handleKeyInput(.terminalRightArrow) 398 | default: 399 | handleKeyInput(i) 400 | } 401 | } 402 | } 403 | 404 | // MARK: - UIGestureRecognizer 405 | 406 | @objc 407 | private func handleTapGestureRecognizer(_ tapGestureRecognizer: UITapGestureRecognizer) { 408 | if tapGestureRecognizer == self.tapGestureRecognizer { 409 | if isFirstResponder == false { 410 | _ = becomeFirstResponder() 411 | } else { 412 | if UIMenuController.shared.isMenuVisible { 413 | if #available(iOS 13.0, *) { 414 | UIMenuController.shared.hideMenu() 415 | } else { 416 | UIMenuController.shared.setMenuVisible(false, animated: true) 417 | } 418 | } 419 | } 420 | } 421 | } 422 | 423 | @objc 424 | private func handleLongPressGestureRecognizer(_ longPressGestureRecognizer: UILongPressGestureRecognizer) { 425 | if longPressGestureRecognizer == self.longPressGestureRecognizer { 426 | if longPressGestureRecognizer.state == .began { 427 | let menu = UIMenuController.shared 428 | let p = longPressGestureRecognizer.location(in: self) 429 | let rect = CGRect(origin: p, size: .zero) 430 | if #available(iOS 13.0, *) { 431 | menu.showMenu(from: self, rect: rect) 432 | } else { 433 | menu.setTargetRect(rect, in: self) 434 | menu.setMenuVisible(true, animated: true) 435 | } 436 | } 437 | } 438 | } 439 | 440 | @objc 441 | private func handlePanGestureRecognizer(_ panGestureRecognizer: UIPanGestureRecognizer) { 442 | if panGestureRecognizer == self.panGestureRecognizer { 443 | if panGestureRecognizer.state == .began { 444 | if mode != .scroll { 445 | let velocity = panGestureRecognizer.velocity(in: panGestureRecognizer.view) 446 | if velocity.x == 0.0 { 447 | // vertical finger move 448 | enterScrollMode() 449 | panGestureRecognizer.cancel() 450 | } else if velocity.y == 0.0 { 451 | // horizontal finger move 452 | // do nothing 453 | } else { 454 | let tanX = abs(velocity.y / velocity.x) 455 | // tan(75deg) = 2 + sqrt(3) => 3.7320.... 456 | if 3.732 < tanX { 457 | // vertical finger move 458 | enterScrollMode() 459 | panGestureRecognizer.cancel() 460 | } 461 | } 462 | } 463 | } 464 | } 465 | } 466 | 467 | // MARK: - Private 468 | 469 | fileprivate func handleKeyInput(_ string: String) { 470 | setNeedsTerminalScrollEnd() 471 | // TODO: Research... 472 | // delegate?.terminalView(self, didHandleKeyInput: string.replacingOccurrences(of: "\n", with: "\r")) 473 | delegate?.terminalView(self, didHandleKeyInput: string) 474 | htermWebView?.setUserGesture() 475 | } 476 | 477 | // MARK: - Public Methods for Input/Scroll Mode Change 478 | 479 | public func enterInputMode() { 480 | guard mode != .input, let htermWebView = htermWebView else { 481 | return 482 | } 483 | htermWebView.isUserInteractionEnabled = false 484 | terminalScroll(.end) // no need setNeedsTerminalScrollEnd() 485 | mode = .input 486 | _ = becomeFirstResponder() // should after mode setting 487 | } 488 | 489 | public func enterScrollMode() { 490 | guard mode != .scroll, let htermWebView = htermWebView else { 491 | return 492 | } 493 | htermWebView.isUserInteractionEnabled = true 494 | _ = resignFirstResponder() // shoud before mode setting 495 | mode = .scroll 496 | } 497 | 498 | // MARK: - Public Methods for Buffer 499 | 500 | public func clearBuffer() { 501 | bufferQueue.async { 502 | self.buffer.removeAll() 503 | DispatchQueue.main.async { [weak self] in 504 | self?.setNeedsLayout() 505 | } 506 | } 507 | } 508 | 509 | public func appendBuffer(_ buf: Data) { 510 | bufferQueue.async { 511 | self.buffer.append(buf) 512 | DispatchQueue.main.async { [weak self] in 513 | self?.setNeedsLayout() 514 | } 515 | } 516 | } 517 | 518 | // MARK: - Public Method for Key Down Event 519 | 520 | public func press(_ event: TerminalKeyDownEvent) { 521 | handleKeyInput(event.string) 522 | } 523 | 524 | // MARK: - Public Methods for Scroll 525 | 526 | public func setNeedsTerminalScrollEnd() { 527 | needsTerminalScrollEnd = true 528 | setNeedsLayout() 529 | } 530 | 531 | public func terminalScroll(_ event: TerminalScrollEvent) { 532 | htermWebView?.htermScroll(event.htermFunction) 533 | } 534 | 535 | // MARK: - Public Method for Clear 536 | 537 | public func terminalClear(_ event: TerminalClearEvent) { 538 | htermWebView?.htermClear(event.htermFunction) 539 | } 540 | 541 | // MARK: - Public Method for Erase 542 | 543 | // danger... be careful. 544 | public func terminalErase(_ event: TerminalEraseEvent) { 545 | htermWebView?.htermErase(event.htermFunction) 546 | } 547 | 548 | // MARK: - Public Methods for Force Reload 549 | 550 | public func reloadTerminal() { 551 | htermWebView?.reloadHterm() 552 | } 553 | 554 | // MARK: - Get Drawing Text 555 | 556 | public func getVisibleText(callback: @escaping ((String) -> Void)) { 557 | guard let htermWebView = htermWebView else { 558 | callback("") 559 | return 560 | } 561 | htermWebView.getVisibleText(callback: callback) 562 | } 563 | 564 | public func getAllText(callback: @escaping ((String) -> Void)) { 565 | guard let htermWebView = htermWebView else { 566 | callback("") 567 | return 568 | } 569 | htermWebView.getAllText(callback: callback) 570 | } 571 | 572 | } 573 | 574 | // MARK: - UIKeyInput 575 | 576 | extension TerminalView: UIKeyInput { 577 | 578 | public var hasText: Bool { 579 | guard let htermWebView = htermWebView else { 580 | return false 581 | } 582 | return htermWebView.isHtermLoaded 583 | } 584 | 585 | public func insertText(_ text: String) { 586 | if isControlKeyPressed == true, isMetaKeyPressed == false, let controled = text.combineWithControlKey() { 587 | handleKeyInput(controled) 588 | } else if isMetaKeyPressed == true, isControlKeyPressed == false, let metaPrefixed = text.combineWithMetaKey() { 589 | handleKeyInput(metaPrefixed) 590 | } else { 591 | handleKeyInput(text) 592 | } 593 | } 594 | 595 | public func deleteBackward() { 596 | delegate?.terminalViewDidHandleDeleteBackward(self) 597 | } 598 | 599 | } 600 | 601 | // MARK: - UITextInput 602 | 603 | extension TerminalView: UITextInput { 604 | 605 | // MARK: - for IME Hack 606 | 607 | public func text(in range: UITextRange) -> String? { 608 | if range == markedTextRangeForIMEHack { 609 | // debugLog("range is markedTextRangeForIMEHack. returning markedTextForIMEHack(\(markedTextForIMEHack ?? "nil"))") 610 | return markedTextForIMEHack 611 | } else if range == selectedTextRangeForIMEHack { 612 | // debugLog("range is selectedTextRangeForIMEHack. returning empty string.") 613 | return "" 614 | } else { 615 | // debugLog("range: \(range)") 616 | return nil 617 | } 618 | } 619 | 620 | public var markedTextRange: UITextRange? { 621 | if let _ = markedTextForIMEHack { 622 | return markedTextRangeForIMEHack 623 | } else { 624 | return nil 625 | } 626 | } 627 | 628 | public var selectedTextRange: UITextRange? { 629 | get { 630 | selectedTextRangeForIMEHack 631 | } 632 | set(selectedTextRange) { 633 | // ignore selectedTextRange 634 | debugLog("selectedTextRange: \(selectedTextRange?.description ?? "nil")") 635 | } 636 | } 637 | 638 | public func setMarkedText(_ markedText: String?, selectedRange: NSRange) { 639 | // debugLog("markedText: \(markedText ?? "nil"), selectedRange: \(selectedRange)") 640 | markedTextForIMEHack = markedText 641 | } 642 | 643 | public func unmarkText() { 644 | // debugLog() 645 | if let markedText = markedTextForIMEHack { 646 | markedTextForIMEHack = nil 647 | handleKeyInput(markedText) 648 | } 649 | } 650 | 651 | public var tokenizer: UITextInputTokenizer { 652 | // debugLog() 653 | guard let textInputStringTokenizerForIMEHack = textInputStringTokenizerForIMEHack else { 654 | fatalError("must not here") 655 | } 656 | return textInputStringTokenizerForIMEHack 657 | } 658 | 659 | // MARK: - Stubs for Swift Compiler 660 | 661 | public var markedTextStyle: [NSAttributedString.Key: Any]? { 662 | get { 663 | debugLog() 664 | return nil 665 | } 666 | set(markedTextStyle) { 667 | debugLog("markedTextStyle: \(markedTextStyle?.description ?? "nil")") 668 | } 669 | } 670 | 671 | public func replace(_ range: UITextRange, withText text: String) { 672 | debugLog("range: \(range), text: \(text)") 673 | } 674 | 675 | public var beginningOfDocument: UITextPosition { 676 | // debugLog() 677 | return UITextPosition() 678 | } 679 | 680 | public var endOfDocument: UITextPosition { 681 | // debugLog() 682 | return UITextPosition() 683 | } 684 | 685 | public func textRange(from fromPosition: UITextPosition, to toPosition: UITextPosition) -> UITextRange? { 686 | // debugLog("fromPosition: \(fromPosition), toPosition: \(toPosition)") 687 | return nil 688 | } 689 | 690 | public func position(from position: UITextPosition, offset: Int) -> UITextPosition? { 691 | // debugLog("position: \(position), offset: \(offset)") 692 | return nil 693 | } 694 | 695 | public func position(from position: UITextPosition, in direction: UITextLayoutDirection, offset: Int) -> UITextPosition? { 696 | debugLog("position: \(position), direction: \(direction), offset: \(offset)") 697 | return nil 698 | } 699 | 700 | public func compare(_ position: UITextPosition, to other: UITextPosition) -> ComparisonResult { 701 | // debugLog("position: \(position), other: \(other)") 702 | return .orderedSame 703 | } 704 | 705 | public func offset(from: UITextPosition, to toPosition: UITextPosition) -> Int { 706 | debugLog("from: \(from), toPosition: \(toPosition)") 707 | return Int.max 708 | } 709 | 710 | public func position(within range: UITextRange, farthestIn direction: UITextLayoutDirection) -> UITextPosition? { 711 | debugLog("range: \(range), direction: \(direction)") 712 | return nil 713 | } 714 | 715 | public func characterRange(byExtending position: UITextPosition, in direction: UITextLayoutDirection) -> UITextRange? { 716 | debugLog("position: \(position), direction: \(direction)") 717 | return nil 718 | } 719 | 720 | public func baseWritingDirection(for position: UITextPosition, in direction: UITextStorageDirection) -> NSWritingDirection { 721 | debugLog("position: \(position), direction: \(direction)") 722 | return .leftToRight 723 | } 724 | 725 | public func setBaseWritingDirection(_ writingDirection: NSWritingDirection, for range: UITextRange) { 726 | debugLog("writingDirection: \(writingDirection), range: \(range)") 727 | } 728 | 729 | public func firstRect(for range: UITextRange) -> CGRect { 730 | // debugLog("range: \(range)") 731 | return .zero 732 | } 733 | 734 | public func caretRect(for position: UITextPosition) -> CGRect { 735 | // debugLog("position: \(position)") 736 | return .zero 737 | } 738 | 739 | public func selectionRects(for range: UITextRange) -> [UITextSelectionRect] { 740 | // debugLog("range: \(range)") 741 | return [] 742 | } 743 | 744 | public func closestPosition(to point: CGPoint) -> UITextPosition? { 745 | debugLog("point: \(point)") 746 | return nil 747 | } 748 | 749 | public func closestPosition(to point: CGPoint, within range: UITextRange) -> UITextPosition? { 750 | debugLog("point: \(point), range: \(range)") 751 | return nil 752 | } 753 | 754 | public func characterRange(at point: CGPoint) -> UITextRange? { 755 | debugLog("point: \(point)") 756 | return nil 757 | } 758 | 759 | } 760 | -------------------------------------------------------------------------------- /Terminal.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 190296D8247D2A8900C019C5 /* Terminal.h in Headers */ = {isa = PBXBuildFile; fileRef = 190296D6247D2A8900C019C5 /* Terminal.h */; settings = {ATTRIBUTES = (Public, ); }; }; 11 | 19029716247D3AE600C019C5 /* TerminalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19029715247D3AE600C019C5 /* TerminalView.swift */; }; 12 | 190F4A2A24854179001A313A /* HtermWebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 190F4A2924854179001A313A /* HtermWebView.swift */; }; 13 | 191819DB2495030E008B8E0E /* TerminalKeyDownEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 191819DA2495030E008B8E0E /* TerminalKeyDownEvent.swift */; }; 14 | 191819DD24950328008B8E0E /* TerminalScrollEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 191819DC24950328008B8E0E /* TerminalScrollEvent.swift */; }; 15 | 191819DF24951926008B8E0E /* TerminalClearEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 191819DE24951926008B8E0E /* TerminalClearEvent.swift */; }; 16 | 1930D09E24801D2100882D16 /* hterm_all.js in Resources */ = {isa = PBXBuildFile; fileRef = 1930D09D24801D2100882D16 /* hterm_all.js */; }; 17 | 1930D0A62480237F00882D16 /* hterm_bridge.js in Resources */ = {isa = PBXBuildFile; fileRef = 1930D0A32480237E00882D16 /* hterm_bridge.js */; }; 18 | 1930D0A72480237F00882D16 /* hterm.css in Resources */ = {isa = PBXBuildFile; fileRef = 1930D0A42480237E00882D16 /* hterm.css */; }; 19 | 1930D0A82480237F00882D16 /* hterm.html in Resources */ = {isa = PBXBuildFile; fileRef = 1930D0A52480237F00882D16 /* hterm.html */; }; 20 | 1930D0AA248030AB00882D16 /* UIView+EdgeConstraints.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1930D0A9248030AB00882D16 /* UIView+EdgeConstraints.swift */; }; 21 | 1932B3FD24925215003E9AFD /* WKWebViewExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1932B3FC24925215003E9AFD /* WKWebViewExtension.swift */; }; 22 | 194A3EC0248A8176005CCAF0 /* String+SpecialKeys.swift in Sources */ = {isa = PBXBuildFile; fileRef = 194A3EBF248A8176005CCAF0 /* String+SpecialKeys.swift */; }; 23 | 194A3EC2248A91D5005CCAF0 /* Character+Control.swift in Sources */ = {isa = PBXBuildFile; fileRef = 194A3EC1248A91D4005CCAF0 /* Character+Control.swift */; }; 24 | 195BB025248AC64C00AB0DCA /* debugLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 195BB024248AC64C00AB0DCA /* debugLog.swift */; }; 25 | 19908884249A2F0C002E4C61 /* UIGestureRecognizer+Cancel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19908883249A2F0C002E4C61 /* UIGestureRecognizer+Cancel.swift */; }; 26 | 199E03D8248F9A5A00E8BE10 /* UIColor+CSSString.swift in Sources */ = {isa = PBXBuildFile; fileRef = 199E03D7248F9A5A00E8BE10 /* UIColor+CSSString.swift */; }; 27 | 19BB8F0B2495269400909321 /* TerminalEraseEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19BB8F0A2495269400909321 /* TerminalEraseEvent.swift */; }; 28 | 19EE7005248E997900389707 /* Character+Meta.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19EE7004248E997900389707 /* Character+Meta.swift */; }; 29 | 19EE7007248EB06400389707 /* TerminalSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19EE7006248EB06400389707 /* TerminalSize.swift */; }; 30 | /* End PBXBuildFile section */ 31 | 32 | /* Begin PBXFileReference section */ 33 | 190296D3247D2A8900C019C5 /* Terminal.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Terminal.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 34 | 190296D6247D2A8900C019C5 /* Terminal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Terminal.h; sourceTree = ""; }; 35 | 190296D7247D2A8900C019C5 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 36 | 19029705247D388C00C019C5 /* .swiftlint.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = .swiftlint.yml; sourceTree = ""; }; 37 | 19029706247D388C00C019C5 /* .gitignore */ = {isa = PBXFileReference; lastKnownFileType = text; path = .gitignore; sourceTree = ""; }; 38 | 19029715247D3AE600C019C5 /* TerminalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalView.swift; sourceTree = ""; }; 39 | 190F4A2924854179001A313A /* HtermWebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HtermWebView.swift; sourceTree = ""; }; 40 | 191819DA2495030E008B8E0E /* TerminalKeyDownEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalKeyDownEvent.swift; sourceTree = ""; }; 41 | 191819DC24950328008B8E0E /* TerminalScrollEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalScrollEvent.swift; sourceTree = ""; }; 42 | 191819DE24951926008B8E0E /* TerminalClearEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalClearEvent.swift; sourceTree = ""; }; 43 | 1930D09D24801D2100882D16 /* hterm_all.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = hterm_all.js; sourceTree = ""; }; 44 | 1930D0A32480237E00882D16 /* hterm_bridge.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = hterm_bridge.js; sourceTree = ""; }; 45 | 1930D0A42480237E00882D16 /* hterm.css */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.css; path = hterm.css; sourceTree = ""; }; 46 | 1930D0A52480237F00882D16 /* hterm.html */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 2; lastKnownFileType = text.html; path = hterm.html; sourceTree = ""; tabWidth = 2; }; 47 | 1930D0A9248030AB00882D16 /* UIView+EdgeConstraints.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+EdgeConstraints.swift"; sourceTree = ""; }; 48 | 1932B3FC24925215003E9AFD /* WKWebViewExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WKWebViewExtension.swift; sourceTree = ""; }; 49 | 193396D825549FB100B7C772 /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; }; 50 | 193FEBE7247FF144004EC6F9 /* build_hterm.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = build_hterm.sh; sourceTree = ""; }; 51 | 194A3EBF248A8176005CCAF0 /* String+SpecialKeys.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+SpecialKeys.swift"; sourceTree = ""; }; 52 | 194A3EC1248A91D4005CCAF0 /* Character+Control.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Character+Control.swift"; sourceTree = ""; }; 53 | 195BB024248AC64C00AB0DCA /* debugLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = debugLog.swift; sourceTree = ""; }; 54 | 19894269249D2E9700CBF00E /* LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE; sourceTree = ""; }; 55 | 1989426A249D2E9800CBF00E /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 56 | 19908883249A2F0C002E4C61 /* UIGestureRecognizer+Cancel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIGestureRecognizer+Cancel.swift"; sourceTree = ""; }; 57 | 199E03D7248F9A5A00E8BE10 /* UIColor+CSSString.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+CSSString.swift"; sourceTree = ""; }; 58 | 19BB8F0A2495269400909321 /* TerminalEraseEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalEraseEvent.swift; sourceTree = ""; }; 59 | 19EE7004248E997900389707 /* Character+Meta.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Character+Meta.swift"; sourceTree = ""; }; 60 | 19EE7006248EB06400389707 /* TerminalSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalSize.swift; sourceTree = ""; }; 61 | /* End PBXFileReference section */ 62 | 63 | /* Begin PBXFrameworksBuildPhase section */ 64 | 190296D0247D2A8900C019C5 /* Frameworks */ = { 65 | isa = PBXFrameworksBuildPhase; 66 | buildActionMask = 2147483647; 67 | files = ( 68 | ); 69 | runOnlyForDeploymentPostprocessing = 0; 70 | }; 71 | /* End PBXFrameworksBuildPhase section */ 72 | 73 | /* Begin PBXGroup section */ 74 | 190296C0247D276B00C019C5 = { 75 | isa = PBXGroup; 76 | children = ( 77 | 193396D825549FB100B7C772 /* Package.swift */, 78 | 19894269249D2E9700CBF00E /* LICENSE */, 79 | 1989426A249D2E9800CBF00E /* README.md */, 80 | 19029704247D386300C019C5 /* dotfiles */, 81 | 193FEBE6247FF138004EC6F9 /* bin */, 82 | 193396D325549C4400B7C772 /* Sources */, 83 | 190296D5247D2A8900C019C5 /* Terminal */, 84 | 190296D4247D2A8900C019C5 /* Products */, 85 | ); 86 | sourceTree = ""; 87 | }; 88 | 190296D4247D2A8900C019C5 /* Products */ = { 89 | isa = PBXGroup; 90 | children = ( 91 | 190296D3247D2A8900C019C5 /* Terminal.framework */, 92 | ); 93 | name = Products; 94 | sourceTree = ""; 95 | }; 96 | 190296D5247D2A8900C019C5 /* Terminal */ = { 97 | isa = PBXGroup; 98 | children = ( 99 | 190296D6247D2A8900C019C5 /* Terminal.h */, 100 | 190296D7247D2A8900C019C5 /* Info.plist */, 101 | ); 102 | path = Terminal; 103 | sourceTree = ""; 104 | }; 105 | 19029704247D386300C019C5 /* dotfiles */ = { 106 | isa = PBXGroup; 107 | children = ( 108 | 19029706247D388C00C019C5 /* .gitignore */, 109 | 19029705247D388C00C019C5 /* .swiftlint.yml */, 110 | ); 111 | name = dotfiles; 112 | sourceTree = ""; 113 | }; 114 | 19029714247D3ACE00C019C5 /* Public */ = { 115 | isa = PBXGroup; 116 | children = ( 117 | 19029715247D3AE600C019C5 /* TerminalView.swift */, 118 | 191819DA2495030E008B8E0E /* TerminalKeyDownEvent.swift */, 119 | 191819DC24950328008B8E0E /* TerminalScrollEvent.swift */, 120 | 191819DE24951926008B8E0E /* TerminalClearEvent.swift */, 121 | 19BB8F0A2495269400909321 /* TerminalEraseEvent.swift */, 122 | 19EE7006248EB06400389707 /* TerminalSize.swift */, 123 | ); 124 | path = Public; 125 | sourceTree = ""; 126 | }; 127 | 1930D09C24801D2100882D16 /* Resources */ = { 128 | isa = PBXGroup; 129 | children = ( 130 | 1930D09D24801D2100882D16 /* hterm_all.js */, 131 | 1930D0A52480237F00882D16 /* hterm.html */, 132 | 1930D0A42480237E00882D16 /* hterm.css */, 133 | 1930D0A32480237E00882D16 /* hterm_bridge.js */, 134 | ); 135 | path = Resources; 136 | sourceTree = ""; 137 | }; 138 | 1930D0AB248030CD00882D16 /* Internal */ = { 139 | isa = PBXGroup; 140 | children = ( 141 | 195BB024248AC64C00AB0DCA /* debugLog.swift */, 142 | 1930D0A9248030AB00882D16 /* UIView+EdgeConstraints.swift */, 143 | 190F4A2924854179001A313A /* HtermWebView.swift */, 144 | 1932B3FC24925215003E9AFD /* WKWebViewExtension.swift */, 145 | 194A3EBF248A8176005CCAF0 /* String+SpecialKeys.swift */, 146 | 194A3EC1248A91D4005CCAF0 /* Character+Control.swift */, 147 | 19EE7004248E997900389707 /* Character+Meta.swift */, 148 | 199E03D7248F9A5A00E8BE10 /* UIColor+CSSString.swift */, 149 | 19908883249A2F0C002E4C61 /* UIGestureRecognizer+Cancel.swift */, 150 | ); 151 | path = Internal; 152 | sourceTree = ""; 153 | }; 154 | 193396D325549C4400B7C772 /* Sources */ = { 155 | isa = PBXGroup; 156 | children = ( 157 | 19CF7EFB25B9D47000E704BC /* Terminal */, 158 | ); 159 | path = Sources; 160 | sourceTree = ""; 161 | }; 162 | 193FEBE6247FF138004EC6F9 /* bin */ = { 163 | isa = PBXGroup; 164 | children = ( 165 | 193FEBE7247FF144004EC6F9 /* build_hterm.sh */, 166 | ); 167 | path = bin; 168 | sourceTree = ""; 169 | }; 170 | 19CF7EFB25B9D47000E704BC /* Terminal */ = { 171 | isa = PBXGroup; 172 | children = ( 173 | 1930D09C24801D2100882D16 /* Resources */, 174 | 19029714247D3ACE00C019C5 /* Public */, 175 | 1930D0AB248030CD00882D16 /* Internal */, 176 | ); 177 | path = Terminal; 178 | sourceTree = ""; 179 | }; 180 | /* End PBXGroup section */ 181 | 182 | /* Begin PBXHeadersBuildPhase section */ 183 | 190296CE247D2A8900C019C5 /* Headers */ = { 184 | isa = PBXHeadersBuildPhase; 185 | buildActionMask = 2147483647; 186 | files = ( 187 | 190296D8247D2A8900C019C5 /* Terminal.h in Headers */, 188 | ); 189 | runOnlyForDeploymentPostprocessing = 0; 190 | }; 191 | /* End PBXHeadersBuildPhase section */ 192 | 193 | /* Begin PBXNativeTarget section */ 194 | 190296D2247D2A8900C019C5 /* Terminal */ = { 195 | isa = PBXNativeTarget; 196 | buildConfigurationList = 190296DB247D2A8900C019C5 /* Build configuration list for PBXNativeTarget "Terminal" */; 197 | buildPhases = ( 198 | 193FEBE5247FF06F004EC6F9 /* build hterm.js if not exists */, 199 | 1902970F247D39CB00C019C5 /* Run SwiftLint Auto Correct */, 200 | 19029712247D3A2300C019C5 /* Run SwiftLint */, 201 | 190296CE247D2A8900C019C5 /* Headers */, 202 | 190296CF247D2A8900C019C5 /* Sources */, 203 | 190296D0247D2A8900C019C5 /* Frameworks */, 204 | 190296D1247D2A8900C019C5 /* Resources */, 205 | ); 206 | buildRules = ( 207 | ); 208 | dependencies = ( 209 | ); 210 | name = Terminal; 211 | productName = Terminal; 212 | productReference = 190296D3247D2A8900C019C5 /* Terminal.framework */; 213 | productType = "com.apple.product-type.framework"; 214 | }; 215 | /* End PBXNativeTarget section */ 216 | 217 | /* Begin PBXProject section */ 218 | 190296C1247D276B00C019C5 /* Project object */ = { 219 | isa = PBXProject; 220 | attributes = { 221 | LastUpgradeCheck = 1230; 222 | ORGANIZATIONNAME = dnpp.org; 223 | TargetAttributes = { 224 | 190296D2247D2A8900C019C5 = { 225 | CreatedOnToolsVersion = 11.5; 226 | LastSwiftMigration = 1150; 227 | }; 228 | }; 229 | }; 230 | buildConfigurationList = 190296C4247D276B00C019C5 /* Build configuration list for PBXProject "Terminal" */; 231 | compatibilityVersion = "Xcode 9.3"; 232 | developmentRegion = en; 233 | hasScannedForEncodings = 0; 234 | knownRegions = ( 235 | en, 236 | Base, 237 | ); 238 | mainGroup = 190296C0247D276B00C019C5; 239 | productRefGroup = 190296D4247D2A8900C019C5 /* Products */; 240 | projectDirPath = ""; 241 | projectRoot = ""; 242 | targets = ( 243 | 190296D2247D2A8900C019C5 /* Terminal */, 244 | ); 245 | }; 246 | /* End PBXProject section */ 247 | 248 | /* Begin PBXResourcesBuildPhase section */ 249 | 190296D1247D2A8900C019C5 /* Resources */ = { 250 | isa = PBXResourcesBuildPhase; 251 | buildActionMask = 2147483647; 252 | files = ( 253 | 1930D0A62480237F00882D16 /* hterm_bridge.js in Resources */, 254 | 1930D0A82480237F00882D16 /* hterm.html in Resources */, 255 | 1930D0A72480237F00882D16 /* hterm.css in Resources */, 256 | 1930D09E24801D2100882D16 /* hterm_all.js in Resources */, 257 | ); 258 | runOnlyForDeploymentPostprocessing = 0; 259 | }; 260 | /* End PBXResourcesBuildPhase section */ 261 | 262 | /* Begin PBXShellScriptBuildPhase section */ 263 | 1902970F247D39CB00C019C5 /* Run SwiftLint Auto Correct */ = { 264 | isa = PBXShellScriptBuildPhase; 265 | buildActionMask = 2147483647; 266 | files = ( 267 | ); 268 | inputFileListPaths = ( 269 | ); 270 | inputPaths = ( 271 | ); 272 | name = "Run SwiftLint Auto Correct"; 273 | outputFileListPaths = ( 274 | ); 275 | outputPaths = ( 276 | ); 277 | runOnlyForDeploymentPostprocessing = 0; 278 | shellPath = /bin/sh; 279 | shellScript = "if [ -x '/opt/homebrew/bin/swiftlint' ]; then\n SWIFTLINT='/opt/homebrew/bin/swiftlint'\nelif [ -x '/usr/local/bin/swiftlint' ]; then\n SWIFTLINT='/usr/local/bin/swiftlint'\nelse\n echo \"error: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n\n\"${SWIFTLINT}\" autocorrect\n"; 280 | }; 281 | 19029712247D3A2300C019C5 /* Run SwiftLint */ = { 282 | isa = PBXShellScriptBuildPhase; 283 | buildActionMask = 2147483647; 284 | files = ( 285 | ); 286 | inputFileListPaths = ( 287 | ); 288 | inputPaths = ( 289 | ); 290 | name = "Run SwiftLint"; 291 | outputFileListPaths = ( 292 | ); 293 | outputPaths = ( 294 | ); 295 | runOnlyForDeploymentPostprocessing = 0; 296 | shellPath = /bin/sh; 297 | shellScript = "if [ -x '/opt/homebrew/bin/swiftlint' ]; then\n SWIFTLINT='/opt/homebrew/bin/swiftlint'\nelif [ -x '/usr/local/bin/swiftlint' ]; then\n SWIFTLINT='/usr/local/bin/swiftlint'\nelse\n echo \"error: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n\n\"${SWIFTLINT}\"\n"; 298 | }; 299 | 193FEBE5247FF06F004EC6F9 /* build hterm.js if not exists */ = { 300 | isa = PBXShellScriptBuildPhase; 301 | buildActionMask = 2147483647; 302 | files = ( 303 | ); 304 | inputFileListPaths = ( 305 | ); 306 | inputPaths = ( 307 | ); 308 | name = "build hterm.js if not exists"; 309 | outputFileListPaths = ( 310 | ); 311 | outputPaths = ( 312 | ); 313 | runOnlyForDeploymentPostprocessing = 0; 314 | shellPath = /bin/sh; 315 | shellScript = "if [ -s \"${SRCROOT}/Resources/hterm_all.js\" ]; then\n echo \"hterm_all.js exists. skip building.\"\nelse\n ${SRCROOT}/bin/build_hterm.sh\nfi\n"; 316 | }; 317 | /* End PBXShellScriptBuildPhase section */ 318 | 319 | /* Begin PBXSourcesBuildPhase section */ 320 | 190296CF247D2A8900C019C5 /* Sources */ = { 321 | isa = PBXSourcesBuildPhase; 322 | buildActionMask = 2147483647; 323 | files = ( 324 | 19029716247D3AE600C019C5 /* TerminalView.swift in Sources */, 325 | 19908884249A2F0C002E4C61 /* UIGestureRecognizer+Cancel.swift in Sources */, 326 | 19EE7005248E997900389707 /* Character+Meta.swift in Sources */, 327 | 190F4A2A24854179001A313A /* HtermWebView.swift in Sources */, 328 | 191819DF24951926008B8E0E /* TerminalClearEvent.swift in Sources */, 329 | 191819DB2495030E008B8E0E /* TerminalKeyDownEvent.swift in Sources */, 330 | 195BB025248AC64C00AB0DCA /* debugLog.swift in Sources */, 331 | 19EE7007248EB06400389707 /* TerminalSize.swift in Sources */, 332 | 1932B3FD24925215003E9AFD /* WKWebViewExtension.swift in Sources */, 333 | 194A3EC2248A91D5005CCAF0 /* Character+Control.swift in Sources */, 334 | 19BB8F0B2495269400909321 /* TerminalEraseEvent.swift in Sources */, 335 | 1930D0AA248030AB00882D16 /* UIView+EdgeConstraints.swift in Sources */, 336 | 191819DD24950328008B8E0E /* TerminalScrollEvent.swift in Sources */, 337 | 194A3EC0248A8176005CCAF0 /* String+SpecialKeys.swift in Sources */, 338 | 199E03D8248F9A5A00E8BE10 /* UIColor+CSSString.swift in Sources */, 339 | ); 340 | runOnlyForDeploymentPostprocessing = 0; 341 | }; 342 | /* End PBXSourcesBuildPhase section */ 343 | 344 | /* Begin XCBuildConfiguration section */ 345 | 190296C5247D276B00C019C5 /* Debug */ = { 346 | isa = XCBuildConfiguration; 347 | buildSettings = { 348 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 349 | CLANG_WARN_BOOL_CONVERSION = YES; 350 | CLANG_WARN_COMMA = YES; 351 | CLANG_WARN_CONSTANT_CONVERSION = YES; 352 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 353 | CLANG_WARN_EMPTY_BODY = YES; 354 | CLANG_WARN_ENUM_CONVERSION = YES; 355 | CLANG_WARN_INFINITE_RECURSION = YES; 356 | CLANG_WARN_INT_CONVERSION = YES; 357 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 358 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 359 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 360 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 361 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 362 | CLANG_WARN_STRICT_PROTOTYPES = YES; 363 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 364 | CLANG_WARN_UNREACHABLE_CODE = YES; 365 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 366 | ENABLE_STRICT_OBJC_MSGSEND = YES; 367 | ENABLE_TESTABILITY = YES; 368 | GCC_NO_COMMON_BLOCKS = YES; 369 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 370 | GCC_WARN_ABOUT_RETURN_TYPE = YES; 371 | GCC_WARN_UNDECLARED_SELECTOR = YES; 372 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 373 | GCC_WARN_UNUSED_FUNCTION = YES; 374 | GCC_WARN_UNUSED_VARIABLE = YES; 375 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 376 | ONLY_ACTIVE_ARCH = YES; 377 | }; 378 | name = Debug; 379 | }; 380 | 190296C6247D276B00C019C5 /* Release */ = { 381 | isa = XCBuildConfiguration; 382 | buildSettings = { 383 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 384 | CLANG_WARN_BOOL_CONVERSION = YES; 385 | CLANG_WARN_COMMA = YES; 386 | CLANG_WARN_CONSTANT_CONVERSION = YES; 387 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 388 | CLANG_WARN_EMPTY_BODY = YES; 389 | CLANG_WARN_ENUM_CONVERSION = YES; 390 | CLANG_WARN_INFINITE_RECURSION = YES; 391 | CLANG_WARN_INT_CONVERSION = YES; 392 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 393 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 394 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 395 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 396 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 397 | CLANG_WARN_STRICT_PROTOTYPES = YES; 398 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 399 | CLANG_WARN_UNREACHABLE_CODE = YES; 400 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 401 | ENABLE_STRICT_OBJC_MSGSEND = YES; 402 | GCC_NO_COMMON_BLOCKS = YES; 403 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 404 | GCC_WARN_ABOUT_RETURN_TYPE = YES; 405 | GCC_WARN_UNDECLARED_SELECTOR = YES; 406 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 407 | GCC_WARN_UNUSED_FUNCTION = YES; 408 | GCC_WARN_UNUSED_VARIABLE = YES; 409 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 410 | }; 411 | name = Release; 412 | }; 413 | 190296D9247D2A8900C019C5 /* Debug */ = { 414 | isa = XCBuildConfiguration; 415 | buildSettings = { 416 | ALWAYS_SEARCH_USER_PATHS = NO; 417 | CLANG_ANALYZER_NONNULL = YES; 418 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 419 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 420 | CLANG_CXX_LIBRARY = "libc++"; 421 | CLANG_ENABLE_MODULES = YES; 422 | CLANG_ENABLE_OBJC_ARC = YES; 423 | CLANG_ENABLE_OBJC_WEAK = YES; 424 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 425 | CLANG_WARN_BOOL_CONVERSION = YES; 426 | CLANG_WARN_COMMA = YES; 427 | CLANG_WARN_CONSTANT_CONVERSION = YES; 428 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 429 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 430 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 431 | CLANG_WARN_EMPTY_BODY = YES; 432 | CLANG_WARN_ENUM_CONVERSION = YES; 433 | CLANG_WARN_INFINITE_RECURSION = YES; 434 | CLANG_WARN_INT_CONVERSION = YES; 435 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 436 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 437 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 438 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 439 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 440 | CLANG_WARN_STRICT_PROTOTYPES = YES; 441 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 442 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 443 | CLANG_WARN_UNREACHABLE_CODE = YES; 444 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 445 | CODE_SIGN_IDENTITY = ""; 446 | CODE_SIGN_STYLE = Automatic; 447 | COPY_PHASE_STRIP = NO; 448 | CURRENT_PROJECT_VERSION = 1; 449 | DEBUG_INFORMATION_FORMAT = dwarf; 450 | DEFINES_MODULE = YES; 451 | DEVELOPMENT_TEAM = ""; 452 | DYLIB_COMPATIBILITY_VERSION = 1; 453 | DYLIB_CURRENT_VERSION = 1; 454 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 455 | ENABLE_STRICT_OBJC_MSGSEND = YES; 456 | ENABLE_TESTABILITY = YES; 457 | GCC_C_LANGUAGE_STANDARD = gnu11; 458 | GCC_DYNAMIC_NO_PIC = NO; 459 | GCC_NO_COMMON_BLOCKS = YES; 460 | GCC_OPTIMIZATION_LEVEL = 0; 461 | GCC_PREPROCESSOR_DEFINITIONS = ( 462 | "DEBUG=1", 463 | "$(inherited)", 464 | ); 465 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 466 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 467 | GCC_WARN_UNDECLARED_SELECTOR = YES; 468 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 469 | GCC_WARN_UNUSED_FUNCTION = YES; 470 | GCC_WARN_UNUSED_VARIABLE = YES; 471 | INFOPLIST_FILE = Terminal/Info.plist; 472 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 473 | LD_RUNPATH_SEARCH_PATHS = ( 474 | "$(inherited)", 475 | "@executable_path/Frameworks", 476 | "@loader_path/Frameworks", 477 | ); 478 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 479 | MTL_FAST_MATH = YES; 480 | ONLY_ACTIVE_ARCH = YES; 481 | PRODUCT_BUNDLE_IDENTIFIER = org.dnpp.Terminal; 482 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 483 | SDKROOT = iphoneos; 484 | SKIP_INSTALL = YES; 485 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 486 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 487 | SWIFT_VERSION = 5.0; 488 | TARGETED_DEVICE_FAMILY = "1,2"; 489 | VERSIONING_SYSTEM = "apple-generic"; 490 | VERSION_INFO_PREFIX = ""; 491 | }; 492 | name = Debug; 493 | }; 494 | 190296DA247D2A8900C019C5 /* Release */ = { 495 | isa = XCBuildConfiguration; 496 | buildSettings = { 497 | ALWAYS_SEARCH_USER_PATHS = NO; 498 | CLANG_ANALYZER_NONNULL = YES; 499 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 500 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 501 | CLANG_CXX_LIBRARY = "libc++"; 502 | CLANG_ENABLE_MODULES = YES; 503 | CLANG_ENABLE_OBJC_ARC = YES; 504 | CLANG_ENABLE_OBJC_WEAK = YES; 505 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 506 | CLANG_WARN_BOOL_CONVERSION = YES; 507 | CLANG_WARN_COMMA = YES; 508 | CLANG_WARN_CONSTANT_CONVERSION = YES; 509 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 510 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 511 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 512 | CLANG_WARN_EMPTY_BODY = YES; 513 | CLANG_WARN_ENUM_CONVERSION = YES; 514 | CLANG_WARN_INFINITE_RECURSION = YES; 515 | CLANG_WARN_INT_CONVERSION = YES; 516 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 517 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 518 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 519 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 520 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 521 | CLANG_WARN_STRICT_PROTOTYPES = YES; 522 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 523 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 524 | CLANG_WARN_UNREACHABLE_CODE = YES; 525 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 526 | CODE_SIGN_IDENTITY = ""; 527 | CODE_SIGN_STYLE = Automatic; 528 | COPY_PHASE_STRIP = NO; 529 | CURRENT_PROJECT_VERSION = 1; 530 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 531 | DEFINES_MODULE = YES; 532 | DEVELOPMENT_TEAM = ""; 533 | DYLIB_COMPATIBILITY_VERSION = 1; 534 | DYLIB_CURRENT_VERSION = 1; 535 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 536 | ENABLE_NS_ASSERTIONS = NO; 537 | ENABLE_STRICT_OBJC_MSGSEND = YES; 538 | GCC_C_LANGUAGE_STANDARD = gnu11; 539 | GCC_NO_COMMON_BLOCKS = YES; 540 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 541 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 542 | GCC_WARN_UNDECLARED_SELECTOR = YES; 543 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 544 | GCC_WARN_UNUSED_FUNCTION = YES; 545 | GCC_WARN_UNUSED_VARIABLE = YES; 546 | INFOPLIST_FILE = Terminal/Info.plist; 547 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 548 | LD_RUNPATH_SEARCH_PATHS = ( 549 | "$(inherited)", 550 | "@executable_path/Frameworks", 551 | "@loader_path/Frameworks", 552 | ); 553 | MTL_ENABLE_DEBUG_INFO = NO; 554 | MTL_FAST_MATH = YES; 555 | PRODUCT_BUNDLE_IDENTIFIER = org.dnpp.Terminal; 556 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 557 | SDKROOT = iphoneos; 558 | SKIP_INSTALL = YES; 559 | SWIFT_COMPILATION_MODE = wholemodule; 560 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 561 | SWIFT_VERSION = 5.0; 562 | TARGETED_DEVICE_FAMILY = "1,2"; 563 | VALIDATE_PRODUCT = YES; 564 | VERSIONING_SYSTEM = "apple-generic"; 565 | VERSION_INFO_PREFIX = ""; 566 | }; 567 | name = Release; 568 | }; 569 | /* End XCBuildConfiguration section */ 570 | 571 | /* Begin XCConfigurationList section */ 572 | 190296C4247D276B00C019C5 /* Build configuration list for PBXProject "Terminal" */ = { 573 | isa = XCConfigurationList; 574 | buildConfigurations = ( 575 | 190296C5247D276B00C019C5 /* Debug */, 576 | 190296C6247D276B00C019C5 /* Release */, 577 | ); 578 | defaultConfigurationIsVisible = 0; 579 | defaultConfigurationName = Release; 580 | }; 581 | 190296DB247D2A8900C019C5 /* Build configuration list for PBXNativeTarget "Terminal" */ = { 582 | isa = XCConfigurationList; 583 | buildConfigurations = ( 584 | 190296D9247D2A8900C019C5 /* Debug */, 585 | 190296DA247D2A8900C019C5 /* Release */, 586 | ); 587 | defaultConfigurationIsVisible = 0; 588 | defaultConfigurationName = Release; 589 | }; 590 | /* End XCConfigurationList section */ 591 | }; 592 | rootObject = 190296C1247D276B00C019C5 /* Project object */; 593 | } 594 | --------------------------------------------------------------------------------