Skip to content

Commit

Permalink
feat: Add initial progress bar
Browse files Browse the repository at this point in the history
  • Loading branch information
M7md-Ebrahim committed Jan 10, 2025
1 parent cabc528 commit 086bfc2
Show file tree
Hide file tree
Showing 5 changed files with 217 additions and 1 deletion.
106 changes: 106 additions & 0 deletions Sources/Noora/Components/CLIProgressBar.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import Foundation
import Rainbow


class CLIProgressBar {

let message: String
let successMessage: String?
let errorMessage: String?
let total: Int
let action: (@escaping (String) -> Void) async throws -> Void
let theme: Theme
let terminal: Terminaling
let renderer: Rendering
let standardPipelines: StandardPipelines
var progressBar: ProgressBar

init(
message: String,
successMessage: String?,
errorMessage: String?,
total: Int,
action: @escaping (@escaping (String) -> Void) async throws -> Void,
theme: Theme,
terminal: Terminaling,
renderer: Rendering,
standardPipelines: StandardPipelines,
progressBar: ProgressBar = DefaultProgressBar()
) {
self.message = message
self.successMessage = successMessage
self.errorMessage = errorMessage
self.total = total
self.action = action
self.theme = theme
self.terminal = terminal
self.renderer = renderer
self.standardPipelines = standardPipelines
self.progressBar = progressBar
}

func run() async throws {
if terminal.isInteractive {
try await runInteractive()
} else {
try await runNonInteractive()
}
}

func runInteractive() async throws {

var bar: String = ""
var progressPercentage = 0
var lastMessage = message

progressBar.startProgress(total: total, interval: 0.05) { progressBarState, percentage in
bar = progressBarState
progressPercentage = percentage
self.render(lastMessage, bar, progressPercentage)
}

var _error: Error?
do {
self.render(lastMessage, bar, progressPercentage)
try await action { progressMessage in
lastMessage = progressMessage
self.render(lastMessage, bar, progressPercentage)
}
} catch {
_error = error
}


if _error != nil {
renderer.render(
"\("".hexIfColoredTerminal(theme.danger, terminal)) \(errorMessage ?? message)",
standardPipeline: standardPipelines.error
)
} else {
renderer.render(
CLIProgressBar
.completionMessage(lastMessage, bar, progressPercentage, theme: theme, terminal: terminal),
standardPipeline: standardPipelines.output
)
}

if let _error {
throw _error
}
}
// TODO: Implement runNonInteractive logic
func runNonInteractive() async throws {

}

static func completionMessage(_ message: String, _ bar: String, _ percentage: Int, theme: Theme, terminal: Terminaling) -> String {
"\(message) \(bar.hexIfColoredTerminal(theme.success, terminal)) \(percentage)% | Completed"
}

private func render(_ message: String, _ bar: String, _ percentage: Int) {
renderer.render(
"\(message) \(bar.hexIfColoredTerminal(theme.primary, terminal)) \(percentage)% |",
standardPipeline: standardPipelines.output
)
}
}
29 changes: 29 additions & 0 deletions Sources/Noora/Noora.swift
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,15 @@ public protocol Noorable {
/// - Parameters:
/// - alerts: The warning messages.
func warning(_ alerts: WarningAlert...)

///
func progressBar(
message: String,
successMessage: String?,
errorMessage: String?,
total: Int,
action: @escaping ((String) -> Void) async throws -> Void
) async throws
}

public struct Noora: Noorable {
Expand Down Expand Up @@ -200,4 +209,24 @@ public struct Noora: Noorable {
theme: theme
).run()
}
public func progressBar(
message: String,
successMessage: String? = nil,
errorMessage: String? = nil,
total: Int,
action: @escaping ((String) -> Void) async throws -> Void
) async throws {
let progressBar = CLIProgressBar(
message: message,
successMessage: successMessage,
errorMessage: errorMessage,
total: total,
action: action,
theme: theme,
terminal: terminal,
renderer: Renderer(),
standardPipelines: StandardPipelines()
)
try await progressBar.run()
}
}
53 changes: 53 additions & 0 deletions Sources/Noora/Utilities/ProgressBar.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import Foundation

protocol ProgressBar {
func startProgress(total: Int, interval: TimeInterval?, block: @escaping (String, Int) -> Void)
func stop()
}
class DefaultProgressBar: ProgressBar {
private static let complete = ""
private static let incomplete = ""
private static let width = 30

private var isLoading = true
private var timer: Timer?
private var completed = 0
private var progressPercent = 0

func startProgress(total: Int, interval: TimeInterval? , block: @escaping (String, Int) -> Void) {
isLoading = true

DispatchQueue.global(qos: .userInitiated).async {
let runLoop = RunLoop.current
var index = 1

// Schedule the timer in the current run loop
self.timer = Timer.scheduledTimer(withTimeInterval: interval ?? 0.05, repeats: true) { _ in
if index <= total {
self.update(total, index)
let completedBar = String(repeating: DefaultProgressBar.complete, count: self.completed)
let incompleteBar = String(repeating: DefaultProgressBar.incomplete, count: DefaultProgressBar.width - self.completed)
block(completedBar+incompleteBar, self.progressPercent)
index += 1
} else {
self.timer?.invalidate()
}
}

// Start the run loop to allow the timer to fire
while self.isLoading, runLoop.run(mode: .default, before: .distantFuture) {}
}
}

private func update(_ total: Int, _ index: Int) {
let percentage = Double(index) / Double(total)
completed = Int(percentage * Double(DefaultProgressBar.width))
progressPercent = Int(percentage * 100)
}

func stop() {
isLoading = false
timer?.invalidate()
timer = nil
}
}
28 changes: 28 additions & 0 deletions Sources/examples-cli/Commands/ProgressBarCommand.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import ArgumentParser
import Foundation
import Noora

struct ProgressBarCommand: AsyncParsableCommand {
static let configuration = CommandConfiguration(
commandName: "progress-bar",
abstract: "A component to shows a progress bar"
)
func run() async throws {
try await Noora().progressBar(
message: "Loading",
successMessage: "Manifests loaded",
errorMessage: "Failed to load manifests",
total: 100
) { _ in
try await Task.sleep(nanoseconds: 5_000_000_000)
}
try await Noora().progressBar(
message: "Loading",
successMessage: "Manifests loaded",
errorMessage: "Failed to load manifests",
total: 200
) { _ in
try await Task.sleep(nanoseconds: 10_000_000_000)
}
}
}
2 changes: 1 addition & 1 deletion Sources/examples-cli/ExamplesCLI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@ import Rainbow
struct ExamplesCLI: AsyncParsableCommand {
static let configuration = CommandConfiguration(
abstract: "A command line tool to test the different components available in Noora.",
subcommands: [SingleChoicePromptCommand.self, YesOrNoChoicePromptCommand.self, AlertCommand.self]
subcommands: [SingleChoicePromptCommand.self, YesOrNoChoicePromptCommand.self, AlertCommand.self, ProgressBarCommand.self]
)
}

0 comments on commit 086bfc2

Please sign in to comment.