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