Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add progress step component #139

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open

feat: Add progress step component #139

wants to merge 8 commits into from

Conversation

pepicrft
Copy link
Contributor

@pepicrft pepicrft commented Dec 11, 2024

TL;DR

Added a new progress step component to display step-by-step execution with timing and status indicators.

What changed?

  • Added ProgressStep component that shows execution progress with optional spinner
  • Removed dependencies on Asynchrone and CombineX
  • Updated spinner implementation to use native Timer
  • Added success/error states with timing information
  • Added documentation and examples for the progress step component
  • Updated single and yes/no choice prompts to use consistent completion styling

How to test?

  1. Run the examples CLI with the progress-step command:
swift run examples-cli progress-step
  1. Observe the interactive spinner and progress updates
  2. Test both success and error scenarios
  3. Verify timing information is displayed
  4. Test in both interactive and non-interactive terminal modes

Why make this change?

To provide a consistent way to display step-by-step progress during command execution with visual feedback, timing information, and proper error handling. This improves the user experience by giving clear visibility into long-running operations.

Copy link
Contributor Author

This stack of pull requests is managed by Graphite. Learn more about stacking.

@pepicrft pepicrft changed the title Add progress step feat: Add progress step component Dec 26, 2024
@pepicrft pepicrft requested a review from fortmarek December 26, 2024 15:59
…updates; add new progress step documentation and demo GIFs
@fortmarek
Copy link
Member

@pepicrft is this ready for review? Any reason for this being a draft?

@pepicrft pepicrft marked this pull request as ready for review December 30, 2024 17:05
Copy link
Member

@fortmarek fortmarek left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks amazing 👏

Comment on lines +19 to +20
with:
commitTitleMatch: false
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be kept true – otherwise, it's easy to use the wrong message when there's just one commit

}

func runNonInteractive() async throws {
/// ℹ︎
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// ℹ︎

let terminal: Terminaling
let renderer: Rendering
let standardPipelines: StandardPipelines
var spinner: Spinning
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this a var?

import Foundation
import Rainbow

class ProgressStep {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to be a class?

}
}

// MARK: - Private
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The completionMessage is not private. It should be moved up. FWIW, I'm not a huge fan of the MARK sections and I'd use them sparingly. They often get out-of-date and they don't bring a ton of value (for me, anyways).

Comment on lines +34 to +37
) { _progress in
// _progress can be used to report progress
try await doSomething()
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I'd add an example of how to use the progress to report progress

Comment on lines +72 to +75
standardPipelines.error
.write(
content: " \("".hexIfColoredTerminal(theme.danger, terminal)) \((errorMessage ?? message).hexIfColoredTerminal(theme.muted, terminal)) \(timeString)\n"
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if static errorMessage is the best approach here.
What do you think about passing a callback in the form of (Error) -> String to transform Error into a String? But since we rethrow, it might be also fine as I'd expect the consumers to deal with that error next on their own side.

Comment on lines +63 to +85
}
} catch {
_error = error
}

let elapsedTime = Double(DispatchTime.now().uptimeNanoseconds - start.uptimeNanoseconds) / 1_000_000_000
let timeString = "[\(String(format: "%.1f", elapsedTime))s]".hexIfColoredTerminal(theme.muted, terminal)

if _error != nil {
standardPipelines.error
.write(
content: " \("".hexIfColoredTerminal(theme.danger, terminal)) \((errorMessage ?? message).hexIfColoredTerminal(theme.muted, terminal)) \(timeString)\n"
)
} else {
let message = ProgressStep
.completionMessage(successMessage ?? message, timeString: timeString, theme: theme, terminal: terminal)
standardPipelines.output.write(content: " \(message)\n")
}

// swiftlint:disable:next identifier_name
if let _error {
throw _error
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd extract the following two lines into a helped method/closure:

private func timeString(start: DispatchTime) -> String {
        let elapsedTime = Double(DispatchTime.now().uptimeNanoseconds - start.uptimeNanoseconds) / 1_000_000_000
        return "[\(String(format: "%.1f", elapsedTime))s]".hexIfColoredTerminal(theme.muted, terminal)
}

That way, we can save ourselves some of the dancing with _error

Suggested change
}
} catch {
_error = error
}
let elapsedTime = Double(DispatchTime.now().uptimeNanoseconds - start.uptimeNanoseconds) / 1_000_000_000
let timeString = "[\(String(format: "%.1f", elapsedTime))s]".hexIfColoredTerminal(theme.muted, terminal)
if _error != nil {
standardPipelines.error
.write(
content: " \("".hexIfColoredTerminal(theme.danger, terminal)) \((errorMessage ?? message).hexIfColoredTerminal(theme.muted, terminal)) \(timeString)\n"
)
} else {
let message = ProgressStep
.completionMessage(successMessage ?? message, timeString: timeString, theme: theme, terminal: terminal)
standardPipelines.output.write(content: " \(message)\n")
}
// swiftlint:disable:next identifier_name
if let _error {
throw _error
}
}
let message = ProgressStep
.completionMessage(successMessage ?? message, timeString: timeString(start: start), theme: theme, terminal: terminal)
standardPipelines.output.write(content: " \(message)\n")
} catch {
standardPipelines.error
.write(
content: " \("".hexIfColoredTerminal(theme.danger, terminal)) \((errorMessage ?? message).hexIfColoredTerminal(theme.muted, terminal)) \(timeString(start: start))\n"
)
throw error
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same goes for runInteractive – we can make the code more readable if we skip doing the _error dance.

// MARK: - Private

static func completionMessage(_ message: String, timeString: String? = nil, theme: Theme, terminal: Terminaling) -> String {
"\("✔︎".hexIfColoredTerminal(theme.success, terminal)) \(message)\(timeString != nil ? " \(timeString!)" : "")"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"\("✔︎".hexIfColoredTerminal(theme.success, terminal)) \(message)\(timeString != nil ? " \(timeString!)" : "")"
"\("✔︎".hexIfColoredTerminal(theme.success, terminal)) \(message)\(timeString ?? "")"

Comment on lines +19 to +24
message: String,
successMessage: String?,
errorMessage: String?,
showSpinner: Bool,
action: @escaping (@escaping (String) -> Void) async throws -> Void,
theme: Theme,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd keep the init for dependency injection and pass the rest of the variables through the run invocation

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants