diff --git a/Package.resolved b/Package.resolved index f0e739d..ee34bc3 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "e759c45271facbb3650829c703702a2ac4817adf75a8116cc3d77eae8e3d3bae", + "originHash" : "541a757caafbe47ca9354751ce6a36bcabcc1fb6755f04009a4f675dfe6cb0ad", "pins" : [ { "identity" : "swift-argument-parser", @@ -27,6 +27,15 @@ "revision" : "3907a9438f5b57d317001dc99f3f11b46882272b", "version" : "0.10.0" } + }, + { + "identity" : "yams", + "kind" : "remoteSourceControl", + "location" : "https://github.com/jpsim/Yams.git", + "state" : { + "revision" : "3036ba9d69cf1fd04d433527bc339dc0dc75433d", + "version" : "5.1.3" + } } ], "version" : 3 diff --git a/Package.swift b/Package.swift index 2d8303d..50f7fb6 100644 --- a/Package.swift +++ b/Package.swift @@ -15,6 +15,7 @@ let package = Package( .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.4.0"), .package(url: "https://github.com/pointfreeco/swift-tagged.git", from: "0.10.0"), .package(url: "https://github.com/pointfreeco/swift-concurrency-extras.git", from: "1.1.0"), + .package(url: "https://github.com/jpsim/Yams.git", from: "5.0.6"), ], targets: [ .executableTarget( @@ -23,6 +24,7 @@ let package = Package( .product(name: "ArgumentParser", package: "swift-argument-parser"), .product(name: "TaggedTime", package: "swift-tagged"), .product(name: "ConcurrencyExtras", package: "swift-concurrency-extras"), + .product(name: "Yams", package: "Yams"), ], swiftSettings: [ .enableExperimentalFeature("StrictConcurrency"), ] diff --git a/Sources/ActivityConfiguration.swift b/Sources/ActivityConfiguration.swift new file mode 100644 index 0000000..d9ac19c --- /dev/null +++ b/Sources/ActivityConfiguration.swift @@ -0,0 +1,41 @@ +import Foundation +import Yams +import TaggedTime + +struct ActivityStyleConfig: Codable { + let name: String + let refreshRate: UInt64 + let states: [String] + let checkmark: String? + let prompt: String? +} + +struct ActivityConfiguration { + struct StyleConfig { + let indicator: ActivityIndicator + let checkmark: String + let prompt: String + + static let defaultCheckmark = "✓" + static let defaultPrompt = ">" + } + + static func loadConfiguration(from path: String) throws -> [ActivityStyleConfig] { + let url = URL(fileURLWithPath: path) + let yamlString = try String(contentsOf: url, encoding: .utf8) + let decoder = YAMLDecoder() + return try decoder.decode([ActivityStyleConfig].self, from: yamlString) + } + + static func createStyleConfig(from config: ActivityStyleConfig) -> StyleConfig { + let configuration = ActivityIndicator.Configuration( + refreshRate: Milliseconds(config.refreshRate), + states: config.states + ) + return StyleConfig( + indicator: ActivityIndicator(configuration: configuration), + checkmark: config.checkmark ?? StyleConfig.defaultCheckmark, + prompt: config.prompt ?? StyleConfig.defaultPrompt + ) + } +} \ No newline at end of file diff --git a/Sources/ActivityIndicator+CommandArgument.swift b/Sources/ActivityIndicator+CommandArgument.swift index 5e47c57..6632966 100644 --- a/Sources/ActivityIndicator+CommandArgument.swift +++ b/Sources/ActivityIndicator+CommandArgument.swift @@ -1,23 +1,46 @@ import ArgumentParser +import TaggedTime -enum ActivityIndicatorStyle: String, CaseIterable, ExpressibleByArgument { +enum ActivityIndicatorStyle: ExpressibleByArgument { case dots - case kitt - case snake case spinner + case custom(String) + + init?(argument: String) { + switch argument { + case "dots": self = .dots + case "spinner": self = .spinner + default: self = .custom(argument) + } + } + + static var allCases: [String] { + ["dots", "spinner"] + } } extension ActivityIndicator { - static func make(style: ActivityIndicatorStyle) -> ActivityIndicator { + static func make(style: ActivityIndicatorStyle, configPath: String?) -> (indicator: ActivityIndicator, checkmark: String, prompt: String) { + let defaultConfig = (indicator: ActivityIndicator.spinner, checkmark: ActivityConfiguration.StyleConfig.defaultCheckmark, prompt: ActivityConfiguration.StyleConfig.defaultPrompt) + // Debug logging for configuration loading + print("Style: \(style), Config Path: \(configPath ?? "nil")") + if case let .custom(styleName) = style, let configPath = configPath { + do { + let configs = try ActivityConfiguration.loadConfiguration(from: configPath) + if let matchingConfig = configs.first(where: { $0.name == styleName }) { + let styleConfig = ActivityConfiguration.createStyleConfig(from: matchingConfig) + return (indicator: styleConfig.indicator, checkmark: styleConfig.checkmark, prompt: styleConfig.prompt) + } + } catch { + print("\(ANSI.yellow)[!] progressline: Failed to load custom style '\(styleName)' from config: \(error)\(ANSI.reset)") + } + } + + // Fallback to built-in styles switch style { - case .dots: - .dots - case .kitt: - .kitt - case .snake: - .snake - case .spinner: - .spinner + case .dots: return defaultConfig + case .spinner: return defaultConfig + case .custom(_): return defaultConfig } } -} +} \ No newline at end of file diff --git a/Sources/ActivityIndicator.swift b/Sources/ActivityIndicator.swift index 7534758..2dd69af 100644 --- a/Sources/ActivityIndicator.swift +++ b/Sources/ActivityIndicator.swift @@ -39,39 +39,14 @@ extension ActivityIndicator { return ActivityIndicator(configuration: configuration) }() - static let kitt: ActivityIndicator = { - let configuration = Configuration( - refreshRate: 125, - states: [ - "▰▱▱▱▱", - "▰▰▱▱▱", - "▰▰▰▱▱", - "▱▰▰▰▱", - "▱▱▰▰▰", - "▱▱▱▰▰", - "▱▱▱▱▰", - "▱▱▱▰▰", - "▱▱▰▰▰", - "▱▰▰▰▱", - "▰▰▰▱▱", - "▰▰▱▱▱", - ] - ) - return ActivityIndicator(configuration: configuration) - }() - - static let snake: ActivityIndicator = { + static let spinner: ActivityIndicator = { let configuration = Configuration( refreshRate: 125, states: [ - "▰▱▱▱▱", - "▰▰▱▱▱", - "▰▰▰▱▱", - "▱▰▰▰▱", - "▱▱▰▰▰", - "▱▱▱▰▰", - "▱▱▱▱▰", - "▱▱▱▱▱", + "\\", + "|", + "/", + "-", ] ) return ActivityIndicator(configuration: configuration) diff --git a/Sources/ProgressLine.swift b/Sources/ProgressLine.swift index 27ea90e..71d60ae 100644 --- a/Sources/ProgressLine.swift +++ b/Sources/ProgressLine.swift @@ -15,8 +15,11 @@ struct ProgressLine: AsyncParsableCommand { @Option(name: [.long, .customShort("t")], help: "The static text to display instead of the latest stdin data.") var staticText: String? - @Option(name: [.customLong("activity-style"), .customShort("s")], help: "The style of the activity indicator.") - var activityIndicatorStyle: ActivityIndicatorStyle = .dots + @Option(name: [.customLong("activity-style"), .customShort("s")], help: "The style of the activity indicator (built-in or custom).") + var activityIndicatorStyle: ActivityIndicatorStyle = .spinner + + @Option(name: [.customLong("config-path"), .customShort("c")], help: "Path to the activity styles configuration file.") + var configPath: String? @Option(name: [.customLong("original-log-path"), .customShort("l")], help: "Save the original log to a file.") var originalLogPath: String? @@ -45,23 +48,30 @@ struct ProgressLine: AsyncParsableCommand { let logger = AboveProgressLineLogger(printers: printers) #if DEBUG - let activityIndicator: ActivityIndicator = testMode ? .disabled() : .make(style: activityIndicatorStyle) + let (indicator, checkmark, prompt) = testMode ? + (ActivityIndicator.disabled(), "✓", ">") : + ActivityIndicator.make(style: activityIndicatorStyle, configPath: configPath) #else let testMode = false - let activityIndicator: ActivityIndicator = .make(style: activityIndicatorStyle) + let (indicator, checkmark, prompt) = ActivityIndicator.make(style: activityIndicatorStyle, configPath: configPath) #endif + let progressLineController = await ProgressLineController.buildAndStart( textMode: staticText.map { .staticText($0) } ?? .stdin, printers: printers, logger: logger, - activityIndicator: activityIndicator, + activityIndicator: indicator, + checkmark: checkmark, + prompt: prompt, mockActivityAndDuration: testMode ) + let originalLogController = if let originalLogPath { await OriginalLogController(logger: logger, path: originalLogPath) } else { OriginalLogController?.none } + let matchesController = await MatchesController(logger: logger, regexps: matchesToLog) let logAllController = shouldLogAll ? LogAllController(logger: logger) : nil diff --git a/Sources/ProgressLineController.swift b/Sources/ProgressLineController.swift index 53cb43c..897d7ac 100644 --- a/Sources/ProgressLineController.swift +++ b/Sources/ProgressLineController.swift @@ -39,6 +39,8 @@ final actor ProgressLineController { printers: PrintersHolder, logger: AboveProgressLineLogger, activityIndicator: ActivityIndicator, + checkmark: String, + prompt: String, mockActivityAndDuration: Bool = false ) async -> Self { let progressTracker = ProgressTracker.start() @@ -46,7 +48,9 @@ final actor ProgressLineController { let progressLineFormatter = ProgressLineFormatter( activityIndicator: activityIndicator, windowSizeObserver: windowSizeObserver, - mockActivityAndDuration: mockActivityAndDuration + mockActivityAndDuration: mockActivityAndDuration, + checkmark: checkmark, + prompt: prompt ) let controller = Self( diff --git a/Sources/ProgressLineFormatter.swift b/Sources/ProgressLineFormatter.swift index 113f0f3..1802483 100644 --- a/Sources/ProgressLineFormatter.swift +++ b/Sources/ProgressLineFormatter.swift @@ -21,15 +21,23 @@ final class ProgressLineFormatter: Sendable { private let activityIndicator: ActivityIndicator private let windowSizeObserver: WindowSizeObserver? private let mockActivityAndDuration: Bool + private let checkmark: String + private let prompt: String init( activityIndicator: ActivityIndicator, windowSizeObserver: WindowSizeObserver?, - mockActivityAndDuration: Bool + mockActivityAndDuration: Bool, + checkmark: String, + prompt: String ) { self.activityIndicator = activityIndicator self.windowSizeObserver = windowSizeObserver self.mockActivityAndDuration = mockActivityAndDuration + self.checkmark = checkmark + self.prompt = prompt + // Debug logging for symbols + print("Initialized ProgressLineFormatter with checkmark: '\(checkmark)' and prompt: '\(prompt)'") } func inProgress(progress: Progress) -> String { @@ -38,7 +46,7 @@ final class ProgressLineFormatter: Sendable { let styledActivityIndicator = ANSI.blue + activityIndicator + ANSI.reset let styledDuration = ANSI.bold + formattedDuration + ANSI.reset - let styledPrompt = ANSI.blue + Symbol.prompt + ANSI.reset + let styledPrompt = ANSI.blue + prompt + ANSI.reset return buildResultString( styledActivityIndicator: styledActivityIndicator, @@ -51,9 +59,9 @@ final class ProgressLineFormatter: Sendable { func finished(progress: Progress?) -> String { let formattedDuration = mockActivityAndDuration ? "" : progress.map { formatDuration(from: $0.duration) } - let styledActivityIndicator = ANSI.green + Symbol.checkmark + ANSI.reset + let styledActivityIndicator = ANSI.green + checkmark + ANSI.reset let styledDuration = formattedDuration.map { ANSI.bold + $0 + ANSI.reset } - let styledPrompt = ANSI.green + Symbol.prompt + ANSI.reset + let styledPrompt = ANSI.green + prompt + ANSI.reset return buildResultString( styledActivityIndicator: styledActivityIndicator, diff --git a/custom-styles.yml b/custom-styles.yml new file mode 100644 index 0000000..87bba89 --- /dev/null +++ b/custom-styles.yml @@ -0,0 +1,89 @@ +# Knightrider +- name: kitt # Custom style name + checkmark: "✔" # Custom checkmark + prompt: ">" # Custom prompt + refreshRate: 125 # Custom refresh in miliseconds + states: + - "▰▱▱▱▱" + - "▰▰▱▱▱" + - "▰▰▰▱▱" + - "▱▰▰▰▱" + - "▱▱▰▰▰" + - "▱▱▱▰▰" + - "▱▱▱▱▰" + - "▱▱▱▰▰" + - "▱▱▰▰▰" + - "▱▰▰▰▱" + - "▰▰▰▱▱" + - "▰▰▱▱▱" + +# Snake +- name: snake + checkmark: "✔" + prompt: ">" + refreshRate: 125 + states: + - "▰▱▱▱▱" + - "▰▰▱▱▱" + - "▰▰▰▱▱" + - "▱▰▰▰▱" + - "▱▱▰▰▰" + - "▱▱▱▰▰" + - "▱▱▱▱▰" + - "▱▱▱▱▱" + +# Built-in like animation with hearts +- name: hearts + checkmark: "❤️" + prompt: "💘" + refreshRate: 125 + states: + - 💗 + - 💓 + - 💝 + - 💓 + +# Arrow animation with longer refresh rate +- name: arrows + checkmark: "✔" + prompt: ">" + refreshRate: 150 + states: + - "→ " + - "⇒ " + - "⟹ " + - "⇒ " + +# Blocks progressing +- name: blocks + checkmark: "✔" + prompt: ">" + refreshRate: 100 + states: + - "▒ " + - "▓ " + - "█ " + - "█░ " + - "█▒ " + - "█▓ " + - "██ " + - "██░" + - "██▒" + - "██▓" + - "███" + - "░ " + +# Rotating cross +- name: cross + checkmark: "✔" + prompt: ">" + refreshRate: 100 + states: + - "╔" + - "╦" + - "╗" + - "╣" + - "╝" + - "╩" + - "╚" + - "╠"