├── .gitignore ├── .swift-version ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── contents.xcworkspacedata ├── .travis.yml ├── Package.resolved ├── Package.swift ├── README.md └── Sources └── swift-watch ├── Configuration.swift ├── Event.swift ├── Extensions.swift ├── Logger.swift ├── MainLoop.swift ├── Runner.swift ├── RunnerObserver.swift ├── Watcher.swift └── main.swift /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/swift 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=swift 3 | 4 | ### Swift ### 5 | # Xcode 6 | # 7 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 8 | 9 | ## User settings 10 | xcuserdata/ 11 | 12 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 13 | *.xcscmblueprint 14 | *.xccheckout 15 | 16 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 17 | build/ 18 | DerivedData/ 19 | *.moved-aside 20 | *.pbxuser 21 | !default.pbxuser 22 | *.mode1v3 23 | !default.mode1v3 24 | *.mode2v3 25 | !default.mode2v3 26 | *.perspectivev3 27 | !default.perspectivev3 28 | 29 | ## Obj-C/Swift specific 30 | *.hmap 31 | 32 | ## App packaging 33 | *.ipa 34 | *.dSYM.zip 35 | *.dSYM 36 | 37 | ## Playgrounds 38 | timeline.xctimeline 39 | playground.xcworkspace 40 | 41 | # Swift Package Manager 42 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 43 | # Packages/ 44 | # Package.pins 45 | # Package.resolved 46 | # *.xcodeproj 47 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 48 | # hence it is not needed unless you have added a package configuration file to your project 49 | # .swiftpm 50 | 51 | .build/ 52 | 53 | # CocoaPods 54 | # We recommend against adding the Pods directory to your .gitignore. However 55 | # you should judge for yourself, the pros and cons are mentioned at: 56 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 57 | # Pods/ 58 | # Add this line if you want to avoid checking in source code from the Xcode workspace 59 | # *.xcworkspace 60 | 61 | # Carthage 62 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 63 | # Carthage/Checkouts 64 | 65 | Carthage/Build/ 66 | 67 | # Accio dependency management 68 | Dependencies/ 69 | .accio/ 70 | 71 | # fastlane 72 | # It is recommended to not store the screenshots in the git repo. 73 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 74 | # For more information about the recommended setup visit: 75 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 76 | 77 | fastlane/report.xml 78 | fastlane/Preview.html 79 | fastlane/screenshots/**/*.png 80 | fastlane/test_output 81 | 82 | # Code Injection 83 | # After new code Injection tools there's a generated folder /iOSInjectionProject 84 | # https://github.com/johnno1962/injectionforxcode 85 | 86 | iOSInjectionProject/ 87 | 88 | # End of https://www.toptal.com/developers/gitignore/api/swift 89 | # Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode 90 | # Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode 91 | 92 | ### VisualStudioCode ### 93 | .vscode/* 94 | !.vscode/settings.json 95 | !.vscode/tasks.json 96 | !.vscode/launch.json 97 | !.vscode/extensions.json 98 | !.vscode/*.code-snippets 99 | 100 | # Local History for Visual Studio Code 101 | .history/ 102 | 103 | # Built Visual Studio Code Extensions 104 | *.vsix 105 | 106 | ### VisualStudioCode Patch ### 107 | # Ignore all local history of files 108 | .history 109 | .ionide 110 | 111 | # End of https://www.toptal.com/developers/gitignore/api/visualstudiocode 112 | # Created by https://www.toptal.com/developers/gitignore/api/macos 113 | # Edit at https://www.toptal.com/developers/gitignore?templates=macos 114 | 115 | ### macOS ### 116 | # General 117 | .DS_Store 118 | .AppleDouble 119 | .LSOverride 120 | 121 | # Icon must end with two \r 122 | Icon 123 | 124 | # Thumbnails 125 | ._* 126 | 127 | # Files that might appear in the root of a volume 128 | .DocumentRevisions-V100 129 | .fseventsd 130 | .Spotlight-V100 131 | .TemporaryItems 132 | .Trashes 133 | .VolumeIcon.icns 134 | .com.apple.timemachine.donotpresent 135 | 136 | # Directories potentially created on remote AFP share 137 | .AppleDB 138 | .AppleDesktop 139 | Network Trash Folder 140 | Temporary Items 141 | .apdisk 142 | 143 | ### macOS Patch ### 144 | # iCloud generated files 145 | *.icloud 146 | 147 | # End of https://www.toptal.com/developers/gitignore/api/macos 148 | # Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode 149 | # Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode 150 | 151 | ### VisualStudioCode ### 152 | .vscode/* 153 | !.vscode/settings.json 154 | !.vscode/tasks.json 155 | !.vscode/launch.json 156 | !.vscode/extensions.json 157 | !.vscode/*.code-snippets 158 | 159 | # Local History for Visual Studio Code 160 | .history/ 161 | 162 | # Built Visual Studio Code Extensions 163 | *.vsix 164 | 165 | ### VisualStudioCode Patch ### 166 | # Ignore all local history of files 167 | .history 168 | .ionide 169 | 170 | # End of https://www.toptal.com/developers/gitignore/api/visualstudiocode 171 | -------------------------------------------------------------------------------- /.swift-version: -------------------------------------------------------------------------------- 1 | 4.2 2 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # reference: http://www.objc.io/issue-6/travis-ci.html 2 | osx_image: xcode11 3 | 4 | language: swift 5 | 6 | script: 7 | - swift --version 8 | - swift build 9 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "CommandCougar", 6 | "repositoryURL": "https://github.com/surfandneptune/CommandCougar", 7 | "state": { 8 | "branch": null, 9 | "revision": "33b0211120d2a4bd41c683516ec78cecc7c614cc", 10 | "version": "1.1.1" 11 | } 12 | }, 13 | { 14 | "package": "Files", 15 | "repositoryURL": "https://github.com/JohnSundell/Files", 16 | "state": { 17 | "branch": null, 18 | "revision": "92b57bea0e737e7d92b5ff281f46ec2b59faf91c", 19 | "version": "3.1.0" 20 | } 21 | }, 22 | { 23 | "package": "Rainbow", 24 | "repositoryURL": "https://github.com/onevcat/Rainbow", 25 | "state": { 26 | "branch": null, 27 | "revision": "9c52c1952e9b2305d4507cf473392ac2d7c9b155", 28 | "version": "3.1.5" 29 | } 30 | }, 31 | { 32 | "package": "SKQueue", 33 | "repositoryURL": "https://github.com/daniel-pedersen/SKQueue", 34 | "state": { 35 | "branch": null, 36 | "revision": "a52c15ea2a8f28fde607155cf75c1abe9ea000a7", 37 | "version": "1.2.0" 38 | } 39 | } 40 | ] 41 | }, 42 | "version": 1 43 | } 44 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.4 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: "swift-watch", 8 | platforms: [ 9 | .macOS(.v10_15), 10 | ], 11 | products: [ 12 | .executable( 13 | name: "swift-watch", 14 | targets: [ 15 | "swift-watch", 16 | ] 17 | ), 18 | ], 19 | dependencies: [ 20 | .package( 21 | url: "https://github.com/JohnSundell/Files", 22 | from: "3.1.0" 23 | ), 24 | .package( 25 | url: "https://github.com/daniel-pedersen/SKQueue", 26 | from: "1.2.0" 27 | ), 28 | .package( 29 | url: "https://github.com/surfandneptune/CommandCougar", 30 | from: "1.1.1" 31 | ), 32 | .package( 33 | url: "https://github.com/onevcat/Rainbow", 34 | from: "3.1.0" 35 | ), 36 | ], 37 | targets: [ 38 | .executableTarget( 39 | name: "swift-watch", 40 | dependencies: [ 41 | "Files", 42 | "SKQueue", 43 | "CommandCougar", 44 | "Rainbow", 45 | ] 46 | ), 47 | ] 48 | ) 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # swift-watch 2 | 3 | Watches over your Swift project's source. 4 | 5 | ## Installation 6 | 7 | 1. Build 8 | 9 | ```terminal 10 | git clone https://github.com/regexident/swift-watch.git 11 | cd ./swift-watch 12 | swift build -c release 13 | ``` 14 | 15 | 2. Install in `$PATH` (such as in `/usr/local/bin/`): 16 | 17 | ```terminal 18 | install -m +x "./.build/release/swift-watch" "/usr/local/bin/" 19 | ``` 20 | 21 | ## Usage 22 | 23 | 1. Run `$ cd /path/to/swift/package/` 24 | 2. Run `$ swift watch -x="build"` 25 | 3. Modify some files in `$ cd /path/to/swift/package/` 26 | 4. Watch `swift-watch` do its thing 27 | 28 | ## Options 29 | 30 | ```terminal 31 | OVERVIEW: Watches over your Swift project's source 32 | 33 | Tasks (-x & -s) are executed in the order they appear. 34 | 35 | USAGE: swift watch [options] 36 | 37 | OPTIONS: 38 | -c, --clear Clear output before each execution 39 | -d, --dry-run Do not run any commands, just print them 40 | -q, --quiet Suppress output from swift-watch itself 41 | -p, --postpone Postpone initial execution until the first change 42 | -m, --monochrome Suppress coloring of output from swift-watch itself 43 | -x, --exec= Swift command(s) to execute on changes 44 | -s, --shell= Shell command(s) to execute on changes 45 | -h, --help The help menu 46 | ``` 47 | 48 | ## Roadmap 49 | 50 | - [x] Swift commands 51 | - [x] Shell commands 52 | - [x] Colorful output 53 | - [x] Console clearing 54 | - [x] Lazy mode 55 | - [x] Delayed runs 56 | - [x] Quiet mode 57 | - [x] Dry-run mode 58 | - [ ] Ignore patterns 59 | - [ ] Watch patterns 60 | 61 | ## Shout-out 62 | 63 | *swift-watch* was directly inspired by Rust's [*cargo-watch*](https://github.com/passcod/cargo-watch). 🙌🏻 64 | -------------------------------------------------------------------------------- /Sources/swift-watch/Configuration.swift: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 3 | // License, v. 2.0. If a copy of the MPL was not distributed with this 4 | 5 | import Foundation 6 | 7 | import CommandCougar 8 | 9 | struct Configuration { 10 | let clear: Bool 11 | let dryRun: Bool 12 | let quiet: Bool 13 | let postpone: Bool 14 | let monochrome: Bool 15 | 16 | let tasks: [Task] 17 | let directoryURL: URL 18 | 19 | static let clearOption = Option( 20 | flag: .both(short: "c", long: "clear"), 21 | overview: "Clear output before each execution" 22 | ) 23 | static let dryRunOption = Option( 24 | flag: .both(short: "d", long: "dry-run"), 25 | overview: "Do not run any commands, just print them" 26 | ) 27 | static let quietOption = Option( 28 | flag: .both(short: "q", long: "quiet"), 29 | overview: "Suppress output from swift-watch itself" 30 | ) 31 | static let postponeOption = Option( 32 | flag: .both(short: "p", long: "postpone"), 33 | overview: "Postpone initial execution until the first change" 34 | ) 35 | static let monochromeOption = Option( 36 | flag: .both(short: "m", long: "monochrome"), 37 | overview: "Suppress coloring of output from swift-watch itself" 38 | ) 39 | static let swiftOption = Option( 40 | flag: .both(short: "x", long: "exec"), 41 | overview: "Swift command(s) to execute on changes", 42 | parameterName: "" 43 | ) 44 | static let shellOption = Option( 45 | flag: .both(short: "s", long: "shell"), 46 | overview: "Shell command(s) to execute on changes", 47 | parameterName: "" 48 | ) 49 | } 50 | 51 | extension Configuration { 52 | init(evaluation: CommandEvaluation, directoryURL: URL) throws { 53 | let options = evaluation.options 54 | 55 | let clear = options.contains { $0.flag == Configuration.clearOption.flag } 56 | let dryRun = options.contains { $0.flag == Configuration.dryRunOption.flag } 57 | let quiet = options.contains { $0.flag == Configuration.quietOption.flag } 58 | let postpone = options.contains { $0.flag == Configuration.postponeOption.flag } 59 | let monochrome = options.contains { $0.flag == Configuration.monochromeOption.flag } 60 | let tasks: [Task] = options.compactMap { 61 | guard case let (flag, parameter?) = ($0.flag, $0.parameter) else { 62 | return nil 63 | } 64 | switch flag { 65 | case Configuration.swiftOption.flag: 66 | return .swift(parameter) 67 | case Configuration.shellOption.flag: 68 | return .shell(parameter) 69 | case _: 70 | return nil 71 | } 72 | } 73 | self.init( 74 | clear: clear, 75 | dryRun: dryRun, 76 | quiet: quiet, 77 | postpone: postpone, 78 | monochrome: monochrome, 79 | tasks: tasks, 80 | directoryURL: directoryURL 81 | ) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Sources/swift-watch/Event.swift: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 3 | // License, v. 2.0. If a copy of the MPL was not distributed with this 4 | 5 | import Foundation 6 | 7 | struct TaskError: Swift.Error, CustomStringConvertible { 8 | let statusCode: Int32 9 | 10 | var description: String { 11 | return "Status code \(self.statusCode)" 12 | } 13 | } 14 | 15 | enum TaskResult { 16 | case success 17 | case failure(TaskError) 18 | 19 | var isSuccess: Bool { 20 | switch self { 21 | case .success: return true 22 | case .failure(_): return false 23 | } 24 | } 25 | } 26 | 27 | struct TaskSuite { 28 | let tasks: [Task] 29 | 30 | init(configuration: Configuration) { 31 | self.tasks = configuration.tasks 32 | } 33 | } 34 | 35 | struct TaskSuiteReport { 36 | typealias Result = TaskResult 37 | 38 | let reports: [TaskReport] 39 | } 40 | 41 | enum Task { 42 | case swift(String) 43 | case shell(String) 44 | 45 | var invocation: String { 46 | switch self { 47 | case .swift(let command): return "swift " + command 48 | case .shell(let command): return command 49 | } 50 | } 51 | } 52 | 53 | struct TaskReport { 54 | typealias Result = TaskResult 55 | 56 | let task: Task 57 | let result: Result 58 | } 59 | 60 | enum Event { 61 | case enteredTaskSuite(TaskSuite) 62 | case exitedTaskSuite(TaskSuiteReport) 63 | case enteredTask(Task) 64 | case exitedTask(TaskReport) 65 | } 66 | -------------------------------------------------------------------------------- /Sources/swift-watch/Extensions.swift: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 3 | // License, v. 2.0. If a copy of the MPL was not distributed with this 4 | 5 | import Foundation 6 | 7 | extension Process { 8 | @discardableResult 9 | static func execute(command: String) -> Int32 { 10 | let process = Process() 11 | process.launchPath = "/usr/bin/env" 12 | process.arguments = ["-S", command] 13 | process.launch() 14 | process.waitUntilExit() 15 | return process.terminationStatus 16 | } 17 | 18 | @discardableResult 19 | static func execute(command: [String]) -> Int32 { 20 | let process = Process() 21 | process.launchPath = "/usr/bin/env" 22 | process.arguments = command 23 | process.launch() 24 | process.waitUntilExit() 25 | return process.terminationStatus 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Sources/swift-watch/Logger.swift: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 3 | // License, v. 2.0. If a copy of the MPL was not distributed with this 4 | 5 | import Foundation 6 | 7 | class Logger { 8 | let configuration: Configuration 9 | 10 | fileprivate var colored: Bool { 11 | return !self.configuration.monochrome 12 | } 13 | 14 | fileprivate var quiet: Bool { 15 | return self.configuration.quiet 16 | } 17 | 18 | fileprivate var prefix: String { 19 | return "swift-watch: " 20 | } 21 | 22 | required init(configuration: Configuration) { 23 | self.configuration = configuration 24 | } 25 | } 26 | 27 | extension Logger: RunnerObserver { 28 | func observe(event: Event) { 29 | switch event { 30 | case .enteredTaskSuite(let taskSuite): 31 | self.entered(taskSuite: taskSuite) 32 | case .exitedTaskSuite(let report): 33 | self.exited(taskSuite: report) 34 | case .enteredTask(let task): 35 | self.entered(task: task) 36 | case .exitedTask(let report): 37 | self.exited(task: report) 38 | } 39 | } 40 | } 41 | 42 | extension Logger { 43 | private func entered(taskSuite: TaskSuite) { 44 | guard !self.quiet else { 45 | return // quiet mode 46 | } 47 | print("\n" + self.prefix, terminator: "") 48 | let messageString = "Entering tasks...\n" 49 | let styledMessageString = self.colored ? messageString.lightBlue : messageString 50 | print(styledMessageString) 51 | } 52 | 53 | private func exited(taskSuite report: TaskSuiteReport) { 54 | guard !self.quiet else { 55 | return // quiet mode 56 | } 57 | print(self.prefix, terminator: "") 58 | let taskCount = report.reports.count 59 | let failure = report.reports.first { !$0.result.isSuccess }.map { $0.result } 60 | switch failure { 61 | case .some(.success), .none: 62 | let successString = "success" 63 | let messageString = "Exited \(taskCount) tasks with \(successString).\n" 64 | let styledMessageString = self.colored ? messageString.lightGreen : messageString 65 | print(styledMessageString) 66 | case .some(.failure(let error)): 67 | let failureString = error.description 68 | let messageString = "Exited \(taskCount) tasks with error: \(failureString).\n" 69 | let styledMessageString = self.colored ? messageString.lightRed : messageString 70 | print(styledMessageString) 71 | } 72 | } 73 | 74 | private func entered(task: Task) { 75 | guard !self.quiet else { 76 | return // quiet mode 77 | } 78 | print(self.prefix, terminator: "") 79 | let commandString = task.invocation 80 | let messageString = "Entering task: $ \(commandString).\n" 81 | let styledMessageString = self.colored ? messageString.lightBlue : messageString 82 | print(styledMessageString) 83 | } 84 | 85 | private func exited(task report: TaskReport) { 86 | guard !self.quiet else { 87 | return // quiet mode 88 | } 89 | print("\n" + self.prefix, terminator: "") 90 | switch report.result { 91 | case .success: 92 | let messageString = "Exited task with success.\n" 93 | let styledMessageString = self.colored ? messageString.lightGreen : messageString 94 | print(styledMessageString) 95 | case .failure(let error): 96 | let failureString = error.description 97 | let messageString = "Exited task with error: \(failureString).\n" 98 | let styledMessageString = self.colored ? messageString.lightRed : messageString 99 | print(styledMessageString) 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /Sources/swift-watch/MainLoop.swift: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 3 | // License, v. 2.0. If a copy of the MPL was not distributed with this 4 | 5 | import Foundation 6 | 7 | class MainLoop { 8 | let watcher: Watcher 9 | let runner: Runner 10 | let taskSuite: TaskSuite 11 | let configuration: Configuration 12 | 13 | private var shouldPostpone: Bool { 14 | return self.configuration.postpone 15 | } 16 | 17 | init(configuration: Configuration, observers: [RunnerObserver]) { 18 | let runner = Runner(configuration: configuration, observers: observers) 19 | let taskSuite = TaskSuite(configuration: configuration) 20 | self.watcher = Watcher(configuration: configuration) { changedURL in 21 | runner.schedule(taskSuite: taskSuite, changedURL: changedURL) 22 | } 23 | self.runner = runner 24 | self.taskSuite = taskSuite 25 | self.configuration = configuration 26 | } 27 | 28 | func start() throws { 29 | if !self.shouldPostpone { 30 | self.runner.schedule(taskSuite: self.taskSuite, changedURL: nil) 31 | } 32 | self.watcher.startWatching() 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/swift-watch/Runner.swift: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 3 | // License, v. 2.0. If a copy of the MPL was not distributed with this 4 | 5 | import Foundation 6 | 7 | import Rainbow 8 | 9 | class Runner { 10 | let configuration: Configuration 11 | let observers: [RunnerObserver] 12 | var currentTime: UInt = 0 13 | 14 | private let queue: DispatchQueue = .init(label: "swift-watch") 15 | 16 | private var isRunning: Bool = false 17 | 18 | private var isDryRun: Bool { 19 | return self.configuration.dryRun 20 | } 21 | 22 | private var shouldClear: Bool { 23 | return self.configuration.clear 24 | } 25 | 26 | init(configuration: Configuration, observers: [RunnerObserver]) { 27 | self.configuration = configuration 28 | self.observers = observers 29 | } 30 | 31 | func schedule(taskSuite: TaskSuite, changedURL: URL?) { 32 | guard !self.isRunning else { 33 | if let changedURL = changedURL { 34 | let directoryURL = self.configuration.directoryURL 35 | 36 | let directoryPath: String 37 | var filePath: String 38 | if #available(macOS 13.0, *) { 39 | directoryPath = directoryURL.path() 40 | filePath = String(changedURL.path().trimmingPrefix(directoryPath)) 41 | } else { 42 | directoryPath = directoryURL.path 43 | filePath = String(changedURL.path.dropFirst(directoryPath.count)) 44 | } 45 | 46 | print("Ignoring file change while running tasks: ./\(filePath)") 47 | } 48 | return 49 | } 50 | 51 | self.currentTime += 1 52 | let scheduleTime = self.currentTime 53 | 54 | self.queue.async { [weak self] in 55 | guard let self else { 56 | return 57 | } 58 | 59 | self.isRunning = true 60 | 61 | defer { 62 | self.isRunning = false 63 | } 64 | 65 | if self.shouldClear { 66 | Process.execute(command: "clear") 67 | } 68 | 69 | self.broadcast(event: .enteredTaskSuite(taskSuite)) 70 | 71 | var taskReports: [TaskReport] = [] 72 | for task in taskSuite.tasks { 73 | guard scheduleTime == self.currentTime else { 74 | break 75 | } 76 | let result = self.run(task: task).result 77 | let report = TaskReport(task: task, result: result) 78 | taskReports.append(report) 79 | guard result.isSuccess else { 80 | break 81 | } 82 | } 83 | 84 | let report = TaskSuiteReport(reports: taskReports) 85 | self.broadcast(event: .exitedTaskSuite(report)) 86 | } 87 | } 88 | 89 | private func run(task: Task) -> TaskReport { 90 | self.broadcast(event: .enteredTask(task)) 91 | var statusCode: Int32 = 0 92 | if self.isDryRun { 93 | statusCode = 0 94 | } else { 95 | let invocation = task.invocation 96 | statusCode = Process.execute(command: invocation) 97 | } 98 | let result: TaskResult 99 | switch statusCode { 100 | case 0: 101 | result = .success 102 | case _: 103 | result = .failure(TaskError(statusCode: statusCode)) 104 | } 105 | let report = TaskReport(task: task, result: result) 106 | self.broadcast(event: .exitedTask(report)) 107 | return report 108 | } 109 | 110 | private func broadcast(event: Event) { 111 | for observer in self.observers { 112 | observer.observe(event: event) 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /Sources/swift-watch/RunnerObserver.swift: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 3 | // License, v. 2.0. If a copy of the MPL was not distributed with this 4 | 5 | import Foundation 6 | 7 | protocol RunnerObserver { 8 | func observe(event: Event) 9 | } 10 | -------------------------------------------------------------------------------- /Sources/swift-watch/Watcher.swift: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 3 | // License, v. 2.0. If a copy of the MPL was not distributed with this 4 | 5 | import Foundation 6 | 7 | import Files 8 | import SKQueue 9 | 10 | class Watcher { 11 | enum State { 12 | case idle, busy 13 | } 14 | 15 | let configuration: Configuration 16 | private var state: State = .idle 17 | private var skipped: Bool = false 18 | private var closure: (URL) -> () 19 | private lazy var queue: SKQueue = SKQueue(delegate: self)! 20 | 21 | private var directoryURL: URL { 22 | return self.configuration.directoryURL 23 | } 24 | 25 | @discardableResult 26 | required init(configuration: Configuration, closure: @escaping (URL) -> ()) { 27 | self.configuration = configuration 28 | self.closure = closure 29 | } 30 | 31 | func startWatching() { 32 | self.recursivelyAdd(path: self.directoryURL.path) 33 | } 34 | 35 | func stopWatching() { 36 | self.queue.removeAllPaths() 37 | } 38 | 39 | private func recursivelyAdd(path: String) { 40 | let folder = try! Folder(path: path) 41 | folder.subfolders.forEach { directory in 42 | self.queue.addPath(directory.path) 43 | self.recursivelyAdd(path: directory.path) 44 | } 45 | folder.files.forEach { file in 46 | self.queue.addPath(file.path) 47 | } 48 | } 49 | } 50 | 51 | extension Watcher: SKQueueDelegate { 52 | func receivedNotification(_ notification: SKQueueNotification, path: String, queue: SKQueue) { 53 | guard !notification.intersection([.Rename, .Write, .Delete, .SizeIncrease]).isEmpty else { 54 | return 55 | } 56 | guard case .idle = self.state else { 57 | self.skipped = true 58 | return 59 | } 60 | let fileURL = URL(fileURLWithPath: path) 61 | self.state = .busy 62 | self.closure(fileURL) 63 | self.state = .idle 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Sources/swift-watch/main.swift: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 3 | // License, v. 2.0. If a copy of the MPL was not distributed with this 4 | 5 | import Foundation 6 | 7 | import CommandCougar 8 | 9 | var watchCommand = Command( 10 | name: "swift-watch", 11 | overview: """ 12 | Watches over your Swift project's source 13 | 14 | Tasks (-x & -s) are executed in the order they appear. 15 | """, 16 | callback: nil, 17 | options: [ 18 | Configuration.clearOption, 19 | Configuration.dryRunOption, 20 | Configuration.quietOption, 21 | Configuration.postponeOption, 22 | Configuration.monochromeOption, 23 | Configuration.swiftOption, 24 | Configuration.shellOption, 25 | ], 26 | parameters: [] 27 | ) 28 | 29 | let configuration: Configuration 30 | 31 | do { 32 | let arguments = CommandLine.arguments 33 | let evaluation = try watchCommand.evaluate(arguments: arguments) 34 | 35 | guard evaluation.options[Option.help.flag] == nil else { 36 | exit(0) 37 | } 38 | 39 | let directoryPath: String = FileManager.default.currentDirectoryPath 40 | let directoryURL: URL = URL(fileURLWithPath: directoryPath) 41 | 42 | configuration = try Configuration(evaluation: evaluation, directoryURL: directoryURL) 43 | } catch { 44 | print("ERROR: \(error)\n") 45 | print(watchCommand.help()) 46 | exit(0) 47 | } 48 | 49 | let observers: [RunnerObserver] = [ 50 | Logger(configuration: configuration) 51 | ] 52 | 53 | let loop = MainLoop(configuration: configuration, observers: observers) 54 | 55 | do { 56 | try loop.start() 57 | } catch let error { 58 | print("ERROR: \(error)") 59 | } 60 | 61 | RunLoop.main.run() 62 | --------------------------------------------------------------------------------