From 84f14cac49135306299508b390bcaf84f2f4b10a Mon Sep 17 00:00:00 2001 From: Adam Demasi Date: Sat, 20 Nov 2021 16:54:59 +1030 Subject: [PATCH] [app] General settings cleanup and tweaking --- App/Controllers/SettingsSceneDelegate.swift | 2 +- App/Supporting Files/BridgingHeader.h | 10 ++ App/Supporting Files/Info.plist | 2 + App/UI/Settings/Mac/SettingsGeneralView.swift | 73 ++++------ .../Settings/Mac/SettingsInterfaceView.swift | 23 ++-- App/UI/Settings/PreferencesGroup.swift | 68 +++++++++ App/UI/Settings/PreferencesList.swift | 37 +++++ App/UI/Settings/SettingsPerformanceView.swift | 129 ++++++++++++------ App/UI/Settings/iOS/SettingsFontView.swift | 77 ++++------- App/UI/Settings/iOS/SettingsThemeView.swift | 44 ++---- App/UI/Settings/iOS/SettingsView.swift | 2 +- App/UIDevice+Additions.swift | 76 +++++++++++ Common/Controllers/Preferences.swift | 35 +++-- Common/Controllers/TerminalController.swift | 15 +- Common/Extensions/Color+Additions.swift | 4 + Common/Extensions/String+Localization.swift | 4 + Common/Extensions/UIColor+HBAdditions.h | 36 ----- Common/Extensions/UIColor+HBAdditions.m | 64 --------- Common/Extensions/UIColorAdditions.swift | 96 +++++++++++++ Common/Models/AppFont.swift | 2 + Common/Models/AppStorage.swift | 68 --------- Common/Models/AppTheme.swift | 2 + Common/Supporting Files/NewTermCommon.h | 1 - Common/Supporting Files/PrefixHeader.pch | 3 +- Common/VT100/ColorMap.swift | 79 +++++++---- Common/VT100/StringSupplier.swift | 4 + Common/VT100/TerminalConstants.swift | 2 + Common/VT100/TerminalInputProtocol.swift | 2 + NewTerm.xcodeproj/project.pbxproj | 32 +++-- 29 files changed, 581 insertions(+), 411 deletions(-) create mode 100644 App/UI/Settings/PreferencesGroup.swift create mode 100644 App/UI/Settings/PreferencesList.swift create mode 100644 App/UIDevice+Additions.swift delete mode 100644 Common/Extensions/UIColor+HBAdditions.h delete mode 100644 Common/Extensions/UIColor+HBAdditions.m create mode 100644 Common/Extensions/UIColorAdditions.swift delete mode 100644 Common/Models/AppStorage.swift diff --git a/App/Controllers/SettingsSceneDelegate.swift b/App/Controllers/SettingsSceneDelegate.swift index 5a8a84f..3d9974a 100644 --- a/App/Controllers/SettingsSceneDelegate.swift +++ b/App/Controllers/SettingsSceneDelegate.swift @@ -149,7 +149,7 @@ extension SettingsSceneDelegate: NSToolbarDelegate { } @objc private func selectPerformanceTab() { - switchTab(rootView: SettingsPerformanceView(), size: CGSize(width: 600, height: 350)) + switchTab(rootView: SettingsPerformanceView(), size: CGSize(width: 600, height: 500)) } } diff --git a/App/Supporting Files/BridgingHeader.h b/App/Supporting Files/BridgingHeader.h index afe6444..a215f1c 100644 --- a/App/Supporting Files/BridgingHeader.h +++ b/App/Supporting Files/BridgingHeader.h @@ -3,3 +3,13 @@ // @import NewTermCommon; + +#import + +#if TARGET_OS_MACCATALYST +@import IOKit; + +extern CFTypeRef IOPSCopyPowerSourcesInfo(void); +extern CFArrayRef IOPSCopyPowerSourcesList(CFTypeRef blob); +extern CFDictionaryRef IOPSGetPowerSourceDescription(CFTypeRef blob, CFTypeRef ps); +#endif diff --git a/App/Supporting Files/Info.plist b/App/Supporting Files/Info.plist index 34a708c..2c4ed06 100644 --- a/App/Supporting Files/Info.plist +++ b/App/Supporting Files/Info.plist @@ -84,5 +84,7 @@ Dark CADisableMinimumFrameDuration + CADisableMinimumFrameDurationOnPhone + diff --git a/App/UI/Settings/Mac/SettingsGeneralView.swift b/App/UI/Settings/Mac/SettingsGeneralView.swift index c2f798b..568d803 100644 --- a/App/UI/Settings/Mac/SettingsGeneralView.swift +++ b/App/UI/Settings/Mac/SettingsGeneralView.swift @@ -29,55 +29,42 @@ struct SettingsGeneralView: View { @State private var syncPathBrowsePresented = false var body: some View { - return ScrollView { - VStack(alignment: .leading, spacing: 10) { - GroupBox(label: Text("Bell")) { - VStack(alignment: .leading) { - Toggle("Make beep sound", isOn: preferences.$bellSound) - Toggle("Show heads-up display", isOn: preferences.$bellHUD) - } - .padding(10) - } - - FooterText(text: Text("When a terminal application needs to notify you of something, it rings the bell.")) + PreferencesList { + PreferencesGroup( + header: Text("Bell"), + footer: Text("When a terminal application needs to notify you of something, it rings the bell.") + ) { + Toggle("Make beep sound", isOn: preferences.$bellSound) + Toggle("Show heads-up display", isOn: preferences.$bellHUD) + } - GroupBox(label: Text("Settings Sync")) { - VStack(alignment: .leading) { - Picker("Sync app settings:", selection: preferences.$preferencesSyncService) { - Text("Don’t sync") - .tag(PreferencesSyncService.none) - Text("via iCloud") - .tag(PreferencesSyncService.icloud) - Text("via custom folder") - .tag(PreferencesSyncService.folder) - } - .pickerStyle(InlinePickerStyle()) + PreferencesGroup( + header: Text("Settings Sync"), + footer: Text("Keep your NewTerm settings in sync between your Mac, iPhone, and iPad by selecting iCloud sync. If you just want to keep a backup with a service such as Dropbox, select custom folder sync.") + ) { + Picker("Sync app settings:", selection: preferences.$preferencesSyncService) { + Text("Don’t sync") + .tag(PreferencesSyncService.none) + Text("via iCloud") + .tag(PreferencesSyncService.icloud) + Text("via custom folder") + .tag(PreferencesSyncService.folder) + } - HStack { - TextField("Sync path:", - text: Binding( - get: { preferences.preferencesSyncPath ?? "" }, - set: { value in preferences.preferencesSyncPath = value } - ) - ) - Button("Browse") { - syncPathBrowsePresented.toggle() - } - .fileImporter(isPresented: $syncPathBrowsePresented, - allowedContentTypes: [.folder]) { result in - preferences.preferencesSyncPath = (try? result.get())?.path - } - } - .disabled(preferences.preferencesSyncService != .folder) + HStack { + TextField("Sync path:", text: preferences.$preferencesSyncPath) + Button("Browse") { + syncPathBrowsePresented.toggle() + } + .fileImporter(isPresented: $syncPathBrowsePresented, + allowedContentTypes: [.folder]) { result in + preferences.preferencesSyncPath = (try? result.get())?.path ?? "" } - .padding(10) } - - FooterText(text: Text("Keep your NewTerm settings in sync between your Mac, iPhone, and iPad by selecting iCloud sync. If you just want to keep a backup with a service such as Dropbox, select custom folder sync.")) + .disabled(preferences.preferencesSyncService != .folder) } - .padding(20) } - .navigationBarTitle("General", displayMode: .inline) + .navigationBarTitle("General") } } diff --git a/App/UI/Settings/Mac/SettingsInterfaceView.swift b/App/UI/Settings/Mac/SettingsInterfaceView.swift index c2042c9..4c09ed0 100644 --- a/App/UI/Settings/Mac/SettingsInterfaceView.swift +++ b/App/UI/Settings/Mac/SettingsInterfaceView.swift @@ -50,19 +50,22 @@ struct SettingsInterfaceView: View { } .padding([.leading, .trailing], 7) ) + .padding([.top, .leading, .trailing], 1 / UIScreen.main.scale) Divider() + .padding([.leading, .trailing], 1 / UIScreen.main.scale) TerminalSampleViewRepresentable( fontMetrics: preferences.fontMetrics, colorMap: preferences.colorMap ) } .frame(width: 320) - .clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous)) + .clipShape(RoundedRectangle(cornerRadius: 9, style: .continuous)) .overlay( RoundedRectangle(cornerRadius: 8, style: .continuous) - .stroke(Color(UIColor.tertiarySystemBackground), lineWidth: 1 / UIScreen.main.scale) + .strokeBorder(Color(UIColor.tertiarySystemBackground), lineWidth: 1 / UIScreen.main.scale) .foregroundColor(.clear) ) + .padding([.top, .bottom, .leading], 20) let themes = Picker("Theme", selection: preferences.$themeName) { ForEach(sortedThemes, id: \.key) { key, value in @@ -90,7 +93,6 @@ struct SettingsInterfaceView: View { ) } } - .pickerStyle(InlinePickerStyle()) let fontSize = TextField( "Font Size", @@ -111,18 +113,15 @@ struct SettingsInterfaceView: View { ) .keyboardType(.decimalPad) - return HStack(spacing: 20) { + return HStack(spacing: 0) { sampleView - ScrollView { - VStack(alignment: .leading, spacing: 10) { - Text("Note: You currently need to restart the app to have theme updates apply.") - themes - fonts - fontSize - } + PreferencesList { + Text("Note: You currently need to restart the app to have theme updates apply.") + themes + fonts + fontSize } } - .padding(20) .navigationBarTitle("Interface", displayMode: .inline) } diff --git a/App/UI/Settings/PreferencesGroup.swift b/App/UI/Settings/PreferencesGroup.swift new file mode 100644 index 0000000..1efc9ff --- /dev/null +++ b/App/UI/Settings/PreferencesGroup.swift @@ -0,0 +1,68 @@ +// +// PreferencesGroup.swift +// NewTerm (iOS) +// +// Created by Adam Demasi on 25/9/21. +// + +import SwiftUI + +struct PreferencesGroup: View { + + var header: Header + var footer: Footer + var content: Content + + init(header: Header, footer: Footer, @ViewBuilder content: () -> Content) { + self.header = header + self.footer = footer + self.content = content() + } + + var body: some View { + #if targetEnvironment(macCatalyst) + VStack(alignment: .leading, spacing: 6) { + GroupBox(label: header) { + VStack(alignment: .leading, spacing: 0) { + content + .padding([.leading, .trailing], 6) + .padding([.top, .bottom], 4) + .frame(maxWidth: .infinity, minHeight: 0, alignment: .leading) + } + } + footer + .font(.caption) + .padding([.leading, .trailing], 10) + .foregroundColor(.secondary) + } + .pickerStyle(InlinePickerStyle()) + #else + Section( + header: header, + footer: footer + ) { + content + } + .pickerStyle(InlinePickerStyle()) + #endif + } + +} + +extension PreferencesGroup where Header == EmptyView, Footer: View, Content: View { + init(footer: Footer, @ViewBuilder content: () -> Content) { + self.init(header: EmptyView(), footer: footer, content: content) + } +} + +extension PreferencesGroup where Header: View, Footer == EmptyView, Content: View { + init(header: Header, @ViewBuilder content: () -> Content) { + self.init(header: header, footer: EmptyView(), content: content) + } +} + +extension PreferencesGroup where Header == EmptyView, Footer == EmptyView, Content: View { + init(@ViewBuilder content: () -> Content) { + self.init(header: EmptyView(), footer: EmptyView(), content: content) + } +} diff --git a/App/UI/Settings/PreferencesList.swift b/App/UI/Settings/PreferencesList.swift new file mode 100644 index 0000000..d321c52 --- /dev/null +++ b/App/UI/Settings/PreferencesList.swift @@ -0,0 +1,37 @@ +// +// PreferencesList.swift +// NewTerm (iOS) +// +// Created by Adam Demasi on 25/9/21. +// + +import SwiftUI + +struct PreferencesList: View { + + var content: Content + + init(@ViewBuilder content: () -> Content) { + self.content = content() + } + + var body: some View { +#if targetEnvironment(macCatalyst) + ScrollView { + VStack(alignment: .leading, spacing: 10) { + content + } + .padding([.top, .bottom], 16) + .padding([.leading, .trailing], 20) + } + .navigationBarTitleDisplayMode(.inline) +#else + List { + content + } + .listStyle(InsetGroupedListStyle()) + .navigationBarTitleDisplayMode(.inline) +#endif + } + +} diff --git a/App/UI/Settings/SettingsPerformanceView.swift b/App/UI/Settings/SettingsPerformanceView.swift index e3e1262..ba45a98 100644 --- a/App/UI/Settings/SettingsPerformanceView.swift +++ b/App/UI/Settings/SettingsPerformanceView.swift @@ -9,58 +9,106 @@ import SwiftUI struct SettingsPerformanceView: View { + private struct RefreshRate: Hashable { + var rate: Int + var name: String + + func hash(into hasher: inout Hasher) { + hasher.combine(rate) + } + } + private let refreshRates = [ - 15, - 30, - 60, - 120 - ].filter { item in item <= UIScreen.main.maximumFramesPerSecond } + RefreshRate(rate: 15, name: "Power Saver"), + RefreshRate(rate: 30, name: "Balanced"), + RefreshRate(rate: 60, name: "Performance"), + RefreshRate(rate: 120, name: "Speed Demon") + ].filter { item in item.rate <= UIScreen.main.maximumFramesPerSecond } @ObservedObject var preferences = Preferences.shared - var body: some View { - let list = ForEach(refreshRates, id: \.self) { key in - Button( - action: { - preferences.refreshRate = key - }, - label: { - HStack { - Text("\(key) updates per second") - .foregroundColor(Color(.label)) - Spacer() + private var batteryImageName: String { + let device = UIDevice.current + device.isBatteryMonitoringEnabled = true + let percent = device.batteryLevel + let state = device.batteryState + device.isBatteryMonitoringEnabled = false + if state != .unknown { + if percent < 0.2 { + return "battery.0" + } else if percent < 0.4 { + return "battery.25" + } else if percent < 0.6 { + return "battery.50" + } else if percent < 0.8 { + return "battery.75" + } + } + return "battery.100" + } - if key == preferences.$refreshRate.wrappedValue { - Image(systemName: "checkmark") - .accessibility(label: Text("Selected")) - } - } - } - ) - .animation(.default) + var body: some View { + let list = ForEach(refreshRates, id: \.rate) { item in + Text("\(item.rate) fps: \(String.localize(item.name))") + .font(.body.monospacedDigit()) } - return List { - Section( - header: Text("Refresh Rate"), - footer: Text("Reducing the refresh rate can improve \(UIDevice.current.localizedModel) energy usage, but will cause the terminal display to feel sluggish.\nThe default setting of 60 updates per second is recommended.") + return PreferencesList { + PreferencesGroup( + header: Label( + title: { Text(UIDevice.current.isPortable ? "On AC Power" : "Refresh Rate") }, + icon: { + UIDevice.current.isPortable + ? Image(systemName: "bolt.fill") + .imageScale(.medium) + : nil + } + ), + footer: UIDevice.current.isPortable + ? AnyView(EmptyView()) + : AnyView(Text("The Performance setting is recommended.")) ) { - list + Picker( + selection: preferences.$refreshRateOnAC, + label: EmptyView() + ) { + list + } } - if #available(macOS 12, *) { - Section( - footer: Text("Preserve battery life by reducing refresh rate to 15 updates per second when Low Power Mode is enabled.") + if UIDevice.current.isPortable { + PreferencesGroup( + header: Label( + title: { Text("On Battery") }, + icon: { + Image(systemName: batteryImageName) + .imageScale(.medium) + } + ), + footer: Text("A lower refresh rate improves \(UIDevice.current.deviceModel) battery life, but may cause the terminal display to feel sluggish.\nThe Performance setting is recommended.") + .fixedSize(horizontal: false, vertical: true) ) { - Toggle( - "Reduce Performance in Low Power Mode", - isOn: preferences.$reduceRefreshRateInLPM - ) + Picker( + selection: preferences.$refreshRateOnBattery, + label: EmptyView() + ) { + list + } + } + + if #available(macOS 12, *) { + PreferencesGroup( + footer: Text("Preserve battery life by switching to Power Saver when Low Power Mode is enabled.") + ) { + Toggle( + "Reduce Performance in Low Power Mode", + isOn: preferences.$reduceRefreshRateInLPM + ) + } } } } - .listStyle(InsetGroupedListStyle()) - .navigationBarTitle("Performance", displayMode: .inline) + .navigationBarTitle("Performance") } } @@ -70,12 +118,13 @@ struct SettingsPerformanceView_Previews: PreviewProvider { NavigationView { SettingsPerformanceView() } - .previewDevice("iPhone 13 Pro") + .previewDevice("iPhone 12 Pro") NavigationView { SettingsPerformanceView() } - .previewDevice("iPhone 12 Pro") + .navigationViewStyle(StackNavigationViewStyle()) + .previewDevice("iPad Pro (11-inch) (3rd generation)") } } diff --git a/App/UI/Settings/iOS/SettingsFontView.swift b/App/UI/Settings/iOS/SettingsFontView.swift index 6625c1c..fe22af7 100644 --- a/App/UI/Settings/iOS/SettingsFontView.swift +++ b/App/UI/Settings/iOS/SettingsFontView.swift @@ -17,65 +17,42 @@ struct SettingsFontView: View { @ObservedObject var preferences = Preferences.shared var body: some View { - let sampleView = TerminalSampleViewRepresentable( - fontMetrics: preferences.fontMetrics, - colorMap: preferences.colorMap - ) - - let fontsList = ForEach(sortedFonts, id: \.key) { key, value in - Button( - action: { - preferences.fontName = key - }, - label: { - HStack { - Text(key) - .foregroundColor(.primary) - .font(Font(value.previewFont ?? UIFont.preferredFont(forTextStyle: .body))) - Spacer() + VStack(spacing: 0) { + TerminalSampleViewRepresentable( + fontMetrics: preferences.fontMetrics, + colorMap: preferences.colorMap + ) - if value.previewFont == nil { - Image(systemName: "arrow.down.circle") - .accessibility(label: Text("Not installed. Tap to download.")) - } + PreferencesList { + PreferencesGroup(header: Text("Font")) { + Picker(selection: preferences.$fontName, label: EmptyView()) { + ForEach(sortedFonts, id: \.key) { key, value in + HStack(alignment: .center) { + if value.previewFont == nil { + Image(systemName: "arrow.down.circle") + .font(.body.weight(.medium)) + .foregroundColor(.accentColor) + .accessibility(label: Text("Not installed. Tap to download.")) + } - if key == preferences.$fontName.wrappedValue { - Image(systemName: "checkmark") - .accessibility(label: Text("Selected")) + Text(key) + .font(Font(value.previewFont ?? UIFont.preferredFont(forTextStyle: .body))) + Spacer() + } } } } - ) - .animation(.default) - } - let list = List { - Section(header: Spacer()) { - fontsList - } - - #if os(iOS) && !targetEnvironment(macCatalyst) - Section() { - Stepper( - value: Binding( - get: { preferences.fontSizePhone }, - set: { value in preferences.fontSizePhone = value } - ), - in: 10...20, - step: 1 - ) { - Text("Font Size: \(Int(preferences.fontSizePhone))") +#if !targetEnvironment(macCatalyst) + PreferencesGroup { + Stepper(value: preferences.$fontSizePhone, in: 10...20, step: 1) { + Text("Font Size: \(Int(preferences.fontSizePhone))") + } } +#endif } - #endif - } - .listStyle(InsetGroupedListStyle()) - - return VStack { - sampleView - list } - .navigationBarTitle("Font", displayMode: .inline) + .navigationBarTitle("Font") } } diff --git a/App/UI/Settings/iOS/SettingsThemeView.swift b/App/UI/Settings/iOS/SettingsThemeView.swift index 97e4d8a..88d0bbc 100644 --- a/App/UI/Settings/iOS/SettingsThemeView.swift +++ b/App/UI/Settings/iOS/SettingsThemeView.swift @@ -16,45 +16,21 @@ struct SettingsThemeView: View { @ObservedObject var preferences = Preferences.shared var body: some View { - let sampleView = TerminalSampleViewRepresentable( - fontMetrics: preferences.fontMetrics, - colorMap: preferences.colorMap - ) - - let themesList = ForEach(sortedThemes, id: \.key) { key, value in - Button( - action: { - preferences.themeName = key - }, - label: { - HStack { - Text(key) - .foregroundColor(.primary) - Spacer() + VStack(spacing: 0) { + TerminalSampleViewRepresentable( + fontMetrics: preferences.fontMetrics, + colorMap: preferences.colorMap + ) - if key == preferences.themeName { - Image(systemName: "checkmark") - .accessibility(label: Text("Selected")) - } + PreferencesList { + PreferencesGroup(header: Text("Built in Themes")) { + Picker(selection: preferences.$themeName, label: EmptyView()) { + ForEach(sortedThemes, id: \.key) { item in Text(item.key) } } } - ) - .animation(.default) - } - - let list = List { - Section(footer: Text("Note: You currently need to restart the app to have theme updates apply.")) {} - Section() { - themesList } } - .listStyle(InsetGroupedListStyle()) - - return VStack { - sampleView - list - } - .navigationBarTitle("Theme", displayMode: .inline) + .navigationBarTitle("Theme") } } diff --git a/App/UI/Settings/iOS/SettingsView.swift b/App/UI/Settings/iOS/SettingsView.swift index 1a35479..0e4151e 100644 --- a/App/UI/Settings/iOS/SettingsView.swift +++ b/App/UI/Settings/iOS/SettingsView.swift @@ -76,7 +76,7 @@ struct SettingsView: View { NotificationCenter.default.post(name: RootViewController.settingsViewDoneNotification, object: nil) } }, - label: { Text(.done).bold() } + label: { Text(verbatim: .done).bold() } ) ) #endif diff --git a/App/UIDevice+Additions.swift b/App/UIDevice+Additions.swift new file mode 100644 index 0000000..0ca70ea --- /dev/null +++ b/App/UIDevice+Additions.swift @@ -0,0 +1,76 @@ +// +// UIDevice+Additions.swift +// NewTerm Common +// +// Created by Adam Demasi on 25/9/21. +// + +import UIKit +import Darwin +import UniformTypeIdentifiers + +#if targetEnvironment(macCatalyst) +import IOKit +#endif + +extension UTTagClass { + static let deviceModelCode = UTTagClass(rawValue: "com.apple.device-model-code") +} + +extension UIDevice { + + var isPortable: Bool { + switch userInterfaceIdiom { + case .phone, .pad, .carPlay, .unspecified: + return true + case .tv: + return false + case .mac: + #if targetEnvironment(macCatalyst) + let powerSourcesInfo = IOPSCopyPowerSourcesInfo()!.takeUnretainedValue() + let powerSourcesList = IOPSCopyPowerSourcesList(powerSourcesInfo)!.takeUnretainedValue() as [CFTypeRef] + return !powerSourcesList.isEmpty + #else + return true + #endif + @unknown default: + return true + } + } + + var machine: String { + #if targetEnvironment(macCatalyst) + let key = "hw.model" + #else + let key = "hw.machine" + #endif + var size = size_t() + sysctlbyname(key, nil, &size, nil, 0) + let value = malloc(size) + defer { + value?.deallocate() + } + sysctlbyname(key, value, &size, nil, 0) + guard let cChar = value?.bindMemory(to: CChar.self, capacity: size) else { + #if targetEnvironment(macCatalyst) + return "Mac" + #else + return model + #endif + } + return String(cString: cChar) + } + + var deviceModel: String { + #if targetEnvironment(macCatalyst) + // localizedModel on macOS always returns “iPad” 🙁 + // Grab the device machine identifier directly, then find its name via CoreTypes. + return UTType(tag: machine, + tagClass: .deviceModelCode, + conformingTo: nil)?.localizedDescription ?? "Mac" + #else + return localizedModel + #endif + } + +} diff --git a/Common/Controllers/Preferences.swift b/Common/Controllers/Preferences.swift index afaac48..43167dc 100644 --- a/Common/Controllers/Preferences.swift +++ b/Common/Controllers/Preferences.swift @@ -16,15 +16,15 @@ import SwiftUI import Combine import os.log -public enum KeyboardButtonStyle: Int, PropertyListValue { +public enum KeyboardButtonStyle: Int { case text, icons } -public enum KeyboardTrackpadSensitivity: Int, PropertyListValue { +public enum KeyboardTrackpadSensitivity: Int { case off, low, medium, high } -public enum PreferencesSyncService: Int, PropertyListValue, Identifiable { +public enum PreferencesSyncService: Int, Identifiable { case none, icloud, folder public var id: Self { self } @@ -36,12 +36,12 @@ public class Preferences: NSObject, ObservableObject { public static let shared = Preferences() - public let objectWillChange = ObservableObjectPublisher() - - private let themesPlist = NSDictionary(contentsOf: Bundle.main.url(forResource: "Themes", withExtension: "plist")!)! - - @Published public var fontMetrics = FontMetrics(font: AppFont(), fontSize: 12) - @Published public var colorMap = ColorMap(theme: AppTheme()) + @Published public private(set) var fontMetrics = FontMetrics(font: AppFont(), fontSize: 12) { + willSet { objectWillChange.send() } + } + @Published public private(set) var colorMap = ColorMap(theme: AppTheme()) { + willSet { objectWillChange.send() } + } override init() { super.init() @@ -102,9 +102,9 @@ public class Preferences: NSObject, ObservableObject { } @AppStorage("themeName") - public var themeName: String = "kirb" { + public var themeName: String = "Basic (Dark)" { willSet { objectWillChange.send() } - didSet { fontMetricsChanged() } + didSet { colorMapChanged() } } #if os(iOS) @@ -134,8 +134,13 @@ public class Preferences: NSObject, ObservableObject { willSet { objectWillChange.send() } } - @AppStorage("refreshRate") - public var refreshRate: Int = 60 { + @AppStorage("refreshRateOnAC") + public var refreshRateOnAC: Int = 60 { + willSet { objectWillChange.send() } + } + + @AppStorage("refreshRateOnBattery") + public var refreshRateOnBattery: Int = 60 { willSet { objectWillChange.send() } } @@ -150,7 +155,7 @@ public class Preferences: NSObject, ObservableObject { } @AppStorage("preferencesSyncPath") - public var preferencesSyncPath: String? = nil { + public var preferencesSyncPath: String = "" { willSet { objectWillChange.send() } } @@ -169,11 +174,13 @@ public class Preferences: NSObject, ObservableObject { private func fontMetricsChanged() { let font = AppFont.predefined[fontName] ?? AppFont() + objectWillChange.send() fontMetrics = FontMetrics(font: font, fontSize: CGFloat(fontSize)) } private func colorMapChanged() { let theme = AppTheme.predefined[themeName] ?? AppTheme() + objectWillChange.send() colorMap = ColorMap(theme: theme) #if os(macOS) diff --git a/Common/Controllers/TerminalController.swift b/Common/Controllers/TerminalController.swift index 06cb960..b31f4be 100644 --- a/Common/Controllers/TerminalController.swift +++ b/Common/Controllers/TerminalController.swift @@ -47,7 +47,7 @@ public class TerminalController { private let stringSupplier = StringSupplier() private var processLaunchDate: Date? - private var updateTimer: Timer? + private var updateTimer: CADisplayLink? private var refreshRate: TimeInterval = 60 private var isVisible = true private var readBuffer = Data() @@ -74,8 +74,9 @@ public class TerminalController { internal var shell: String? public init() { + // TODO: Scrollback overflows and throws an error on dirtyLines.insert() Terminal.swift:4117 let options = TerminalOptions(termName: "xterm-256color", - scrollback: 10000) + scrollback: 1000) terminal = Terminal(delegate: self, options: options) stringSupplier.terminal = terminal @@ -90,6 +91,9 @@ public class TerminalController { NotificationCenter.default.addObserver(self, selector: #selector(self.appDidBecomeActive), name: UIApplication.didBecomeActiveNotification, object: nil) #endif + UIDevice.current.isBatteryMonitoringEnabled = true + NotificationCenter.default.addObserver(self, selector: #selector(self.powerStateChanged), name: UIDevice.batteryStateDidChangeNotification, object: nil) + if #available(macOS 12, *) { NotificationCenter.default.addObserver(self, selector: #selector(self.powerStateChanged), name: .NSProcessInfoPowerStateDidChange, object: nil) } @@ -110,7 +114,8 @@ public class TerminalController { ProcessInfo.processInfo.isLowPowerModeEnabled && preferences.reduceRefreshRateInLPM { refreshRate = 15 } else { - refreshRate = TimeInterval(min(preferences.refreshRate, UIScreen.main.maximumFramesPerSecond)) + let currentRate = UIDevice.current.batteryState == .unplugged ? preferences.refreshRateOnBattery : preferences.refreshRateOnAC + refreshRate = TimeInterval(min(currentRate, UIScreen.main.maximumFramesPerSecond)) } if isVisible { startUpdateTimer(fps: refreshRate) @@ -158,7 +163,9 @@ public class TerminalController { private func startUpdateTimer(fps: TimeInterval) { updateTimer?.invalidate() - updateTimer = Timer.scheduledTimer(timeInterval: 1 / fps, target: self, selector: #selector(self.updateTimerFired), userInfo: nil, repeats: true) + updateTimer = CADisplayLink(target: self, selector: #selector(self.updateTimerFired)) + updateTimer?.preferredFramesPerSecond = Int(fps) + updateTimer?.add(to: .main, forMode: .default) } private func stopUpdatingTimer() { diff --git a/Common/Extensions/Color+Additions.swift b/Common/Extensions/Color+Additions.swift index 4e39c14..7eabb01 100644 --- a/Common/Extensions/Color+Additions.swift +++ b/Common/Extensions/Color+Additions.swift @@ -7,6 +7,10 @@ import SwiftTerm +#if os(iOS) +import UIKit +#endif + extension SwiftTerm.Color { convenience init(_ uiColor: UIColor) { diff --git a/Common/Extensions/String+Localization.swift b/Common/Extensions/String+Localization.swift index c12fe60..4f33e27 100644 --- a/Common/Extensions/String+Localization.swift +++ b/Common/Extensions/String+Localization.swift @@ -7,6 +7,10 @@ import Foundation +#if os(iOS) +import UIKit +#endif + public extension String { static func localize(_ key: String, bundle: Bundle? = nil, tableName: String? = nil, comment: String = "") -> String { NSLocalizedString(key, tableName: tableName, bundle: bundle ?? .main, comment: comment) diff --git a/Common/Extensions/UIColor+HBAdditions.h b/Common/Extensions/UIColor+HBAdditions.h deleted file mode 100644 index 4f11e59..0000000 --- a/Common/Extensions/UIColor+HBAdditions.h +++ /dev/null @@ -1,36 +0,0 @@ -#import "CrossPlatformUI.h" - -NS_ASSUME_NONNULL_BEGIN - -/// UIColor (HBAdditions) is a class category in `Cephei` that provides some convenience methods. -@interface Color (HBAdditions) - -/// Creates and returns a color object using data from the specified object. -/// -/// The value is expected to be one of the types specified in initWithPropertyListValue:. -/// -/// @param value The object to retrieve data from. See the discussion for the supported object -/// types. -/// @return The color object. The color information represented by this object is in the device RGB -/// colorspace. -/// @see initWithPropertyListValue: -+ (instancetype)colorWithPropertyListValue:(id)value; - -/// Initializes and returns a color object using data from the specified object. -/// -/// The value is expected to be one of: -/// -/// * An array of 3 or 4 integer RGB or RGBA color components, with values between 0 and 255 (e.g. -/// `@[ 218, 192, 222 ]`) -/// * A CSS-style hex string, with an optional alpha component (e.g. `#DAC0DE` or `#DACODE55`) -/// * A short CSS-style hex string, with an optional alpha component (e.g. `#DC0` or `#DC05`) -/// -/// @param value The object to retrieve data from. See the discussion for the supported object -/// types. -/// @return An initialized color object. The color information represented by this object is in the -/// device RGB colorspace. -- (instancetype)initWithPropertyListValue:(id)value; - -@end - -NS_ASSUME_NONNULL_END diff --git a/Common/Extensions/UIColor+HBAdditions.m b/Common/Extensions/UIColor+HBAdditions.m deleted file mode 100644 index b3d4d81..0000000 --- a/Common/Extensions/UIColor+HBAdditions.m +++ /dev/null @@ -1,64 +0,0 @@ -#import "UIColor+HBAdditions.h" - -@implementation Color (HBAdditions) - -+ (instancetype)colorWithPropertyListValue:(id)value { - return [[self alloc] initWithPropertyListValue:value]; -} - -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wobjc-designated-initializers" -- (instancetype)initWithPropertyListValue:(id)value { - CGFloat r = 0, g = 0, b = 0, a = 0; - - if (!value) { - return nil; - } else if ([value isKindOfClass:NSArray.class] && ((NSArray *)value).count == 3) { - NSArray *array = value; - r = ((NSNumber *)array[0]).integerValue / 255.f; - g = ((NSNumber *)array[1]).integerValue / 255.f; - b = ((NSNumber *)array[2]).integerValue / 255.f; - a = 1; - } else if ([value isKindOfClass:NSString.class]) { - NSString *string = value; - if ([string hasPrefix:@"#"] && (string.length == 7 || string.length == 8 || string.length == 4 || string.length == 5)) { - if (string.length == 4 || string.length == 5) { - NSString *r2 = [string substringWithRange:NSMakeRange(1, 1)]; - NSString *g2 = [string substringWithRange:NSMakeRange(2, 1)]; - NSString *b2 = [string substringWithRange:NSMakeRange(3, 1)]; - NSString *a2 = string.length == 5 ? [string substringWithRange:NSMakeRange(4, 1)] : @"FF"; - string = [NSString stringWithFormat:@"#%1$@%1$@%2$@%2$@%3$@%3$@%4$@%4$@", r2, g2, b2, a2]; - } - - unsigned int hex = 0; - NSScanner *scanner = [NSScanner scannerWithString:string]; - scanner.charactersToBeSkipped = [NSCharacterSet characterSetWithCharactersInString:@"#"]; - [scanner scanHexInt:&hex]; - - if (string.length == 8) { - r = ((hex & 0xFF000000) >> 24) / 255.f; - g = ((hex & 0x00FF0000) >> 16) / 255.f; - b = ((hex & 0x0000FF00) >> 8) / 255.f; - a = ((hex & 0x000000FF) >> 0) / 255.f; - } else { - r = ((hex & 0xFF0000) >> 16) / 255.f; - g = ((hex & 0x00FF00) >> 8) / 255.f; - b = ((hex & 0x0000FF) >> 0) / 255.f; - a = 1; - } - } else { - return nil; - } - } else { - return nil; - } - -#if TARGET_OS_IPHONE - return [UIColor colorWithRed:r green:g blue:b alpha:a]; -#else - return [NSColor colorWithSRGBRed:r green:g blue:b alpha:a]; -#endif -} -#pragma clang diagnostic pop - -@end diff --git a/Common/Extensions/UIColorAdditions.swift b/Common/Extensions/UIColorAdditions.swift new file mode 100644 index 0000000..ace44ae --- /dev/null +++ b/Common/Extensions/UIColorAdditions.swift @@ -0,0 +1,96 @@ +// +// UIColorAdditions.swift +// Alderis +// +// Created by Ryan Nair on 10/5/20. +// Copyright © 2020 HASHBANG Productions. All rights reserved. +// + +import UIKit + +/// ColorPropertyListValue is a protocol representing types that can be passed to the\ +/// `UIColor.init(propertyListValue:)` initialiser. `String` and `Array` both conform to this type. +/// +/// - see: `UIColor.init(propertyListValue:)` +public protocol ColorPropertyListValue {} + +/// A string can represent a `ColorPropertyListValue`. +/// +/// - see: `UIColor.init(propertyListValue:)` +extension String: ColorPropertyListValue {} + +/// An array of integers can represent a `ColorPropertyListValue`. +/// +/// - see: `UIColor.init(propertyListValue:)` +extension Array: ColorPropertyListValue where Element: FixedWidthInteger {} + +/// Alderis provides extensions to `UIColor` for the purpose of serializing and deserializing colors +/// into representations that can be stored in property lists, JSON, and similar formats. +public extension UIColor { + + /// Initializes and returns a color object using data from the specified object. + /// + /// The value is expected to be one of: + /// + /// * An array of 3 or 4 integer RGB or RGBA color components, with values between 0 and 255 (e.g. + /// `[ 218, 192, 222 ]`) + /// * A CSS-style hex string, with an optional alpha component (e.g. `#DAC0DE` or `#DACODE55`) + /// * A short CSS-style hex string, with an optional alpha component (e.g. `#DC0` or `#DC05`) + /// + /// Use `-[UIColor initWithHbcp_propertyListValue:]` to access this method from Objective-C. + /// + /// - parameter value: The object to retrieve data from. See the discussion for the supported object + /// types. + /// - returns: An initialized color object, or nil if the value does not conform to the expected + /// type. The color information represented by this object is in the device RGB colorspace. + /// - see: `propertyListValue` + @nonobjc convenience init?(propertyListValue: ColorPropertyListValue?) { + if let array = propertyListValue as? [Int], array.count == 3 || array.count == 4 { + let floats = array.map { CGFloat($0) } + self.init(red: floats[0] / 255, + green: floats[1] / 255, + blue: floats[2] / 255, + alpha: array.count == 4 ? floats[3] : 1) + return + } else if var string = propertyListValue as? String { + if string.count == 4 || string.count == 5 { + let r = String(repeating: string[string.index(string.startIndex, offsetBy: 1)], count: 2) + let g = String(repeating: string[string.index(string.startIndex, offsetBy: 2)], count: 2) + let b = String(repeating: string[string.index(string.startIndex, offsetBy: 3)], count: 2) + let a = string.count == 5 ? String(repeating: string[string.index(string.startIndex, offsetBy: 4)], count: 2) : "FF" + string = String(format: "%@%@%@%@", r, g, b, a) + } + + var hex: UInt64 = 0 + let scanner = Scanner(string: string) + guard scanner.scanString("#") != nil, + scanner.scanHexInt64(&hex) else { + return nil + } + + + if string.count == 9 { + self.init(red: CGFloat((hex & 0xFF000000) >> 24) / 255, + green: CGFloat((hex & 0x00FF0000) >> 16) / 255, + blue: CGFloat((hex & 0x0000FF00) >> 8) / 255, + alpha: CGFloat((hex & 0x000000FF) >> 0) / 255) + return + } else { + var alpha: Float = 1 + if scanner.scanString(":") != nil { + // Continue scanning to get the alpha component. + alpha = scanner.scanFloat() ?? 1 + } + + self.init(red: CGFloat((hex & 0xFF0000) >> 16) / 255, + green: CGFloat((hex & 0x00FF00) >> 8) / 255, + blue: CGFloat((hex & 0x0000FF) >> 0) / 255, + alpha: CGFloat(alpha)) + return + } + } + + return nil + } + +} diff --git a/Common/Models/AppFont.swift b/Common/Models/AppFont.swift index 2c1e4be..59774e9 100644 --- a/Common/Models/AppFont.swift +++ b/Common/Models/AppFont.swift @@ -5,6 +5,8 @@ // Created by Adam Demasi on 3/4/21. // +import UIKit + public struct AppFont: Codable { public static let predefined: [String: AppFont] = { diff --git a/Common/Models/AppStorage.swift b/Common/Models/AppStorage.swift deleted file mode 100644 index 1fc1561..0000000 --- a/Common/Models/AppStorage.swift +++ /dev/null @@ -1,68 +0,0 @@ -// -// AppStorage.swift -// NewTerm (iOS) -// -// Created by Adam Demasi on 5/4/21. -// - -import Foundation -import SwiftUI -import Combine - -// Replica of SwiftUI AppStorage so we can use it on iOS 13. - -public protocol PropertyListValue {} -extension Array: PropertyListValue where Element: PropertyListValue {} -extension Dictionary: PropertyListValue where Key == String, Value: PropertyListValue {} -extension String: PropertyListValue {} -extension Data: PropertyListValue {} -extension Date: PropertyListValue {} -extension Bool: PropertyListValue {} -extension Int: PropertyListValue {} -extension Double: PropertyListValue {} -extension Optional: PropertyListValue where Wrapped: PropertyListValue {} - -@propertyWrapper -public struct AppStorage: DynamicProperty { - - let key: String - let defaultValue: Value - let store: UserDefaults - - public init(wrappedValue: Value, _ key: String, store: UserDefaults = .standard) { - self.defaultValue = wrappedValue - self.key = key - self.store = store - } - - public var wrappedValue: Value { - get { return store.object(forKey: key) as? Value ?? defaultValue } - nonmutating set { - store.set(newValue, forKey: key) - NotificationCenter.default.post(name: Preferences.didChangeNotification, object: nil) - } - } - - public var projectedValue: Binding { - Binding( - get: { wrappedValue }, - set: { value in wrappedValue = value } - ) - } - -} - -//extension AppStorage where Value: EnumPropertyListValue { -// public var wrappedValue: Value { -// get { -// if let rawValue = store.object(forKey: key) as? Int { -// return Value(rawValue: rawValue) ?? defaultValue -// } -// return defaultValue -// } -// nonmutating set { -// store.set(newValue.rawValue, forKey: key) -// NotificationCenter.default.post(name: Preferences.didChangeNotification, object: nil) -// } -// } -//} diff --git a/Common/Models/AppTheme.swift b/Common/Models/AppTheme.swift index 44c2d9e..9975a25 100644 --- a/Common/Models/AppTheme.swift +++ b/Common/Models/AppTheme.swift @@ -5,6 +5,8 @@ // Created by Adam Demasi on 5/4/21. // +import Foundation + public struct AppTheme: Codable { public static let predefined: [String: AppTheme] = { diff --git a/Common/Supporting Files/NewTermCommon.h b/Common/Supporting Files/NewTermCommon.h index 74f6118..cb696cc 100644 --- a/Common/Supporting Files/NewTermCommon.h +++ b/Common/Supporting Files/NewTermCommon.h @@ -6,6 +6,5 @@ // #import "NSArray+Additions.h" -#import "UIColor+HBAdditions.h" #import "CrossPlatformUI.h" #import "CompactConstraint.h" diff --git a/Common/Supporting Files/PrefixHeader.pch b/Common/Supporting Files/PrefixHeader.pch index 144dcc3..5f4c10e 100644 --- a/Common/Supporting Files/PrefixHeader.pch +++ b/Common/Supporting Files/PrefixHeader.pch @@ -5,6 +5,5 @@ // Created by Adam Demasi on 20/6/19. // -@import Foundation; - #import +#import diff --git a/Common/VT100/ColorMap.swift b/Common/VT100/ColorMap.swift index a3f30ba..f36ac3e 100644 --- a/Common/VT100/ColorMap.swift +++ b/Common/VT100/ColorMap.swift @@ -14,6 +14,12 @@ import AppKit import SwiftTerm import os.log +public enum AnsiColorCode: Int, CaseIterable { + case black, red, green, yellow, blue, purple, cyan, white + case brightBlack, brightRed, brightGreen, brightYellow + case brightBlue, brightPurple, brightCyan, brightWhite +} + public struct ColorMap { public let background: UIColor @@ -22,7 +28,7 @@ public struct ColorMap { public let foregroundCursor: UIColor public let backgroundCursor: UIColor - public let ansiColors: [UIColor] + public let ansiColors: [AnsiColorCode: UIColor] public let isDark: Bool @@ -33,37 +39,56 @@ public struct ColorMap { #endif public init(theme: AppTheme) { - background = UIColor(propertyListValue: theme.background) - foreground = UIColor(propertyListValue: theme.text) - foregroundBold = UIColor(propertyListValue: theme.boldText) - foregroundCursor = UIColor(propertyListValue: theme.cursor) + background = UIColor(propertyListValue: theme.background) ?? .systemGroupedBackground + foreground = UIColor(propertyListValue: theme.text) ?? .systemGray6 + foregroundBold = UIColor(propertyListValue: theme.boldText) ?? .label + foregroundCursor = UIColor(propertyListValue: theme.cursor) ?? .systemGreen backgroundCursor = foregroundCursor isDark = theme.isDark + // TODO: For some reason .systemCyan doesn’t exist on macOS 12? Revisit this soon. + var cyan: UIColor! + #if !targetEnvironment(macCatalyst) + if #available(iOS 15, *) { + cyan = .systemCyan + } + #endif + if cyan == nil { + cyan = UIColor(dynamicProvider: { _ in + var h: CGFloat = 0, s: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 0 + UIColor.systemBlue.getHue(&h, saturation: &s, brightness: &b, alpha: &a) + return UIColor(hue: h, saturation: s * 0.7, brightness: b * 1.3, alpha: a) + }) + } + + var ansiColors: [AnsiColorCode: UIColor] = [ + .black: .label, + .red: .systemRed, + .green: .systemGreen, + .yellow: .systemYellow, + .blue: .systemBlue, + .purple: .systemPurple, + .cyan: cyan, + .white: foreground, + .brightBlack: .label, + .brightRed: .systemRed, + .brightGreen: .systemGreen, + .brightYellow: .systemYellow, + .brightBlue: .systemBlue, + .brightPurple: .systemPurple, + .brightCyan: cyan, + .brightWhite: foregroundBold + ] + if let colorTable = theme.colorTable, colorTable.count == 16 { - ansiColors = colorTable.map { item in UIColor(propertyListValue: item) } - } else { - // System 7.5 colors, why not? - ansiColors = [ - UIColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 1), - UIColor(red: 0.6, green: 0.0, blue: 0.0, alpha: 1), - UIColor(red: 0.0, green: 0.6, blue: 0.0, alpha: 1), - UIColor(red: 0.6, green: 0.4, blue: 0.0, alpha: 1), - UIColor(red: 0.0, green: 0.0, blue: 0.6, alpha: 1), - UIColor(red: 0.6, green: 0.0, blue: 0.6, alpha: 1), - UIColor(red: 0.0, green: 0.6, blue: 0.6, alpha: 1), - UIColor(red: 0.6, green: 0.6, blue: 0.6, alpha: 1), - UIColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 1), - UIColor(red: 1.0, green: 0.0, blue: 0.0, alpha: 1), - UIColor(red: 0.0, green: 1.0, blue: 0.0, alpha: 1), - UIColor(red: 1.0, green: 1.0, blue: 0.0, alpha: 1), - UIColor(red: 0.0, green: 0.0, blue: 1.0, alpha: 1), - UIColor(red: 1.0, green: 0.0, blue: 1.0, alpha: 1), - UIColor(red: 0.0, green: 1.0, blue: 1.0, alpha: 1), - UIColor(red: 1.0, green: 1.0, blue: 1.0, alpha: 1) - ] + for (i, value) in colorTable.enumerated() { + if let color = UIColor(propertyListValue: value) { + ansiColors[.allCases[i]] = color + } + } } + self.ansiColors = ansiColors } public func color(for termColor: Attribute.Color, isForeground: Bool, isBold: Bool = false, isCursor: Bool = false) -> UIColor { @@ -89,7 +114,7 @@ public struct ColorMap { let index = Int(ansi) + (isBold && ansi < 248 ? 8 : 0) if index < 16 { // ANSI color (0-15) - return ansiColors[index] + return ansiColors[.allCases[index]]! } else if index < 232 { // 256-color table (16-231) let tableIndex = index - 16 diff --git a/Common/VT100/StringSupplier.swift b/Common/VT100/StringSupplier.swift index dc6d43a..63bdc0d 100644 --- a/Common/VT100/StringSupplier.swift +++ b/Common/VT100/StringSupplier.swift @@ -8,6 +8,10 @@ import Foundation import SwiftTerm +#if os(iOS) +import UIKit +#endif + open class StringSupplier { open var terminal: Terminal? diff --git a/Common/VT100/TerminalConstants.swift b/Common/VT100/TerminalConstants.swift index 38d988d..394ea0f 100644 --- a/Common/VT100/TerminalConstants.swift +++ b/Common/VT100/TerminalConstants.swift @@ -15,6 +15,8 @@ public struct ScreenSize: Equatable { self.cols = cols self.rows = rows } + + public static let `default` = ScreenSize(cols: 80, rows: 25) } public struct EscapeSequences { diff --git a/Common/VT100/TerminalInputProtocol.swift b/Common/VT100/TerminalInputProtocol.swift index 1dbbe0f..5d72288 100644 --- a/Common/VT100/TerminalInputProtocol.swift +++ b/Common/VT100/TerminalInputProtocol.swift @@ -5,6 +5,8 @@ // Created by Adam Demasi on 20/6/19. // +import Foundation + public protocol TerminalInputProtocol: AnyObject { func receiveKeyboardInput(data: Data) diff --git a/NewTerm.xcodeproj/project.pbxproj b/NewTerm.xcodeproj/project.pbxproj index c2531e8..f3e7626 100644 --- a/NewTerm.xcodeproj/project.pbxproj +++ b/NewTerm.xcodeproj/project.pbxproj @@ -14,17 +14,20 @@ 4E2492D026230E59002C47CB /* TerminalController+ITermExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E2492CF26230E59002C47CB /* TerminalController+ITermExtensions.swift */; }; 4E32378F26FB2D4400AEDA06 /* UIApplication+Additions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E32378E26FB2D4400AEDA06 /* UIApplication+Additions.swift */; }; 4E32379026FB4B3300AEDA06 /* String+Localization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E98081926184A2500E41883 /* String+Localization.swift */; }; + 4E3237A426FB530C00AEDA06 /* UIColorAdditions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E3237A326FB530C00AEDA06 /* UIColorAdditions.swift */; }; 4E3237FB26FC5FC300AEDA06 /* ColorBars.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E3237FA26FC5FC300AEDA06 /* ColorBars.swift */; }; + 4E3237FE26FEB0D800AEDA06 /* UIDevice+Additions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E3237FC26FEB0D600AEDA06 /* UIDevice+Additions.swift */; }; 4E460121261EC8C0004DBCC2 /* UpdateCheckManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E460120261EC8C0004DBCC2 /* UpdateCheckManager.swift */; }; 4E460129261FF23F004DBCC2 /* SettingsSceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E460128261FF23F004DBCC2 /* SettingsSceneDelegate.swift */; }; 4E479710262D44D8003CF48C /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 4E479713262D44D8003CF48C /* Localizable.stringsdict */; }; + 4E57499826FEE90700D94DF5 /* PreferencesGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E57499726FEE90700D94DF5 /* PreferencesGroup.swift */; }; + 4E57499A26FEED9800D94DF5 /* PreferencesList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E57499926FEED9800D94DF5 /* PreferencesList.swift */; }; 4E5E817526F2EBE000C4DB6D /* SettingsPerformanceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E5E817426F2EBE000C4DB6D /* SettingsPerformanceView.swift */; }; 4E98081C261850E600E41883 /* KeyValueView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E98081B261850E600E41883 /* KeyValueView.swift */; }; 4E98081E2618551D00E41883 /* SettingsFontView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E98081D2618551D00E41883 /* SettingsFontView.swift */; }; 4E9808222618601F00E41883 /* SettingsThemeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E9808212618601F00E41883 /* SettingsThemeView.swift */; }; 4E980826261873CE00E41883 /* AppFont.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E980824261860CA00E41883 /* AppFont.swift */; }; 4E980828261879D200E41883 /* SettingsAboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E980827261879D200E41883 /* SettingsAboutView.swift */; }; - 4E980830261AA9E100E41883 /* AppStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E98082E261AA9DC00E41883 /* AppStorage.swift */; }; 4E980833261ACA0400E41883 /* AppTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E980831261AC9FA00E41883 /* AppTheme.swift */; }; 4E9B32E0260C66B5006A1FBC /* Bundle+NewTermAdditions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E9B32DF260C66B5006A1FBC /* Bundle+NewTermAdditions.swift */; }; 4E9B32EB261DCFC4006A1FBC /* TextInputBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E9B32EA261DCFC4006A1FBC /* TextInputBase.swift */; }; @@ -54,12 +57,10 @@ CF71BC9A22BB6E0300AFB1E1 /* CrossPlatformUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF71BC6022BB691700AFB1E1 /* CrossPlatformUI.swift */; }; CF71BC9B22BB6E0300AFB1E1 /* TerminalInputProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF71BC6222BB69EF00AFB1E1 /* TerminalInputProtocol.swift */; }; CF71BC9C22BB6E1100AFB1E1 /* NSArray+Additions.m in Sources */ = {isa = PBXBuildFile; fileRef = CFBB969422B5481B00585BE6 /* NSArray+Additions.m */; }; - CF71BC9D22BB6E1100AFB1E1 /* UIColor+HBAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = CFBB968022B5481B00585BE6 /* UIColor+HBAdditions.m */; }; CF71BCA822BB6E1100AFB1E1 /* NSLayoutConstraint+CompactConstraint.m in Sources */ = {isa = PBXBuildFile; fileRef = CFBB96B922B5481B00585BE6 /* NSLayoutConstraint+CompactConstraint.m */; }; CF71BCA922BB6E1100AFB1E1 /* UIView+CompactConstraint.m in Sources */ = {isa = PBXBuildFile; fileRef = CFBB96B722B5481B00585BE6 /* UIView+CompactConstraint.m */; }; CF71BCC322BB727B00AFB1E1 /* libcurses.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = CF71BCC222BB727B00AFB1E1 /* libcurses.tbd */; }; CF71BCC622BB737200AFB1E1 /* NSArray+Additions.h in Headers */ = {isa = PBXBuildFile; fileRef = CFBB969322B5481B00585BE6 /* NSArray+Additions.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CF71BCC722BB737200AFB1E1 /* UIColor+HBAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = CFBB967F22B5481B00585BE6 /* UIColor+HBAdditions.h */; settings = {ATTRIBUTES = (Public, ); }; }; CF71BCC822BB737200AFB1E1 /* CrossPlatformUI.h in Headers */ = {isa = PBXBuildFile; fileRef = CF71BC6422BB6A2B00AFB1E1 /* CrossPlatformUI.h */; settings = {ATTRIBUTES = (Public, ); }; }; CF71BCD622BB737200AFB1E1 /* CompactConstraint.h in Headers */ = {isa = PBXBuildFile; fileRef = CFBB96BC22B5481B00585BE6 /* CompactConstraint.h */; settings = {ATTRIBUTES = (Public, ); }; }; CF71BCD722BB737200AFB1E1 /* NSLayoutConstraint+CompactConstraint.h in Headers */ = {isa = PBXBuildFile; fileRef = CFBB96BB22B5481B00585BE6 /* NSLayoutConstraint+CompactConstraint.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -94,7 +95,7 @@ /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ - CF71BC9522BB6DEA00AFB1E1 /* PBXContainerItemProxy */ = { + 4E3237A726FB569500AEDA06 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = CF22F9A91FFB97F7003175DE /* Project object */; proxyType = 1; @@ -124,10 +125,14 @@ 4E0A97A32685D1CD0022F569 /* SettingsAcknowledgementsTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsAcknowledgementsTextView.swift; sourceTree = ""; }; 4E2492CF26230E59002C47CB /* TerminalController+ITermExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TerminalController+ITermExtensions.swift"; sourceTree = ""; }; 4E32378E26FB2D4400AEDA06 /* UIApplication+Additions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIApplication+Additions.swift"; sourceTree = ""; }; + 4E3237A326FB530C00AEDA06 /* UIColorAdditions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIColorAdditions.swift; sourceTree = ""; }; 4E3237FA26FC5FC300AEDA06 /* ColorBars.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorBars.swift; sourceTree = ""; }; + 4E3237FC26FEB0D600AEDA06 /* UIDevice+Additions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIDevice+Additions.swift"; sourceTree = ""; }; 4E460120261EC8C0004DBCC2 /* UpdateCheckManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateCheckManager.swift; sourceTree = ""; }; 4E460128261FF23F004DBCC2 /* SettingsSceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSceneDelegate.swift; sourceTree = ""; }; 4E479712262D44D8003CF48C /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Localizable.stringsdict; sourceTree = ""; }; + 4E57499726FEE90700D94DF5 /* PreferencesGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesGroup.swift; sourceTree = ""; }; + 4E57499926FEED9800D94DF5 /* PreferencesList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesList.swift; sourceTree = ""; }; 4E5E817426F2EBE000C4DB6D /* SettingsPerformanceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsPerformanceView.swift; sourceTree = ""; }; 4E98081926184A2500E41883 /* String+Localization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Localization.swift"; sourceTree = ""; }; 4E98081B261850E600E41883 /* KeyValueView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyValueView.swift; sourceTree = ""; }; @@ -135,7 +140,6 @@ 4E9808212618601F00E41883 /* SettingsThemeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsThemeView.swift; sourceTree = ""; }; 4E980824261860CA00E41883 /* AppFont.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppFont.swift; sourceTree = ""; }; 4E980827261879D200E41883 /* SettingsAboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsAboutView.swift; sourceTree = ""; }; - 4E98082E261AA9DC00E41883 /* AppStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStorage.swift; sourceTree = ""; }; 4E980831261AC9FA00E41883 /* AppTheme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppTheme.swift; sourceTree = ""; }; 4E9B32DF260C66B5006A1FBC /* Bundle+NewTermAdditions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+NewTermAdditions.swift"; sourceTree = ""; }; 4E9B32EA261DCFC4006A1FBC /* TextInputBase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextInputBase.swift; sourceTree = ""; }; @@ -169,8 +173,6 @@ CFBB966122B546DA00585BE6 /* NewTerm.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = NewTerm.app; sourceTree = BUILT_PRODUCTS_DIR; }; CFBB967C22B5481B00585BE6 /* HUDView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HUDView.swift; sourceTree = ""; }; CFBB967D22B5481B00585BE6 /* TerminalSampleView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TerminalSampleView.swift; sourceTree = ""; }; - CFBB967F22B5481B00585BE6 /* UIColor+HBAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIColor+HBAdditions.h"; sourceTree = ""; }; - CFBB968022B5481B00585BE6 /* UIColor+HBAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIColor+HBAdditions.m"; sourceTree = ""; }; CFBB968222B5481B00585BE6 /* TabCollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabCollectionViewCell.swift; sourceTree = ""; }; CFBB968322B5481B00585BE6 /* TabToolbarViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabToolbarViewController.swift; sourceTree = ""; }; CFBB968522B5481B00585BE6 /* TerminalKeyInput.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TerminalKeyInput.swift; sourceTree = ""; }; @@ -238,7 +240,6 @@ children = ( 4E980824261860CA00E41883 /* AppFont.swift */, 4E980831261AC9FA00E41883 /* AppTheme.swift */, - 4E98082E261AA9DC00E41883 /* AppStorage.swift */, ); path = Models; sourceTree = ""; @@ -347,6 +348,7 @@ CFBB968E22B5481B00585BE6 /* LaunchScreen.storyboard */, CFBB96BD22B5481B00585BE6 /* Supporting Files */, 4E9B32DF260C66B5006A1FBC /* Bundle+NewTermAdditions.swift */, + 4E3237FC26FEB0D600AEDA06 /* UIDevice+Additions.swift */, ); path = App; sourceTree = ""; @@ -380,6 +382,8 @@ 4E98081B261850E600E41883 /* KeyValueView.swift */, 4EA7D40826F3109A0031F078 /* IconView.swift */, 4EA7D40A26F35A1D0031F078 /* GroupedButtonStyle.swift */, + 4E57499926FEED9800D94DF5 /* PreferencesList.swift */, + 4E57499726FEE90700D94DF5 /* PreferencesGroup.swift */, ); path = Settings; sourceTree = ""; @@ -421,10 +425,9 @@ children = ( CFBB969322B5481B00585BE6 /* NSArray+Additions.h */, CFBB969422B5481B00585BE6 /* NSArray+Additions.m */, - CFBB967F22B5481B00585BE6 /* UIColor+HBAdditions.h */, - CFBB968022B5481B00585BE6 /* UIColor+HBAdditions.m */, 4E9FB97126172079005AFCC8 /* Color+Additions.swift */, 4E98081926184A2500E41883 /* String+Localization.swift */, + 4E3237A326FB530C00AEDA06 /* UIColorAdditions.swift */, ); path = Extensions; sourceTree = ""; @@ -540,7 +543,6 @@ files = ( CF71BCDC22BB737700AFB1E1 /* NewTermCommon.h in Headers */, CF71BCC622BB737200AFB1E1 /* NSArray+Additions.h in Headers */, - CF71BCC722BB737200AFB1E1 /* UIColor+HBAdditions.h in Headers */, CF71BCC822BB737200AFB1E1 /* CrossPlatformUI.h in Headers */, CFD553912345E315005805D1 /* ncurses.h in Headers */, CFD553902345E315005805D1 /* ncurses_dll.h in Headers */, @@ -713,16 +715,15 @@ buildActionMask = 2147483647; files = ( 4E980826261873CE00E41883 /* AppFont.swift in Sources */, - 4E980830261AA9E100E41883 /* AppStorage.swift in Sources */, CF71BC9C22BB6E1100AFB1E1 /* NSArray+Additions.m in Sources */, 4E2492D026230E59002C47CB /* TerminalController+ITermExtensions.swift in Sources */, 4E980833261ACA0400E41883 /* AppTheme.swift in Sources */, - CF71BC9D22BB6E1100AFB1E1 /* UIColor+HBAdditions.m in Sources */, 4E9FB97226172079005AFCC8 /* Color+Additions.swift in Sources */, CF71BCDE22BB73C200AFB1E1 /* Global.swift in Sources */, CF71BCA822BB6E1100AFB1E1 /* NSLayoutConstraint+CompactConstraint.m in Sources */, 4E3237FB26FC5FC300AEDA06 /* ColorBars.swift in Sources */, CF71BCA922BB6E1100AFB1E1 /* UIView+CompactConstraint.m in Sources */, + 4E3237A426FB530C00AEDA06 /* UIColorAdditions.swift in Sources */, CF71BCF122BB8AAB00AFB1E1 /* FontMetrics.swift in Sources */, CF71BC9722BB6E0300AFB1E1 /* Preferences.swift in Sources */, CF71BC9822BB6E0300AFB1E1 /* SubProcess.swift in Sources */, @@ -742,6 +743,7 @@ files = ( 4EA7D40F26F37D480031F078 /* AboutSceneDelegate.swift in Sources */, 4EA7D40D26F366820031F078 /* ActivityView.swift in Sources */, + 4E57499826FEE90700D94DF5 /* PreferencesGroup.swift in Sources */, CFBB96C922B5481B00585BE6 /* TerminalSampleView.swift in Sources */, CFBB96DA22B5481B00585BE6 /* TerminalSessionViewController.swift in Sources */, 4EB057672621A91400766B8A /* TerminalSplitViewController.swift in Sources */, @@ -757,6 +759,7 @@ CFBB96D322B5481B00585BE6 /* KeyboardButton.swift in Sources */, 4E980828261879D200E41883 /* SettingsAboutView.swift in Sources */, CFBB96D122B5481B00585BE6 /* KeyboardPopupToolbar.swift in Sources */, + 4E57499A26FEED9800D94DF5 /* PreferencesList.swift in Sources */, 4EB0576B2622A16400766B8A /* SplitGrabberView.swift in Sources */, 4ED1273125F9E8F60049F59B /* UIColor+AppColors.swift in Sources */, 4E32378F26FB2D4400AEDA06 /* UIApplication+Additions.swift in Sources */, @@ -767,6 +770,7 @@ CFBB96CD22B5481B00585BE6 /* TerminalKeyInput.swift in Sources */, 4E9FB99426184356005AFCC8 /* SettingsView.swift in Sources */, 4E460129261FF23F004DBCC2 /* SettingsSceneDelegate.swift in Sources */, + 4E3237FE26FEB0D800AEDA06 /* UIDevice+Additions.swift in Sources */, CFBB96CB22B5481B00585BE6 /* TabCollectionViewCell.swift in Sources */, CFBB96D222B5481B00585BE6 /* TerminalTextView.swift in Sources */, 4E9B32EB261DCFC4006A1FBC /* TextInputBase.swift in Sources */, @@ -788,7 +792,7 @@ CF71BC9622BB6DEA00AFB1E1 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = CF71BC8322BB6DBA00AFB1E1 /* NewTerm Common */; - targetProxy = CF71BC9522BB6DEA00AFB1E1 /* PBXContainerItemProxy */; + targetProxy = 4E3237A726FB569500AEDA06 /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */