diff options
author | Navan Chauhan <navanchauhan@gmail.com> | 2024-07-27 17:33:53 -0600 |
---|---|---|
committer | Navan Chauhan <navanchauhan@gmail.com> | 2024-07-27 17:33:53 -0600 |
commit | a6fd12f4562bfa0247c483b4b0ffabb6e7f64957 (patch) | |
tree | bc4bbf2c4f4e9e0e19fa26b4ebf733a8cafda3f0 |
-rw-r--r-- | .gitignore | 8 | ||||
-rw-r--r-- | Package.resolved | 78 | ||||
-rw-r--r-- | Package.swift | 27 | ||||
-rw-r--r-- | Sources/iGopherBrowserGTK/BrowserView.swift | 179 | ||||
-rw-r--r-- | Sources/iGopherBrowserGTK/ToolbarView.swift | 38 | ||||
-rw-r--r-- | Sources/iGopherBrowserGTK/iGopherBrowserGTK.swift | 21 | ||||
-rw-r--r-- | Tests/iGopherBrowserGTKTests/iGopherBrowserGTKTests.swift | 12 |
7 files changed, 363 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0023a53 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/Package.resolved b/Package.resolved new file mode 100644 index 0000000..c0ef7fd --- /dev/null +++ b/Package.resolved @@ -0,0 +1,78 @@ +{ + "originHash" : "0b8a8a31d2e41ebf9bd9991cc9065d0de7fbdbd08bbf046bb2b353e6f2e45fb2", + "pins" : [ + { + "identity" : "adwaita", + "kind" : "remoteSourceControl", + "location" : "https://github.com/AparokshaUI/Adwaita", + "state" : { + "revision" : "c82957e2398a766458ff22129db26b46de75248b", + "version" : "0.2.6" + } + }, + { + "identity" : "levenshteintransformations", + "kind" : "remoteSourceControl", + "location" : "https://github.com/david-swift/LevenshteinTransformations", + "state" : { + "revision" : "c5665914a26d3f96e1b724acc7254b344acf4d05", + "version" : "0.1.2" + } + }, + { + "identity" : "swift-atomics", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-atomics.git", + "state" : { + "revision" : "cd142fd2f64be2100422d658e7411e39489da985", + "version" : "1.2.0" + } + }, + { + "identity" : "swift-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-collections.git", + "state" : { + "revision" : "3d2dc41a01f9e49d84f0a3925fb858bed64f702d", + "version" : "1.1.2" + } + }, + { + "identity" : "swift-gopher", + "kind" : "remoteSourceControl", + "location" : "https://github.com/navanchauhan/swift-gopher.git", + "state" : { + "branch" : "master", + "revision" : "b1d4c766f4bdd0f5c37d72941a73b4f8b6a3272c" + } + }, + { + "identity" : "swift-nio", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio", + "state" : { + "revision" : "e4abde8be0e49dc7d66e6eed651254accdcd9533", + "version" : "2.69.0" + } + }, + { + "identity" : "swift-nio-transport-services", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-transport-services.git", + "state" : { + "revision" : "38ac8221dd20674682148d6451367f89c2652980", + "version" : "1.21.0" + } + }, + { + "identity" : "swift-system", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-system.git", + "state" : { + "revision" : "d2ba781702a1d8285419c15ee62fd734a9437ff5", + "version" : "1.3.2" + } + } + ], + "version" : 3 +} diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..b567f78 --- /dev/null +++ b/Package.swift @@ -0,0 +1,27 @@ +// swift-tools-version: 5.10 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "iGopherBrowserGTK", + platforms: [ + .macOS("10.15") + ], + dependencies: [ + .package(url: "https://github.com/AparokshaUI/Adwaita", from: "0.2.0"), + .package(url: "https://github.com/navanchauhan/swift-gopher.git", branch: "master") + ], + targets: [ + // Targets are the basic building blocks of a package, defining a module or a test suite. + // Targets can depend on other targets in this package and products from dependencies. + .executableTarget( + name: "iGopherBrowserGTK", dependencies: [ + .product(name: "Adwaita", package: "Adwaita"), + .product(name: "SwiftGopherClient", package: "swift-gopher") + ]), + .testTarget( + name: "iGopherBrowserGTKTests", + dependencies: ["iGopherBrowserGTK"]), + ] +) diff --git a/Sources/iGopherBrowserGTK/BrowserView.swift b/Sources/iGopherBrowserGTK/BrowserView.swift new file mode 100644 index 0000000..dce6d85 --- /dev/null +++ b/Sources/iGopherBrowserGTK/BrowserView.swift @@ -0,0 +1,179 @@ +// +// BrowserView.swift +// +// +// Created by Navan Chauhan on 7/27/24. +// + +import Adwaita +import Foundation +import GopherHelpers +import swiftGopherClient + +public func getHostAndPort( + from urlString: String, defaultPort: Int = 70, defaultHost: String = "gopher.navan.dev" +) -> (host: String, port: Int, selector: String) { + if let urlComponents = URLComponents(string: urlString), + let host = urlComponents.host + { + let port = urlComponents.port ?? defaultPort + let selector = urlComponents.path + return (host, port, selector) + } else { + // Fallback for simpler formats like "localhost:8080" + let components = urlString.split(separator: ":") + let host = components.first.map(String.init) ?? defaultHost + + var port = (components.count > 1 ? Int(components[1]) : nil) ?? defaultPort + var selector = "/" + + if components.count > 1 { + let portCompString = components[1] + let portCompComponents = portCompString.split(separator: "/", maxSplits: 1) + if portCompComponents.count > 1 { + port = Int(portCompComponents[0]) ?? defaultPort + selector = "/" + portCompComponents[1] + + } else if portCompComponents.count == 1 { + port = Int(portCompComponents[0]) ?? defaultPort + } + } + + return (host, port, selector) + } +} + +struct GopherNode: Identifiable, Equatable { + static func == (lhs: GopherNode, rhs: GopherNode) -> Bool { + return lhs.host == rhs.host && lhs.port == rhs.port && lhs.selector == rhs.selector + } + + let id = UUID() + var host: String + let port: Int + var selector: String + var message: String? + let item: gopherItem? + var children: [GopherNode]? +} + +struct BrowserView: View { + var app: GTUIApp + + @State private var backwardStack: [GopherNode] = [] + @State private var forwardStack: [GopherNode] = [] + + @State var url: String = "gopher://gopher.navan.dev:70" + @State private var gopherItems: [gopherItem] = [] + + let client = GopherClient() + + var view: Body { + ScrollView { + List(.init(gopherItems.indices), selection: nil) { idx in + if gopherItems[idx].parsedItemType == .info { + Text(gopherItems[idx].message) + // .frame(minHeight: 20) + .halign(.start) + .padding(10, .leading) + } else if gopherItems[idx].parsedItemType == .directory { + Button(gopherItems[idx].message) { + performGopherRequest( + host: gopherItems[idx].host, port: gopherItems[idx].port, + selector: gopherItems[idx].selector) + } + } else { + Text(gopherItems[idx].message) + } + + } + } + .topToolbar { + HStack { + Button(icon: .default(icon: .goPrevious)) { + if let curNode = backwardStack.popLast() { + forwardStack.append(curNode) + if let prevNode = backwardStack.popLast() { + performGopherRequest( + host: prevNode.host, port: prevNode.port, selector: prevNode.selector, + clearForward: false) + } + } + } + .padding() + Button(icon: .default(icon: .goNext)) { + if let nextNode = forwardStack.popLast() { + performGopherRequest( + host: nextNode.host, port: nextNode.port, selector: nextNode.selector, + clearForward: false) + } + } + .padding() + EntryRow("Enter a URL", text: $url) + .suffix { + Button(icon: .default(icon: .editCopy)) { State<Any>.copy(url) } + .flat() + .verticalCenter() + } + .padding(10) + Button("Go") { + performGopherRequest(clearForward: false) + } + } + } + .onAppear { + performGopherRequest() + } + } + + private func performGopherRequest( + host: String = "", port: Int = -1, selector: String = "", clearForward: Bool = true + ) { + var res = getHostAndPort(from: self.url) + + if host != "" { + res.host = host + if selector != "" { + res.selector = selector + } else { + res.selector = "" + } + } + + if port != -1 { + res.port = port + } + + self.url = "\(res.host):\(res.port)\(res.selector)" + client.sendRequest(to: res.host, port: res.port, message: "\(res.selector)\r\n") { result in + switch result { + case .success(let resp): + self.gopherItems = resp + let newNode = GopherNode( + host: res.host, port: res.port, selector: selector, item: nil, + children: convertToHostNodes(resp)) + backwardStack.append(newNode) + if clearForward { + forwardStack.removeAll() + } + case .failure(let error): + self.gopherItems = [ + gopherItem(rawLine: "Error \(error)") + ] + } + } + } + + private func convertToHostNodes(_ responseItems: [gopherItem]) -> [GopherNode] { + var returnItems: [GopherNode] = [] + responseItems.forEach { item in + if item.parsedItemType != .info { + returnItems.append( + GopherNode( + host: item.host, port: item.port, selector: item.selector, message: item.message, + item: item, children: nil)) + } + } + return returnItems + } +} diff --git a/Sources/iGopherBrowserGTK/ToolbarView.swift b/Sources/iGopherBrowserGTK/ToolbarView.swift new file mode 100644 index 0000000..494e09e --- /dev/null +++ b/Sources/iGopherBrowserGTK/ToolbarView.swift @@ -0,0 +1,38 @@ +// +// ToolbarView.swift +// +// +// Created by Navan Chauhan on 7/27/24. +// + +import Adwaita + +struct ToolbarView: View { + + var app: GTUIApp + var window: GTUIApplicationWindow + + var view: Body { + HeaderBar.end { + Menu(icon: .default(icon: .openMenu), app: app, window: window) { + MenuButton("New Window", window: false) { + app.addWindow("main") + } + .keyboardShortcut("n".ctrl()) + MenuButton("Close Window") { + window.close() + } + .keyboardShortcut("w".ctrl()) + MenuSection { + MenuButton("Quit", window: false) { + app.quit() + } + .keyboardShortcut("q".ctrl()) + } + } + .primary() + .tooltip("Main Menu") + } + } + +} diff --git a/Sources/iGopherBrowserGTK/iGopherBrowserGTK.swift b/Sources/iGopherBrowserGTK/iGopherBrowserGTK.swift new file mode 100644 index 0000000..db5ed45 --- /dev/null +++ b/Sources/iGopherBrowserGTK/iGopherBrowserGTK.swift @@ -0,0 +1,21 @@ +// The Swift Programming Language +// https://docs.swift.org/swift-book + +import Adwaita + +@main +struct iGopherBrowserGTK: App { + let id = "com.navanchauhan.igopherbrowsergtk" + var app: GTUIApp! + + var scene: Scene { + Window(id: "main") { window in + BrowserView(app: app) + .topToolbar { + ToolbarView(app: app, window: window) + } + } + .defaultSize(width: 800, height: 800) + .title("iGopherBrowser (GTK Version)") + } +} diff --git a/Tests/iGopherBrowserGTKTests/iGopherBrowserGTKTests.swift b/Tests/iGopherBrowserGTKTests/iGopherBrowserGTKTests.swift new file mode 100644 index 0000000..6bed949 --- /dev/null +++ b/Tests/iGopherBrowserGTKTests/iGopherBrowserGTKTests.swift @@ -0,0 +1,12 @@ +import XCTest +@testable import iGopherBrowserGTK + +final class iGopherBrowserGTKTests: XCTestCase { + func testExample() throws { + // XCTest Documentation + // https://developer.apple.com/documentation/xctest + + // Defining Test Cases and Test Methods + // https://developer.apple.com/documentation/xctest/defining_test_cases_and_test_methods + } +} |