diff --git a/App.xcworkspace/contents.xcworkspacedata b/App.xcworkspace/contents.xcworkspacedata
index 291a93a..744b073 100644
--- a/App.xcworkspace/contents.xcworkspacedata
+++ b/App.xcworkspace/contents.xcworkspacedata
@@ -35,12 +35,12 @@
location = "group:Foundation/SymbolPicker">
+ location = "group:Foundation/SwiftUIPolyfill">
+ location = "group:External/XTerminalUI">
+ location = "group:Foundation/XMLCoder">
diff --git a/Application/Rayon/Extension/Store/RayonStore+SwiftUI.swift b/Application/Rayon/Extension/Store/RayonStore+SwiftUI.swift
deleted file mode 100644
index 344891f..0000000
--- a/Application/Rayon/Extension/Store/RayonStore+SwiftUI.swift
+++ /dev/null
@@ -1,28 +0,0 @@
-//
-// RayonStore+SwiftUI.swift
-// Rayon (macOS)
-//
-// Created by Lakr Aream on 2022/3/1.
-//
-
-import RayonModule
-import SwiftUI
-
-extension RayonStore {
- func createNewWindowGroup(for view: T) -> Window {
- UIBridge.openNewWindow(from: view)
- }
-
- func beginBatchScriptExecution(for snippet: RDSnippet.ID, and machines: [RDMachine.ID]) {
- let snippet = snippetGroup[snippet]
- guard snippet.code.count > 0 else {
- return
- }
- guard machines.count > 0 else {
- RayonStore.presentError("No machine was selected for execution")
- return
- }
- let view = BatchSnippetExecView(snippet: snippet, machines: machines)
- UIBridge.openNewWindow(from: view)
- }
-}
diff --git a/Application/Rayon/Extension/UIBridge/PresentError.swift b/Application/Rayon/Extension/UIBridge/PresentError.swift
index 364e312..989308f 100644
--- a/Application/Rayon/Extension/UIBridge/PresentError.swift
+++ b/Application/Rayon/Extension/UIBridge/PresentError.swift
@@ -22,7 +22,7 @@ extension UIBridge {
}
}
- static func presentError(with message: String, delay: Double = 0.5) {
+ static func presentError(with message: String, delay: Double = 0) {
debugPrint(" \(message)")
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
let alert = NSAlert()
diff --git a/Application/Rayon/Extension/Utils/Generic.swift b/Application/Rayon/Extension/Utils/Generic.swift
index 9003b73..5ad1d42 100644
--- a/Application/Rayon/Extension/Utils/Generic.swift
+++ b/Application/Rayon/Extension/Utils/Generic.swift
@@ -10,6 +10,16 @@ import RayonModule
import SwiftUI
enum RayonUtil {
+ static func findWindow() -> NSWindow? {
+ if let key = NSApp.keyWindow {
+ return key
+ }
+ for window in NSApp.windows where window.isVisible {
+ return window
+ }
+ return nil
+ }
+
static func selectIdentity() -> RDIdentity.ID? {
assert(!Thread.isMainThread, "select identity must be called from background thread")
@@ -20,10 +30,17 @@ enum RayonUtil {
mainActor {
var panelRef: NSPanel?
+ var windowRef: NSWindow?
let controller = NSHostingController(rootView: Group {
IdentityPickerSheetView {
selection = $0
- if let panel = panelRef { panel.close() }
+ if let panel = panelRef {
+ if let windowRef = windowRef {
+ windowRef.endSheet(panel)
+ } else {
+ panel.close()
+ }
+ }
sem.signal()
}
.environmentObject(RayonStore.shared)
@@ -34,13 +51,60 @@ enum RayonUtil {
panel.title = ""
panel.titleVisibility = .hidden
- if let keyWindow = NSApp.keyWindow {
+ if let keyWindow = findWindow() {
+ windowRef = keyWindow
+ keyWindow.beginSheet(panel) { _ in }
+ } else {
+ sem.signal()
+ }
+ }
+ sem.wait()
+ return selection
+ }
+
+ static func selectMachine(allowMany: Bool = true) -> [RDMachine.ID] {
+ assert(!Thread.isMainThread, "select identity must be called from background thread")
+
+ var selection = [RDMachine.ID]()
+ let sem = DispatchSemaphore(value: 0)
+
+ debugPrint("Picking Machine")
+
+ mainActor {
+ var panelRef: NSPanel?
+ var windowRef: NSWindow?
+ let controller = NSHostingController(rootView: Group {
+ MachinePickerView(onComplete: {
+ selection = $0
+ if let panel = panelRef {
+ if let windowRef = windowRef {
+ windowRef.endSheet(panel)
+ } else {
+ panel.close()
+ }
+ }
+ sem.signal()
+ }, allowSelectMany: allowMany)
+ .environmentObject(RayonStore.shared)
+ .frame(width: 700, height: 400)
+ })
+ let panel = NSPanel(contentViewController: controller)
+ panelRef = panel
+ panel.title = ""
+ panel.titleVisibility = .hidden
+
+ if let keyWindow = findWindow() {
+ windowRef = keyWindow
keyWindow.beginSheet(panel) { _ in }
} else {
- panel.makeKeyAndOrderFront(nil)
+ sem.signal()
}
}
sem.wait()
return selection
}
+
+ static func selectOneMachine() -> RDMachine.ID? {
+ selectMachine(allowMany: false).first
+ }
}
diff --git a/Application/Rayon/Extension/Window/CreateWindow.swift b/Application/Rayon/Extension/Window/CreateWindow.swift
deleted file mode 100644
index 1138339..0000000
--- a/Application/Rayon/Extension/Window/CreateWindow.swift
+++ /dev/null
@@ -1,55 +0,0 @@
-//
-// CreateWindow.swift
-// Rayon (macOS)
-//
-// Created by Lakr Aream on 2022/2/12.
-//
-
-import AppKit
-import SwiftUI
-
-class NSCloseProtectedWindow: NSWindow {
- var forceClose: Bool = false
-
- override func close() {
- guard !forceClose else {
- super.close()
- return
- }
- UIBridge.requiresConfirmation(
- message: "Are you sure you want to close this window?"
- ) { confirmed in
- guard confirmed else {
- return
- }
- super.close()
- }
- }
-}
-
-extension UIBridge {
- @discardableResult
- static func openNewWindow(from view: T) -> Window {
- let hostView = NSHostingView(rootView: view)
- let window = NSCloseProtectedWindow(
- contentRect: NSRect(x: 0, y: 0, width: 700, height: 400),
- styleMask: [
- .titled, .closable, .miniaturizable, .resizable, .fullSizeContentView,
- ],
- backing: .buffered,
- defer: false
- )
- window.animationBehavior = .alertPanel
- window.center()
- // Assign the toolbar to the window object
- let toolbar = NSToolbar(identifier: UUID().uuidString)
- window.toolbar = toolbar
- toolbar.insertItem(withItemIdentifier: .toggleSidebar, at: 0)
- window.toolbarStyle = .unifiedCompact
- window.titleVisibility = .visible
- window.contentView = hostView
- window.makeKeyAndOrderFront(nil)
- window.isReleasedWhenClosed = false
- return window
- }
-}
diff --git a/Application/Rayon/Extension/Window/HostWindow.swift b/Application/Rayon/Extension/Window/HostWindow.swift
deleted file mode 100644
index f716f73..0000000
--- a/Application/Rayon/Extension/Window/HostWindow.swift
+++ /dev/null
@@ -1,71 +0,0 @@
-//
-// HostWindow.swift
-// Rayon
-//
-// Created by Lakr Aream on 2022/2/11.
-//
-
-import SwiftUI
-
-class WindowObserver: ObservableObject {
- weak var window: Window?
-}
-
-// https://lostmoa.com/blog/ReadingTheCurrentWindowInANewSwiftUILifecycleApp/
-
-// extension EnvironmentValues {
-// struct IsKeyWindowKey: EnvironmentKey {
-// static var defaultValue: Bool = false
-// typealias Value = Bool
-// }
-//
-// fileprivate(set) var isKeyWindow: Bool {
-// get {
-// self[IsKeyWindowKey.self]
-// }
-// set {
-// self[IsKeyWindowKey.self] = newValue
-// }
-// }
-// }
-
-#if canImport(UIKit)
- typealias Window = UIWindow
-#elseif canImport(AppKit)
- typealias Window = NSWindow
-#else
- #error("Unsupported platform")
-#endif
-
-#if canImport(UIKit)
- struct HostingWindowFinder: UIViewRepresentable {
- var callback: (Window?) -> Void
-
- func makeUIView(context _: Context) -> UIView {
- let view = UIView()
- DispatchQueue.main.async { [weak view] in
- self.callback(view?.window)
- }
- return view
- }
-
- func updateUIView(_: UIView, context _: Context) {}
- }
-
-#elseif canImport(AppKit)
- struct HostingWindowFinder: NSViewRepresentable {
- var callback: (Window?) -> Void
-
- func makeNSView(context _: Self.Context) -> NSView {
- let view = NSView()
- DispatchQueue.main.async { [weak view] in
- self.callback(view?.window)
- }
- return view
- }
-
- func updateNSView(_: NSView, context _: Context) {}
- }
-#else
- #error("Unsupported platform")
-#endif
diff --git a/Application/Rayon/Interface/ApplicationControllers/Application/MainView.swift b/Application/Rayon/Interface/ApplicationControllers/Application/MainView.swift
index a9159d7..b7474e4 100644
--- a/Application/Rayon/Interface/ApplicationControllers/Application/MainView.swift
+++ b/Application/Rayon/Interface/ApplicationControllers/Application/MainView.swift
@@ -13,9 +13,6 @@ private var isBootstrapCompleted = false
struct MainView: View {
@EnvironmentObject var store: RayonStore
- @StateObject
- var windowObserver: WindowObserver = .init()
-
@State var openLicenseAgreementView: Bool = false
var body: some View {
@@ -45,33 +42,6 @@ struct MainView: View {
.ignoresSafeArea()
.animation(.easeInOut(duration: 0.5), value: store.globalProgressInPresent)
)
- .background(
- HostingWindowFinder { [weak windowObserver] window in
- windowObserver?.window = window
- guard let window = window else {
- return
- }
- guard !isBootstrapCompleted else {
- return
- }
- isBootstrapCompleted = true
-
- window.tabbingMode = .disallowed
- let windows = NSApplication
- .shared
- .windows
- var notKeyWindow = windows
- .filter { !$0.isKeyWindow }
- if notKeyWindow.count > 0,
- notKeyWindow.count == windows.count
- {
- // don't close them all
- notKeyWindow.removeFirst()
- }
- notKeyWindow.forEach { $0.close() }
- window.tabbingMode = .automatic
- }
- )
.toolbar {
ToolbarItem(placement: .navigation) {
Button {
diff --git a/Application/Rayon/Interface/ApplicationControllers/Application/Sidebar.swift b/Application/Rayon/Interface/ApplicationControllers/Application/Sidebar.swift
index 3b205e5..305df30 100644
--- a/Application/Rayon/Interface/ApplicationControllers/Application/Sidebar.swift
+++ b/Application/Rayon/Interface/ApplicationControllers/Application/Sidebar.swift
@@ -135,6 +135,7 @@ struct SidebarView: View {
}
.sheet(isPresented: $openServerSelector, onDismiss: nil, content: {
MachinePickerView(onComplete: { machines in
+ if machines.isEmpty { return }
for machine in machines {
terminalManager.createSession(withMachineID: machine)
}
diff --git a/Application/Rayon/Interface/ApplicationControllers/Identity/IdentityPickerView.swift b/Application/Rayon/Interface/ApplicationControllers/Identity/IdentityPickerView.swift
index 3a67585..50dc26c 100644
--- a/Application/Rayon/Interface/ApplicationControllers/Identity/IdentityPickerView.swift
+++ b/Application/Rayon/Interface/ApplicationControllers/Identity/IdentityPickerView.swift
@@ -26,11 +26,11 @@ struct IdentityPickerSheetView: View {
defer {
presentationMode.wrappedValue.dismiss()
}
- if !confirmed {
+ if confirmed {
+ onComplete(currentSelection)
+ } else {
onComplete(nil)
- return
}
- onComplete(currentSelection)
}
.sheet(isPresented: $openCreateSheet, onDismiss: nil) {
EditIdentityManager(selection: .constant(nil)) {
diff --git a/Application/Rayon/Interface/ApplicationControllers/Machine/MachinePickerView.swift b/Application/Rayon/Interface/ApplicationControllers/Machine/MachinePickerView.swift
index 95f9b23..1f82467 100644
--- a/Application/Rayon/Interface/ApplicationControllers/Machine/MachinePickerView.swift
+++ b/Application/Rayon/Interface/ApplicationControllers/Machine/MachinePickerView.swift
@@ -25,11 +25,11 @@ struct MachinePickerView: View {
) { confirmed in
var shouldDismiss = false
defer { if shouldDismiss { presentationMode.wrappedValue.dismiss() } }
- if !confirmed {
- shouldDismiss = true
- return
+ if confirmed {
+ onComplete([RDIdentity.ID](currentSelection))
+ } else {
+ onComplete([])
}
- onComplete([RDIdentity.ID](currentSelection))
shouldDismiss = true
}
}
@@ -65,6 +65,7 @@ struct MachinePickerView: View {
}
}
}
+ .frame(maxHeight: 500)
.requiresSheetFrame()
}
diff --git a/Application/Rayon/Interface/ApplicationControllers/Machine/MachineView.swift b/Application/Rayon/Interface/ApplicationControllers/Machine/MachineView.swift
index 5f8a5ea..497a052 100644
--- a/Application/Rayon/Interface/ApplicationControllers/Machine/MachineView.swift
+++ b/Application/Rayon/Interface/ApplicationControllers/Machine/MachineView.swift
@@ -15,7 +15,7 @@ struct MachineView: View {
@State var openEditSheet: Bool = false
- let redactedColor: Color = .green
+ let redactedColor: Color = .accentColor
var body: some View {
contentView
diff --git a/Application/Rayon/Interface/ApplicationControllers/PortForward/PortForwardBackend.swift b/Application/Rayon/Interface/ApplicationControllers/PortForward/PortForwardBackend.swift
new file mode 100644
index 0000000..71ae988
--- /dev/null
+++ b/Application/Rayon/Interface/ApplicationControllers/PortForward/PortForwardBackend.swift
@@ -0,0 +1,124 @@
+//
+// PortForwardBackend.swift
+// Rayon (macOS)
+//
+// Created by Lakr Aream on 2022/3/11.
+//
+
+import Combine
+import NSRemoteShell
+import RayonModule
+
+class PortForwardBackend: ObservableObject {
+ static let shared = PortForwardBackend()
+
+ private init() {}
+
+ @Published var container = [Context]()
+ struct Context: Identifiable {
+ var id = UUID()
+ let info: RDPortForward
+ let machine: RDMachine
+ let shell: NSRemoteShell
+ }
+
+ @Published var lastHint: [Context.ID: String] = [:]
+
+ func createSession(withPortForwardID pid: RDPortForward.ID) {
+ if sessionExists(withPortForwardID: pid) {
+ // multiple start
+ return
+ }
+ let portFwd = RayonStore.shared.portForwardGroup[pid]
+ guard portFwd.isValid(),
+ let mid = portFwd.usingMachine
+ else {
+ UIBridge.presentError(with: "Invalid Info")
+ return
+ }
+ let machine = RayonStore.shared.machineGroup[mid]
+ guard machine.isNotPlaceholder() else {
+ UIBridge.presentError(with: "Invalid Info")
+ return
+ }
+ let context = Context(
+ info: portFwd,
+ machine: machine,
+ shell: .init()
+ )
+ container.append(context)
+ beginLifecycle(for: context)
+ }
+
+ func putHint(for pid: RDPortForward.ID, with: String) {
+ mainActor {
+ self.lastHint[pid] = with
+ }
+ }
+
+ func beginLifecycle(for context: Context) {
+ DispatchQueue.global().async {
+ self.putHint(for: context.info.id, with: "awaiting connect")
+ context.shell
+ .setupConnectionHost(context.machine.remoteAddress)
+ .setupConnectionPort(NSNumber(value: Int(context.machine.remotePort) ?? 0))
+ .setupConnectionTimeout(6)
+ .requestConnectAndWait()
+ guard context.shell.isConnected else {
+ self.putHint(for: context.info.id, with: "failed connect")
+ return
+ }
+ guard let str = context.machine.associatedIdentity,
+ let aid = UUID(uuidString: str)
+ else {
+ self.putHint(for: context.info.id, with: "failed authenticate")
+ return
+ }
+ let identity = RayonStore.shared.identityGroup[aid]
+ guard !identity.username.isEmpty else {
+ self.putHint(for: context.info.id, with: "failed authenticate")
+ return
+ }
+ identity.callAuthenticationWith(remote: context.shell)
+ guard context.shell.isAuthenicated else {
+ self.putHint(for: context.info.id, with: "failed authenticate")
+ return
+ }
+ self.putHint(for: context.info.id, with: "forward running")
+ switch context.info.forwardOrientation {
+ case .listenRemote:
+ context.shell.createPortForward(
+ withRemotePort: NSNumber(value: context.info.bindPort),
+ withForwardTargetHost: context.info.targetHost,
+ withForwardTargetPort: NSNumber(value: context.info.targetPort)
+ ) {
+ true // we are using shell.disconnect for shutdown
+ }
+ case .listenLocal:
+ context.shell.createPortForward(
+ withLocalPort: NSNumber(value: context.info.bindPort),
+ withForwardTargetHost: context.info.targetHost,
+ withForwardTargetPort: NSNumber(value: context.info.targetPort)
+ ) {
+ true // we are using shell.disconnect for shutdown
+ }
+ }
+ self.putHint(for: context.info.id, with: "forward stopped")
+ }
+ }
+
+ func sessionExists(withPortForwardID pid: RDPortForward.ID) -> Bool {
+ container.first { $0.info.id == pid } != nil
+ }
+
+ func endSession(withPortForwardID pid: RDPortForward.ID) {
+ let index = container.firstIndex { $0.info.id == pid }
+ putHint(for: pid, with: "terminating")
+ if let index = index {
+ let context = container.remove(at: index)
+ DispatchQueue.global().async {
+ context.shell.requestDisconnectAndWait()
+ }
+ }
+ }
+}
diff --git a/Application/Rayon/Interface/ApplicationControllers/PortForward/PortForwardManager.swift b/Application/Rayon/Interface/ApplicationControllers/PortForward/PortForwardManager.swift
index 5a2715f..aa0ac78 100644
--- a/Application/Rayon/Interface/ApplicationControllers/PortForward/PortForwardManager.swift
+++ b/Application/Rayon/Interface/ApplicationControllers/PortForward/PortForwardManager.swift
@@ -1,5 +1,5 @@
//
-// PortForwardManager.swift
+// PortForwardView.swift
// Rayon (macOS)
//
// Created by Lakr Aream on 2022/3/10.
@@ -9,8 +9,235 @@ import RayonModule
import SwiftUI
struct PortForwardManager: View {
+ @EnvironmentObject var store: RayonStore
+ @StateObject var backend = PortForwardBackend.shared
+
+ var tableItems: [RDPortForward] {
+ store
+ .portForwardGroup
+ .forwards
+ .filter {
+ if searchText.count == 0 {
+ return true
+ }
+ if $0.targetHost.lowercased().contains(searchText) {
+ return true
+ }
+ if String($0.targetPort).contains(searchText) {
+ return true
+ }
+ if String($0.bindPort).contains(searchText) {
+ return true
+ }
+ return false
+ }
+ .sorted(using: sortOrder)
+ }
+
+ @State var searchText: String = ""
+ @State var selection: Set = []
+ @State var sortOrder: [KeyPathComparator] = []
+
+ var startButtonCanWork: Bool {
+ for sel in selection {
+ if !backend.sessionExists(withPortForwardID: sel) {
+ return true
+ }
+ }
+ return false
+ }
+
+ var stopButtonCanWork: Bool {
+ for sel in selection {
+ if backend.sessionExists(withPortForwardID: sel) {
+ return true
+ }
+ }
+ return false
+ }
+
var body: some View {
- Group {}
- .navigationTitle("Port Forward")
+ Group {
+ if tableItems.isEmpty {
+ Text("No Port Forward Available")
+ .expended()
+ } else {
+ table
+ }
+ }
+ .requiresFrame()
+ .toolbar {
+ ToolbarItem {
+ Button {
+ removeButtonTapped()
+ } label: {
+ Label("Remove", systemImage: "minus")
+ }
+ .keyboardShortcut(.delete, modifiers: [])
+ .disabled(selection.count == 0)
+ }
+ ToolbarItem {
+ Button {
+ duplicateButtonTapped()
+ } label: {
+ Label("Duplicate", systemImage: "plus.square.on.square")
+ }
+ .disabled(selection.count == 0)
+ }
+ ToolbarItem {
+ Button {
+ store.portForwardGroup.insert(.init())
+ } label: {
+ Label("Add", systemImage: "plus")
+ }
+ .keyboardShortcut(KeyboardShortcut(
+ .init(unicodeScalarLiteral: "n"),
+ modifiers: .command
+ ))
+ }
+ ToolbarItem {
+ Button {
+ stopPortForward()
+ } label: {
+ Label("Stop Select", systemImage: "stop.fill")
+ }
+ .disabled(selection.isEmpty)
+ .disabled(!stopButtonCanWork)
+ }
+ ToolbarItem {
+ Button {
+ startPortForward()
+ } label: {
+ Label("Open Select", systemImage: "play.fill")
+ }
+ .disabled(selection.isEmpty)
+ .disabled(!startButtonCanWork)
+ }
+ }
+ .searchable(text: $searchText)
+ .navigationTitle("Port Forward - \(store.portForwardGroup.count) available")
+ }
+
+ var table: some View {
+ Table(selection: $selection, sortOrder: $sortOrder) {
+ TableColumn("Action") { data in
+ Button {
+ if backend.sessionExists(withPortForwardID: data.id) {
+ backend.endSession(withPortForwardID: data.id)
+ } else {
+ backend.createSession(withPortForwardID: data.id)
+ }
+ } label: {
+ Text(backend.sessionExists(withPortForwardID: data.id) ? "Terminate" : "Open")
+ }
+ }
+ .width(80)
+ TableColumn("Status") { data in
+ Text(backend.lastHint[data.id] ?? "Ready")
+ }
+ TableColumn("Forward Orientation") { data in
+ Picker(selection: $store.portForwardGroup[data.id].forwardOrientation) {
+ ForEach(RDPortForward.ForwardOrientation.allCases, id: \.self) { acase in
+ Text(acase.rawValue)
+ .tag(acase)
+ }
+ } label: {
+ if data.forwardOrientation == .listenLocal {
+ Image(systemName: "arrow.right")
+ } else if data.forwardOrientation == .listenRemote {
+ Image(systemName: "arrow.left")
+ } else {
+ Image(systemName: "questionmark")
+ }
+ }
+ .disabled(backend.sessionExists(withPortForwardID: data.id))
+ }
+ TableColumn("Forward Through Machine") { data in
+ HStack {
+ Text(data.getMachineName() ?? "Not Selected")
+ Spacer()
+ Button {
+ DispatchQueue.global().async {
+ let selection = RayonUtil.selectOneMachine()
+ mainActor {
+ store.portForwardGroup[data.id].usingMachine = selection
+ }
+ }
+ } label: {
+ Text("...")
+ }
+ .disabled(backend.sessionExists(withPortForwardID: data.id))
+ }
+ }
+ TableColumn("Bind Port", value: \.bindPort) { data in
+ TextField("Bind Port", text: .init(get: {
+ String(data.bindPort)
+ }, set: { str in
+ store.portForwardGroup[data.id].bindPort = Int(str) ?? 0
+ }))
+ .disabled(backend.sessionExists(withPortForwardID: data.id))
+ }
+ .width(65)
+ TableColumn("Target Host", value: \.targetHost) { data in
+ TextField("Host Address", text: $store.portForwardGroup[data.id].targetHost)
+ }
+ TableColumn("Target Port", value: \.targetPort) { data in
+ TextField("Target Port", text: .init(get: {
+ String(data.targetPort)
+ }, set: { str in
+ store.portForwardGroup[data.id].targetPort = Int(str) ?? 0
+ }))
+ .disabled(backend.sessionExists(withPortForwardID: data.id))
+ }
+ .width(65)
+ TableColumn("Description") { data in
+ ScrollView(.horizontal, showsIndicators: false) {
+ Text(data.shortDescription())
+ }
+ }
+ } rows: {
+ ForEach(tableItems) { item in
+ TableRow(item)
+ }
+ }
+ }
+
+ func removeButtonTapped() {
+ UIBridge.requiresConfirmation(
+ message: "Are you sure you want to remove \(selection.count) items?"
+ ) { y in
+ if y {
+ for selected in selection {
+ store.portForwardGroup.delete(selected)
+ backend.endSession(withPortForwardID: selected)
+ }
+ }
+ }
+ }
+
+ func duplicateButtonTapped() {
+ UIBridge.requiresConfirmation(
+ message: "Are you sure you want to duplicate \(selection.count) items?"
+ ) { y in
+ if y {
+ for selected in selection {
+ var read = store.portForwardGroup[selected]
+ read.id = .init()
+ store.portForwardGroup.insert(read)
+ }
+ }
+ }
+ }
+
+ func startPortForward() {
+ for selected in selection {
+ backend.createSession(withPortForwardID: selected)
+ }
+ }
+
+ func stopPortForward() {
+ for selected in selection {
+ backend.endSession(withPortForwardID: selected)
+ }
}
}
diff --git a/Application/Rayon/Interface/ApplicationControllers/Snippet/SnippetView.swift b/Application/Rayon/Interface/ApplicationControllers/Snippet/SnippetView.swift
index 28069b7..a6dcf75 100644
--- a/Application/Rayon/Interface/ApplicationControllers/Snippet/SnippetView.swift
+++ b/Application/Rayon/Interface/ApplicationControllers/Snippet/SnippetView.swift
@@ -73,7 +73,6 @@ struct SnippetFloatingPanelView: View {
@EnvironmentObject var store: RayonStore
@State var openEdit: Bool = false
- @State var openServerPicker: Bool = false
var body: some View {
Group {
@@ -99,7 +98,7 @@ struct SnippetFloatingPanelView: View {
}
.foregroundColor(.accentColor)
Button {
- chooseMachine()
+ begin()
} label: {
Image(systemName: "paperplane.fill")
.frame(width: 15)
@@ -109,15 +108,6 @@ struct SnippetFloatingPanelView: View {
.sheet(isPresented: $openEdit, onDismiss: nil) {
EditSnippetSheetView(inEdit: snippet)
}
- .sheet(isPresented: $openServerPicker, onDismiss: nil, content: {
- MachinePickerView(onComplete: { machines in
- store.beginBatchScriptExecution(for: snippet, and: machines)
- }, allowSelectMany: true)
- })
- }
-
- func chooseMachine() {
- openServerPicker = true
}
func duplicateButtonTapped() {
@@ -137,10 +127,6 @@ struct SnippetFloatingPanelView: View {
}
}
- func beginExecutionOn(machines: [RDMachine.ID]) {
- debugPrint("will execute on \(machines)")
- }
-
func deleteButtonTapped() {
UIBridge.requiresConfirmation(
message: "You are about to delete this item"
@@ -149,4 +135,45 @@ struct SnippetFloatingPanelView: View {
store.snippetGroup.delete(snippet)
}
}
+
+ func begin() {
+ DispatchQueue.global().async {
+ let snippet = RayonStore.shared.snippetGroup[snippet]
+ guard snippet.code.count > 0 else {
+ return
+ }
+ let machines = RayonUtil.selectMachine()
+ guard machines.count > 0 else {
+ RayonStore.presentError("No machine was selected for execution")
+ return
+ }
+ mainActor {
+ var panelRef: NSPanel?
+ var windowRef: NSWindow?
+ let controller = NSHostingController(rootView: Group {
+ BatchSnippetExecView(snippet: snippet, machines: machines) {
+ if let panelRef = panelRef {
+ if let windowRef = windowRef {
+ windowRef.endSheet(panelRef)
+ } else {
+ panelRef.close()
+ }
+ }
+ }
+ .frame(width: 700, height: 400)
+ })
+ let panel = NSPanel(contentViewController: controller)
+ panelRef = panel
+ panel.title = ""
+ panel.titleVisibility = .hidden
+
+ if let keyWindow = RayonUtil.findWindow() {
+ windowRef = keyWindow
+ keyWindow.beginSheet(panel) { _ in }
+ } else {
+ panel.makeKeyAndOrderFront(nil)
+ }
+ }
+ }
+ }
}
diff --git a/Application/Rayon/Interface/SnippetController/BatchSnippetExecContext.swift b/Application/Rayon/Interface/SnippetController/BatchSnippetExecContext.swift
index 7867407..351b000 100644
--- a/Application/Rayon/Interface/SnippetController/BatchSnippetExecContext.swift
+++ b/Application/Rayon/Interface/SnippetController/BatchSnippetExecContext.swift
@@ -50,8 +50,9 @@ class BatchSnippetExecContext: ObservableObject {
debugPrint("\(self) \(#function)")
}
- private var completed: Bool = false
- private var shellObjects: [RDMachine.ID: NSRemoteShell] = [:]
+ var shellObjects: [RDMachine.ID: NSRemoteShell] = [:]
+ var completed: Bool = false
+
private var requiredIdentities: [RDMachine.ID: RDIdentity] = [:]
private var shellContinue: [RDMachine.ID: Bool] = [:]
private var receivedBuffer: [RDMachine.ID: String] = [:]
diff --git a/Application/Rayon/Interface/SnippetController/BatchSnippetExecView.swift b/Application/Rayon/Interface/SnippetController/BatchSnippetExecView.swift
index e6cf063..84c054d 100644
--- a/Application/Rayon/Interface/SnippetController/BatchSnippetExecView.swift
+++ b/Application/Rayon/Interface/SnippetController/BatchSnippetExecView.swift
@@ -10,17 +10,16 @@ import RayonModule
import SwiftUI
struct BatchSnippetExecView: View {
- internal init(snippet: RDSnippet, machines: [RDMachine.ID]) {
+ internal init(snippet: RDSnippet, machines: [RDMachine.ID], onComplete: @escaping () -> Void) {
self.snippet = snippet
self.machines = machines
+ self.onComplete = onComplete
_context = .init(wrappedValue: .init(snippet: snippet, machines: machines))
}
let snippet: RDSnippet
let machines: [RDMachine.ID]
-
- @StateObject
- var windowObserver: WindowObserver = .init()
+ let onComplete: () -> Void
@StateObject var store: RayonStore = .shared
@@ -33,24 +32,33 @@ struct BatchSnippetExecView: View {
}
var builder: some View {
+ SheetTemplate.makeSheet(
+ title: "Batch Exec",
+ body: AnyView(sheetBody)
+ ) { _ in
+ func doExit() {
+ onComplete()
+ DispatchQueue.global().async {
+ context.shellObjects
+ .values
+ .forEach { $0.requestDisconnectAndWait() }
+ }
+ }
+ if !context.completed {
+ UIBridge.requiresConfirmation(message: "Are you sure you want to quit?") { y in
+ if y { doExit() }
+ }
+ } else {
+ doExit()
+ }
+ }
+ }
+
+ var sheetBody: some View {
NavigationView {
BatchSnippetSidebarView()
BatchExecMainView()
}
- .background(
- HostingWindowFinder { [weak windowObserver] window in
- windowObserver?.window = window
- setWindowTitle()
- }
- )
- .onAppear {
- setWindowTitle()
- }
.requiresFrame()
}
-
- func setWindowTitle() {
- windowObserver.window?.title = "Batch Execution: \(snippet.name)"
- windowObserver.window?.subtitle = "\(machines.count) target in queue"
- }
}
diff --git a/Application/Rayon/Interface/SnippetController/BatchSnippetSidebarView.swift b/Application/Rayon/Interface/SnippetController/BatchSnippetSidebarView.swift
index 352f7b8..5d32b7a 100644
--- a/Application/Rayon/Interface/SnippetController/BatchSnippetSidebarView.swift
+++ b/Application/Rayon/Interface/SnippetController/BatchSnippetSidebarView.swift
@@ -29,7 +29,7 @@ struct BatchSnippetSidebarView: View {
}
}
}
- .listStyle(SidebarListStyle())
+ .listStyle(PlainListStyle())
.frame(minWidth: 200)
}
}
diff --git a/Application/Rayon/Interface/TerminalController/TerminalManager+Context.swift b/Application/Rayon/Interface/TerminalController/TerminalManager+Context.swift
index 2b1b0fb..9c4134a 100644
--- a/Application/Rayon/Interface/TerminalController/TerminalManager+Context.swift
+++ b/Application/Rayon/Interface/TerminalController/TerminalManager+Context.swift
@@ -102,7 +102,6 @@ extension TerminalManager {
title = machine.name
Context.queue.async {
self.processBootstrap()
- self.processShutdown()
}
}
@@ -119,7 +118,6 @@ extension TerminalManager {
remoteType = .machine
Context.queue.async {
self.processBootstrap()
- self.processShutdown()
}
}
@@ -139,6 +137,10 @@ extension TerminalManager {
}
func processBootstrap() {
+ defer {
+ mainActor { self.processShutdown(exitFromShell: true) }
+ }
+
setupShellData()
debugPrint("\(self) \(#function) \(machine.id)")
@@ -155,6 +157,9 @@ extension TerminalManager {
.setupTitleChain { [weak self] str in
self?.title = str
}
+ .setupSizeChain { [weak self] size in
+ self?.termianlSize = size
+ }
shell.requestConnectAndWait()
@@ -235,16 +240,17 @@ extension TerminalManager {
self?.continueDecision ?? false
}
- putInformation("")
- putInformation("[*] Connection Closed")
-
// leave loop
debugPrint("\(self) \(#function) defer \(machine.id)")
processShutdown()
}
- func processShutdown() {
+ func processShutdown(exitFromShell: Bool = false) {
+ if exitFromShell {
+ putInformation("")
+ putInformation("[*] Connection Closed")
+ }
continueDecision = false
Context.queue.async {
self.shell.requestDisconnectAndWait()
diff --git a/Application/Rayon/Interface/TerminalController/TerminalView.swift b/Application/Rayon/Interface/TerminalController/TerminalView.swift
index faba8a0..ca75621 100644
--- a/Application/Rayon/Interface/TerminalController/TerminalView.swift
+++ b/Application/Rayon/Interface/TerminalController/TerminalView.swift
@@ -12,24 +12,12 @@ import XTerminalUI
struct TerminalView: View {
@StateObject var context: TerminalManager.Context
@State var interfaceToken = UUID()
- @State var terminalSize: CGSize = .init(width: 80, height: 40)
var body: some View {
Group {
if context.interfaceToken == interfaceToken {
- GeometryReader { r in
- VStack {
- context.termInterface
- .onChange(of: r.size) { _ in
- guard context.interfaceToken == interfaceToken else {
- debugPrint("interface token mismatch")
- return
- }
- updateTerminalSize()
- }
- .padding(r.size.width > 600 ? 8 : 2)
- }
- }
+ context.termInterface
+ .padding(2)
} else {
Text("Terminal Transfer To Another Window")
}
@@ -98,27 +86,4 @@ struct TerminalView: View {
.animation(.spring(), value: context.interfaceDisabled)
.disabled(context.interfaceDisabled)
}
-
- func updateTerminalSize() {
- let core = context.termInterface
- let origSize = terminalSize
- DispatchQueue.global().async {
- let newSize = core.requestTerminalSize()
- guard newSize.width > 5, newSize.height > 5 else {
- debugPrint("ignoring malformed terminal size: \(newSize)")
- return
- }
- if newSize != origSize {
- mainActor {
- guard context.interfaceToken == interfaceToken else {
- debugPrint("interface token mismatch")
- return
- }
- debugPrint("new terminal size: \(newSize)")
- terminalSize = newSize
- context.shell.explicitRequestStatusPickup()
- }
- }
- }
- }
}
diff --git a/Application/Rayon/Rayon.xcodeproj/project.pbxproj b/Application/Rayon/Rayon.xcodeproj/project.pbxproj
index e0ee8c8..dbc8279 100644
--- a/Application/Rayon/Rayon.xcodeproj/project.pbxproj
+++ b/Application/Rayon/Rayon.xcodeproj/project.pbxproj
@@ -33,8 +33,6 @@
5068E44C27CE3AAE00F8B770 /* RayonModule in Frameworks */ = {isa = PBXBuildFile; productRef = 5068E44B27CE3AAE00F8B770 /* RayonModule */; };
5082A1CF27D0E0F300C71C0F /* EULA in Resources */ = {isa = PBXBuildFile; fileRef = 5082A1CD27D0E0F300C71C0F /* EULA */; };
5082A1D027D0E0F300C71C0F /* LICENSE in Resources */ = {isa = PBXBuildFile; fileRef = 5082A1CE27D0E0F300C71C0F /* LICENSE */; };
- 5091898527B80A56003C1B0F /* CreateWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5091898427B80A56003C1B0F /* CreateWindow.swift */; };
- 509820A127CDB354005FAA97 /* RayonStore+SwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 509820A027CDB354005FAA97 /* RayonStore+SwiftUI.swift */; };
509F4E5227B9502500ADAF5C /* AgreementView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 509F4E5027B9502500ADAF5C /* AgreementView.swift */; };
509F7F4127B2AF7F00F28752 /* RayonApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 509F7F2C27B2AF7E00F28752 /* RayonApp.swift */; };
509F7F4727B2AF7F00F28752 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 509F7F2F27B2AF7F00F28752 /* Assets.xcassets */; };
@@ -59,10 +57,10 @@
50A8A6AC27B850E9004F6DDC /* BatchSnippetExecContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50A8A6AA27B850E9004F6DDC /* BatchSnippetExecContext.swift */; };
50BBA54B27B7F08800CE7BB1 /* MachinePickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50BBA54927B7F08800CE7BB1 /* MachinePickerView.swift */; };
50BBA54F27B8000900CE7BB1 /* Colorful in Frameworks */ = {isa = PBXBuildFile; productRef = 50BBA54E27B8000900CE7BB1 /* Colorful */; };
+ 50DB352127DAE999002DEE72 /* PortForwardBackend.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50DB352027DAE999002DEE72 /* PortForwardBackend.swift */; };
50DDB33E27B4009800C471C0 /* IdentityPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50DDB33C27B4009800C471C0 /* IdentityPickerView.swift */; };
50F64E5A27B3CB35008D52FB /* EditIdentitiesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50F64E5827B3CB35008D52FB /* EditIdentitiesView.swift */; };
50F64E5F27B3D922008D52FB /* MachineCreateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50F64E5D27B3D922008D52FB /* MachineCreateView.swift */; };
- 50FC9AED27B66ED1005D9755 /* HostWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50FC9AEB27B66ED1005D9755 /* HostWindow.swift */; };
50FD878827D9CC3300EE3A5C /* PortForwardManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50FD878727D9CC3300EE3A5C /* PortForwardManager.swift */; };
/* End PBXBuildFile section */
@@ -101,8 +99,6 @@
5068AB2727B580C600564D1D /* Generic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Generic.swift; sourceTree = ""; };
5082A1CD27D0E0F300C71C0F /* EULA */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = EULA; path = ../../../Resources/EULA; sourceTree = ""; };
5082A1CE27D0E0F300C71C0F /* LICENSE */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = LICENSE; path = ../../../Resources/LICENSE; sourceTree = ""; };
- 5091898427B80A56003C1B0F /* CreateWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateWindow.swift; sourceTree = ""; };
- 509820A027CDB354005FAA97 /* RayonStore+SwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RayonStore+SwiftUI.swift"; sourceTree = ""; };
509F4E5027B9502500ADAF5C /* AgreementView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgreementView.swift; sourceTree = ""; };
509F7F2C27B2AF7E00F28752 /* RayonApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RayonApp.swift; sourceTree = ""; };
509F7F2F27B2AF7F00F28752 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
@@ -128,10 +124,10 @@
50A8A6A727B84EF9004F6DDC /* BatchTerminalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchTerminalView.swift; sourceTree = ""; };
50A8A6AA27B850E9004F6DDC /* BatchSnippetExecContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchSnippetExecContext.swift; sourceTree = ""; };
50BBA54927B7F08800CE7BB1 /* MachinePickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MachinePickerView.swift; sourceTree = ""; };
+ 50DB352027DAE999002DEE72 /* PortForwardBackend.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PortForwardBackend.swift; sourceTree = ""; };
50DDB33C27B4009800C471C0 /* IdentityPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentityPickerView.swift; sourceTree = ""; };
50F64E5827B3CB35008D52FB /* EditIdentitiesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditIdentitiesView.swift; sourceTree = ""; };
50F64E5D27B3D922008D52FB /* MachineCreateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MachineCreateView.swift; sourceTree = ""; };
- 50FC9AEB27B66ED1005D9755 /* HostWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HostWindow.swift; sourceTree = ""; };
50FD878727D9CC3300EE3A5C /* PortForwardManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PortForwardManager.swift; sourceTree = ""; };
/* End PBXFileReference section */
@@ -193,15 +189,6 @@
path = Menubar;
sourceTree = "";
};
- 5098209C27CDB081005FAA97 /* Window */ = {
- isa = PBXGroup;
- children = (
- 5091898427B80A56003C1B0F /* CreateWindow.swift */,
- 50FC9AEB27B66ED1005D9755 /* HostWindow.swift */,
- );
- path = Window;
- sourceTree = "";
- };
5098209D27CDB09A005FAA97 /* Utils */ = {
isa = PBXGroup;
children = (
@@ -219,14 +206,6 @@
path = View;
sourceTree = "";
};
- 5098209F27CDB34B005FAA97 /* Store */ = {
- isa = PBXGroup;
- children = (
- 509820A027CDB354005FAA97 /* RayonStore+SwiftUI.swift */,
- );
- path = Store;
- sourceTree = "";
- };
509F7F2627B2AF7E00F28752 = {
isa = PBXGroup;
children = (
@@ -289,11 +268,9 @@
509F7F7E27B2CB2D00F28752 /* Extension */ = {
isa = PBXGroup;
children = (
- 5098209F27CDB34B005FAA97 /* Store */,
509F7FA127B303E900F28752 /* UIBridge */,
5098209D27CDB09A005FAA97 /* Utils */,
5098209E27CDB0A1005FAA97 /* View */,
- 5098209C27CDB081005FAA97 /* Window */,
);
path = Extension;
sourceTree = "";
@@ -384,6 +361,7 @@
isa = PBXGroup;
children = (
50FD878727D9CC3300EE3A5C /* PortForwardManager.swift */,
+ 50DB352027DAE999002DEE72 /* PortForwardBackend.swift */,
);
path = PortForward;
sourceTree = "";
@@ -499,9 +477,7 @@
505E763127B5053B008D80F6 /* AlignedLabel.swift in Sources */,
50FD878827D9CC3300EE3A5C /* PortForwardManager.swift in Sources */,
5020A65B27D98B6B00F6EA0D /* TerminalManager.swift in Sources */,
- 5091898527B80A56003C1B0F /* CreateWindow.swift in Sources */,
509F4E5227B9502500ADAF5C /* AgreementView.swift in Sources */,
- 509820A127CDB354005FAA97 /* RayonStore+SwiftUI.swift in Sources */,
509F7F7627B2C95F00F28752 /* Sidebar.swift in Sources */,
50F64E5F27B3D922008D52FB /* MachineCreateView.swift in Sources */,
509F7F6F27B2C32900F28752 /* MainView.swift in Sources */,
@@ -512,8 +488,8 @@
509F7F7D27B2CB0D00F28752 /* WelcomeView.swift in Sources */,
509F7FB327B3893000F28752 /* GenericComparator.swift in Sources */,
509F7F9A27B2F84100F28752 /* SheetTemplate.swift in Sources */,
+ 50DB352127DAE999002DEE72 /* PortForwardBackend.swift in Sources */,
509F7FAD27B309EE00F28752 /* IdentityManager.swift in Sources */,
- 50FC9AED27B66ED1005D9755 /* HostWindow.swift in Sources */,
5027915827CE448200EB22DD /* MenubarLoader.swift in Sources */,
5020A66027D9983300F6EA0D /* TerminalManager+Context.swift in Sources */,
);
@@ -642,7 +618,7 @@
CODE_SIGN_IDENTITY = "-";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
- CURRENT_PROJECT_VERSION = 1;
+ CURRENT_PROJECT_VERSION = 2;
DEVELOPMENT_TEAM = "";
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
@@ -656,7 +632,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 12.0;
- MARKETING_VERSION = 1.7;
+ MARKETING_VERSION = 1.8;
PRODUCT_BUNDLE_IDENTIFIER = wiki.qaq.rayon.debug;
PRODUCT_NAME = Rayon;
SDKROOT = macosx;
@@ -674,7 +650,7 @@
CODE_SIGN_IDENTITY = "-";
CODE_SIGN_STYLE = Manual;
COMBINE_HIDPI_IMAGES = YES;
- CURRENT_PROJECT_VERSION = 1;
+ CURRENT_PROJECT_VERSION = 2;
DEVELOPMENT_TEAM = "";
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
@@ -688,7 +664,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 12.0;
- MARKETING_VERSION = 1.7;
+ MARKETING_VERSION = 1.8;
PRODUCT_BUNDLE_IDENTIFIER = wiki.qaq.ray0n;
PRODUCT_NAME = Rayon;
PROVISIONING_PROFILE_SPECIFIER = "";
diff --git a/Application/mRayon/mRayon.xcodeproj/project.pbxproj b/Application/mRayon/mRayon.xcodeproj/project.pbxproj
index c0a9334..dd9499f 100644
--- a/Application/mRayon/mRayon.xcodeproj/project.pbxproj
+++ b/Application/mRayon/mRayon.xcodeproj/project.pbxproj
@@ -7,7 +7,6 @@
objects = {
/* Begin PBXBuildFile section */
- 4D8DDECD27D9A5FB00E1C974 /* SwiftUIPolyfill in Frameworks */ = {isa = PBXBuildFile; productRef = 4D8DDECC27D9A5FB00E1C974 /* SwiftUIPolyfill */; };
500D726827CF1BC000149A43 /* MachineElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 500D726727CF1BC000149A43 /* MachineElement.swift */; };
500D726A27CF1C0800149A43 /* View.swift in Sources */ = {isa = PBXBuildFile; fileRef = 500D726927CF1C0800149A43 /* View.swift */; };
500D726D27CF269700149A43 /* PlaceholderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 500D726C27CF269700149A43 /* PlaceholderView.swift */; };
@@ -56,6 +55,7 @@
50D3AEFB27D05D6D0085F899 /* EditMachineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50D3AEFA27D05D6D0085F899 /* EditMachineView.swift */; };
50D3AEFD27D063A30085F899 /* PickIdentityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50D3AEFC27D063A30085F899 /* PickIdentityView.swift */; };
50D77BE727D2F0FB00177190 /* SettingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50D77BE627D2F0FB00177190 /* SettingView.swift */; };
+ 50DB351B27DA0B3A002DEE72 /* SwiftUIPolyfill in Frameworks */ = {isa = PBXBuildFile; productRef = 50DB351A27DA0B3A002DEE72 /* SwiftUIPolyfill */; };
50FD853A27D0445D00F6F01A /* EditSnippetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50FD853927D0445D00F6F01A /* EditSnippetView.swift */; };
/* End PBXBuildFile section */
@@ -117,7 +117,7 @@
505E326727D2626900054573 /* MachineStatus in Frameworks */,
501FF79D27CEFC6700380D59 /* Colorful in Frameworks */,
505E326927D2642700054573 /* MachineStatusView in Frameworks */,
- 4D8DDECD27D9A5FB00E1C974 /* SwiftUIPolyfill in Frameworks */,
+ 50DB351B27DA0B3A002DEE72 /* SwiftUIPolyfill in Frameworks */,
501FF7A127CEFC9900380D59 /* CodeMirrorUI in Frameworks */,
501FF79B27CEFC6300380D59 /* RayonModule in Frameworks */,
);
@@ -329,7 +329,7 @@
505E326227D2602E00054573 /* XTerminalUI */,
505E326627D2626900054573 /* MachineStatus */,
505E326827D2642700054573 /* MachineStatusView */,
- 4D8DDECC27D9A5FB00E1C974 /* SwiftUIPolyfill */,
+ 50DB351A27DA0B3A002DEE72 /* SwiftUIPolyfill */,
);
productName = mRayon;
productReference = 501FF78727CEFADD00380D59 /* mRayon.app */;
@@ -482,7 +482,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 14.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 15.2;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
@@ -537,7 +537,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 14.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 15.2;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = iphoneos;
@@ -576,7 +576,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
- MARKETING_VERSION = 1.7;
+ MARKETING_VERSION = 1.8;
PRODUCT_BUNDLE_IDENTIFIER = wiki.qaq.mRayon;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "QAQ Wiki iOS Wildcard";
@@ -617,7 +617,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
- MARKETING_VERSION = 1.7;
+ MARKETING_VERSION = 1.8;
PRODUCT_BUNDLE_IDENTIFIER = wiki.qaq.ray0n;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "QAQ Wiki iOS Distribution Wildcard";
@@ -653,10 +653,6 @@
/* End XCConfigurationList section */
/* Begin XCSwiftPackageProductDependency section */
- 4D8DDECC27D9A5FB00E1C974 /* SwiftUIPolyfill */ = {
- isa = XCSwiftPackageProductDependency;
- productName = SwiftUIPolyfill;
- };
501FF79A27CEFC6300380D59 /* RayonModule */ = {
isa = XCSwiftPackageProductDependency;
productName = RayonModule;
@@ -685,6 +681,10 @@
isa = XCSwiftPackageProductDependency;
productName = MachineStatusView;
};
+ 50DB351A27DA0B3A002DEE72 /* SwiftUIPolyfill */ = {
+ isa = XCSwiftPackageProductDependency;
+ productName = SwiftUIPolyfill;
+ };
/* End XCSwiftPackageProductDependency section */
};
rootObject = 501FF77F27CEFADD00380D59 /* Project object */;
diff --git a/Application/mRayon/mRayon/Interface/Generic/AgreementView.swift b/Application/mRayon/mRayon/Interface/Generic/AgreementView.swift
index d060b10..d864f7a 100644
--- a/Application/mRayon/mRayon/Interface/Generic/AgreementView.swift
+++ b/Application/mRayon/mRayon/Interface/Generic/AgreementView.swift
@@ -19,14 +19,14 @@ struct AgreementView: View {
// .navigationViewStyle(StackNavigationViewStyle())
}
- struct AddButtonStyle : ViewModifier {
+ struct AddButtonStyle: ViewModifier {
func body(content: Content) -> some View {
if #available(iOS 15.0, *) {
content.buttonStyle(.borderedProminent)
}
}
}
-
+
var contentView: some View {
ScrollView {
VStack(alignment: .leading, spacing: 5) {
@@ -51,7 +51,7 @@ struct AgreementView: View {
.bold()
.frame(width: 250)
}
- .modifier(AddButtonStyle())
+ .modifier(AddButtonStyle())
Spacer()
}
}
diff --git a/Application/mRayon/mRayon/Interface/Navigator/SidebarView.swift b/Application/mRayon/mRayon/Interface/Navigator/SidebarView.swift
index 1ffda7f..33022c6 100644
--- a/Application/mRayon/mRayon/Interface/Navigator/SidebarView.swift
+++ b/Application/mRayon/mRayon/Interface/Navigator/SidebarView.swift
@@ -124,7 +124,8 @@ struct SidebarView: View {
action: {
monitorManager.end(for: context.id)
},
- tint: .red)
+ tint: .red
+ ),
])
}
}
@@ -156,7 +157,8 @@ struct SidebarView: View {
action: {
terminalManager.end(for: context.id)
},
- tint: .red)
+ tint: .red
+ ),
])
}
}
@@ -197,7 +199,8 @@ struct SidebarView: View {
action: {
delete()
},
- tint: .red)
+ tint: .red
+ ),
])
.contextMenu {
Button {
@@ -241,7 +244,8 @@ struct SidebarView: View {
action: {
delete()
},
- tint: .red)
+ tint: .red
+ ),
])
.contextMenu {
Button {
diff --git a/Application/mRayon/mRayon/Interface/Navigator/WelcomeView.swift b/Application/mRayon/mRayon/Interface/Navigator/WelcomeView.swift
index 96caf35..8533d4d 100644
--- a/Application/mRayon/mRayon/Interface/Navigator/WelcomeView.swift
+++ b/Application/mRayon/mRayon/Interface/Navigator/WelcomeView.swift
@@ -10,10 +10,10 @@ import RayonModule
import SwiftUI
@available(iOS 15.0, *)
-struct WelcomeViewModifier15 : ViewModifier {
- let parent : WelcomeView
- @FocusState var textFieldIsFocused : Bool
-
+struct WelcomeViewModifier15: ViewModifier {
+ let parent: WelcomeView
+ @FocusState var textFieldIsFocused: Bool
+
func body(content: Content) -> some View {
content
.onSubmit {
@@ -27,6 +27,7 @@ struct WelcomeViewModifier15 : ViewModifier {
}
})
}
+
init(parent: WelcomeView) {
self.parent = parent
}
@@ -36,7 +37,7 @@ struct WelcomeView: View {
@EnvironmentObject var store: RayonStore
@State public var quickConnect: String = ""
-
+
@State var buttonDisabled: Bool = true
@State var suggestion: String? = nil
diff --git a/Application/mRayon/mRayon/Interface/Snippet/EditSnippetView.swift b/Application/mRayon/mRayon/Interface/Snippet/EditSnippetView.swift
index 8b133b4..276c8c3 100644
--- a/Application/mRayon/mRayon/Interface/Snippet/EditSnippetView.swift
+++ b/Application/mRayon/mRayon/Interface/Snippet/EditSnippetView.swift
@@ -8,8 +8,8 @@
import CodeMirrorUI
import RayonModule
import SwiftUI
-import SymbolPicker
import SwiftUIPolyfill
+import SymbolPicker
struct EditSnippetView: View {
@Environment(\.presentationMode) var presentationMode
diff --git a/Application/mRayon/mRayon/Interface/Terminal/TerminalView.swift b/Application/mRayon/mRayon/Interface/Terminal/TerminalView.swift
index 3ca418a..3f4ed62 100644
--- a/Application/mRayon/mRayon/Interface/Terminal/TerminalView.swift
+++ b/Application/mRayon/mRayon/Interface/Terminal/TerminalView.swift
@@ -75,17 +75,17 @@ struct TerminalView: View {
TextField("Key To Send", text: $controlKey, onCommit: {
sendCtrl()
})
- .disableAutocorrection(true)
- .textInputAutocapitalization(.never)
- .onChange(of: controlKey) { newValue in
- guard let f = newValue.uppercased().last else {
- if !controlKey.isEmpty { controlKey = "" }
- return
- }
- if controlKey != String(f) {
- controlKey = String(f)
- }
+ .disableAutocorrection(true)
+ .textInputAutocapitalization(.never)
+ .onChange(of: controlKey) { newValue in
+ guard let f = newValue.uppercased().last else {
+ if !controlKey.isEmpty { controlKey = "" }
+ return
}
+ if controlKey != String(f) {
+ controlKey = String(f)
+ }
+ }
Button {
sendCtrl()
} label: {
diff --git a/External/NSRemoteShell/Sources/NSRemoteShell/NSLocalForward.h b/External/NSRemoteShell/Sources/NSRemoteShell/NSLocalForward.h
index b066756..6e9db39 100644
--- a/External/NSRemoteShell/Sources/NSRemoteShell/NSLocalForward.h
+++ b/External/NSRemoteShell/Sources/NSRemoteShell/NSLocalForward.h
@@ -8,7 +8,7 @@
#import "GenericHeaders.h"
#import "GenericNetworking.h"
#import "NSRemoteShell.h"
-#import "NSRemoteChannleSocketPair.h"
+#import "NSRemoteChannelSocketPair.h"
NS_ASSUME_NONNULL_BEGIN
diff --git a/External/NSRemoteShell/Sources/NSRemoteShell/NSLocalForward.m b/External/NSRemoteShell/Sources/NSRemoteShell/NSLocalForward.m
index 590ea92..cd6126e 100644
--- a/External/NSRemoteShell/Sources/NSRemoteShell/NSLocalForward.m
+++ b/External/NSRemoteShell/Sources/NSRemoteShell/NSLocalForward.m
@@ -77,7 +77,7 @@ - (void)uncheckedConcurrencyCallNonblockingOperations {
- (void)uncheckedConcurrencyProcessAllSocket {
NSMutableArray *newArray = [[NSMutableArray alloc] init];
- for (NSRemoteChannleSocketPair *pair in self.forwardSocketPair) {
+ for (NSRemoteChannelSocketPair *pair in self.forwardSocketPair) {
if (![pair uncheckedConcurrencyInsanityCheckAndReturnDidSuccess]) {
[pair uncheckedConcurrencyDisconnectAndPrepareForRelease];
continue;
@@ -129,9 +129,8 @@ - (void)uncheckedConcurrencyChannelMainSocketAccept {
return;
}
NSLog(@"created channel for forward socket %d %p", forwardsock, channel);
- NSRemoteChannleSocketPair *pair = [[NSRemoteChannleSocketPair alloc] initWithSocket:forwardsock
- withChannel:channel
- withTimeout:self.timeout];
+ NSRemoteChannelSocketPair *pair = [[NSRemoteChannelSocketPair alloc] initWithSocket:forwardsock
+ withChannel:channel];
[self.forwardSocketPair addObject:pair];
}
}
@@ -164,7 +163,7 @@ - (void)uncheckedConcurrencyDisconnectAndPrepareForRelease {
[GenericNetworking destroyNativeSocket:socket];
self.representedSession = NULL;
self.representedSocket = NULL;
- for (NSRemoteChannleSocketPair *pair in self.forwardSocketPair) {
+ for (NSRemoteChannelSocketPair *pair in self.forwardSocketPair) {
[pair uncheckedConcurrencyDisconnectAndPrepareForRelease];
}
self.forwardSocketPair = [[NSMutableArray alloc] init];
diff --git a/External/NSRemoteShell/Sources/NSRemoteShell/NSRemoteChannleSocketPair.h b/External/NSRemoteShell/Sources/NSRemoteShell/NSRemoteChannelSocketPair.h
similarity index 55%
rename from External/NSRemoteShell/Sources/NSRemoteShell/NSRemoteChannleSocketPair.h
rename to External/NSRemoteShell/Sources/NSRemoteShell/NSRemoteChannelSocketPair.h
index 68fdf11..5a647c6 100644
--- a/External/NSRemoteShell/Sources/NSRemoteShell/NSRemoteChannleSocketPair.h
+++ b/External/NSRemoteShell/Sources/NSRemoteShell/NSRemoteChannelSocketPair.h
@@ -1,5 +1,5 @@
//
-// NSRemoteChannleSocketPair.h
+// NSRemoteChannelSocketPair.h
//
//
// Created by Lakr Aream on 2022/3/9.
@@ -11,17 +11,14 @@
NS_ASSUME_NONNULL_BEGIN
-@interface NSRemoteChannleSocketPair : NSObject
+@interface NSRemoteChannelSocketPair : NSObject
@property (nonatomic, readwrite, assign) int socket;
@property (nonatomic, readwrite, nullable, assign) LIBSSH2_CHANNEL *channel;
@property (nonatomic, readwrite, assign) BOOL completed;
-@property (nonatomic, readwrite, nonnull, strong) NSDate *lastDataAvailable;
-@property (nonatomic, readwrite, nonnull, strong) NSNumber *timeout;
- (instancetype)initWithSocket:(int)socket
- withChannel:(LIBSSH2_CHANNEL*)channel
- withTimeout:(NSNumber*)timeout;
+ withChannel:(LIBSSH2_CHANNEL*)channel;
@end
diff --git a/External/NSRemoteShell/Sources/NSRemoteShell/NSRemoteChannleSocketPair.m b/External/NSRemoteShell/Sources/NSRemoteShell/NSRemoteChannelSocketPair.m
similarity index 82%
rename from External/NSRemoteShell/Sources/NSRemoteShell/NSRemoteChannleSocketPair.m
rename to External/NSRemoteShell/Sources/NSRemoteShell/NSRemoteChannelSocketPair.m
index ba45f4c..5f197a4 100644
--- a/External/NSRemoteShell/Sources/NSRemoteShell/NSRemoteChannleSocketPair.m
+++ b/External/NSRemoteShell/Sources/NSRemoteShell/NSRemoteChannelSocketPair.m
@@ -1,25 +1,22 @@
//
-// NSRemoteChannleSocketPair.m
+// NSRemoteChannelSocketPair.m
//
//
// Created by Lakr Aream on 2022/3/9.
//
-#import "NSRemoteChannleSocketPair.h"
+#import "NSRemoteChannelSocketPair.h"
-@implementation NSRemoteChannleSocketPair
+@implementation NSRemoteChannelSocketPair
- (instancetype)initWithSocket:(int)socket
withChannel:(LIBSSH2_CHANNEL*)channel
- withTimeout:(NSNumber*)timeout
{
self = [super init];
if (self) {
_socket = socket;
_channel = channel;
_completed = NO;
- _lastDataAvailable = [[NSDate alloc] init];
- _timeout = timeout;
}
return self;
}
@@ -48,7 +45,6 @@ - (void)uncheckedConcurrencyProcessReadWrite {
}
wr += i;
}
- self.lastDataAvailable = [[NSDate alloc] init];
}
} while (0);
do {
@@ -63,13 +59,14 @@ - (void)uncheckedConcurrencyProcessReadWrite {
if (i <= 0) { self.completed = YES; return; }
wr += i;
}
- self.lastDataAvailable = [[NSDate alloc] init];
} else if (len != LIBSSH2_ERROR_EAGAIN) {
NSLog(@"libssh2_channel_read returns failure %ld", len);
self.completed = YES;
return;
}
} while (0);
+ // connection may send 0 tcp packet data but still keep alive
+ // so only check eof
}
- (void)setCompleted:(BOOL)completed {
@@ -90,10 +87,6 @@ - (BOOL)uncheckedConcurrencyInsanityCheckAndReturnDidSuccess {
if (self.completed) { break; }
if (![self seatbeltCheckPassed]) { break; }
if (libssh2_channel_eof(self.channel)) { break; }
- if ([self.timeout doubleValue] > 0) {
- NSDate *terminate = [self.lastDataAvailable dateByAddingTimeInterval:[self.timeout doubleValue]];
- if ([terminate timeIntervalSinceNow] < 0) { break; }
- }
return YES;
} while (0);
return NO;
diff --git a/External/NSRemoteShell/Sources/NSRemoteShell/NSRemoteForward.h b/External/NSRemoteShell/Sources/NSRemoteShell/NSRemoteForward.h
index 12557b7..b42f7b6 100644
--- a/External/NSRemoteShell/Sources/NSRemoteShell/NSRemoteForward.h
+++ b/External/NSRemoteShell/Sources/NSRemoteShell/NSRemoteForward.h
@@ -8,7 +8,7 @@
#import "GenericHeaders.h"
#import "GenericNetworking.h"
#import "NSRemoteShell.h"
-#import "NSRemoteChannleSocketPair.h"
+#import "NSRemoteChannelSocketPair.h"
NS_ASSUME_NONNULL_BEGIN
diff --git a/External/NSRemoteShell/Sources/NSRemoteShell/NSRemoteForward.m b/External/NSRemoteShell/Sources/NSRemoteShell/NSRemoteForward.m
index 0f1ddea..c04c28d 100644
--- a/External/NSRemoteShell/Sources/NSRemoteShell/NSRemoteForward.m
+++ b/External/NSRemoteShell/Sources/NSRemoteShell/NSRemoteForward.m
@@ -90,15 +90,14 @@ - (void)uncheckedConcurrencyListenerAccept {
LIBSSH2_CHANNEL_SHUTDOWN(channel);
return;
}
- NSRemoteChannleSocketPair *pair = [[NSRemoteChannleSocketPair alloc] initWithSocket:socket
- withChannel:channel
- withTimeout:self.timeout];
+ NSRemoteChannelSocketPair *pair = [[NSRemoteChannelSocketPair alloc] initWithSocket:socket
+ withChannel:channel];
[self.forwardSocketPair addObject:pair];
}
- (void)uncheckedConcurrencyProcessAllSocket {
NSMutableArray *newArray = [[NSMutableArray alloc] init];
- for (NSRemoteChannleSocketPair *pair in self.forwardSocketPair) {
+ for (NSRemoteChannelSocketPair *pair in self.forwardSocketPair) {
if (![pair uncheckedConcurrencyInsanityCheckAndReturnDidSuccess]) {
[pair uncheckedConcurrencyDisconnectAndPrepareForRelease];
continue;
@@ -141,7 +140,7 @@ - (void)uncheckedConcurrencyDisconnectAndPrepareForRelease {
LIBSSH2_LISTENER *listener = self.representedListener;
self.representedListener = NULL;
while (libssh2_channel_forward_cancel(listener) == LIBSSH2_ERROR_EAGAIN) {};
- for (NSRemoteChannleSocketPair *pair in self.forwardSocketPair) {
+ for (NSRemoteChannelSocketPair *pair in self.forwardSocketPair) {
[pair uncheckedConcurrencyDisconnectAndPrepareForRelease];
}
self.forwardSocketPair = [[NSMutableArray alloc] init];
diff --git a/External/XTerminalUI/Sources/XTerminalUI/XTerminalUI+AppKit.swift b/External/XTerminalUI/Sources/XTerminalUI/XTerminalUI+AppKit.swift
index 3a11ddc..fcfb1b8 100644
--- a/External/XTerminalUI/Sources/XTerminalUI/XTerminalUI+AppKit.swift
+++ b/External/XTerminalUI/Sources/XTerminalUI/XTerminalUI+AppKit.swift
@@ -41,6 +41,12 @@ import Foundation
associatedCore.setupBellChain(callback: callback)
return self
}
+
+ @discardableResult
+ public func setupSizeChain(callback: ((CGSize) -> Void)?) -> Self {
+ associatedCore.setupSizeChain(callback: callback)
+ return self
+ }
public func write(_ str: String) {
associatedCore.write(str)
diff --git a/External/XTerminalUI/Sources/XTerminalUI/XTerminalUI+SwiftUI.swift b/External/XTerminalUI/Sources/XTerminalUI/XTerminalUI+SwiftUI.swift
index 88b7120..c9a5521 100644
--- a/External/XTerminalUI/Sources/XTerminalUI/XTerminalUI+SwiftUI.swift
+++ b/External/XTerminalUI/Sources/XTerminalUI/XTerminalUI+SwiftUI.swift
@@ -38,6 +38,12 @@ import SwiftUI
correspondingView.setupBellChain(callback: callback)
return self
}
+
+ @discardableResult
+ public func setupSizeChain(callback: ((CGSize) -> Void)?) -> Self {
+ correspondingView.setupSizeChain(callback: callback)
+ return self
+ }
public func write(_ str: String) {
correspondingView.write(str)
diff --git a/External/XTerminalUI/Sources/XTerminalUI/XTerminalUI+UIKit.swift b/External/XTerminalUI/Sources/XTerminalUI/XTerminalUI+UIKit.swift
index 39d07fc..736ff00 100644
--- a/External/XTerminalUI/Sources/XTerminalUI/XTerminalUI+UIKit.swift
+++ b/External/XTerminalUI/Sources/XTerminalUI/XTerminalUI+UIKit.swift
@@ -39,7 +39,13 @@
associatedCore.setupBellChain(callback: callback)
return self
}
-
+
+ @discardableResult
+ public func setupSizeChain(callback: ((CGSize) -> Void)?) -> Self {
+ associatedCore.setupSizeChain(callback: callback)
+ return self
+ }
+
public func write(_ str: String) {
associatedCore.write(str)
}
diff --git a/External/XTerminalUI/Sources/XTerminalUI/XTerminalUI.swift b/External/XTerminalUI/Sources/XTerminalUI/XTerminalUI.swift
index f7879d5..9c5208b 100644
--- a/External/XTerminalUI/Sources/XTerminalUI/XTerminalUI.swift
+++ b/External/XTerminalUI/Sources/XTerminalUI/XTerminalUI.swift
@@ -27,6 +27,9 @@ protocol XTerminal {
@discardableResult
func setupBellChain(callback: (() -> Void)?) -> Self
+
+ @discardableResult
+ func setupSizeChain(callback: ((CGSize) -> Void)?) -> Self
func write(_ str: String)
@@ -106,6 +109,12 @@ class XTerminalCore: XTerminal {
associatedScriptDelegate.onBellChain = callback
return self
}
+
+ @discardableResult
+ func setupSizeChain(callback: ((CGSize) -> Void)?) -> Self {
+ associatedScriptDelegate.onSizeChain = callback
+ return self
+ }
var writeBuffer: [Data] = []
let lock = NSLock()
@@ -120,8 +129,8 @@ class XTerminalCore: XTerminal {
lock.lock()
writeBuffer.append(data)
lock.unlock()
- DispatchQueue.global().async { [weak self] in
- self?.writeData()
+ DispatchQueue.global().async {
+ self.writeData()
}
}
@@ -129,13 +138,22 @@ class XTerminalCore: XTerminal {
guard writeLock.try() else {
return
}
- defer { writeLock.unlock() }
+ defer {
+ writeLock.unlock()
+ lock.lock()
+ let recall = !writeBuffer.isEmpty
+ lock.unlock()
+ if recall {
+ // to avoid stack overflow
+ DispatchQueue.global().async {
+ self.writeData()
+ }
+ }
+ }
// wait for the webview to load
while !associatedWebDelegate.navigateCompleted { usleep(1000) }
- let webView = associatedWebView
-
lock.lock()
let copy = writeBuffer
writeBuffer = []
@@ -143,10 +161,15 @@ class XTerminalCore: XTerminal {
let writes = copy
.map { $0.base64EncodedString() }
- for write in writes {
+ scriptBridgeWrite(writes, to: associatedWebView)
+ }
+
+ func scriptBridgeWrite(_ base64Array: [String], to webView: WKWebView) {
+ assert(!Thread.isMainThread)
+ for write in base64Array {
var attempt = 0
var success: Bool = false
- while (!success && attempt < 5) {
+ while !success, attempt < 5 {
attempt += 1
let sem = DispatchSemaphore(value: 0)
DispatchQueue.main.async {
@@ -161,7 +184,7 @@ class XTerminalCore: XTerminal {
sem.signal()
}
}
- let _ = sem.wait(timeout: .now() + 1)
+ _ = sem.wait(timeout: .now() + 1)
usleep(1000)
}
}
diff --git a/External/XTerminalUI/Sources/XTerminalUI/XTerminalWebScriptHandler.swift b/External/XTerminalUI/Sources/XTerminalUI/XTerminalWebScriptHandler.swift
index 72c697f..6074282 100644
--- a/External/XTerminalUI/Sources/XTerminalUI/XTerminalWebScriptHandler.swift
+++ b/External/XTerminalUI/Sources/XTerminalUI/XTerminalWebScriptHandler.swift
@@ -12,6 +12,7 @@ class XTerminalWebScriptHandler: NSObject, WKScriptMessageHandler {
var onBellChain: (() -> Void)?
var onTitleChain: ((String) -> Void)?
var onDataChain: ((String) -> Void)?
+ var onSizeChain: ((CGSize) -> Void)?
func userContentController(
_: WKUserContentController,
@@ -30,16 +31,34 @@ class XTerminalWebScriptHandler: NSObject, WKScriptMessageHandler {
onTitleChain?(msg)
case "data":
onDataChain?(msg)
+ case "size":
+ if let size = ResizeData.fromString(msg) {
+ onSizeChain?(size)
+ }
default:
debugPrint("unrecognized message magic")
debugPrint(message.body)
}
}
+ struct ResizeData: Codable {
+ var cols: Int
+ var rows: Int
+ static func fromString(_ str: String) -> CGSize? {
+ if let data = str.data(using: .utf8),
+ let dec = try? JSONDecoder().decode(ResizeData.self, from: data)
+ {
+ return .init(width: dec.cols, height: dec.rows)
+ }
+ return nil
+ }
+ }
+
deinit {
debugPrint("\(self) __deinit__")
onBellChain = nil
onDataChain = nil
onTitleChain = nil
+ onSizeChain = nil
}
}
diff --git a/External/XTerminalUI/Sources/XTerminalUI/XTerminalWebViewDelegate.swift b/External/XTerminalUI/Sources/XTerminalUI/XTerminalWebViewDelegate.swift
index d42ee02..488b3a2 100644
--- a/External/XTerminalUI/Sources/XTerminalUI/XTerminalWebViewDelegate.swift
+++ b/External/XTerminalUI/Sources/XTerminalUI/XTerminalWebViewDelegate.swift
@@ -23,6 +23,16 @@ class XTerminalWebViewDelegate: NSObject, WKNavigationDelegate, WKUIDelegate {
// the buffer chain will that holds a retain to shell
// to fool the release logic for disconnect and cleanup
debugPrint("\(self) __deinit__")
- userContentController?.removeScriptMessageHandler(forName: "callbackHandler")
+ #if DEBUG
+ if Thread.isMainThread {
+ userContentController?.removeScriptMessageHandler(forName: "callbackHandler")
+ } else {
+ fatalError("Malformed dispatch")
+ }
+ #else
+ if Thread.isMainThread {
+ userContentController?.removeScriptMessageHandler(forName: "callbackHandler")
+ }
+ #endif
}
}
diff --git a/External/XTerminalUI/Sources/XTerminalUI/xterm/assets/index.6d29077b.js b/External/XTerminalUI/Sources/XTerminalUI/xterm/assets/index.6d29077b.js
new file mode 100644
index 0000000..61a6fee
--- /dev/null
+++ b/External/XTerminalUI/Sources/XTerminalUI/xterm/assets/index.6d29077b.js
@@ -0,0 +1 @@
+var g=Object.defineProperty,w=Object.defineProperties;var p=Object.getOwnPropertyDescriptors;var d=Object.getOwnPropertySymbols;var h=Object.prototype.hasOwnProperty,b=Object.prototype.propertyIsEnumerable;var m=(t,e,o)=>e in t?g(t,e,{enumerable:!0,configurable:!0,writable:!0,value:o}):t[e]=o,i=(t,e)=>{for(var o in e||(e={}))h.call(e,o)&&m(t,o,e[o]);if(d)for(var o of d(e))b.call(e,o)&&m(t,o,e[o]);return t},c=(t,e)=>w(t,p(e));import{x as u,a as f,b as k,d as y}from"./vendor.79a29ec7.js";const x=function(){const e=document.createElement("link").relList;if(e&&e.supports&&e.supports("modulepreload"))return;for(const n of document.querySelectorAll('link[rel="modulepreload"]'))r(n);new MutationObserver(n=>{for(const s of n)if(s.type==="childList")for(const a of s.addedNodes)a.tagName==="LINK"&&a.rel==="modulepreload"&&r(a)}).observe(document,{childList:!0,subtree:!0});function o(n){const s={};return n.integrity&&(s.integrity=n.integrity),n.referrerpolicy&&(s.referrerPolicy=n.referrerpolicy),n.crossorigin==="use-credentials"?s.credentials="include":n.crossorigin==="anonymous"?s.credentials="omit":s.credentials="same-origin",s}function r(n){if(n.ep)return;n.ep=!0;const s=o(n);fetch(n.href,s)}};x();function l(t,e){e?t.options.theme=c(i({},u.exports.MaterialDark),{background:"#00000000"}):t.options.theme=c(i({},u.exports.Material),{background:"#FFFFFF00"})}function H(t){return new Uint8Array(atob(t).split("").map(M))}function M(t){return t.charCodeAt(0)}function T(){const t=document.getElementById("terminal"),e=new f.exports.Terminal({allowTransparency:!0,theme:{background:"transparent"},rendererType:"dom"}),o=new k.exports.FitAddon;return e.loadAddon(o),e.open(t),o.fit(),new ResizeObserver(y(()=>{var s;o.fit();const r={cols:e.cols,rows:e.rows},n={magic:"size",msg:JSON.stringify(r)};console.log("resize",n),(s=window.webkit)==null||s.messageHandlers.callbackHandler.postMessage(n)},100)).observe(t),e.focus(),e.onTitleChange(r=>{var s;const n={magic:"title",msg:r};(s=window.webkit)==null||s.messageHandlers.callbackHandler.postMessage(n)}),e.onData(r=>{var s;const n={magic:"data",msg:r};(s=window.webkit)==null||s.messageHandlers.callbackHandler.postMessage(n)}),e}f.exports.Terminal.prototype.writeBase64=function(t){this.write(H(t))};function A(){const t=T();window.fit=()=>{},window.term=t,window.terminal=t,window.send=e=>{var r;const o={magic:"command",msg:e};(r=window.webkit)==null||r.messageHandlers.callbackHandler.postMessage(o)},l(t,window.matchMedia("(prefers-color-scheme: dark)").matches),window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change",e=>{e.matches?l(t,!0):l(t,!1)}),setTimeout(function(){var o;const e={magic:"bell",msg:"null"};(o=window.webkit)==null||o.messageHandlers.callbackHandler.postMessage(e)},1e3)}A();
diff --git a/External/XTerminalUI/Sources/XTerminalUI/xterm/assets/index.94116c96.js b/External/XTerminalUI/Sources/XTerminalUI/xterm/assets/index.94116c96.js
deleted file mode 100644
index a85abf8..0000000
--- a/External/XTerminalUI/Sources/XTerminalUI/xterm/assets/index.94116c96.js
+++ /dev/null
@@ -1 +0,0 @@
-var g=Object.defineProperty,p=Object.defineProperties;var w=Object.getOwnPropertyDescriptors;var d=Object.getOwnPropertySymbols;var h=Object.prototype.hasOwnProperty,b=Object.prototype.propertyIsEnumerable;var m=(t,e,n)=>e in t?g(t,e,{enumerable:!0,configurable:!0,writable:!0,value:n}):t[e]=n,i=(t,e)=>{for(var n in e||(e={}))h.call(e,n)&&m(t,n,e[n]);if(d)for(var n of d(e))b.call(e,n)&&m(t,n,e[n]);return t},c=(t,e)=>p(t,w(e));import{x as u,a as f,b as y,d as k}from"./vendor.79a29ec7.js";const x=function(){const e=document.createElement("link").relList;if(e&&e.supports&&e.supports("modulepreload"))return;for(const r of document.querySelectorAll('link[rel="modulepreload"]'))s(r);new MutationObserver(r=>{for(const o of r)if(o.type==="childList")for(const a of o.addedNodes)a.tagName==="LINK"&&a.rel==="modulepreload"&&s(a)}).observe(document,{childList:!0,subtree:!0});function n(r){const o={};return r.integrity&&(o.integrity=r.integrity),r.referrerpolicy&&(o.referrerPolicy=r.referrerpolicy),r.crossorigin==="use-credentials"?o.credentials="include":r.crossorigin==="anonymous"?o.credentials="omit":o.credentials="same-origin",o}function s(r){if(r.ep)return;r.ep=!0;const o=n(r);fetch(r.href,o)}};x();function l(t,e){e?t.options.theme=c(i({},u.exports.MaterialDark),{background:"#00000000"}):t.options.theme=c(i({},u.exports.Material),{background:"#FFFFFF00"})}function T(t){return new Uint8Array(atob(t).split("").map(A))}function A(t){return t.charCodeAt(0)}function F(){const t=document.getElementById("terminal"),e=new f.exports.Terminal({allowTransparency:!0,theme:{background:"transparent"},rendererType:"dom"}),n=new y.exports.FitAddon;return e.loadAddon(n),e.open(t),n.fit(),new ResizeObserver(k(()=>{n.fit(),console.log("resize")},100)).observe(t),e.focus(),e.onTitleChange(s=>{var o;const r={magic:"title",msg:s};(o=window.webkit)==null||o.messageHandlers.callbackHandler.postMessage(r)}),e.onData(s=>{var o;const r={magic:"data",msg:s};(o=window.webkit)==null||o.messageHandlers.callbackHandler.postMessage(r)}),e}f.exports.Terminal.prototype.writeBase64=function(t){this.write(T(t))};function M(){const t=F();window.fit=()=>{},window.term=t,window.terminal=t,window.send=e=>{var s;const n={magic:"command",msg:e};(s=window.webkit)==null||s.messageHandlers.callbackHandler.postMessage(n)},l(t,window.matchMedia("(prefers-color-scheme: dark)").matches),window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change",e=>{e.matches?l(t,!0):l(t,!1)}),setTimeout(function(){var n;const e={magic:"bell",msg:"null"};(n=window.webkit)==null||n.messageHandlers.callbackHandler.postMessage(e)},1e3)}M();
diff --git a/External/XTerminalUI/Sources/XTerminalUI/xterm/index.html b/External/XTerminalUI/Sources/XTerminalUI/xterm/index.html
index fd3cd7a..233133c 100644
--- a/External/XTerminalUI/Sources/XTerminalUI/xterm/index.html
+++ b/External/XTerminalUI/Sources/XTerminalUI/xterm/index.html
@@ -29,7 +29,7 @@
-webkit-user-select: none;
}
-
+
diff --git a/Foundation/RayonModule/Sources/RayonModule/PortForward/RDPortForward+Group.swift b/Foundation/RayonModule/Sources/RayonModule/PortForward/RDPortForward+Group.swift
index b4e9eb5..ea1ff8c 100644
--- a/Foundation/RayonModule/Sources/RayonModule/PortForward/RDPortForward+Group.swift
+++ b/Foundation/RayonModule/Sources/RayonModule/PortForward/RDPortForward+Group.swift
@@ -23,7 +23,6 @@ public struct RDPortForwardGroup: Codable, Identifiable, Equatable {
}
public mutating func insert(_ value: AssociatedType) {
- guard !value.name.isEmpty else { return }
if let index = forwards.firstIndex(where: { $0.id == value.id }) {
forwards[index] = value
} else {
diff --git a/Foundation/RayonModule/Sources/RayonModule/PortForward/RDPortForward.swift b/Foundation/RayonModule/Sources/RayonModule/PortForward/RDPortForward.swift
index 6110542..62e77e5 100644
--- a/Foundation/RayonModule/Sources/RayonModule/PortForward/RDPortForward.swift
+++ b/Foundation/RayonModule/Sources/RayonModule/PortForward/RDPortForward.swift
@@ -11,7 +11,6 @@ import NSRemoteShell
public struct RDPortForward: Codable, Identifiable, Equatable {
public init(
id: UUID = .init(),
- name: String = "",
forwardOrientation: ForwardOrientation = .listenLocal,
bindPort: Int = 0,
targetHost: String = "",
@@ -20,7 +19,6 @@ public struct RDPortForward: Codable, Identifiable, Equatable {
attachment: [String: String] = [:]
) {
self.id = id
- self.name = name
self.forwardOrientation = forwardOrientation
self.bindPort = bindPort
self.targetHost = targetHost
@@ -31,15 +29,25 @@ public struct RDPortForward: Codable, Identifiable, Equatable {
public var id: UUID
- public var name: String
-
- public enum ForwardOrientation: String, Codable {
- case listenLocal
- case listenRemote
+ public enum ForwardOrientation: String, Codable, CaseIterable {
+ case listenLocal = "Local"
+ case listenRemote = "Remote"
}
public var forwardOrientation: ForwardOrientation = .listenLocal
+ public var forwardReversed: Bool {
+ get {
+ forwardOrientation == .listenRemote
+ }
+ set {
+ switch newValue {
+ case true: forwardOrientation = .listenRemote
+ case false: forwardOrientation = .listenLocal
+ }
+ }
+ }
+
public var bindPort: Int // UInt32 maybe?
public var targetHost: String
@@ -49,7 +57,7 @@ public struct RDPortForward: Codable, Identifiable, Equatable {
public var attachment: [String: String]
- func isValid() -> Bool {
+ public func isValid() -> Bool {
guard bindPort >= 0, bindPort <= 65535,
!targetHost.isEmpty,
targetPort >= 0, targetPort <= 65535,
@@ -60,4 +68,27 @@ public struct RDPortForward: Codable, Identifiable, Equatable {
}
return true
}
+
+ public func getMachineName() -> String? {
+ guard let mid = usingMachine else {
+ return nil
+ }
+ let machine = RayonStore.shared.machineGroup[mid]
+ if machine.isNotPlaceholder() {
+ return machine.name
+ }
+ return nil
+ }
+
+ public func shortDescription() -> String {
+ guard isValid() else {
+ return "Invalid"
+ }
+ switch forwardOrientation {
+ case .listenLocal:
+ return "localhost:\(bindPort) --ssh-tunnel-\(getMachineName() ?? "Unknown")-> \(targetHost):\(targetPort)"
+ case .listenRemote:
+ return "\(getMachineName() ?? "Unknown"):\(bindPort) --ssh-tunnel-localhost-> \(targetHost):\(targetPort)"
+ }
+ }
}
diff --git a/Foundation/RayonModule/SwiftUIPolyfill b/Foundation/RayonModule/SwiftUIPolyfill
deleted file mode 160000
index dac3ea2..0000000
--- a/Foundation/RayonModule/SwiftUIPolyfill
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit dac3ea279fe1204171ded1b153f7692a7abb41b0
diff --git a/Foundation/SwiftUIPolyfill/Package.swift b/Foundation/SwiftUIPolyfill/Package.swift
index dcdf8b5..94162c0 100644
--- a/Foundation/SwiftUIPolyfill/Package.swift
+++ b/Foundation/SwiftUIPolyfill/Package.swift
@@ -6,9 +6,7 @@ import PackageDescription
let package = Package(
name: "SwiftUIPolyfill",
platforms: [
- .macOS(.v11),
.iOS(.v14),
- .watchOS(.v7),
],
products: [
.library(
diff --git a/Foundation/SwiftUIPolyfill/Sources/SwiftUIPolyfill/SwiftUIPolyfill.swift b/Foundation/SwiftUIPolyfill/Sources/SwiftUIPolyfill/SwiftUIPolyfill.swift
index 769eb18..31c8acc 100644
--- a/Foundation/SwiftUIPolyfill/Sources/SwiftUIPolyfill/SwiftUIPolyfill.swift
+++ b/Foundation/SwiftUIPolyfill/Sources/SwiftUIPolyfill/SwiftUIPolyfill.swift
@@ -1,31 +1,30 @@
import Foundation
import SwiftUI
-//@available(iOS, introduced:14.0, obsoleted:15.0)
-//@available(tvOS, unavailable)
-//@available(watchOS, unavailable)
-//public protocol TextSelectability {
+// @available(iOS, introduced:14.0, obsoleted:15.0)
+// @available(tvOS, unavailable)
+// @available(watchOS, unavailable)
+// public protocol TextSelectability {
// static var allowsSelection: Bool { get }
-//}
+// }
//
-//@available(iOS, introduced:14.0, obsoleted:15.0)
-//@available(tvOS, unavailable)
-//@available(watchOS, unavailable)
-//extension View {
+// @available(iOS, introduced:14.0, obsoleted:15.0)
+// @available(tvOS, unavailable)
+// @available(watchOS, unavailable)
+// extension View {
// public func textSelection(_ selectability: S) -> some View where S : TextSelectability {
// return self
// }
-//}
+// }
-@available(iOS, introduced:14.0, obsoleted:15.0)
+@available(iOS, introduced: 14.0, obsoleted: 15.0)
extension Date {
public struct FormatStylePolyfill {
-
/// The locale to use when formatting date and time values.
public var date: Date.FormatStylePolyfill.DateStyle?
-
+
public var time: Date.FormatStylePolyfill.TimeStyle?
-
+
/// The locale to use when formatting date and time values.
public var locale: Locale
@@ -44,7 +43,7 @@ extension Date {
/// - timeZone: The time zone with which to specify date and time values.
/// - capitalizationContext: The capitalization formatting context used when formatting date and time values.
/// - Note: Always specify the date length, time length, or the date components to be included in the formatted string with the symbol modifiers. Otherwise, an empty string will be returned when you use the instance to format a `Date`.
- public init(date: Date.FormatStylePolyfill.DateStyle? = nil, time: Date.FormatStylePolyfill.TimeStyle? = nil, locale: Locale = .autoupdatingCurrent, calendar: Calendar = .autoupdatingCurrent, timeZone: TimeZone = .autoupdatingCurrent, capitalizationContext: Any? = nil) {
+ public init(date: Date.FormatStylePolyfill.DateStyle? = nil, time: Date.FormatStylePolyfill.TimeStyle? = nil, locale: Locale = .autoupdatingCurrent, calendar: Calendar = .autoupdatingCurrent, timeZone: TimeZone = .autoupdatingCurrent, capitalizationContext _: Any? = nil) {
self.date = date
self.time = time
self.locale = locale
@@ -54,11 +53,10 @@ extension Date {
}
}
-@available(iOS, introduced:14.0, obsoleted:15.0)
+@available(iOS, introduced: 14.0, obsoleted: 15.0)
extension Date.FormatStylePolyfill {
-
/// Predefined date styles varied in lengths or the components included. The exact format depends on the locale.
- public struct DateStyle : Codable, Hashable {
+ public struct DateStyle: Codable, Hashable {
/// Excludes the date part.
public static let omitted = Date.FormatStylePolyfill.DateStyle(style: "omitted")
@@ -74,8 +72,7 @@ extension Date.FormatStylePolyfill {
/// Shows the complete day. For example, "Wednesday, October 21, 2015".
public static let complete = Date.FormatStylePolyfill.DateStyle(style: "complete")
-
- public let style : String
+ public let style: String
public init(style: String) {
self.style = style
@@ -83,8 +80,7 @@ extension Date.FormatStylePolyfill {
}
/// Predefined time styles varied in lengths or the components included. The exact format depends on the locale.
- public struct TimeStyle : Codable, Hashable {
-
+ public struct TimeStyle: Codable, Hashable {
/// Excludes the time part.
public static let omitted = Date.FormatStylePolyfill.TimeStyle(style: "omitted")
@@ -97,25 +93,24 @@ extension Date.FormatStylePolyfill {
/// For example, `4:29:24 PM PDT`, `16:29:24 GMT`.
public static let complete = Date.FormatStylePolyfill.TimeStyle(style: "complete")
- public let style : String
+ public let style: String
public init(style: String) {
self.style = style
}
}
-
}
-@available(iOS, introduced:14.0, obsoleted:15.0)
+@available(iOS, introduced: 14.0, obsoleted: 15.0)
extension Date {
public func formatted() -> String {
let formatter = DateFormatter()
return formatter.string(from: self)
}
-
+
public func formatted(date: Date.FormatStylePolyfill.DateStyle, time: Date.FormatStylePolyfill.TimeStyle) -> String {
let formatter = DateFormatter()
- switch (date.style) {
+ switch date.style {
case "complete":
formatter.dateStyle = .full
case "long":
@@ -129,8 +124,8 @@ extension Date {
default:
formatter.dateStyle = .full
}
-
- switch (time.style) {
+
+ switch time.style {
case "complete":
formatter.timeStyle = .full
case "standard":
@@ -144,17 +139,15 @@ extension Date {
}
return formatter.string(from: self)
}
-
}
-
public struct CopyableText: View {
@State var text: String
-
+
public init(_ text: String) {
self.text = text
}
-
+
public var body: some View {
if #available(iOS 15.0, *) {
Text(self.text)
@@ -170,11 +163,9 @@ public struct CopyableText: View {
}
}
-
-@available(iOS, introduced:14.0, obsoleted:15.0)
+@available(iOS, introduced: 14.0, obsoleted: 15.0)
@available(macOS, unavailable)
public struct TextInputAutocapitalization {
-
/// Defines an autocapitalizing behavior that will not capitalize anything.
public static var never = TextInputAutocapitalization("never")
@@ -188,19 +179,19 @@ public struct TextInputAutocapitalization {
/// Defines an autocapitalizing behavior that will capitalize every letter.
public static var characters = TextInputAutocapitalization("characters")
-
+
public var style: String
public init(_ style: String) {
self.style = style
}
}
-@available(iOS, introduced:14.0, obsoleted:15.0)
+@available(iOS, introduced: 14.0, obsoleted: 15.0)
@available(macOS, unavailable)
extension View {
public func textInputAutocapitalization(_ autocapitalization: TextInputAutocapitalization?) -> some View {
let _style = autocapitalization?.style
- switch (_style) {
+ switch _style {
case "never":
return self.autocapitalization(.none)
case "words":
@@ -215,9 +206,8 @@ extension View {
}
}
-@available(iOS, introduced:14.0, obsoleted:15.0)
+@available(iOS, introduced: 14.0, obsoleted: 15.0)
public struct InterfaceOrientation {
-
public static let portrait = InterfaceOrientation()
public static let portraitUpsideDown = InterfaceOrientation()
@@ -226,12 +216,10 @@ public struct InterfaceOrientation {
public static let landscapeRight = InterfaceOrientation()
- public init() {
-
- }
+ public init() {}
}
-@available(iOS, introduced:14.0, obsoleted:15.0)
+@available(iOS, introduced: 14.0, obsoleted: 15.0)
extension View {
/// Overrides the orientation of the preview.
///
@@ -249,24 +237,22 @@ extension View {
///
/// - Parameter value: An orientation to use for preview.
/// - Returns: A preview that uses the given orientation.
- public func previewInterfaceOrientation(_ value: InterfaceOrientation) -> some View {
- return self
+ public func previewInterfaceOrientation(_: InterfaceOrientation) -> some View {
+ self
}
-
}
-@available(iOS, introduced:14.0, obsoleted:15.0)
-extension Section where Parent == Text, Content : View, Footer == EmptyView {
- public init(_ title: S, @ViewBuilder content: () -> Content) where S : StringProtocol {
+@available(iOS, introduced: 14.0, obsoleted: 15.0)
+extension Section where Parent == Text, Content: View, Footer == EmptyView {
+ public init(_ title: S, @ViewBuilder content: () -> Content) where S: StringProtocol {
self.init(content: content, header: {
Text(title)
})
}
}
-@available(iOS, introduced:14.0, obsoleted:15.0)
+@available(iOS, introduced: 14.0, obsoleted: 15.0)
public struct SearchFieldPlacement {
-
public static let automatic = SearchFieldPlacement()
@available(tvOS, unavailable)
@@ -276,28 +262,25 @@ public struct SearchFieldPlacement {
@available(watchOS, unavailable)
public static let sidebar = SearchFieldPlacement()
- public init() {
-
- }
+ public init() {}
}
-@available(iOS, introduced:14.0, obsoleted:15.0)
+@available(iOS, introduced: 14.0, obsoleted: 15.0)
extension View {
- public func searchable(text: Binding, placement: SearchFieldPlacement = .automatic, prompt: Text? = nil) -> some View {
- return self
+ public func searchable(text _: Binding, placement _: SearchFieldPlacement = .automatic, prompt _: Text? = nil) -> some View {
+ self
}
}
-@available(iOS, introduced:14.0, obsoleted:15.0)
+@available(iOS, introduced: 14.0, obsoleted: 15.0)
extension View {
- @inlinable public func overlay(alignment: Alignment = .center, @ViewBuilder content: () -> V) -> some View where V : View {
- return self.overlay(content(), alignment: alignment)
+ @inlinable public func overlay(alignment: Alignment = .center, @ViewBuilder content: () -> V) -> some View where V: View {
+ overlay(content(), alignment: alignment)
}
}
-@available(iOS, introduced:14.0, obsoleted:15.0)
+@available(iOS, introduced: 14.0, obsoleted: 15.0)
extension PrimitiveButtonStyle {
-
/// The default button style, based on the button's context.
///
/// If you create a button directly on a blank canvas, the style varies by
@@ -311,9 +294,7 @@ extension PrimitiveButtonStyle {
/// You can override a button's style. To apply the default style to a
/// button, or to a view that contains buttons, use the
/// ``View/buttonStyle(_:)-66fbx`` modifier.
- public static var bordered : DefaultButtonStyle {
- get {
- return DefaultButtonStyle.automatic
- }
+ public static var bordered: DefaultButtonStyle {
+ DefaultButtonStyle.automatic
}
}
diff --git a/Foundation/SwiftUIPolyfill/Sources/SwiftUIPolyfill/SwipeActions.swift b/Foundation/SwiftUIPolyfill/Sources/SwiftUIPolyfill/SwipeActions.swift
index 5d52725..51efc57 100644
--- a/Foundation/SwiftUIPolyfill/Sources/SwiftUIPolyfill/SwipeActions.swift
+++ b/Foundation/SwiftUIPolyfill/Sources/SwiftUIPolyfill/SwipeActions.swift
@@ -4,23 +4,24 @@ import SwiftUI
// Button in swipe action, renders text or image and can have background color
public struct SwipeActionButton: View, Identifiable {
static let width: CGFloat = 70
-
+
public let id = UUID()
let text: String
let icon: String
let action: () -> Void
let tint: Color?
-
+
public init(text: String,
- icon: String,
- action: @escaping () -> Void,
- tint: Color? = nil) {
+ icon: String,
+ action: @escaping () -> Void,
+ tint: Color? = nil)
+ {
self.text = text
self.icon = icon
self.action = action
self.tint = tint ?? .gray
}
-
+
public var body: some View {
ZStack {
tint
@@ -38,28 +39,29 @@ public struct SwipeActionButton: View, Identifiable {
// Adds custom swipe actions to a given view
@available(iOS 13.0, *)
struct SwipeActionView: ViewModifier {
- // How much does the user have to swipe at least to reveal buttons on either side
+ // How much does the user have to swipe at least to reveal buttons on either side
private static let minSwipeableWidth = SwipeActionButton.width * 0.8
-
- // Buttons at the leading (left-hand) side
+
+ // Buttons at the leading (left-hand) side
let leading: [SwipeActionButton]
- // Can you full swipe the leading side
+ // Can you full swipe the leading side
let allowsFullSwipeLeading: Bool
- // Buttons at the trailing (right-hand) side
+ // Buttons at the trailing (right-hand) side
let trailing: [SwipeActionButton]
- // Can you full swipe the trailing side
+ // Can you full swipe the trailing side
let allowsFullSwipeTrailing: Bool
-
+
private let totalLeadingWidth: CGFloat!
private let totalTrailingWidth: CGFloat!
-
+
@State private var offset: CGFloat = 0
@State private var prevOffset: CGFloat = 0
-
+
init(leading: [SwipeActionButton] = [],
allowsFullSwipeLeading: Bool = false,
trailing: [SwipeActionButton] = [],
- allowsFullSwipeTrailing: Bool = false) {
+ allowsFullSwipeTrailing: Bool = false)
+ {
self.leading = leading
self.allowsFullSwipeLeading = allowsFullSwipeLeading && !leading.isEmpty
self.trailing = trailing
@@ -67,15 +69,14 @@ struct SwipeActionView: ViewModifier {
totalLeadingWidth = SwipeActionButton.width * CGFloat(leading.count)
totalTrailingWidth = SwipeActionButton.width * CGFloat(trailing.count)
}
-
+
func body(content: Content) -> some View {
- // Use a GeometryReader to get the size of the view on which we're adding
- // the custom swipe actions.
+ // Use a GeometryReader to get the size of the view on which we're adding
+ // the custom swipe actions.
GeometryReader { geo in
// Place leading buttons, the wrapped content and trailing buttons
// in an HStack with no spacing.
HStack(spacing: 0) {
-
// If any swiping on the left-hand side has occurred, reveal
// leading buttons. This also resolves button flickering.
if offset > 0 {
@@ -85,8 +86,8 @@ struct SwipeActionView: ViewModifier {
button(for: leading.first)
.frame(width: offset, height: geo.size.height)
} else {
- // If we aren't in a full swipe, render all buttons with widths
- // proportional to the swipe length.
+ // If we aren't in a full swipe, render all buttons with widths
+ // proportional to the swipe length.
ForEach(leading) { actionView in
button(for: actionView)
.frame(width: individualButtonWidth(edge: .leading),
@@ -94,27 +95,27 @@ struct SwipeActionView: ViewModifier {
}
}
}
-
- // This is the list row itself
+
+ // This is the list row itself
content
// Add horizontal padding as we removed it to allow the
// swipe buttons to occupy full row height.
.padding(.horizontal, 16)
.frame(width: geo.size.width, height: geo.size.height, alignment: .leading)
.offset(x: (offset > 0) ? 0 : offset)
-
+
// If any swiping on the right-hand side has occurred, reveal
// trailing buttons. This also resolves button flickering.
if offset < 0 {
Group {
- // If the user has swiped enough for it to qualify as a full swipe,
- // render just the last button across the entire swipe length.
+ // If the user has swiped enough for it to qualify as a full swipe,
+ // render just the last button across the entire swipe length.
if fullSwipeEnabled(edge: .trailing, width: geo.size.width) {
button(for: trailing.last)
.frame(width: -offset, height: geo.size.height)
} else {
- // If we aren't in a full swipe, render all buttons with widths
- // proportional to the swipe length.
+ // If we aren't in a full swipe, render all buttons with widths
+ // proportional to the swipe length.
ForEach(trailing) { actionView in
button(for: actionView)
.frame(width: individualButtonWidth(edge: .trailing),
@@ -135,50 +136,50 @@ struct SwipeActionView: ViewModifier {
// prevent the gesture from interfering with List vertical scrolling.
.gesture(DragGesture(minimumDistance: 10,
coordinateSpace: .local)
- .onChanged { gesture in
- // Compute the total swipe based on the gesture values.
- var total = gesture.translation.width + prevOffset
- if !allowsFullSwipeLeading {
- total = min(total, totalLeadingWidth)
- }
- if !allowsFullSwipeTrailing {
- total = max(total, -totalTrailingWidth)
- }
- offset = total
- }
- .onEnded { _ in
- // Adjust the offset based on if the user has swiped enough to reveal
- // all the buttons or not. Also handles full swipe logic.
- if offset > SwipeActionView.minSwipeableWidth && !leading.isEmpty {
- if !checkAndHandleFullSwipe(for: leading, edge: .leading, width: geo.size.width) {
- offset = totalLeadingWidth
- }
- } else if offset < -SwipeActionView.minSwipeableWidth && !trailing.isEmpty {
- if !checkAndHandleFullSwipe(for: trailing, edge: .trailing, width: -geo.size.width) {
- offset = -totalTrailingWidth
+ .onChanged { gesture in
+ // Compute the total swipe based on the gesture values.
+ var total = gesture.translation.width + prevOffset
+ if !allowsFullSwipeLeading {
+ total = min(total, totalLeadingWidth)
+ }
+ if !allowsFullSwipeTrailing {
+ total = max(total, -totalTrailingWidth)
+ }
+ offset = total
}
- } else {
- offset = 0
- }
- prevOffset = offset
- })
+ .onEnded { _ in
+ // Adjust the offset based on if the user has swiped enough to reveal
+ // all the buttons or not. Also handles full swipe logic.
+ if offset > SwipeActionView.minSwipeableWidth, !leading.isEmpty {
+ if !checkAndHandleFullSwipe(for: leading, edge: .leading, width: geo.size.width) {
+ offset = totalLeadingWidth
+ }
+ } else if offset < -SwipeActionView.minSwipeableWidth, !trailing.isEmpty {
+ if !checkAndHandleFullSwipe(for: trailing, edge: .trailing, width: -geo.size.width) {
+ offset = -totalTrailingWidth
+ }
+ } else {
+ offset = 0
+ }
+ prevOffset = offset
+ })
}
- // Remove internal row padding to allow the buttons to occupy full row height
+ // Remove internal row padding to allow the buttons to occupy full row height
.listRowInsets(EdgeInsets())
}
-
+
// Checks if full swipe is supported and currently active for the given edge.
// The current threshold is at half of the row width.
private func fullSwipeEnabled(edge: Edge, width: CGFloat) -> Bool {
let threshold = abs(width) / 2
- switch (edge) {
+ switch edge {
case .leading:
return allowsFullSwipeLeading && offset > threshold
case .trailing:
return allowsFullSwipeTrailing && -offset > threshold
}
}
-
+
// Creates the view for each SwipeActionButton. Also assigns it
// a tap gesture to handle the click and reset the offset.
private func button(for button: SwipeActionButton?) -> some View {
@@ -189,8 +190,8 @@ struct SwipeActionView: ViewModifier {
prevOffset = 0
}
}
-
- // Calculates width for each button, proportional to the swipe.
+
+ // Calculates width for each button, proportional to the swipe.
private func individualButtonWidth(edge: Edge) -> CGFloat {
switch edge {
case .leading:
@@ -199,13 +200,14 @@ struct SwipeActionView: ViewModifier {
return (offset < 0) ? (abs(offset) / CGFloat(trailing.count)) : 0
}
}
-
- // Checks if the view is in full swipe. If so, trigger the action on the
- // correct button (left- or right-most one), make it full the entire row
- // and schedule everything to be reset after a while.
+
+ // Checks if the view is in full swipe. If so, trigger the action on the
+ // correct button (left- or right-most one), make it full the entire row
+ // and schedule everything to be reset after a while.
private func checkAndHandleFullSwipe(for collection: [SwipeActionButton],
edge: Edge,
- width: CGFloat) -> Bool {
+ width: CGFloat) -> Bool
+ {
if fullSwipeEnabled(edge: edge, width: width) {
offset = width * CGFloat(collection.count) * 1.2
((edge == .leading) ? collection.first : collection.last)?.action()
@@ -218,20 +220,19 @@ struct SwipeActionView: ViewModifier {
return false
}
}
-
+
private enum Edge {
case leading, trailing
}
}
-
@available(iOS 15.0, *)
-struct SwipeActionModifier15 : ViewModifier {
+struct SwipeActionModifier15: ViewModifier {
let leading: [SwipeActionButton]
let allowsFullSwipeLeading: Bool
let trailing: [SwipeActionButton]
let allowsFullSwipeTrailing: Bool
-
+
func body(content: Content) -> some View {
ForEach(leading) { button in
content.swipeActions(edge: .leading, allowsFullSwipe: allowsFullSwipeLeading) {
@@ -253,12 +254,13 @@ struct SwipeActionModifier15 : ViewModifier {
.tint(button.tint)
}
}
-
}
+
init(leading: [SwipeActionButton] = [],
allowsFullSwipeLeading: Bool = false,
trailing: [SwipeActionButton] = [],
- allowsFullSwipeTrailing: Bool = false) {
+ allowsFullSwipeTrailing: Bool = false)
+ {
self.leading = leading
self.allowsFullSwipeLeading = allowsFullSwipeLeading && !leading.isEmpty
self.trailing = trailing
@@ -266,19 +268,20 @@ struct SwipeActionModifier15 : ViewModifier {
}
}
-extension View {
+public extension View {
@ViewBuilder
- public func swipeActions(leading: [SwipeActionButton] = [],
+ func swipeActions(leading: [SwipeActionButton] = [],
allowsFullSwipeLeading: Bool = false,
trailing: [SwipeActionButton] = [],
- allowsFullSwipeTrailing: Bool = false) -> some View {
+ allowsFullSwipeTrailing: Bool = false) -> some View
+ {
if #available(iOS 15.0, *) {
self.modifier(SwipeActionModifier15(leading: leading,
- allowsFullSwipeLeading: allowsFullSwipeLeading,
- trailing: trailing,
- allowsFullSwipeTrailing: allowsFullSwipeTrailing))
+ allowsFullSwipeLeading: allowsFullSwipeLeading,
+ trailing: trailing,
+ allowsFullSwipeTrailing: allowsFullSwipeTrailing))
} else {
- self.modifier(SwipeActionView(leading: leading,
+ modifier(SwipeActionView(leading: leading,
allowsFullSwipeLeading: allowsFullSwipeLeading,
trailing: trailing,
allowsFullSwipeTrailing: allowsFullSwipeTrailing))
@@ -286,8 +289,7 @@ extension View {
}
}
-
-//struct CustomSwipeActionTest: View {
+// struct CustomSwipeActionTest: View {
// var body: some View {
// List(1..<20) {
// Text("List view item at row \($0)")
@@ -314,4 +316,4 @@ extension View {
// allowsFullSwipeTrailing: true)
// }
// }
-//}
+// }
diff --git a/Foundation/SwiftUIPolyfill/Tests/SwiftUIPolyfillTests/SwiftUIPolyfillTests.swift b/Foundation/SwiftUIPolyfill/Tests/SwiftUIPolyfillTests/SwiftUIPolyfillTests.swift
deleted file mode 100644
index 97dd93a..0000000
--- a/Foundation/SwiftUIPolyfill/Tests/SwiftUIPolyfillTests/SwiftUIPolyfillTests.swift
+++ /dev/null
@@ -1,11 +0,0 @@
-import XCTest
-@testable import SwiftUIPolyfill
-
-final class SwiftUIPolyfillTests: XCTestCase {
- func testExample() throws {
- // This is an example of a functional test case.
- // Use XCTAssert and related functions to verify your tests produce the correct
- // results.
- XCTAssertEqual(SwiftUIPolyfill().text, "Hello, World!")
- }
-}
diff --git a/Foundation/XMLCoder/Sources/XMLCoder/Auxiliaries/KeyedStorage.swift b/Foundation/XMLCoder/Sources/XMLCoder/Auxiliaries/KeyedStorage.swift
index d128a48..b3bfd0b 100644
Binary files a/Foundation/XMLCoder/Sources/XMLCoder/Auxiliaries/KeyedStorage.swift and b/Foundation/XMLCoder/Sources/XMLCoder/Auxiliaries/KeyedStorage.swift differ