mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-05-24 05:40:15 +00:00
macOS: support migrations when restoring window state (#12461)
First two commits fix the issue when upgrading from 1.2.x to 1.3.x. (#11304) > To double check if this pr really fixes the issue, you can either archive a release build, sign with the same profile, and override manually. > > Or you can find the `savedState` files (located in `~/Library/Daemon\ Containers/<uuid>`), can copy them the local build dir (which is what I did), and run the debug build. Following commits add tests for migrations and some logs. **Currently the minimum version is set to 1.2.x**, since there's a lot changes comparing to 1.1.x. It will be difficult to restore `Ghostty.SplitNode` -> `SplitTree<Ghostty.SurfaceView>` without introducing a lot of checks.
This commit is contained in:
@@ -185,6 +185,7 @@
|
||||
Features/Terminal/ErrorView.swift,
|
||||
Features/Terminal/TerminalController.swift,
|
||||
Features/Terminal/TerminalRestorable.swift,
|
||||
"Features/Terminal/TerminalRestorableState+InteralState.swift",
|
||||
Features/Terminal/TerminalTabColor.swift,
|
||||
Features/Terminal/TerminalView.swift,
|
||||
Features/Terminal/TerminalViewContainer.swift,
|
||||
|
||||
@@ -859,8 +859,6 @@ class AppDelegate: NSObject,
|
||||
}
|
||||
|
||||
func application(_ app: NSApplication, willEncodeRestorableState coder: NSCoder) {
|
||||
Self.logger.debug("application will save window state")
|
||||
|
||||
guard ghostty.config.windowSaveState != "never" else { return }
|
||||
|
||||
// Encode our quick terminal state if we have it.
|
||||
|
||||
@@ -3,15 +3,23 @@ import Cocoa
|
||||
struct QuickTerminalRestorableState: TerminalRestorable {
|
||||
static var version: Int { 1 }
|
||||
|
||||
let focusedSurface: String?
|
||||
let surfaceTree: SplitTree<Ghostty.SurfaceView>
|
||||
let screenStateEntries: QuickTerminalScreenStateCache.Entries
|
||||
var focusedSurface: String? {
|
||||
internalState.focusedSurface
|
||||
}
|
||||
|
||||
var surfaceTree: SplitTree<Ghostty.SurfaceView> {
|
||||
internalState.surfaceTree
|
||||
}
|
||||
|
||||
var screenStateEntries: QuickTerminalScreenStateCache.Entries {
|
||||
internalState.screenStateEntries
|
||||
}
|
||||
|
||||
private let internalState: InternalState<Ghostty.SurfaceView>
|
||||
|
||||
init(from controller: QuickTerminalController) {
|
||||
controller.saveScreenState(exitFullscreen: true)
|
||||
self.focusedSurface = controller.focusedSurface?.id.uuidString
|
||||
self.surfaceTree = controller.surfaceTree
|
||||
self.screenStateEntries = controller.screenStateCache.stateByDisplay
|
||||
self.internalState = .init(from: controller)
|
||||
}
|
||||
|
||||
init(copy other: QuickTerminalRestorableState) {
|
||||
@@ -24,3 +32,37 @@ struct QuickTerminalRestorableState: TerminalRestorable {
|
||||
return config
|
||||
}
|
||||
}
|
||||
|
||||
extension QuickTerminalRestorableState {
|
||||
/// Internal State we use to perform unit tests
|
||||
///
|
||||
/// Since we can't really change the type of `QuickTerminalRestorableState`
|
||||
/// due to `CodableBridge<QuickTerminalRestorableState>` supporting secure coding,
|
||||
/// we use an internal type to perform migration and tests
|
||||
struct InternalState<ViewType: NSView & Codable & Identifiable>: Codable {
|
||||
// MARK: - Version 1 (1.3.0)
|
||||
let focusedSurface: String?
|
||||
let surfaceTree: SplitTree<ViewType>
|
||||
let screenStateEntries: QuickTerminalScreenStateCache.Entries
|
||||
|
||||
init(
|
||||
focusedSurface: String?,
|
||||
surfaceTree: SplitTree<ViewType>,
|
||||
screenStateEntries: QuickTerminalScreenStateCache.Entries,
|
||||
) {
|
||||
self.focusedSurface = focusedSurface
|
||||
self.surfaceTree = surfaceTree
|
||||
self.screenStateEntries = screenStateEntries
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension QuickTerminalRestorableState.InternalState where ViewType == Ghostty.SurfaceView {
|
||||
init(from controller: QuickTerminalController) {
|
||||
self.init(
|
||||
focusedSurface: controller.focusedSurface?.id.uuidString,
|
||||
surfaceTree: controller.surfaceTree,
|
||||
screenStateEntries: controller.screenStateCache.stateByDisplay,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ protocol TerminalRestorable: Codable {
|
||||
static var selfKey: String { get }
|
||||
static var versionKey: String { get }
|
||||
static var version: Int { get }
|
||||
/// Minimum version that can be decoded safely
|
||||
static var minimumVersion: Int { get }
|
||||
init(copy other: Self)
|
||||
|
||||
/// Returns a base configuration to use when restoring terminal surfaces.
|
||||
@@ -11,10 +13,20 @@ protocol TerminalRestorable: Codable {
|
||||
var baseConfig: Ghostty.SurfaceConfiguration? { get }
|
||||
}
|
||||
|
||||
extension TerminalRestorable {
|
||||
static var minimumVersion: Int { version }
|
||||
}
|
||||
|
||||
extension TerminalRestorable {
|
||||
static var selfKey: String { "state" }
|
||||
static var versionKey: String { "version" }
|
||||
|
||||
private var debugDescription: String {
|
||||
withUnsafePointer(to: self) { ptr in
|
||||
"<\(ptr)>[version: \(Self.version)]"
|
||||
}
|
||||
}
|
||||
|
||||
/// Default implementation returns nil (no custom base config).
|
||||
var baseConfig: Ghostty.SurfaceConfiguration? { nil }
|
||||
|
||||
@@ -22,11 +34,14 @@ extension TerminalRestorable {
|
||||
// If the version doesn't match then we can't decode. In the future we can perform
|
||||
// version upgrading or something but for now we only have one version so we
|
||||
// don't bother.
|
||||
guard aDecoder.decodeInteger(forKey: Self.versionKey) == Self.version else {
|
||||
let current = aDecoder.decodeInteger(forKey: Self.versionKey)
|
||||
guard current >= Self.minimumVersion else {
|
||||
AppDelegate.logger.error("error restoring terminal: version not supported: expected=\(Self.minimumVersion, privacy: .public), got=\(current, privacy: .public)")
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let v = aDecoder.decodeObject(of: CodableBridge<Self>.self, forKey: Self.selfKey) else {
|
||||
AppDelegate.logger.error("error restoring terminal: decode failed")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -36,33 +51,59 @@ extension TerminalRestorable {
|
||||
func encode(with coder: NSCoder) {
|
||||
coder.encode(Self.version, forKey: Self.versionKey)
|
||||
coder.encode(CodableBridge(self), forKey: Self.selfKey)
|
||||
|
||||
AppDelegate.logger.debug("saved terminal state: \(debugDescription)")
|
||||
}
|
||||
}
|
||||
|
||||
/// The state stored for terminal window restoration.
|
||||
class TerminalRestorableState: TerminalRestorable {
|
||||
class var version: Int { 7 }
|
||||
final class TerminalRestorableState: TerminalRestorable {
|
||||
static var version: Int { 7 }
|
||||
static var minimumVersion: Int { 5 }
|
||||
|
||||
let focusedSurface: String?
|
||||
let surfaceTree: SplitTree<Ghostty.SurfaceView>
|
||||
let effectiveFullscreenMode: FullscreenMode?
|
||||
let tabColor: TerminalTabColor
|
||||
let titleOverride: String?
|
||||
var focusedSurface: String? {
|
||||
internalState.focusedSurface
|
||||
}
|
||||
var surfaceTree: SplitTree<Ghostty.SurfaceView> {
|
||||
internalState.surfaceTree
|
||||
}
|
||||
var effectiveFullscreenMode: FullscreenMode? {
|
||||
internalState.effectiveFullscreenMode
|
||||
}
|
||||
var tabColor: TerminalTabColor? {
|
||||
internalState.tabColor
|
||||
}
|
||||
var titleOverride: String? {
|
||||
internalState.titleOverride
|
||||
}
|
||||
|
||||
/// Internal State we use to perform unit tests
|
||||
///
|
||||
/// Since we can't really change the type of `TerminalRestorableState`
|
||||
/// due to `CodableBridge<TerminalRestorableState>` supporting secure coding,
|
||||
/// we use an internal type to perform migration and tests
|
||||
private let internalState: InternalState<Ghostty.SurfaceView>
|
||||
|
||||
init(from controller: TerminalController) {
|
||||
self.focusedSurface = controller.focusedSurface?.id.uuidString
|
||||
self.surfaceTree = controller.surfaceTree
|
||||
self.effectiveFullscreenMode = controller.fullscreenStyle?.fullscreenMode
|
||||
self.tabColor = (controller.window as? TerminalWindow)?.tabColor ?? .none
|
||||
self.titleOverride = controller.titleOverride
|
||||
internalState = .init(from: controller)
|
||||
}
|
||||
|
||||
required init(copy other: TerminalRestorableState) {
|
||||
self.surfaceTree = other.surfaceTree
|
||||
self.focusedSurface = other.focusedSurface
|
||||
self.effectiveFullscreenMode = other.effectiveFullscreenMode
|
||||
self.tabColor = other.tabColor
|
||||
self.titleOverride = other.titleOverride
|
||||
self.internalState = other.internalState
|
||||
}
|
||||
|
||||
/// This is just wrapper around internalState
|
||||
///
|
||||
/// - Important: If you intend to add more things, go to `InternalState`.
|
||||
init(from decoder: any Decoder) throws {
|
||||
self.internalState = try InternalState<Ghostty.SurfaceView>(from: decoder)
|
||||
}
|
||||
|
||||
/// This is just wrapper around internalState
|
||||
///
|
||||
/// - Important: If you intend to add more things, go to `InternalState`.
|
||||
func encode(to encoder: any Encoder) throws {
|
||||
try internalState.encode(to: encoder)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,6 +140,7 @@ class TerminalWindowRestoration: NSObject, NSWindowRestoration {
|
||||
// because window restoration is only ever invoked on app start so we
|
||||
// don't have to deal with config reloads.
|
||||
if appDelegate.ghostty.config.windowSaveState == "never" {
|
||||
AppDelegate.logger.warning("skip restoration: window-save-state=never")
|
||||
completionHandler(nil, nil)
|
||||
return
|
||||
}
|
||||
@@ -121,8 +163,10 @@ class TerminalWindowRestoration: NSObject, NSWindowRestoration {
|
||||
return
|
||||
}
|
||||
|
||||
// Restore our tab color
|
||||
(window as? TerminalWindow)?.tabColor = state.tabColor
|
||||
// Restore our tab color and avoid unnecessary `invalidateRestorableState` calls
|
||||
if let tabColor = state.tabColor {
|
||||
(window as? TerminalWindow)?.tabColor = tabColor
|
||||
}
|
||||
|
||||
// Restore the tab title override
|
||||
c.titleOverride = state.titleOverride
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import AppKit
|
||||
|
||||
extension TerminalRestorableState {
|
||||
/// Internal State we use to perform unit tests
|
||||
///
|
||||
/// Since we can't really change the type of `TerminalRestorableState`
|
||||
/// due to `CodableBridge<TerminalRestorableState>` supporting secure coding,
|
||||
/// we use an internal type to perform migration and tests
|
||||
struct InternalState<ViewType: NSView & Codable & Identifiable>: Codable {
|
||||
// MARK: - Version 5 (1.2.3)
|
||||
let focusedSurface: String?
|
||||
let surfaceTree: SplitTree<ViewType>
|
||||
|
||||
// MARK: - Version 7 (1.3.0)
|
||||
let effectiveFullscreenMode: FullscreenMode?
|
||||
let tabColor: TerminalTabColor?
|
||||
let titleOverride: String?
|
||||
|
||||
init(
|
||||
focusedSurface: String?,
|
||||
surfaceTree: SplitTree<ViewType>,
|
||||
effectiveFullscreenMode: FullscreenMode?,
|
||||
tabColor: TerminalTabColor?,
|
||||
titleOverride: String?,
|
||||
) {
|
||||
self.focusedSurface = focusedSurface
|
||||
self.surfaceTree = surfaceTree
|
||||
self.effectiveFullscreenMode = effectiveFullscreenMode
|
||||
self.tabColor = tabColor
|
||||
self.titleOverride = titleOverride
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension TerminalRestorableState.InternalState where ViewType == Ghostty.SurfaceView {
|
||||
init(from controller: TerminalController) {
|
||||
self.init(
|
||||
focusedSurface: controller.focusedSurface?.id.uuidString,
|
||||
surfaceTree: controller.surfaceTree,
|
||||
effectiveFullscreenMode: controller.fullscreenStyle?.fullscreenMode,
|
||||
tabColor: (controller.window as? TerminalWindow)?.tabColor,
|
||||
titleOverride: controller.titleOverride,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,7 @@ class MockView: NSView, Codable, Identifiable {
|
||||
|
||||
struct SplitTreeTests {
|
||||
/// Creates a two-view horizontal split tree (view1 | view2).
|
||||
private func makeHorizontalSplit() throws -> (SplitTree<MockView>, MockView, MockView) {
|
||||
static func makeHorizontalSplit() throws -> (SplitTree<MockView>, MockView, MockView) {
|
||||
let view1 = MockView()
|
||||
let view2 = MockView()
|
||||
var tree = SplitTree<MockView>(view: view1)
|
||||
@@ -36,6 +36,11 @@ struct SplitTreeTests {
|
||||
return (tree, view1, view2)
|
||||
}
|
||||
|
||||
/// Creates a two-view horizontal split tree (view1 | view2).
|
||||
private func makeHorizontalSplit() throws -> (SplitTree<MockView>, MockView, MockView) {
|
||||
try Self.makeHorizontalSplit()
|
||||
}
|
||||
|
||||
// MARK: - Empty and Non-Empty
|
||||
|
||||
@Test func emptyTreeIsEmpty() {
|
||||
|
||||
215
macos/Tests/Terminal/TerminalRestorableTests.swift
Normal file
215
macos/Tests/Terminal/TerminalRestorableTests.swift
Normal file
@@ -0,0 +1,215 @@
|
||||
import Testing
|
||||
import AppKit
|
||||
@testable import Ghostty
|
||||
|
||||
@Suite
|
||||
struct TerminalRestorableTests {
|
||||
@Test
|
||||
func areYouForgettingToAddMigrationTests() {
|
||||
#expect(TerminalRestorableState.version == 7)
|
||||
#expect(TerminalRestorableState.minimumVersion == 5)
|
||||
|
||||
#expect(QuickTerminalRestorableState.version == 1)
|
||||
#expect(QuickTerminalRestorableState.minimumVersion == 1)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@Test func quickTerminalRestorableFromV1() throws {
|
||||
/* v1
|
||||
let tree = try SplitTreeTests.makeHorizontalSplit()
|
||||
let state = DummyQuickTerminalRestorableState(
|
||||
focusedSurface: "123",
|
||||
surfaceTree: tree.0,
|
||||
screenStateEntries: [:],
|
||||
)
|
||||
let data = try archive(CodableBridge(state), className: "CodableBridge<QuickTerminal>")
|
||||
print(data.base64EncodedString())
|
||||
print(tree.1.id)
|
||||
print(tree.2.id)
|
||||
*/
|
||||
|
||||
let decoded: CodableBridge<DummyQuickTerminalRestorableState> = try unarchive(v1QTData, className: "CodableBridge<QuickTerminal>")
|
||||
let state = decoded.value.internalState
|
||||
|
||||
#expect(state.focusedSurface == "123")
|
||||
#expect(state.screenStateEntries.isEmpty)
|
||||
#expect(state.surfaceTree.contains(where: { $0.id.uuidString == "2F2F2D93-944C-474A-83BA-4DC1868C3EB9" }))
|
||||
#expect(state.surfaceTree.contains(where: { $0.id.uuidString == "994C673F-B4C5-49EE-B044-65006652636D" }))
|
||||
}
|
||||
|
||||
// To generate old data: created a dummy class, archive, and copy the printed result
|
||||
@MainActor
|
||||
@Test func restoreTerminal57() throws {
|
||||
|
||||
// let tree = try SplitTreeTests.makeHorizontalSplit()
|
||||
// let state = DummyTerminalRestorableState(
|
||||
// focusedSurface: "v5",
|
||||
// surfaceTree: tree.0,
|
||||
// )
|
||||
// let data = try archive(CodableBridge(state), className: "CodableBridge<Terminal>")
|
||||
// print(data.base64EncodedString())
|
||||
// print()
|
||||
// print(tree.1.id)
|
||||
// print(tree.2.id)
|
||||
|
||||
let v5 = try unarchive(v5Data, className: "CodableBridge<Terminal>", as: CodableBridge<DummyTerminalRestorableState>.self)
|
||||
.value.internalState
|
||||
#expect(v5.focusedSurface == "v5")
|
||||
#expect(v5.effectiveFullscreenMode == nil)
|
||||
#expect(v5.tabColor == nil)
|
||||
#expect(v5.titleOverride == nil)
|
||||
#expect(v5.surfaceTree.contains(where: { $0.id.uuidString == "926F3F2A-824C-40C9-87CA-2CDCA4E11049" }))
|
||||
#expect(v5.surfaceTree.contains(where: { $0.id.uuidString == "AC5E829B-85FD-4C69-B196-2EE469C72A90" }))
|
||||
|
||||
// let tree = try SplitTreeTests.makeHorizontalSplit()
|
||||
// let state = DummyTerminalRestorableState(
|
||||
// focusedSurface: "v7",
|
||||
// surfaceTree: tree.0,
|
||||
// effectiveFullscreenMode: .native,
|
||||
// tabColor: .green,
|
||||
// titleOverride: "1.3.0"
|
||||
// )
|
||||
// let data = try archive(CodableBridge(state), className: "CodableBridge<Terminal>")
|
||||
// print(data.base64EncodedString())
|
||||
// print()
|
||||
// print(tree.1.id)
|
||||
// print(tree.2.id)
|
||||
|
||||
let v7 = try unarchive(v7Data, className: "CodableBridge<Terminal>", as: CodableBridge<DummyTerminalRestorableState>.self)
|
||||
.value.internalState
|
||||
#expect(v7.focusedSurface == "v7")
|
||||
#expect(v7.effectiveFullscreenMode == .native)
|
||||
#expect(v7.tabColor == .green)
|
||||
#expect(v7.titleOverride == "1.3.0")
|
||||
#expect(v7.surfaceTree.contains(where: { $0.id.uuidString == "5D580A7A-81EA-47C6-BB9A-AD4B1783E478" }))
|
||||
#expect(v7.surfaceTree.contains(where: { $0.id.uuidString == "96EA1189-7482-41BC-A6CD-26E5190E4BFA" }))
|
||||
|
||||
// let tree = try SplitTreeTests.makeHorizontalSplit()
|
||||
// let state = DummyTerminalRestorableState(
|
||||
// .init(
|
||||
// focusedSurface: "v7 generic",
|
||||
// surfaceTree: tree.0,
|
||||
// effectiveFullscreenMode: .native,
|
||||
// tabColor: .green,
|
||||
// titleOverride: "tip"
|
||||
// )
|
||||
// )
|
||||
// let data = try archive(CodableBridge(state), className: "CodableBridge<Terminal>")
|
||||
// print(data.base64EncodedString())
|
||||
// print()
|
||||
// print(tree.1.id)
|
||||
// print(tree.2.id)
|
||||
|
||||
let v7Generic = try unarchive(v7GenericData, className: "CodableBridge<Terminal>", as: CodableBridge<DummyTerminalRestorableState>.self)
|
||||
.value.internalState
|
||||
#expect(v7Generic.focusedSurface == "v7 generic")
|
||||
#expect(v7Generic.effectiveFullscreenMode == .native)
|
||||
#expect(v7Generic.tabColor == .green)
|
||||
#expect(v7Generic.titleOverride == "tip")
|
||||
#expect(v7Generic.surfaceTree.contains(where: { $0.id.uuidString == "953CE952-D91D-4D36-AC72-9D0F1F6BCE73" }))
|
||||
#expect(v7Generic.surfaceTree.contains(where: { $0.id.uuidString == "D3223569-2E01-4BC5-9DB2-DBFC3AFF46D1" }))
|
||||
}
|
||||
}
|
||||
|
||||
private extension TerminalRestorableTests {
|
||||
func archive<T: NSObject & NSSecureCoding>(_ obj: T, className: String?) throws -> Data {
|
||||
let archiver = NSKeyedArchiver(requiringSecureCoding: true)
|
||||
defer { archiver.finishEncoding() }
|
||||
if let className {
|
||||
archiver.setClassName(className, for: T.self)
|
||||
}
|
||||
archiver.encode(obj, forKey: NSKeyedArchiveRootObjectKey)
|
||||
return archiver.encodedData
|
||||
}
|
||||
|
||||
func unarchive<T: NSObject & NSSecureCoding>(_ data: Data, className: String?, as: T.Type = T.self) throws -> T {
|
||||
let unarchiver = try NSKeyedUnarchiver(forReadingFrom: data)
|
||||
defer { unarchiver.finishDecoding()}
|
||||
if let className {
|
||||
unarchiver.setClass(T.self, forClassName: className)
|
||||
}
|
||||
unarchiver.requiresSecureCoding = true
|
||||
let result = unarchiver.decodeObject(of: T.self, forKey: NSKeyedArchiveRootObjectKey)
|
||||
return try #require(result)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Dummy States
|
||||
|
||||
@MainActor
|
||||
private final class DummyTerminalRestorableState: TerminalRestorable {
|
||||
static var version: Int {
|
||||
TerminalRestorableState.version
|
||||
}
|
||||
|
||||
static var minimumVersion: Int {
|
||||
TerminalRestorableState.minimumVersion
|
||||
}
|
||||
|
||||
required init(copy other: DummyTerminalRestorableState) {
|
||||
internalState = other.internalState
|
||||
}
|
||||
|
||||
let internalState: TerminalRestorableState.InternalState<MockView>
|
||||
|
||||
init(_ internalState: TerminalRestorableState.InternalState<MockView>) {
|
||||
self.internalState = internalState
|
||||
}
|
||||
|
||||
required init(from decoder: any Decoder) throws {
|
||||
self.internalState = try TerminalRestorableState.InternalState<MockView>(from: decoder)
|
||||
}
|
||||
|
||||
func encode(to encoder: any Encoder) throws {
|
||||
try internalState.encode(to: encoder)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
struct DummyQuickTerminalRestorableState: TerminalRestorable {
|
||||
static var version: Int = QuickTerminalRestorableState.version
|
||||
|
||||
static var minimumVersion: Int = QuickTerminalRestorableState.minimumVersion
|
||||
|
||||
init(copy other: DummyQuickTerminalRestorableState) {
|
||||
internalState = other.internalState
|
||||
}
|
||||
|
||||
let internalState: QuickTerminalRestorableState.InternalState<MockView>
|
||||
|
||||
init(_ internalState: QuickTerminalRestorableState.InternalState<MockView>) {
|
||||
self.internalState = internalState
|
||||
}
|
||||
|
||||
init(from decoder: any Decoder) throws {
|
||||
self.internalState = try QuickTerminalRestorableState.InternalState<MockView>(from: decoder)
|
||||
}
|
||||
|
||||
func encode(to encoder: any Encoder) throws {
|
||||
try internalState.encode(to: encoder)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - QuickTerminal V1 (1.3.0)
|
||||
|
||||
private let v1QTData = Data(base64Encoded: """
|
||||
YnBsaXN0MDDUAQIDBAUGBwpYJHZlcnNpb25ZJGFyY2hpdmVyVCR0b3BYJG9iamVjdHMSAAGGoF8QD05TS2V5ZWRBcmNoaXZlctEICVRyb290gAGkCwwRElUkbnVsbNINDg8QVGRhdGFWJGNsYXNzgAKAA08RA6hicGxpc3QwMNQBAgMEBQYHClgkdmVyc2lvblkkYXJjaGl2ZXJUJHRvcFgkb2JqZWN0cxIAAYagXxAPTlNLZXllZEFyY2hpdmVy0QgJVXZhbHVlgAGvECALDBkaGxwfJicvMDEyODlFRkdISU9QVldYXF1jaWpwcVUkbnVsbNMNDg8QFBhXTlMua2V5c1pOUy5vYmplY3RzViRjbGFzc6MREhOAAoADgASjFRYXgAWAB4AIgBhfEBJzY3JlZW5TdGF0ZUVudHJpZXNeZm9jdXNlZFN1cmZhY2Vbc3VyZmFjZVRyZWXSDg8dHqCABtIgISIjWiRjbGFzc25hbWVYJGNsYXNzZXNeTlNNdXRhYmxlQXJyYXmjIiQlV05TQXJyYXlYTlNPYmplY3RTMTIz0w0ODygrGKIpKoAJgAqiLC2AC4AMgBhXdmVyc2lvblRyb290EAHTDQ4PMzUYoTSADaE2gA6AGFVzcGxpdNMNDg86PxikOzw9PoAPgBCAEYASpEBBQkOAE4AZgBqAHYAYVXJpZ2h0VXJhdGlvVGxlZnRZZGlyZWN0aW9u0w0OD0pMGKFLgBShTYAVgBhUdmlld9MNDg9RUxihUoAWoVSAF4AYUmlkXxAkOTk0QzY3M0YtQjRDNS00OUVFLUIwNDQtNjUwMDY2NTI2MzZE0iAhWVpfEBNOU011dGFibGVEaWN0aW9uYXJ5o1lbJVxOU0RpY3Rpb25hcnkjP+AAAAAAAADTDQ4PXmAYoUuAFKFhgBuAGNMNDg9kZhihUoAWoWeAHIAYXxAkMkYyRjJEOTMtOTQ0Qy00NzRBLTgzQkEtNERDMTg2OEMzRUI50w0OD2ttGKFsgB6hboAfgBhaaG9yaXpvbnRhbNMNDg9ycxigoIAYAAgAEQAaACQAKQAyADcASQBMAFIAVAB3AH0AhACMAJcAngCiAKQApgCoAKwArgCwALIAtADJANgA5ADpAOoA7ADxAPwBBQEUARgBIAEpAS0BNAE3ATkBOwE+AUABQgFEAUwBUQFTAVoBXAFeAWABYgFkAWoBcQF2AXgBegF8AX4BgwGFAYcBiQGLAY0BkwGZAZ4BqAGvAbEBswG1AbcBuQG+AcUBxwHJAcsBzQHPAdIB+QH+AhQCGAIlAi4CNQI3AjkCOwI9Aj8CRgJIAkoCTAJOAlACdwJ+AoACggKEAoYCiAKTApoCmwKcAAAAAAAAAgEAAAAAAAAAdQAAAAAAAAAAAAAAAAAAAp7RExRaJGNsYXNzbmFtZV8QHENvZGFibGVCcmlkZ2U8UXVpY2tUZXJtaW5hbD4ACAARABoAJAApADIANwBJAEwAUQBTAFgAXgBjAGgAbwBxAHMEHwQiBC0AAAAAAAACAQAAAAAAAAAVAAAAAAAAAAAAAAAAAAAETA==
|
||||
""")!
|
||||
|
||||
// MARK: - Terminal V5 (1.2.3)
|
||||
|
||||
private let v5Data = Data(base64Encoded: """
|
||||
YnBsaXN0MDDUAQIDBAUGBwpYJHZlcnNpb25ZJGFyY2hpdmVyVCR0b3BYJG9iamVjdHMSAAGGoF8QD05TS2V5ZWRBcmNoaXZlctEICVRyb290gAGkCwwRElUkbnVsbNINDg8QVGRhdGFWJGNsYXNzgAKAA08RA01icGxpc3QwMNQBAgMEBQYHClgkdmVyc2lvblkkYXJjaGl2ZXJUJHRvcFgkb2JqZWN0cxIAAYagXxAPTlNLZXllZEFyY2hpdmVy0QgJVXZhbHVlgAGvEB0LDBcYGRoiIyQlKyw4OTo7PEJDSUpLUlNZX2BmZ1UkbnVsbNMNDg8QExZXTlMua2V5c1pOUy5vYmplY3RzViRjbGFzc6IREoACgAOiFBWABIAFgBVeZm9jdXNlZFN1cmZhY2Vbc3VyZmFjZVRyZWVSdjXTDQ4PGx4WohwdgAaAB6IfIIAIgAmAFVd2ZXJzaW9uVHJvb3QQAdMNDg8mKBahJ4AKoSmAC4AVVXNwbGl00w0ODy0yFqQuLzAxgAyADYAOgA+kMzQ1NoAQgBaAF4AagBVVcmlnaHRVcmF0aW9UbGVmdFlkaXJlY3Rpb27TDQ4PPT8WoT6AEaFAgBKAFVR2aWV30w0OD0RGFqFFgBOhR4AUgBVSaWRfECRBQzVFODI5Qi04NUZELTRDNjktQjE5Ni0yRUU0NjlDNzJBOTDSTE1OT1okY2xhc3NuYW1lWCRjbGFzc2VzXxATTlNNdXRhYmxlRGljdGlvbmFyeaNOUFFcTlNEaWN0aW9uYXJ5WE5TT2JqZWN0Iz/gAAAAAAAA0w0OD1RWFqE+gBGhV4AYgBXTDQ4PWlwWoUWAE6FdgBmAFV8QJDkyNkYzRjJBLTgyNEMtNDBDOS04N0NBLTJDRENBNEUxMTA0OdMNDg9hYxahYoAboWSAHIAVWmhvcml6b250YWzTDQ4PaGkWoKCAFQAIABEAGgAkACkAMgA3AEkATABSAFQAdAB6AIEAiQCUAJsAngCgAKIApQCnAKkAqwC6AMYAyQDQANMA1QDXANoA3ADeAOAA6ADtAO8A9gD4APoA/AD+AQABBgENARIBFAEWARgBGgEfASEBIwElAScBKQEvATUBOgFEAUsBTQFPAVEBUwFVAVoBYQFjAWUBZwFpAWsBbgGVAZoBpQGuAcQByAHVAd4B5wHuAfAB8gH0AfYB+AH/AgECAwIFAgcCCQIwAjcCOQI7Aj0CPwJBAkwCUwJUAlUAAAAAAAACAQAAAAAAAABrAAAAAAAAAAAAAAAAAAACV9ETFFokY2xhc3NuYW1lXxAXQ29kYWJsZUJyaWRnZTxUZXJtaW5hbD4ACAARABoAJAApADIANwBJAEwAUQBTAFgAXgBjAGgAbwBxAHMDxAPHA9IAAAAAAAACAQAAAAAAAAAVAAAAAAAAAAAAAAAAAAAD7A==
|
||||
""")!
|
||||
|
||||
// MARK: - Terminal V7 (1.3.0)
|
||||
|
||||
private let v7Data = Data(base64Encoded: """
|
||||
YnBsaXN0MDDUAQIDBAUGBwpYJHZlcnNpb25ZJGFyY2hpdmVyVCR0b3BYJG9iamVjdHMSAAGGoF8QD05TS2V5ZWRBcmNoaXZlctEICVRyb290gAGkCwwRElUkbnVsbNINDg8QVGRhdGFWJGNsYXNzgAKAA08RA71icGxpc3QwMNQBAgMEBQYHClgkdmVyc2lvblkkYXJjaGl2ZXJUJHRvcFgkb2JqZWN0cxIAAYagXxAPTlNLZXllZEFyY2hpdmVy0QgJVXZhbHVlgAGvECMLDB0eHyAhIiMkLC0uLzU2QkNERUZMTVNUVVxdY2lqcHF1dlUkbnVsbNMNDg8QFhxXTlMua2V5c1pOUy5vYmplY3RzViRjbGFzc6UREhMUFYACgAOABIAFgAalFxgZGhuAB4AIgAmAIYAigBlfEBdlZmZlY3RpdmVGdWxsc2NyZWVuTW9kZV5mb2N1c2VkU3VyZmFjZVtzdXJmYWNlVHJlZVh0YWJDb2xvcl10aXRsZU92ZXJyaWRlVm5hdGl2ZVJ2N9MNDg8lKByiJieACoALoikqgAyADYAZV3ZlcnNpb25Ucm9vdBAB0w0ODzAyHKExgA6hM4APgBlVc3BsaXTTDQ4PNzwcpDg5OjuAEIARgBKAE6Q9Pj9AgBSAGoAbgB6AGVVyaWdodFVyYXRpb1RsZWZ0WWRpcmVjdGlvbtMNDg9HSRyhSIAVoUqAFoAZVHZpZXfTDQ4PTlAcoU+AF6FRgBiAGVJpZF8QJDk2RUExMTg5LTc0ODItNDFCQy1BNkNELTI2RTUxOTBFNEJGQdJWV1hZWiRjbGFzc25hbWVYJGNsYXNzZXNfEBNOU011dGFibGVEaWN0aW9uYXJ5o1haW1xOU0RpY3Rpb25hcnlYTlNPYmplY3QjP+AAAAAAAADTDQ4PXmAcoUiAFaFhgByAGdMNDg9kZhyhT4AXoWeAHYAZXxAkNUQ1ODBBN0EtODFFQS00N0M2LUJCOUEtQUQ0QjE3ODNFNDc40w0OD2ttHKFsgB+hboAggBlaaG9yaXpvbnRhbNMNDg9ycxygoIAZEAdVMS4zLjAACAARABoAJAApADIANwBJAEwAUgBUAHoAgACHAI8AmgChAKcAqQCrAK0ArwCxALcAuQC7AL0AvwDBAMMA3QDsAPgBAQEPARYBGQEgASMBJQEnASoBLAEuATABOAE9AT8BRgFIAUoBTAFOAVABVgFdAWIBZAFmAWgBagFvAXEBcwF1AXcBeQF/AYUBigGUAZsBnQGfAaEBowGlAaoBsQGzAbUBtwG5AbsBvgHlAeoB9QH+AhQCGAIlAi4CNwI+AkACQgJEAkYCSAJPAlECUwJVAlcCWQKAAocCiQKLAo0CjwKRApwCowKkAqUCpwKpAAAAAAAAAgEAAAAAAAAAdwAAAAAAAAAAAAAAAAAAAq/RExRaJGNsYXNzbmFtZV8QF0NvZGFibGVCcmlkZ2U8VGVybWluYWw+AAgAEQAaACQAKQAyADcASQBMAFEAUwBYAF4AYwBoAG8AcQBzBDQENwRCAAAAAAAAAgEAAAAAAAAAFQAAAAAAAAAAAAAAAAAABFw=
|
||||
""")!
|
||||
|
||||
// MARK: - Terminal V7 Generic (tip)
|
||||
|
||||
private let v7GenericData = Data(base64Encoded: """
|
||||
YnBsaXN0MDDUAQIDBAUGBwpYJHZlcnNpb25ZJGFyY2hpdmVyVCR0b3BYJG9iamVjdHMSAAGGoF8QD05TS2V5ZWRBcmNoaXZlctEICVRyb290gAGkCwwRElUkbnVsbNINDg8QVGRhdGFWJGNsYXNzgAKAA08RA8NicGxpc3QwMNQBAgMEBQYHClgkdmVyc2lvblkkYXJjaGl2ZXJUJHRvcFgkb2JqZWN0cxIAAYagXxAPTlNLZXllZEFyY2hpdmVy0QgJVXZhbHVlgAGvECMLDB0eHyAhIiMkLC0uLzU2QkNERUZMTVNUVVxdY2lqcHF1dlUkbnVsbNMNDg8QFhxXTlMua2V5c1pOUy5vYmplY3RzViRjbGFzc6UREhMUFYACgAOABIAFgAalFxgZGhuAB4AIgAmAIYAigBlfEBdlZmZlY3RpdmVGdWxsc2NyZWVuTW9kZV5mb2N1c2VkU3VyZmFjZVtzdXJmYWNlVHJlZVh0YWJDb2xvcl10aXRsZU92ZXJyaWRlVm5hdGl2ZVp2NyBnZW5lcmlj0w0ODyUoHKImJ4AKgAuiKSqADIANgBlXdmVyc2lvblRyb290EAHTDQ4PMDIcoTGADqEzgA+AGVVzcGxpdNMNDg83PBykODk6O4AQgBGAEoATpD0+P0CAFIAagBuAHoAZVXJpZ2h0VXJhdGlvVGxlZnRZZGlyZWN0aW9u0w0OD0dJHKFIgBWhSoAWgBlUdmlld9MNDg9OUByhT4AXoVGAGIAZUmlkXxAkRDMyMjM1NjktMkUwMS00QkM1LTlEQjItREJGQzNBRkY0NkQx0lZXWFlaJGNsYXNzbmFtZVgkY2xhc3Nlc18QE05TTXV0YWJsZURpY3Rpb25hcnmjWFpbXE5TRGljdGlvbmFyeVhOU09iamVjdCM/4AAAAAAAANMNDg9eYByhSIAVoWGAHIAZ0w0OD2RmHKFPgBehZ4AdgBlfECQ5NTNDRTk1Mi1EOTFELTREMzYtQUM3Mi05RDBGMUY2QkNFNzPTDQ4Pa20coWyAH6FugCCAGVpob3Jpem9udGFs0w0OD3JzHKCggBkQB1N0aXAACAARABoAJAApADIANwBJAEwAUgBUAHoAgACHAI8AmgChAKcAqQCrAK0ArwCxALcAuQC7AL0AvwDBAMMA3QDsAPgBAQEPARYBIQEoASsBLQEvATIBNAE2ATgBQAFFAUcBTgFQAVIBVAFWAVgBXgFlAWoBbAFuAXABcgF3AXkBewF9AX8BgQGHAY0BkgGcAaMBpQGnAakBqwGtAbIBuQG7Ab0BvwHBAcMBxgHtAfIB/QIGAhwCIAItAjYCPwJGAkgCSgJMAk4CUAJXAlkCWwJdAl8CYQKIAo8CkQKTApUClwKZAqQCqwKsAq0CrwKxAAAAAAAAAgEAAAAAAAAAdwAAAAAAAAAAAAAAAAAAArXRExRaJGNsYXNzbmFtZV8QF0NvZGFibGVCcmlkZ2U8VGVybWluYWw+AAgAEQAaACQAKQAyADcASQBMAFEAUwBYAF4AYwBoAG8AcQBzBDoEPQRIAAAAAAAAAgEAAAAAAAAAFQAAAAAAAAAAAAAAAAAABGI=
|
||||
""")!
|
||||
Reference in New Issue
Block a user