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