├── .gitignore ├── .swiftlint.yml ├── .github └── workflows │ └── swift.yml ├── scripts └── release ├── Tests └── SwiftLibraryTests │ ├── SwiftLibraryTests.swift │ └── CommandTests.swift ├── Package.swift ├── Sources ├── SwiftLibrary │ ├── Requirement.swift │ ├── SwiftLibrary.swift │ ├── PackageData.swift │ └── Command.swift └── swift-library │ ├── Util.swift │ └── main.swift ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: 2 | - line_length 3 | - trailing_comma 4 | 5 | excluded: 6 | - .build 7 | - Tests 8 | 9 | identifier_name: 10 | excluded: 11 | - APODIDAE_VERSION 12 | - v3 13 | - v4 14 | -------------------------------------------------------------------------------- /.github/workflows/swift.yml: -------------------------------------------------------------------------------- 1 | name: Swift 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: macOS-10.14 9 | 10 | steps: 11 | - uses: actions/checkout@v1 12 | - name: Build 13 | run: swift build 14 | - name: Test 15 | run: swift test 16 | -------------------------------------------------------------------------------- /scripts/release: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ $# -eq 0 ] 4 | then 5 | echo "No version supplied" 6 | exit 1 7 | fi 8 | 9 | git tag -s -a $1 -m '' 10 | git push --follow-tags 11 | open 'https://github.com/kiliankoe/SwiftLibrary/releases/new' 12 | 13 | # echo "public let APODIDAE_VERSION = \""$(git describe)"\"" > Sources/ApodidaeCore/Version.swift 14 | swift build -c release 15 | 16 | cd .build/release 17 | tar -zcf swiftlibrary.tar.gz swift-library 18 | shasum -a 256 swiftlibrary.tar.gz 19 | 20 | open . 21 | -------------------------------------------------------------------------------- /Tests/SwiftLibraryTests/SwiftLibraryTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import SwiftLibrary 3 | 4 | final class SwiftLibraryTests: XCTestCase { 5 | func testLiveFetch() { 6 | let e = expectation(description: "receive data") 7 | 8 | SwiftLibrary.query("Swift Package Manager") { result in 9 | switch result { 10 | case .failure(let error): 11 | XCTFail("Received error: \(error)") 12 | e.fulfill() 13 | case .success(let packages): 14 | XCTAssertFalse(packages.isEmpty) 15 | e.fulfill() 16 | } 17 | } 18 | 19 | waitForExpectations(timeout: 5) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.0 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "SwiftLibrary", 7 | products: [ 8 | .library( 9 | name: "SwiftLibrary", 10 | targets: ["SwiftLibrary"]), 11 | .executable( 12 | name: "swift-library", 13 | targets: ["swift-library"]) 14 | ], 15 | dependencies: [], 16 | targets: [ 17 | .target( 18 | name: "SwiftLibrary", 19 | dependencies: []), 20 | .target( 21 | name: "swift-library", 22 | dependencies: ["SwiftLibrary"]), 23 | .testTarget( 24 | name: "SwiftLibraryTests", 25 | dependencies: ["SwiftLibrary"]), 26 | ] 27 | ) 28 | -------------------------------------------------------------------------------- /Sources/SwiftLibrary/Requirement.swift: -------------------------------------------------------------------------------- 1 | public enum Requirement { 2 | case tag(String) 3 | case branch(String) 4 | case revision(String) 5 | 6 | public var packageString: String { 7 | switch self { 8 | case .tag(let version): 9 | return "from: \"\(version)\"" 10 | case .branch(let branch): 11 | return ".branch(\"\(branch)\")" 12 | case .revision(let revision): 13 | return ".revision(\"\(revision)\")" 14 | } 15 | } 16 | } 17 | 18 | extension Requirement: Equatable { 19 | public static func == (lhs: Requirement, rhs: Requirement) -> Bool { 20 | switch (lhs, rhs) { 21 | case (.tag(let lhsv), .tag(let rhsv)): return lhsv == rhsv 22 | case (.branch(let lhsb), .branch(let rhsb)): return lhsb == rhsb 23 | case (.revision(let lhsr), .revision(let rhsr)): return lhsr == rhsr 24 | default: return false 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Kilian Koeltzsch 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /Sources/swift-library/Util.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Dispatch 3 | import SwiftLibrary 4 | 5 | func allPackages(query: String) -> [PackageData] { 6 | let semaphore = DispatchSemaphore(value: 0) 7 | 8 | var packages: [PackageData] = [] 9 | 10 | SwiftLibrary.query(query) { result in 11 | switch result { 12 | case .failure(let error): 13 | print(error.localizedDescription) 14 | semaphore.signal() 15 | case .success(let fetchedPackages): 16 | packages = fetchedPackages 17 | semaphore.signal() 18 | } 19 | } 20 | 21 | semaphore.wait() 22 | return packages 23 | } 24 | 25 | func firstPackage(query: String) -> PackageData? { 26 | return allPackages(query: query).first 27 | } 28 | 29 | func run(cmd: String, args: [String]) { 30 | let task = Process() 31 | task.launchPath = cmd 32 | task.arguments = args 33 | task.launch() 34 | } 35 | 36 | #if canImport(AppKit) 37 | import AppKit 38 | func addToPasteboard(string: String) { 39 | 40 | let pb = NSPasteboard.general 41 | pb.string(forType: .string) 42 | pb.declareTypes([NSPasteboard.PasteboardType.string], owner: nil) 43 | pb.setString(string, forType: .string) 44 | 45 | } 46 | #endif 47 | -------------------------------------------------------------------------------- /Sources/SwiftLibrary/SwiftLibrary.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum SwiftLibrary { 4 | public static func query(_ query: String, 5 | session: URLSession = .shared, 6 | completion: @escaping (Result<[PackageData], Error>) -> Void) { 7 | let task = session.dataTask(with: urlRequest(with: query)) { data, _, error in 8 | guard let data = data, error == nil else { 9 | completion(.failure(error!)) 10 | return 11 | } 12 | 13 | let decoder = JSONDecoder() 14 | decoder.keyDecodingStrategy = .convertFromSnakeCase 15 | do { 16 | let packages = try decoder.decode([PackageData].self, from: data) 17 | completion(.success(packages)) 18 | } catch { 19 | completion(.failure(error)) 20 | } 21 | } 22 | task.resume() 23 | } 24 | 25 | private static func urlRequest(with query: String) -> URLRequest { 26 | var urlComponents = URLComponents(string: "https://api.swiftpm.co/packages.json")! 27 | let queryItem = URLQueryItem(name: "query", value: query) 28 | urlComponents.queryItems = [queryItem] 29 | guard let url = urlComponents.url else { fatalError("Failed to create URL with query: \(query)") } 30 | return URLRequest(url: url) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/swift-library/main.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftLibrary 3 | 4 | let args = Array(CommandLine.arguments.dropFirst()) 5 | if args.first?.contains("help") ?? false { 6 | print(Command.exampleUsage) 7 | exit(0) 8 | } 9 | 10 | guard let command = Command(from: args) else { 11 | print("Unrecognized command '\(args.joined(separator: " "))'") 12 | print("See --help for example usage.") 13 | exit(1) 14 | } 15 | 16 | switch command { 17 | case .search(let query): 18 | let packages = allPackages(query: query) 19 | guard !packages.isEmpty else { 20 | print("No packages matching query found.") 21 | exit(0) 22 | } 23 | for package in packages { 24 | print(package.shortDescription) 25 | usleep(15_000) // The short delay helps the eye follow the results output. 26 | } 27 | case .info(let input): 28 | guard let package = firstPackage(query: input) else { 29 | print("No package matching query found.") 30 | exit(1) 31 | } 32 | print(package.longDescription) 33 | case .home(let input): 34 | guard let package = firstPackage(query: input) else { 35 | print("No package matching query found.") 36 | exit(1) 37 | } 38 | run(cmd: "/usr/bin/open", args: [package.url.absoluteString]) 39 | case .add(let input): 40 | guard let package = firstPackage(query: input.package) else { 41 | print("No package matching query found.") 42 | exit(1) 43 | } 44 | let dependencyString = ".package(url: \"\(package.url.absoluteString)\", \(input.requirement?.packageString ?? ".branch(\"master\")"))" 45 | #if os(macOS) 46 | addToPasteboard(string: dependencyString) 47 | print("Your clipboard has been updated, just add it to your package manifest.") 48 | #else 49 | print("Copy the following and add it to your package manifest.") 50 | print(dependencyString) 51 | #endif 52 | } 53 | -------------------------------------------------------------------------------- /Sources/SwiftLibrary/PackageData.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct PackageData: Decodable { 4 | public let id: Int 5 | public let url: URL 6 | public let host: String 7 | public let repositoryIdentifier: String 8 | public let name: String 9 | public let description: String? 10 | public let license: String? 11 | public let stars: Int 12 | public let updatedAt: String // FIXME 13 | 14 | public let versions: [Version] 15 | 16 | public var latest: Version { 17 | guard let first = self.versions.first else { 18 | fatalError("No version found, according to Dave this shouldn't happen :P") 19 | } 20 | return first 21 | } 22 | 23 | public var latestRelease: Version? { 24 | self.versions.first { $0.kind == .tag } 25 | } 26 | } 27 | 28 | extension PackageData { 29 | public struct Version: Decodable { 30 | public let name: String 31 | public let kind: Kind 32 | public let releasedAgo: String? 33 | public let swiftVersions: String 34 | public let supportedMacosVersion: String? 35 | public let supportedIosVersion: String? 36 | public let supportedWatchosVersion: String? 37 | public let supportedTvosVersion: String? 38 | public let libraryCount: Int 39 | public let executableCount: Int 40 | 41 | public enum Kind: String, Decodable { 42 | case branch, tag 43 | } 44 | } 45 | } 46 | 47 | extension PackageData { 48 | public var shortDescription: String { 49 | var output = """ 50 | - \(repositoryIdentifier) 51 | \(url.absoluteString) 52 | """ 53 | if let description = self.description, !description.isEmpty { 54 | output += "\n \(description)" 55 | } 56 | return output 57 | } 58 | 59 | public var longDescription: String { 60 | let libraryString = latest.libraryCount == 1 ? "library" : "libraries" 61 | let executableString = latest.executableCount == 1 ? "executable" : "executables" 62 | 63 | var output = """ 64 | \(repositoryIdentifier) \(latest.name) \(latestRelease?.name ?? "") 65 | \(description ?? "No description available") 66 | 67 | \(stars) stargazers 68 | 69 | Licensed under \(license ?? "n/a"). 70 | Supports Swift \(latest.swiftVersions). 71 | Last released \(latestRelease?.releasedAgo ?? "n/a") ago. 72 | Contains \(latest.libraryCount) \(libraryString). 73 | Contains \(latest.executableCount) \(executableString). 74 | 75 | """ 76 | 77 | if let macOS = latest.supportedMacosVersion { 78 | output += "\nSupports macOS \(macOS)" 79 | } 80 | if let iOS = latest.supportedIosVersion { 81 | output += "\nSupports iOS \(iOS)" 82 | } 83 | if let tvOS = latest.supportedTvosVersion { 84 | output += "\nSupports tvOS \(tvOS)" 85 | } 86 | if let watchOS = latest.supportedWatchosVersion { 87 | output += "\nSupports watchOS \(watchOS)" 88 | } 89 | return output 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![header](https://user-images.githubusercontent.com/2625584/63201247-a94b2100-c084-11e9-960e-b3c479dd4afe.png) 2 | 3 | SwiftLibrary is intended to be the quickest way to search for packages in the Swift ecosystem. By design, Swift pulls dependencies from any git repo, most of which are hosted on GitHub, but distributed amongst many thousands of users. This is fantastic to work with, but what it gains in ease of use, this method definitely lacks in discoverability. 4 | 5 | Fortunately, projects like [swiftpm.co](https://swiftpm.co) (big thanks to [Dave Verwer](https://daveverwer.com)!) exist, which is a relatively new and growing index of Swift packages. Using SwiftLibrary you can search that index directly from your CLI and quickly find the package you're looking for. 6 | 7 | 8 | ## Installation 9 | 10 | ``` 11 | $ brew tap kiliankoe/formulae 12 | $ brew install swiftlibrary 13 | ``` 14 | 15 | SwiftLibrary conveniently installs as `swift-library` which enables you to just call it as if it were a subcommand on swift itself as `swift library ...`. See the usage examples below for more. 16 | 17 | You can of course also install SwiftLibrary manually. 18 | 19 | ``` 20 | $ git clone https://github.com/kiliankoe/SwiftLibrary 21 | $ cd SwiftLibrary 22 | $ swift build -c release 23 | $ cp .build/release/swift-library /usr/local/bin/swift-library 24 | ``` 25 | 26 | 27 | 28 | ## Usage 29 | 30 | SwiftLibrary exposes a handful of commands. Their use is probably best shown with a few examples. 31 | 32 | #### Searching for packages 33 | 34 | ``` 35 | $ swift library search yaml 36 | - behrang/YamlSwift 37 | https://github.com/behrang/YamlSwift.git 38 | Load YAML and JSON documents using Swift 39 | - jpsim/Yams 40 | https://github.com/jpsim/Yams.git 41 | A Sweet and Swifty YAML parser. 42 | ... 43 | ``` 44 | 45 | #### Getting info on a package 46 | 47 | ``` 48 | $ swift library info yams 49 | jpsim/Yams 2.0.0 50 | A Sweet and Swifty YAML parser. 51 | 52 | 407 stargazers 53 | 407 watchers 54 | 55 | Licensed under MIT. 56 | Supports Swift 5, 4.2, 4. 57 | Last released: 4 months ago. 58 | Contains 1 library/libraries. 59 | Contains 0 executable(s). 60 | ``` 61 | 62 | You can also run `swift library home yams` to directly open the homepage to a specific package in your browser. You may know this feature from homebrew. 63 | 64 | #### Adding a package 65 | 66 | ``` 67 | $ swift library add yams 68 | Your clipboard has been updated, just add it to your package manifest. 69 | ``` 70 | 71 | For the time being SwiftLibrary will not edit your manifest directly, but add everything you need to your clipboard so you can paste it directly into your package manifest. 72 | 73 | It's also possible to add a specific version or other requirement. All you have to do is add `@requirement` to the end of the package. This syntax may feel familiar if you've used npm. The following all work. 74 | 75 | ````shell 76 | $ swift library add yams@2.0.0 77 | $ swift library add yams@tag:2.0.0 # same as above 78 | $ swift library add yams@version:2.0.0 # same as above 79 | $ swift library add yams@branch:master 80 | $ swift library add yams@revision:c947a30 81 | $ swift library add yams@commit:c947a30 # same as above 82 | ```` 83 | 84 | 85 | 86 | For convenience a shorthand syntax for the available commands is also available. You can use `s` instead of `search`, `i` instead of `info`, `h` instead of `home` and `a` or `+` instead of `add`. 87 | 88 | 89 | 90 | ## Questions or Feedback 91 | 92 | Did you run into any issues or have questions? Please don't hesitate to [open an issue](https://github.com/kiliankoe/SwiftLibrary/issues/new) or find me [@kiliankoe](https://twitter.com/kiliankoe) on Twitter. 93 | -------------------------------------------------------------------------------- /Tests/SwiftLibraryTests/CommandTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import SwiftLibrary 3 | 4 | class CommandTests: XCTestCase { 5 | func testInit() { 6 | let validCommands = [ 7 | ["search", "foo"], 8 | ["s", "bar"], 9 | ["info", "foo"], 10 | ["i", "bar"], 11 | ["home", "foo"], 12 | ["h", "bar"], 13 | ["add", "baz"], 14 | ["a", "foo"], 15 | ["+", "bar"], 16 | ] 17 | for command in validCommands { 18 | if Command(from: command) == nil { 19 | XCTFail("Failed to initialize valid command with \(command)") 20 | return 21 | } 22 | } 23 | 24 | let invalidCommands = [ 25 | ["install"], 26 | ["foobar"], 27 | ] 28 | for command in invalidCommands { 29 | if Command(from: command) != nil { 30 | XCTFail("Initialized invalid command") 31 | } 32 | } 33 | } 34 | 35 | func testSimpleQueries() { 36 | XCTAssertEqual(Command(from: ["search", "foo", "bar"])!, Command.search("foo bar")) 37 | XCTAssertEqual(Command(from: ["info", "foo", "bar"])!, Command.info("foo bar")) 38 | XCTAssertEqual(Command(from: ["home", "foo", "bar"])!, Command.home("foo bar")) 39 | XCTAssertEqual(Command(from: ["add", "foo", "bar"])!, Command.add(package: "foo bar", requirement: nil)) 40 | } 41 | 42 | func testAdd() { 43 | XCTAssertEqual(Command(from: ["add", "foobar"])!, Command.add(package: "foobar", requirement: nil)) 44 | XCTAssertEqual(Command(from: ["add", "foobar@1.1.0"])!, Command.add(package: "foobar", requirement: .tag("1.1.0"))) 45 | XCTAssertEqual(Command(from: ["add", "foobar@tag:1.1.0"]), Command.add(package: "foobar", requirement: .tag("1.1.0"))) 46 | XCTAssertEqual(Command(from: ["add", "foobar@version:1.1.0"]), Command.add(package: "foobar", requirement: .tag("1.1.0"))) 47 | XCTAssertEqual(Command(from: ["add", "foobar@branch:master"]), Command.add(package: "foobar", requirement: .branch("master"))) 48 | XCTAssertEqual(Command(from: ["add", "foobar@revision:barfoo"]), Command.add(package: "foobar", requirement: .revision("barfoo"))) 49 | XCTAssertEqual(Command(from: ["add", "foobar@commit:barfoo"]), Command.add(package: "foobar", requirement: .revision("barfoo"))) 50 | 51 | XCTAssertNil(Command(from: ["add", "foobar@"])) 52 | XCTAssertNil(Command(from: ["add", "foobar@foo:bar"])) 53 | XCTAssertNil(Command(from: ["add", "foobar@:bar"])) 54 | XCTAssertNil(Command(from: ["add", "foobar@ "])) 55 | } 56 | 57 | func testEquality() { 58 | XCTAssertEqual(Command.search("foo"), Command.search("foo")) 59 | XCTAssertNotEqual(Command.search("foo"), Command.search("bar")) 60 | 61 | XCTAssertEqual(Command.info("foo"), Command.info("foo")) 62 | XCTAssertNotEqual(Command.info("foo"), Command.info("bar")) 63 | 64 | XCTAssertEqual(Command.home("foo"), Command.home("foo")) 65 | XCTAssertNotEqual(Command.home("foo"), Command.home("bar")) 66 | 67 | XCTAssertEqual(Command.add(package: "foo", requirement: .tag("0.1.0")), Command.add(package: "foo", requirement: .tag("0.1.0"))) 68 | XCTAssertNotEqual(Command.add(package: "foo", requirement: .tag("0.1.0")), Command.add(package: "bar", requirement: .tag("0.1.0"))) 69 | XCTAssertNotEqual(Command.add(package: "foo", requirement: .tag("0.1.0")), Command.add(package: "foo", requirement: .tag("1.0.0"))) 70 | XCTAssertNotEqual(Command.add(package: "foo", requirement: .tag("0.1.0")), Command.add(package: "bar", requirement: .tag("1.0.0"))) 71 | 72 | XCTAssertNotEqual(Command.info("foo"), Command.add(package: "foo", requirement: .tag("1.0.0"))) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Sources/SwiftLibrary/Command.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum Command { 4 | case search(String) 5 | case info(String) 6 | case home(String) 7 | case add(package: String, requirement: Requirement?) 8 | 9 | public static var exampleUsage: String { 10 | return """ 11 | Commands: 12 | swift library search 13 | Search for packages matching query. 14 | swift library info 15 | Get additional info to a package. 16 | swift library home 17 | Open the homepage of a package in your browser. 18 | swift library add 19 | Add the given package to your Package.swift's dependencies. 20 | """ 21 | } 22 | 23 | public init?(from strings: [String]) { 24 | guard 25 | strings.count > 0, 26 | let command = strings.first 27 | else { return nil } 28 | 29 | let query = strings[1...].joined(separator: " ") 30 | guard !query.isEmpty else { return nil } 31 | 32 | switch command.lowercased() { 33 | case "search", "s": self = .search(query) 34 | case "info", "i": self = .info(query) 35 | case "home", "h": self = .home(query) 36 | case "add", "a", "+": 37 | // TODO: It would probably be easier to tackle this via regex instead of splitting strings... 38 | guard query.contains("@") else { 39 | // easy case first... 40 | self = .add(package: query, requirement: nil) 41 | return 42 | } 43 | 44 | let queryComponents = query 45 | .trimmingCharacters(in: .whitespaces) 46 | .split(separator: "@", maxSplits: 1) 47 | 48 | guard 49 | queryComponents.count == 2, 50 | let packageName = queryComponents.first, 51 | let requirement = queryComponents.last 52 | else { return nil } 53 | 54 | guard requirement.contains(":") else { 55 | // no specifically named requirement means it's a tag 56 | self = .add(package: String(packageName), requirement: .tag(String(requirement))) 57 | return 58 | } 59 | 60 | // Continuing here if the add command includes a *specific* named requirement, e.g. @tag:0.1.0 or @branch:master 61 | let specificComponents = requirement.split(separator: ":", maxSplits: 1) 62 | guard 63 | let specificRequirementName = specificComponents.first, 64 | let specificRequirementValue = specificComponents.last 65 | else { return nil } 66 | 67 | switch specificRequirementName { 68 | case "tag", "version": 69 | self = .add(package: String(packageName), requirement: .tag(String(specificRequirementValue))) 70 | case "branch": 71 | self = .add(package: String(packageName), requirement: .branch(String(specificRequirementValue))) 72 | case "revision", "commit": 73 | self = .add(package: String(packageName), requirement: .revision(String(specificRequirementValue))) 74 | default: return nil // It would probably make sense to start returning actual errors at some point... 75 | } 76 | default: 77 | return nil 78 | } 79 | } 80 | } 81 | 82 | extension Command: Equatable { 83 | public static func == (lhs: Command, rhs: Command) -> Bool { 84 | switch (lhs, rhs) { 85 | case (.search(let lhss), .search(let rhss)): return lhss == rhss 86 | case (.info(let lhsi), .info(let rhsi)): return lhsi == rhsi 87 | case (.home(let lhsh), .home(let rhsh)): return lhsh == rhsh 88 | case (.add(let lhsp, let lhsr), .add(let rhsp, let rhsr)): 89 | return lhsp == rhsp && lhsr == rhsr 90 | default: return false 91 | } 92 | } 93 | } 94 | --------------------------------------------------------------------------------