├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── contents.xcworkspacedata ├── LICENSE ├── Makefile ├── Package.swift ├── README.md ├── Sources └── airdrop │ ├── AirDropCLI.swift │ ├── ConsoleIO.swift │ └── main.swift └── Tests ├── LinuxMain.swift └── airdrop-cli-tests ├── AirdropCLITests.swift └── XCTestManifests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Volodymyr Klymenko 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PREFIX ?= /usr/local 2 | BINDIR := $(PREFIX)/bin 3 | 4 | all: build 5 | 6 | build: 7 | swift build -c release --disable-sandbox $(FLAGS) 8 | 9 | install: build 10 | install -d $(DESTDIR)$(BINDIR) 11 | install -m 755 ".build/release/airdrop" "$(DESTDIR)$(BINDIR)" 12 | 13 | uninstall: 14 | rm -rf $(DESTDIR)$(BINDIR)/airdrop 15 | 16 | clean: 17 | rm -rf .build 18 | 19 | .PHONY: all build install uninstall clean 20 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "airdrop-cli", 8 | targets: [ 9 | .target( 10 | name: "airdrop", 11 | dependencies: []), 12 | .testTarget( 13 | name: "airdrop-cli-tests", 14 | dependencies: ["airdrop"]), 15 | ] 16 | ) 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # airdrop-cli 2 | 3 | A command-line tool that allows you to share files and URLs with Apple 4 | devices using AirDrop from your terminal. 5 | 6 | ## Prerequisities 7 | - Xcode 8 | - Xcode Command Line Tools 9 | 10 | ## Installation 11 | 12 | `airdrop-cli` is available for install with [Homebrew](https://brew.sh/) 13 | 14 | ``` 15 | brew install vldmrkl/formulae/airdrop-cli 16 | ``` 17 | 18 | or 19 | 20 | ``` 21 | brew tap vldmrkl/formulae 22 | brew install airdrop-cli 23 | ``` 24 | 25 | ### Building from source 26 | 27 | Before attempting to build this project from its source code, make sure 28 | that you have Xcode version 11.4 (or higher) & GNU `make` installed. 29 | 30 | Then, clone the project and use the Makefile 31 | 32 | ``` 33 | git clone https://github.com/vldmrkl/airdrop-cli.git 34 | cd airdrop-cli 35 | make 36 | sudo make install 37 | ``` 38 | 39 | By default, make will build and install the project within 40 | `/usr/local/bin`. This project location can be changed by appending 41 | a new prefix to the `PREFIX` variable. 42 | 43 | ``` 44 | make PREFIX="YOUR_PREFIX_HERE" 45 | ``` 46 | 47 | You can append other Swift flags, in case you may need them for your 48 | specific build, to the `FLAGS` variable. 49 | 50 | ## Usage 51 | 52 | ![airdrop-cli-demo](https://user-images.githubusercontent.com/26641473/103395121-762ef380-4afa-11eb-9bc8-6cf6068edf32.gif) 53 | 54 | To airdrop files, run: 55 | 56 | ``` 57 | airdrop /path/to/your/file 58 | ``` 59 | 60 | You can also airdrop URLs: 61 | 62 | ``` 63 | airdrop https://apple.com/ 64 | ``` 65 | 66 | You can pass as many paths as you want. As long as theese file URLs are correct, 67 | the command will work. 68 | -------------------------------------------------------------------------------- /Sources/airdrop/AirDropCLI.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AirDropCLI.swift 3 | // airdrop 4 | // 5 | // Created by Volodymyr Klymenko on 2020-12-30. 6 | // 7 | 8 | import Foundation 9 | 10 | import Cocoa 11 | 12 | enum OptionType: String { 13 | case help = "h" 14 | case unknown 15 | 16 | init(value: String) { 17 | switch value { 18 | case "-h", "--help": self = .help 19 | default: self = .unknown 20 | } 21 | } 22 | } 23 | 24 | class AirDropCLI: NSObject, NSApplicationDelegate, NSSharingServiceDelegate { 25 | let consoleIO = ConsoleIO() 26 | 27 | func applicationDidFinishLaunching(_ aNotification: Notification) { 28 | let argCount = Int(CommandLine.argc) 29 | guard argCount >= 2 else { 30 | consoleIO.printUsage() 31 | exit(0) 32 | } 33 | 34 | let argument = CommandLine.arguments[1] 35 | if argCount == 2 && argument.hasPrefix("-") { 36 | let (option, _) = getOption(argument) 37 | 38 | if option == .help { 39 | consoleIO.printUsage() 40 | } else { 41 | consoleIO.writeMessage("Unknown option, see usage.\n", to: .error) 42 | consoleIO.printUsage() 43 | } 44 | 45 | exit(0) 46 | } 47 | 48 | let pathsToFiles = Array(CommandLine.arguments[1 ..< argCount]) 49 | 50 | shareFiles(pathsToFiles) 51 | 52 | if #available(macOS 13.0, *) { 53 | NSApp.setActivationPolicy(.accessory) 54 | } 55 | } 56 | 57 | func getOption(_ option: String) -> (option:OptionType, value: String) { 58 | return (OptionType(value: option), option) 59 | } 60 | 61 | func shareFiles(_ pathsToFiles: [String]) { 62 | guard let service: NSSharingService = NSSharingService(named: .sendViaAirDrop) 63 | else { 64 | exit(2) 65 | } 66 | 67 | var filesToShare: [URL] = [] 68 | 69 | for pathToFile in pathsToFiles { 70 | if let url = URL(string: pathToFile), url.scheme != nil { 71 | filesToShare.append(url) 72 | } else { 73 | let fileURL: URL = NSURL.fileURL(withPath: pathToFile, isDirectory: false) 74 | filesToShare.append(fileURL.standardizedFileURL) 75 | } 76 | } 77 | 78 | if service.canPerform(withItems: filesToShare) { 79 | service.delegate = self 80 | service.perform(withItems: filesToShare) 81 | } else { 82 | consoleIO.writeMessage("Can't perform: file is likely to be nonexistent.", to: .error) 83 | exit(1) 84 | } 85 | } 86 | 87 | func sharingService(_ sharingService: NSSharingService, willShareItems items: [Any]) { 88 | consoleIO.writeMessage("Sending \(items.count) files:") 89 | for item in items { 90 | consoleIO.writeMessage("📩 \(item)") 91 | } 92 | } 93 | 94 | func sharingService(_ sharingService: NSSharingService, didShareItems items: [Any]) { 95 | consoleIO.writeMessage("✅ Files were sent successfully!") 96 | exit(0) 97 | } 98 | 99 | func sharingService(_ sharingService: NSSharingService, didFailToShareItems items: [Any], error: Error) { 100 | consoleIO.writeMessage(error.localizedDescription, to: .error) 101 | exit(1) 102 | } 103 | 104 | func sharingService(_ sharingService: NSSharingService, sourceFrameOnScreenForShareItem item: Any) -> NSRect { 105 | return NSRect(x: 0, y: 0, width: 400, height: 100) 106 | } 107 | 108 | func sharingService(_ sharingService: NSSharingService, sourceWindowForShareItems items: [Any], sharingContentScope: UnsafeMutablePointer) -> NSWindow? { 109 | let airDropMenuWindow = NSWindow(contentRect: .init(origin: .zero, 110 | size: .init(width: 1, 111 | height: 1)), 112 | styleMask: [.closable], 113 | backing: .buffered, 114 | defer: false) 115 | 116 | airDropMenuWindow.center() 117 | airDropMenuWindow.level = .popUpMenu 118 | airDropMenuWindow.makeKeyAndOrderFront(nil) 119 | 120 | return airDropMenuWindow 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /Sources/airdrop/ConsoleIO.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConsoleIO.swift 3 | // airdrop 4 | // 5 | // Created by Volodymyr Klymenko on 2020-12-30. 6 | // 7 | 8 | import Foundation 9 | 10 | enum OutputType { 11 | case error 12 | case standard 13 | } 14 | 15 | class ConsoleIO { 16 | func writeMessage(_ message: String, to: OutputType = .standard) { 17 | switch to { 18 | case .standard: 19 | print("\(message)") 20 | case .error: 21 | fputs("\n❌ Error: \(message)\n", stderr) 22 | } 23 | } 24 | 25 | func printUsage() { 26 | let executableName = (CommandLine.arguments[0] as NSString).lastPathComponent 27 | 28 | writeMessage("usage: \(executableName) []") 29 | writeMessage(" args – URLs or paths to files, which you'd like to AirDrop") 30 | writeMessage("\nOPTIONS:") 31 | writeMessage(" -h, --help – print help info") 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Sources/airdrop/main.swift: -------------------------------------------------------------------------------- 1 | // 2 | // main.swift 3 | // airdrop 4 | // 5 | // Created by Volodymyr Klymenko on 2020-12-30. 6 | // 7 | 8 | import Cocoa 9 | 10 | let airDropCLI = AirDropCLI() 11 | let app = NSApplication.shared 12 | 13 | app.delegate = airDropCLI 14 | app.run() 15 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import AirdropCLITests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += AirdropCLITests.allTests() 7 | XCTMain(tests) 8 | -------------------------------------------------------------------------------- /Tests/airdrop-cli-tests/AirdropCLITests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import class Foundation.Bundle 3 | 4 | final class AirdropCLITests: XCTestCase { 5 | func testExample() throws { 6 | // This is an example of a functional test case. 7 | // Use XCTAssert and related functions to verify your tests produce the correct 8 | // results. 9 | 10 | // Some of the APIs that we use below are available in macOS 10.13 and above. 11 | guard #available(macOS 10.13, *) else { 12 | return 13 | } 14 | 15 | let fooBinary = productsDirectory.appendingPathComponent("airdrop") 16 | 17 | let process = Process() 18 | process.executableURL = fooBinary 19 | 20 | let pipe = Pipe() 21 | process.standardOutput = pipe 22 | 23 | try process.run() 24 | process.waitUntilExit() 25 | 26 | let data = pipe.fileHandleForReading.readDataToEndOfFile() 27 | let output = String(data: data, encoding: .utf8) 28 | 29 | XCTAssertEqual(output, "Hello, world!\n") 30 | } 31 | 32 | /// Returns path to the built products directory. 33 | var productsDirectory: URL { 34 | #if os(macOS) 35 | for bundle in Bundle.allBundles where bundle.bundlePath.hasSuffix(".xctest") { 36 | return bundle.bundleURL.deletingLastPathComponent() 37 | } 38 | fatalError("couldn't find the products directory") 39 | #else 40 | return Bundle.main.bundleURL 41 | #endif 42 | } 43 | 44 | static var allTests = [ 45 | ("testExample", testExample), 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /Tests/airdrop-cli-tests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(AirdropCLITests.allTests), 7 | ] 8 | } 9 | #endif 10 | --------------------------------------------------------------------------------