From 3fd8039ac38af2127cbe5d7540e15158d6d00c22 Mon Sep 17 00:00:00 2001 From: Adam Demasi Date: Thu, 24 Mar 2022 15:47:54 +1030 Subject: [PATCH] [app] Redesign keyboard toolbar; try to make it more SwiftUI-y --- App/UI/Keyboard/KeyboardKeyButtonStyle.swift | 88 +++-- .../Keyboard/KeyboardPopupToolbarView.swift | 199 ---------- App/UI/Keyboard/KeyboardToolbar.swift | 88 +---- App/UI/Keyboard/KeyboardToolbarView.swift | 356 ++++++++++++------ App/UI/Keyboard/TerminalKeyInput.swift | 80 ++-- App/UI/Terminal/TerminalSampleView.swift | 3 +- Common/Controllers/SubProcess.swift | 6 +- Common/Controllers/TerminalController.swift | 16 +- Common/Extensions/String+Additions.swift | 6 +- Common/VT100/TerminalConstants.swift | 61 ++- Common/VT100/TerminalInputProtocol.swift | 2 +- NewTerm.xcodeproj/project.pbxproj | 4 - 12 files changed, 401 insertions(+), 508 deletions(-) delete mode 100644 App/UI/Keyboard/KeyboardPopupToolbarView.swift diff --git a/App/UI/Keyboard/KeyboardKeyButtonStyle.swift b/App/UI/Keyboard/KeyboardKeyButtonStyle.swift index 8fbc02c..8d255b7 100644 --- a/App/UI/Keyboard/KeyboardKeyButtonStyle.swift +++ b/App/UI/Keyboard/KeyboardKeyButtonStyle.swift @@ -6,45 +6,66 @@ // import SwiftUI +import SwiftUIX struct KeyboardKeyButtonStyle: ButtonStyle { - var selected: Bool = false - var shadow: Bool = false - var fixedWidth: CGFloat? + var selected = false + var shadow = false + var halfHeight = false + var widthRatio: CGFloat? func makeBody(configuration: Configuration) -> some View { - if fixedWidth == nil { - configuration.label - .font(.system(size: isBigDevice ? 15 : 13)) - .frame(height: (isBigDevice ? 35 : 30)) - .padding(.horizontal, 8) - .background(configuration.isPressed ? Color(.keyBackgroundHighlighted) : (selected ? Color(.keyBackgroundSelected) : Color(.keyBackgroundNormal))) - .cornerRadius(isBigDevice ? 6 : 4) - .shadow(color: shadow ? Color.black.opacity(0.8) : .clear, radius: 0, x: 0, y: shadow ? 1 : 0) - .animation(nil) + var height: CGFloat = 45 + let width = widthRatio == nil ? nil : height * widthRatio! + var fontSize: CGFloat = isBigDevice ? 18 : 15 + var cornerRadius: CGFloat = isBigDevice ? 6 : 4 + if halfHeight { + height = (height / 2) - 1 + fontSize *= 0.9 + cornerRadius *= 0.75 + } + + let backgroundColor: Color + if configuration.isPressed { + backgroundColor = Color(.keyBackgroundHighlighted) + } else if selected { + backgroundColor = Color(.keyBackgroundSelected) } else { + backgroundColor = Color(.keyBackgroundNormal) + } + + return HStack(alignment: .center, spacing: 0) { configuration.label - .font(.system(size: isBigDevice ? 15 : 13)) - .frame(width: fixedWidth!, height: (isBigDevice ? 35 : 30)) - .background(configuration.isPressed ? Color(.keyBackgroundHighlighted) : (selected ? Color(.keyBackgroundSelected) : Color(.keyBackgroundNormal))) - .cornerRadius(isBigDevice ? 6 : 4) - .shadow(color: shadow ? Color.black.opacity(0.8) : .clear, radius: 0, x: 0, y: shadow ? 1 : 0) - .animation(nil) + .font(Font(UIFont.monospacedDigitSystemFont(ofSize: fontSize, weight: .regular))) + .padding(.horizontal, halfHeight ? 2 : 8) + .padding(.vertical, halfHeight ? 0 : 6) + .foregroundColor(selected && !configuration.isPressed ? .black : .white) } + .frame(minWidth: height, maxWidth: width) + .frame(height: height) + .background( + backgroundColor + .cornerRadius(cornerRadius) + .shadow(color: shadow ? Color.black.opacity(0.8) : .clear, + radius: 0, + x: 0, + y: shadow ? 1 : 0) + ) + .animation(nil) } - init(selected: Bool = false, hasShadow shadow: Bool = false, fixedWidth: CGFloat? = nil) { + init(selected: Bool = false, hasShadow shadow: Bool = false, halfHeight: Bool = false, widthRatio: CGFloat? = nil) { self.selected = selected self.shadow = shadow - self.fixedWidth = fixedWidth + self.halfHeight = halfHeight + self.widthRatio = widthRatio } } extension ButtonStyle where Self == KeyboardKeyButtonStyle { - ///A button style that mimicks the keys of the software keyboard. - static func keyboardKey(selected: Bool = false, hasShadow shadow: Bool = false, fixedWidth: CGFloat? = nil) -> KeyboardKeyButtonStyle { - return KeyboardKeyButtonStyle(selected: selected, hasShadow: shadow, fixedWidth: fixedWidth) + static func keyboardKey(selected: Bool = false, hasShadow shadow: Bool = false, halfHeight: Bool = false, widthRatio: CGFloat? = nil) -> KeyboardKeyButtonStyle { + return KeyboardKeyButtonStyle(selected: selected, hasShadow: shadow, halfHeight: halfHeight, widthRatio: widthRatio) } } @@ -52,29 +73,30 @@ struct KeyboardKeyButtonStyleContainer: View { var body: some View { HStack(alignment: .center, spacing: 5) { Button { - + } label: { Text("Ctrl") } .buttonStyle(.keyboardKey()) Button { - + } label: { - Image(systemName: "arrow.down") + Image(systemName: .arrowDown) } - .buttonStyle(.keyboardKey(fixedWidth: 31)) + .buttonStyle(.keyboardKey(widthRatio: 1)) } .padding() } } struct KeyboardKeyButtonStyleContainer_Previews: PreviewProvider { - static var previews: some View { - ForEach(ColorScheme.allCases, id: \.self) { scheme in + static var previews: some View { + ForEach(ColorScheme.allCases, id: \.self) { scheme in KeyboardKeyButtonStyleContainer() - .preferredColorScheme(scheme) - .previewLayout(.sizeThatFits) - } - } + .preferredColorScheme(scheme) + .previewLayout(.sizeThatFits) + } + } } + diff --git a/App/UI/Keyboard/KeyboardPopupToolbarView.swift b/App/UI/Keyboard/KeyboardPopupToolbarView.swift deleted file mode 100644 index a283cfa..0000000 --- a/App/UI/Keyboard/KeyboardPopupToolbarView.swift +++ /dev/null @@ -1,199 +0,0 @@ -// -// KeyboardPopupToolbarView.swift -// NewTerm (iOS) -// -// Created by Chris Harper on 11/21/21. -// - -import SwiftUI -import NewTermCommon - -struct KeyboardPopupToolbarView: View { - - struct KeyType { - enum function: Int, Equatable, CaseIterable { - case f1 - case f2 - case f3 - case f4 - case f5 - case f6 - case f7 - case f8 - case f9 - case f10 - case f11 - case f12 - - var text: String { - return "F\(self.rawValue)" - } - } - - enum leading: Int, CaseIterable { - case home - case end - - var text: String { - switch self { - case .home: - return "Home" - case .end: - return "End" - } - } - } - - enum paging: Int, CaseIterable { - case pgUp - case pgDn - - var text: String { - switch self { - case .pgUp: - return "PgUp" - case .pgDn: - return "PgDn" - } - } - } - - enum trailing: Int, CaseIterable { - case frwdDel - - var imageName: String { - switch self { - case .frwdDel: - return "delete.forward" - } - } - - } - } - - var functionButtonGroup: some View { - HStack(alignment: .center, spacing: 5) { - ForEach(KeyType.function.allCases, id: \.self) { button in - Button { - switch button { - case .f1: - break - case .f2: - break - case .f3: - break - case .f4: - break - case .f5: - break - case .f6: - break - case .f7: - break - case .f8: - break - case .f9: - break - case .f10: - break - case .f11: - break - case .f12: - break - } - } label: { - Text("\(button.text)") - } - .buttonStyle( - .keyboardKey(fixedWidth: 35) - ) - } - } - } - - var leadingButtonGroup: some View { - HStack(alignment: .center, spacing: 5) { - // 1: home; 2: end - ForEach(KeyType.leading.allCases, id: \.self) { button in - Button { - switch button { - case .home: - break - case .end: - break - } - } label: { - Text(button.text) - } - .buttonStyle( - .keyboardKey(fixedWidth: 50) - ) - } - } - } - - var pagingButtonGroup: some View { - HStack(alignment: .center, spacing: 5) { - ForEach(KeyType.paging.allCases, id: \.self) { button in - Button { - switch button { - case .pgUp: - break - case .pgDn: - break - } - } label: { - Text(button.text) - } - .buttonStyle( - .keyboardKey(fixedWidth: 50) - ) - } - } - } - - var trailingButtonGroup: some View { - HStack(alignment: .center, spacing: 5) { - ForEach(KeyType.trailing.allCases, id: \.self) { button in - Button { - switch button { - case .frwdDel: - break - } - } label: { - Image(systemName: button.imageName) - } - .buttonStyle(.keyboardKey()) - } - } - } - - var body: some View { - VStack(spacing: 5) { - ScrollView(.horizontal, showsIndicators: false) { - functionButtonGroup - .padding(.horizontal, 5) - } - HStack(alignment: .center, spacing: 10) { - leadingButtonGroup - pagingButtonGroup - Spacer() - trailingButtonGroup - } - .padding(.horizontal, 5) - } - .frame(maxWidth: .infinity, maxHeight: isBigDevice ? 96 : 80) - .background(Color(.keyboardToolbarBackground)) - } - -} - -struct KeyboardPopupToolbarView_Previews: PreviewProvider { - static var previews: some View { - ForEach(ColorScheme.allCases, id: \.self) { scheme in - KeyboardPopupToolbarView() - .preferredColorScheme(scheme) - .previewLayout(.sizeThatFits) - } - } -} diff --git a/App/UI/Keyboard/KeyboardToolbar.swift b/App/UI/Keyboard/KeyboardToolbar.swift index 042862e..e1b5f0c 100644 --- a/App/UI/Keyboard/KeyboardToolbar.swift +++ b/App/UI/Keyboard/KeyboardToolbar.swift @@ -7,90 +7,42 @@ // import UIKit +import SwiftUIX -class KeyboardToolbar: UIView { +class KeyboardToolbar: UIInputView { - var ctrlKey: KeyboardButton! - var metaKey: KeyboardButton! - var tabKey: KeyboardButton! - var moreKey: KeyboardButton! + private var hostingView: UIHostingView! - var upKey: KeyboardButton! - var downKey: KeyboardButton! - var leftKey: KeyboardButton! - var rightKey: KeyboardButton! + init() { + super.init(frame: .zero, inputViewStyle: .keyboard) - func setUp() { - let backdropView = UIVisualEffectView(effect: UIBlurEffect(style: .systemChromeMaterial)) - backdropView.frame = bounds - backdropView.autoresizingMask = [ .flexibleWidth, .flexibleHeight ] - addSubview(backdropView) + translatesAutoresizingMaskIntoConstraints = false + allowsSelfSizing = true - let backdropColorView = UIView() - backdropColorView.frame = backdropView.contentView.bounds - backdropColorView.autoresizingMask = [ .flexibleWidth, .flexibleHeight ] - backdropColorView.backgroundColor = .keyboardToolbarBackground - backdropView.contentView.addSubview(backdropColorView) + hostingView = UIHostingView(rootView: KeyboardToolbarView()) + hostingView.translatesAutoresizingMaskIntoConstraints = false + hostingView.shouldResizeToFitContent = true + hostingView.setContentHuggingPriority(.fittingSizeLevel, for: .vertical) + addSubview(hostingView) - let height = isSmallDevice ? 35 : 43 - let outerXSpacing = CGFloat(3) - let xSpacing = CGFloat(6) - let topSpacing = CGFloat(isSmallDevice ? 2 : 4) - let bottomSpacing = CGFloat(1) - - let spacerView = UIView() - - let sortedViews: [UIView] = [ - ctrlKey, metaKey, tabKey, moreKey, spacerView, - upKey, downKey, leftKey, rightKey - ] - - let stackView = UIStackView(arrangedSubviews: sortedViews) - stackView.translatesAutoresizingMaskIntoConstraints = false - stackView.axis = .horizontal - stackView.spacing = xSpacing - addSubview(stackView) - - addCompactConstraints([ - "self.height = height", - "stackView.top = toolbar.top + topSpacing", - "stackView.bottom = toolbar.bottom - bottomSpacing", - "stackView.left = safe.left + outerXSpacing", - "stackView.right = safe.right - outerXSpacing" - ], metrics: [ - "height": height, - "outerXSpacing": outerXSpacing, - "topSpacing": topSpacing, - "bottomSpacing": bottomSpacing - ], views: [ - "toolbar": self, - "stackView": stackView + NSLayoutConstraint.activate([ + hostingView.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor), + hostingView.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor), + hostingView.topAnchor.constraint(equalTo: self.topAnchor), + hostingView.bottomAnchor.constraint(equalTo: self.bottomAnchor) ]) } - override var intrinsicContentSize: CGSize { - var size = super.intrinsicContentSize - size.height = isSmallDevice ? 36 : 44 - return size - } - -} - -extension KeyboardToolbar: UIToolbarDelegate { - - func position(for bar: UIBarPositioning) -> UIBarPosition { - // Helps UIToolbar figure out where to place the shadow line - return .bottom + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") } } extension KeyboardToolbar: UIInputViewAudioFeedback { - var enableInputClicksWhenVisible: Bool { // Conforming to allows the buttons to make the click sound // when tapped - return true + true } - } diff --git a/App/UI/Keyboard/KeyboardToolbarView.swift b/App/UI/Keyboard/KeyboardToolbarView.swift index df09b3e..51ac7fb 100644 --- a/App/UI/Keyboard/KeyboardToolbarView.swift +++ b/App/UI/Keyboard/KeyboardToolbarView.swift @@ -7,135 +7,268 @@ import SwiftUI import NewTermCommon +import SwiftUIX + +struct Key { + var label: String + var glyph: String? + var imageName: SFSymbolName? + var preferredStyle: KeyboardButtonStyle? + var isToggle = false + var halfHeight = false + var widthRatio: CGFloat? +} + +enum Toolbar: CaseIterable { + case primary, secondary, fnKeys + + var keys: [ToolbarKey] { + switch self { + case .primary: + return [ + .control, .escape, .tab, .more, + .variableSpace, + .arrows + ] + + case .secondary: + return [ + .home, .end, + .variableSpace, + .pageUp, .pageDown, + .variableSpace, + .delete, + .variableSpace, + .fnKeys + ] + + case .fnKeys: + // TODO: + return Array(1...12).map { i in .fnKey } + } + } +} + +enum ToolbarKey: Int, CaseIterable { + // Special + case fixedSpace, variableSpace, arrows + // Primary - leading + case control, escape, tab, more + // Primary - trailing + case up, down, left, right + // Secondary - navigation + case home, end, pageUp, pageDown + // Secondary - extras + case delete, fnKeys + // Fn keys + case fnKey + + var key: Key { + switch self { + // Special + case .fixedSpace, .variableSpace, .arrows: + return Key(label: "") + + // Primary - leading + case .control: return Key(label: .localize("Control"), + glyph: .localize("Ctrl"), + imageName: .control, + isToggle: true) + case .escape: return Key(label: .localize("Escape"), + glyph: .localize("Esc"), + imageName: .escape) + case .tab: return Key(label: .localize("Tab"), + imageName: .arrowRightToLine) + case .more: return Key(label: .localize("More"), + imageName: .ellipsis, + preferredStyle: .icons, + isToggle: true) + // Primary - trailing + case .up: return Key(label: .localize("Up"), + imageName: .arrowUp, + preferredStyle: .icons, + halfHeight: true, + widthRatio: 1) + case .down: return Key(label: .localize("Down"), + imageName: .arrowDown, + preferredStyle: .icons, + halfHeight: true, + widthRatio: 1) + case .left: return Key(label: .localize("Left"), + imageName: .arrowLeft, + preferredStyle: .icons, + halfHeight: true, + widthRatio: 1) + case .right: return Key(label: .localize("Right"), + imageName: .arrowRight, + preferredStyle: .icons, + halfHeight: true, + widthRatio: 1) + // Secondary - navigation + case .home: return Key(label: .localize("Home"), + widthRatio: 1.25) + case .end: return Key(label: .localize("End"), + widthRatio: 1.25) + case .pageUp: return Key(label: .localize("Page Up"), + glyph: .localize("PgUp"), + widthRatio: 1.25) + case .pageDown: return Key(label: .localize("Page Down"), + glyph: .localize("PgDn"), + widthRatio: 1.25) + + // Secondary - extras + case .delete: return Key(label: .localize("Delete Forward"), + glyph: .localize("Del"), + imageName: .deleteRight, + preferredStyle: .icons, + widthRatio: 1) + case .fnKeys: return Key(label: .localize("Function Keys"), + glyph: .localize("Fn"), + isToggle: true, + widthRatio: 1) + + // Fn keys + case .fnKey: //(index: let index): + let index = 1 + return Key(label: "F\(index + 1)", preferredStyle: .text, widthRatio: 1.25) + } + } +} struct KeyboardToolbarView: View { + + let toolbars: [Toolbar] = [.fnKeys, .secondary, .primary] - struct KeyType { - enum leading: Int, CaseIterable { - case control - case escape - case tab - case more - - var text: String { - switch self { - case .control: - return "Ctrl" - case .escape: - return "Esc" - case .tab: - return "Tab" - case .more: - return "More" - } - } - - var imageName: String { - switch self { - case .control: - return "control" - case .escape: - return "escape" - case .tab: - return "arrow.right.to.line" - case .more: - return "ellipsis" + @State var toggledKeys = Set() + + @State var outerSize = CGSize.zero + + @ObservedObject var preferences = Preferences.shared + + private func isToolbarVisible(_ toolbar: Toolbar) -> Bool { + switch toolbar { + case .primary: return true + case .secondary: return toggledKeys.contains(.more) + case .fnKeys: return toggledKeys.contains(.fnKeys) + } + } + + private func toolbarView(for toolbar: Toolbar) -> some View { + let view = HStack(alignment: .center, spacing: 5) { + ForEach(toolbar.keys, id: \.self) { key in + switch key { + case .fixedSpace: + EmptyView() + + case .variableSpace: + Spacer(minLength: 0) + + case .arrows: + arrowsView + + default: + button(for: key) } } } - - enum trailing: Int, CaseIterable { - case up - case down - case left - case right - - var imageName: String { - switch self { - case .up: - return "arrow.up" - case .down: - return "arrow.down" - case .left: - return "arrow.left" - case .right: - return "arrow.right" + .padding(.horizontal, 4) + .padding(.top, 5) + + switch toolbar { + case .primary, .secondary: + return AnyView( + view + .frame(width: outerSize.width) + ) + + case .fnKeys: + return AnyView( + CocoaScrollView(.horizontal, showsIndicators: false) { + view } - } + .frame(width: outerSize.width) + ) } } - - @State var ctrlKeySelected = false - @State var moreKeySelected = false - - @ObservedObject var preferences = Preferences.shared - - var leadingButtonGroup: some View { - HStack(alignment: .center, spacing: 5) { - ForEach(KeyType.leading.allCases, id: \.self) { button in - Button { - switch button { - case .control: - ctrlKeySelected.toggle() - case .escape: - break - case .tab: - break - case .more: - moreKeySelected.toggle() - } - } label: { - switch preferences.keyboardAccessoryStyle { - case .icons: - Image(systemName: button.imageName) - case .text: - Text(button.text) - } + + func button(for key: ToolbarKey, halfHeight: Bool = false) -> some View { + Button { + UIDevice.current.playInputClick() + if key.key.isToggle { + if toggledKeys.contains(key) { + toggledKeys.remove(key) + } else { + toggledKeys.insert(key) } - .buttonStyle( - button == .control ? .keyboardKey(selected: ctrlKeySelected) : button == .more ? .keyboardKey(selected: moreKeySelected) : .keyboardKey() - ) + } + } label: { + switch key { + case .up, .down, .left, .right: + Image(systemName: key.key.imageName!) + .frame(width: 14, height: 14, alignment: .center) + .accessibilityLabel(key.key.label) + + default: +// HStack(alignment: .center, spacing: 0) { +// Spacer(minLength: 0) + VStack(alignment: .trailing, spacing: 3) { + HStack(spacing: 0) { + if let imageName = key.key.imageName, + key.key.preferredStyle != .text { + Image(systemName: imageName) + .frame(width: 14, height: 14, alignment: .center) + .padding(.trailing, 1.5) + .accessibilityLabel(key.key.label) + } + } + .frame(height: 16) + + Text((key.key.glyph ?? key.key.label).localizedLowercase) + } +// } } } + .buttonStyle(.keyboardKey(selected: toggledKeys.contains(key), + hasShadow: true, + halfHeight: halfHeight, + widthRatio: key.key.widthRatio)) } - - var trailingButtonGroup: some View { - HStack(alignment: .center, spacing: 5) { - ForEach(KeyType.trailing.allCases, id: \.self) { button in - Button { - switch button { - case .up: - break - case .down: - break - case .left: - break - case .right: - break - } - } label: { - Image(systemName: button.imageName) - } - .buttonStyle( - .keyboardKey(fixedWidth: 30) - ) + + var arrowsView: some View { + // “Scissor” +// VStack(alignment: .center, spacing: 1.5) { +// button(for: .up, halfHeight: true) +// HStack(spacing: 1.5) { +// button(for: .left, halfHeight: true) +// button(for: .down, halfHeight: true) +// button(for: .right, halfHeight: true) +// } +// } + + // “Butterfly” + HStack(spacing: 2) { + button(for: .left) + VStack(spacing: 2) { + button(for: .up, halfHeight: true) + button(for: .down, halfHeight: true) } + button(for: .right) } } - + var body: some View { - VStack(spacing: 0) { - if moreKeySelected { - KeyboardPopupToolbarView() - } - HStack(alignment: .center, spacing: 5) { - leadingButtonGroup - Spacer() - trailingButtonGroup + ZStack(alignment: .bottom) { + Color.systemIndigo + .frame(height: 0) + .captureSize(in: $outerSize) + + VStack(spacing: 0) { + ForEach(toolbars, id: \.self) { toolbar in + if isToolbarVisible(toolbar) { + toolbarView(for: toolbar) + } + } } - .padding(.horizontal, 5) - .frame(maxWidth: .infinity, maxHeight: isBigDevice ? 48 : 40) - .background(Color(.keyboardToolbarBackground)) } } } @@ -146,7 +279,6 @@ struct KeyboardToolbarView_Previews: PreviewProvider { VStack{ Spacer() KeyboardToolbarView() - // .padding() .preferredColorScheme(scheme) .previewLayout(.sizeThatFits) } diff --git a/App/UI/Keyboard/TerminalKeyInput.swift b/App/UI/Keyboard/TerminalKeyInput.swift index f9ef6ba..5668b0e 100644 --- a/App/UI/Keyboard/TerminalKeyInput.swift +++ b/App/UI/Keyboard/TerminalKeyInput.swift @@ -7,6 +7,7 @@ // import UIKit +import NewTermCommon class TerminalKeyInput: TextInputBase { @@ -45,7 +46,7 @@ class TerminalKeyInput: TextInputBase { // Should be [UIKey], but I can’t use @available(iOS 13.4, *) on a property private var pressedKeys = [Any]() - private lazy var keyValues: [KeyboardButton: [UInt8]] = [ + private lazy var keyValues: [KeyboardButton: [UTF8Char]] = [ metaKey: EscapeSequences.meta, tabKey: EscapeSequences.tab, upKey: EscapeSequences.up, @@ -59,7 +60,7 @@ class TerminalKeyInput: TextInputBase { moreToolbar.deleteKey: EscapeSequences.delete, ] - private lazy var keyAppValues: [KeyboardButton: [UInt8]] = [ + private lazy var keyAppValues: [KeyboardButton: [UTF8Char]] = [ upKey: EscapeSequences.upApp, downKey: EscapeSequences.downApp, leftKey: EscapeSequences.leftApp, @@ -136,15 +137,15 @@ class TerminalKeyInput: TextInputBase { } else { toolbar = KeyboardToolbar() toolbar!.translatesAutoresizingMaskIntoConstraints = false - toolbar!.ctrlKey = ctrlKey - toolbar!.metaKey = metaKey - toolbar!.tabKey = tabKey - toolbar!.moreKey = moreKey - toolbar!.upKey = upKey - toolbar!.downKey = downKey - toolbar!.leftKey = leftKey - toolbar!.rightKey = rightKey - toolbar!.setUp() +// toolbar!.ctrlKey = ctrlKey +// toolbar!.metaKey = metaKey +// toolbar!.tabKey = tabKey +// toolbar!.moreKey = moreKey +// toolbar!.upKey = upKey +// toolbar!.downKey = downKey +// toolbar!.leftKey = leftKey +// toolbar!.rightKey = rightKey +// toolbar!.setUp() } ctrlKey.addTarget(self, action: #selector(self.ctrlKeyPressed), for: .touchUpInside) @@ -175,16 +176,16 @@ class TerminalKeyInput: TextInputBase { moreToolbar.trailingAnchor.constraint(equalTo: self.trailingAnchor), moreToolbarBottomConstraint, - ctrlKey.widthAnchor.constraint(greaterThanOrEqualTo: metaKey.widthAnchor), - metaKey.widthAnchor.constraint(greaterThanOrEqualTo: ctrlKey.widthAnchor), - metaKey.widthAnchor.constraint(greaterThanOrEqualTo: tabKey.widthAnchor), - tabKey.widthAnchor.constraint(greaterThanOrEqualTo: metaKey.widthAnchor), - tabKey.widthAnchor.constraint(greaterThanOrEqualTo: moreKey.widthAnchor), - moreKey.widthAnchor.constraint(greaterThanOrEqualTo: tabKey.widthAnchor) +// ctrlKey.widthAnchor.constraint(greaterThanOrEqualTo: metaKey.widthAnchor), +// metaKey.widthAnchor.constraint(greaterThanOrEqualTo: ctrlKey.widthAnchor), +// metaKey.widthAnchor.constraint(greaterThanOrEqualTo: tabKey.widthAnchor), +// tabKey.widthAnchor.constraint(greaterThanOrEqualTo: metaKey.widthAnchor), +// tabKey.widthAnchor.constraint(greaterThanOrEqualTo: moreKey.widthAnchor), +// moreKey.widthAnchor.constraint(greaterThanOrEqualTo: tabKey.widthAnchor) ]) - NSLayoutConstraint.activate([ upKey, downKey, leftKey, rightKey ].map { view in view.widthAnchor.constraint(equalTo: view.heightAnchor) }) - squareButtonConstraints = [ ctrlKey, metaKey, tabKey, moreKey ].map { view in view.widthAnchor.constraint(equalTo: view.heightAnchor) } +// NSLayoutConstraint.activate([ upKey, downKey, leftKey, rightKey ].map { view in view.widthAnchor.constraint(equalTo: view.heightAnchor) }) +// squareButtonConstraints = [ ctrlKey, metaKey, tabKey, moreKey ].map { view in view.widthAnchor.constraint(equalTo: view.heightAnchor) } NotificationCenter.default.addObserver(self, selector: #selector(self.preferencesUpdated), name: Preferences.didChangeNotification, object: nil) preferencesUpdated() @@ -194,23 +195,21 @@ class TerminalKeyInput: TextInputBase { fatalError("init(coder:) has not been implemented") } - override var inputAccessoryView: UIView? { - return toolbar - } + override var inputAccessoryView: UIView? { toolbar } @objc func preferencesUpdated() { - let preferences = Preferences.shared - let style = preferences.keyboardAccessoryStyle - - for button in buttons { - button.style = style - } - - // Enable 1:1 width:height aspect ratio if using icons style - switch style { - case .text: NSLayoutConstraint.deactivate(squareButtonConstraints) - case .icons: NSLayoutConstraint.activate(squareButtonConstraints) - } +// let preferences = Preferences.shared +// let style = preferences.keyboardAccessoryStyle +// +// for button in buttons { +// button.style = style +// } +// +// // Enable 1:1 width:height aspect ratio if using icons style +// switch style { +// case .text: NSLayoutConstraint.deactivate(squareButtonConstraints) +// case .icons: NSLayoutConstraint.activate(squareButtonConstraints) +// } } // MARK: - Callbacks @@ -334,10 +333,10 @@ class TerminalKeyInput: TextInputBase { override func insertText(_ text: String) { // Used by the software keyboard only. See pressesBegan(_:with:) below for hardware keyboard. - let data = text.utf8.map { character -> UInt8 in + let data = text.utf8.map { character -> UTF8Char in // Convert newline to carriage return if character == 0x0A { - return 0x0D + return EscapeSequences.return.first! } if ctrlDown { return EscapeSequences.asciiToControl(character) @@ -430,8 +429,7 @@ class TerminalKeyInput: TextInputBase { override func paste(_ sender: Any?) { if let string = UIPasteboard.general.string { - let data = [UInt8](string.utf8) - terminalInputDelegate!.receiveKeyboardInput(data: data) + terminalInputDelegate!.receiveKeyboardInput(data: string.utf8Array) } } @@ -445,7 +443,7 @@ class TerminalKeyInput: TextInputBase { return false } - var keyData: [UInt8] + var keyData: [UTF8Char] switch key.keyCode { case .keyboardReturnOrEnter: keyData = EscapeSequences.return case .keyboardEscape: keyData = EscapeSequences.meta @@ -489,7 +487,7 @@ class TerminalKeyInput: TextInputBase { .keyboardF8, .keyboardF9, .keyboardF10, .keyboardF11, .keyboardF12: keyData = EscapeSequences.fn[key.keyCode.rawValue - UIKeyboardHIDUsage.keyboardF1.rawValue] - default: keyData = [UInt8](key.characters.utf8) + default: keyData = key.characters.utf8Array } // If we didn’t get anything to type, nothing else to do here. @@ -613,7 +611,7 @@ extension TerminalKeyInput: TerminalPasswordInputViewDelegate { // password autofill. Send a return if it seems like a password was actually received, // otherwise just pretend it was typed like normal. if password.count > 2 { - terminalInputDelegate!.receiveKeyboardInput(data: [UInt8](password.utf8)) + terminalInputDelegate!.receiveKeyboardInput(data: password.utf8Array) terminalInputDelegate!.receiveKeyboardInput(data: EscapeSequences.return) } else { insertText(password) diff --git a/App/UI/Terminal/TerminalSampleView.swift b/App/UI/Terminal/TerminalSampleView.swift index ac9cdb6..8bdfacf 100644 --- a/App/UI/Terminal/TerminalSampleView.swift +++ b/App/UI/Terminal/TerminalSampleView.swift @@ -36,8 +36,7 @@ class TerminalSampleView: UIView { addSubview(textView) if let colorTest = try? Data(contentsOf: Bundle.main.url(forResource: "colortest", withExtension: "txt")!) { - let bytes = Array(colorTest) - terminal?.feed(byteArray: bytes) + terminal?.feed(byteArray: [UTF8Char](colorTest)) } } diff --git a/Common/Controllers/SubProcess.swift b/Common/Controllers/SubProcess.swift index 469ac75..7867afc 100644 --- a/Common/Controllers/SubProcess.swift +++ b/Common/Controllers/SubProcess.swift @@ -22,7 +22,7 @@ enum SubProcessIOError: Error { protocol SubProcessDelegate: AnyObject { func subProcessDidConnect() - func subProcess(didReceiveData data: [UInt8]) + func subProcess(didReceiveData data: [UTF8Char]) func subProcess(didDisconnectWithError error: Error?) func subProcess(didReceiveError error: Error) } @@ -179,14 +179,14 @@ class SubProcess { default: // Read from output and notify delegate. - let bytes = buffer.bindMemory(to: UInt8.self, capacity: bytesRead) + let bytes = buffer.bindMemory(to: UTF8Char.self, capacity: bytesRead) let data = Array(UnsafeBufferPointer(start: bytes, count: bytesRead)) delegate?.subProcess(didReceiveData: data) } buffer.deallocate() } - func write(data: [UInt8]) { + func write(data: [UTF8Char]) { queue.async { guard let fileDescriptor = self.fileDescriptor else { return diff --git a/Common/Controllers/TerminalController.swift b/Common/Controllers/TerminalController.swift index 8c5b49a..c9c4731 100644 --- a/Common/Controllers/TerminalController.swift +++ b/Common/Controllers/TerminalController.swift @@ -50,7 +50,7 @@ public class TerminalController { private var updateTimer: CADisplayLink? private var refreshRate: TimeInterval = 60 private var isVisible = true - private var readBuffer = [UInt8]() + private var readBuffer = [UTF8Char]() internal var terminalQueue = DispatchQueue(label: "ws.hbang.Terminal.terminal-queue") @@ -197,22 +197,22 @@ public class TerminalController { // MARK: - Terminal - public func readInputStream(_ data: [UInt8]) { + public func readInputStream(_ data: [UTF8Char]) { terminalQueue.async { self.readBuffer += data } } private func readInputStream(_ data: Data) { - readInputStream([UInt8](data)) + readInputStream([UTF8Char](data)) } - public func write(_ data: [UInt8]) { + public func write(_ data: [UTF8Char]) { subProcess?.write(data: data) } public func write(_ data: Data) { - write([UInt8](data)) + write([UTF8Char](data)) } @objc private func updateTimerFired() { @@ -294,7 +294,7 @@ extension TerminalController: TerminalDelegate { public func send(source: Terminal, data: ArraySlice) { terminalQueue.async { - self.write([UInt8](data)) + self.write([UTF8Char](data)) } } @@ -355,7 +355,7 @@ extension TerminalController: TerminalInputProtocol { public var applicationCursor: Bool { terminal?.applicationCursor ?? false } - public func receiveKeyboardInput(data: [UInt8]) { + public func receiveKeyboardInput(data: [UTF8Char]) { // Forward the data from the keyboard directly to the subprocess subProcess!.write(data: data) } @@ -368,7 +368,7 @@ extension TerminalController: SubProcessDelegate { // Yay } - func subProcess(didReceiveData data: [UInt8]) { + func subProcess(didReceiveData data: [UTF8Char]) { // Simply forward the input stream down the VT100 processor. When it notices changes to the // screen, it should invoke our refresh delegate below. readInputStream(data) diff --git a/Common/Extensions/String+Additions.swift b/Common/Extensions/String+Additions.swift index a063ba3..a89ff40 100644 --- a/Common/Extensions/String+Additions.swift +++ b/Common/Extensions/String+Additions.swift @@ -8,7 +8,11 @@ import Foundation extension String { - var cString: UnsafeMutablePointer? { + public var cString: UnsafeMutablePointer? { strdup(self) } + + public var utf8Array: [UTF8Char] { + Array(utf8) + } } diff --git a/Common/VT100/TerminalConstants.swift b/Common/VT100/TerminalConstants.swift index 86e7ef6..d5e6ad2 100644 --- a/Common/VT100/TerminalConstants.swift +++ b/Common/VT100/TerminalConstants.swift @@ -23,46 +23,35 @@ public struct EscapeSequences { // https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-PC-Style-Function-Keys - public static let backspace: [UInt8] = [0x7F] // \x7F - public static let meta: [UInt8] = [0x1B] // \e - public static let tab: [UInt8] = [0x09] // \t - public static let `return`: [UInt8] = [0x0D] // \r + public static let backspace = "\u{7f}".utf8Array + public static let meta = "\u{1b}".utf8Array + public static let tab = "\t".utf8Array + public static let `return` = "\r".utf8Array - public static let up: [UInt8] = [0x1B, 0x5B, 0x41] // \e[A - public static let upApp: [UInt8] = [0x1B, 0x4F, 0x41] // \eOA - public static let down: [UInt8] = [0x1B, 0x5B, 0x42] // \e[B - public static let downApp: [UInt8] = [0x1B, 0x4F, 0x42] // \eOB - public static let left: [UInt8] = [0x1B, 0x5B, 0x44] // \e[D - public static let leftApp: [UInt8] = [0x1B, 0x4F, 0x44] // \eOD - public static let leftMeta: [UInt8] = [0x62] // \eb (removed \e) - public static let right: [UInt8] = [0x1B, 0x5B, 0x43] // \e[C - public static let rightApp: [UInt8] = [0x1B, 0x4F, 0x43] // \eOC - public static let rightMeta: [UInt8] = [0x66] // \ef (removed \e) + public static let up = "\u{1b}[A".utf8Array + public static let upApp = "\u{1b}OA".utf8Array + public static let down = "\u{1b}[B".utf8Array + public static let downApp = "\u{1b}OB".utf8Array + public static let left = "\u{1b}[D".utf8Array + public static let leftApp = "\u{1b}OD".utf8Array + public static let leftMeta = "b".utf8Array // (removed \e) + public static let right = "\u{1b}[C".utf8Array + public static let rightApp = "\u{1b}OC".utf8Array + public static let rightMeta = "f".utf8Array // (removed \e) - public static let home: [UInt8] = [0x1B, 0x5B, 0x48] // \e[H - public static let homeApp: [UInt8] = [0x1B, 0x4F, 0x48] // \eOH - public static let end: [UInt8] = [0x1B, 0x5B, 0x46] // \e[F - public static let endApp: [UInt8] = [0x1B, 0x4F, 0x46] // \eOF - public static let pageUp: [UInt8] = [0x1B, 0x5B, 0x35, 0x7E] // \e[5~ - public static let pageDown: [UInt8] = [0x1B, 0x5B, 0x36, 0x7E] // \e[6~ - public static let delete: [UInt8] = [0x1B, 0x5B, 0x33, 0x7E] // \e[3~ + public static let home = "\u{1b}[H".utf8Array + public static let homeApp = "\u{1b}OH".utf8Array + public static let end = "\u{1b}[F".utf8Array + public static let endApp = "\u{1b}OF".utf8Array + public static let pageUp = "\u{1b}[5~".utf8Array + public static let pageDown = "\u{1b}[6~".utf8Array + public static let delete = "\u{1b}[3~".utf8Array - public static let fn: [[UInt8]] = [ - [0x1B, 0x4F, 0x50], // \eOP - [0x1B, 0x4F, 0x51], // \eOQ - [0x1B, 0x4F, 0x52], // \eOR - [0x1B, 0x4F, 0x53], // \eOS - [0x1B, 0x5B, 0x31, 0x35, 0x7E], // \e[15~ - [0x1B, 0x5B, 0x31, 0x37, 0x7E], // \e[17~ - [0x1B, 0x5B, 0x31, 0x38, 0x7E], // \e[18~ - [0x1B, 0x5B, 0x31, 0x39, 0x7E], // \e[19~ - [0x1B, 0x5B, 0x32, 0x30, 0x7E], // \e[20~ - [0x1B, 0x5B, 0x32, 0x31, 0x7E], // \e[21~ - [0x1B, 0x5B, 0x32, 0x33, 0x7E], // \e[23~ - [0x1B, 0x5B, 0x32, 0x34, 0x7E], // \e[24~ - ] + public static let fn = [ + "OP", "OQ", "OR", "OS", "[15~", "[17~", "[18~", "[19~", "[20~", "[21~", "[23~", "[24~" + ].map { "\u{1b}\($0)".utf8Array } - public static func asciiToControl(_ character: UInt8) -> UInt8 { + public static func asciiToControl(_ character: UTF8Char) -> UTF8Char { var newCharacter = character // Translate capital to lowercase if character >= 0x41 && character <= 0x5A { // >= 'A' <= 'Z' diff --git a/Common/VT100/TerminalInputProtocol.swift b/Common/VT100/TerminalInputProtocol.swift index 0c63c31..4bf98e5 100644 --- a/Common/VT100/TerminalInputProtocol.swift +++ b/Common/VT100/TerminalInputProtocol.swift @@ -9,7 +9,7 @@ import Foundation public protocol TerminalInputProtocol: AnyObject { - func receiveKeyboardInput(data: [UInt8]) + func receiveKeyboardInput(data: [UTF8Char]) var applicationCursor: Bool { get } diff --git a/NewTerm.xcodeproj/project.pbxproj b/NewTerm.xcodeproj/project.pbxproj index ae0e492..56cb39d 100644 --- a/NewTerm.xcodeproj/project.pbxproj +++ b/NewTerm.xcodeproj/project.pbxproj @@ -7,7 +7,6 @@ objects = { /* Begin PBXBuildFile section */ - 1E1B78AF274A1D4300F885CC /* KeyboardPopupToolbarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E1B78AE274A1D4300F885CC /* KeyboardPopupToolbarView.swift */; }; 1E4A7332274A0CED00211604 /* KeyboardToolbarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E4A7331274A0CED00211604 /* KeyboardToolbarView.swift */; }; 1E4A7334274A0D5F00211604 /* KeyboardKeyButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E4A7333274A0D5F00211604 /* KeyboardKeyButtonStyle.swift */; }; 1EEB4F66262CF591005E5B79 /* SafariView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EEB4F65262CF591005E5B79 /* SafariView.swift */; }; @@ -125,7 +124,6 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 1E1B78AE274A1D4300F885CC /* KeyboardPopupToolbarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardPopupToolbarView.swift; sourceTree = ""; }; 1E4A7331274A0CED00211604 /* KeyboardToolbarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardToolbarView.swift; sourceTree = ""; }; 1E4A7333274A0D5F00211604 /* KeyboardKeyButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardKeyButtonStyle.swift; sourceTree = ""; }; 1EEB4F65262CF591005E5B79 /* SafariView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariView.swift; sourceTree = ""; }; @@ -432,7 +430,6 @@ 4ECEE6EC25F9B87E00FAD26B /* TerminalPasswordInputView.swift */, 1E4A7331274A0CED00211604 /* KeyboardToolbarView.swift */, 1E4A7333274A0D5F00211604 /* KeyboardKeyButtonStyle.swift */, - 1E1B78AE274A1D4300F885CC /* KeyboardPopupToolbarView.swift */, ); path = Keyboard; sourceTree = ""; @@ -779,7 +776,6 @@ 4ECFC23825FA3D2C007B0F51 /* LayoutGuide.swift in Sources */, 4E98081C261850E600E41883 /* KeyValueView.swift in Sources */, 4ECEE6ED25F9B87E00FAD26B /* TerminalPasswordInputView.swift in Sources */, - 1E1B78AF274A1D4300F885CC /* KeyboardPopupToolbarView.swift in Sources */, CFBB96EE22B5481B00585BE6 /* TerminalSceneDelegate.swift in Sources */, CFBB96EC22B5481B00585BE6 /* AppDelegate.swift in Sources */, CF6B3BAA244C4538000A608B /* LogoHeaderView.swift in Sources */,