├── .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 | 
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 |
--------------------------------------------------------------------------------