Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 19 additions & 6 deletions cli/Sources/Noora/Components/TextPrompt.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ struct TextPrompt {
let title: TerminalText?
let prompt: TerminalText
let description: TerminalText?
let defaultValue: String?
let theme: Theme
let content: Content
let terminal: Terminaling
Expand Down Expand Up @@ -63,7 +64,14 @@ struct TextPrompt {

logger?.debug("Prompted '\(prompt.plain())'")

let validationResult = validator.validate(input: input, rules: validationRules)
let resolvedInput: String
if input.isEmpty, let defaultValue {
resolvedInput = defaultValue
} else {
resolvedInput = input
}

let validationResult = validator.validate(input: resolvedInput, rules: validationRules)

switch validationResult {
case .success:
Expand All @@ -72,11 +80,11 @@ struct TextPrompt {
return run(errors: error.errors)
}

renderResult(input: input)
renderResult(input: resolvedInput)

logger?.debug("Responded \(input) to prompt '\(prompt.plain())'")
logger?.debug("Responded \(resolvedInput) to prompt '\(prompt.plain())'")

return input
return resolvedInput
}

private func render(input: String, withCursor: Bool = true, errors: [ValidatableError] = []) {
Expand All @@ -88,9 +96,14 @@ struct TextPrompt {
.boldIfColoredTerminal(terminal)
}

let input = "\(input)\(withCursor ? "█" : "")".hexIfColoredTerminal(theme.secondary, terminal)
let inputDisplay = "\(input)\(withCursor ? "█" : "")".hexIfColoredTerminal(theme.secondary, terminal)

message += "\(title != nil ? "\n" : "")\(titleOffset)\(prompt.formatted(theme: theme, terminal: terminal)) \(input)"
message += "\(title != nil ? "\n" : "")\(titleOffset)\(prompt.formatted(theme: theme, terminal: terminal)) \(inputDisplay)"

if input.isEmpty, let defaultValue, withCursor {
let defaultHint = "Press Enter to use \(defaultValue)".hexIfColoredTerminal(theme.muted, terminal)
message += "\n\(titleOffset)\(defaultHint)"
}

if !errors.isEmpty {
var errorMessage = "\(content.textPromptValidationErrorsTitle):\n\(titleOffset)"
Expand Down
11 changes: 9 additions & 2 deletions cli/Sources/Noora/Noora.swift
Original file line number Diff line number Diff line change
Expand Up @@ -204,14 +204,17 @@ public protocol Noorable: Sendable {
/// - title: The thing the user is being prompted for.
/// - prompt: The prompt message.
/// - description: An optional description to clarify what the prompt is for.
/// - collapseOnSelection: Whether the prompt should be collapsed on answered.
/// - defaultValue: An optional default value. When provided and the user submits empty input,
/// this value is returned. A hint is shown below the prompt while input is empty.
/// - collapseOnAnswer: Whether the prompt should be collapsed on answered.
/// - renderer: A rendering interface that holds the UI state.
/// - validationRules: An array of rules used for input validation.
/// - Returns: The user's response.
/// - Returns: The user's response, or the default value if the input was empty.
func textPrompt(
title: TerminalText?,
prompt: TerminalText,
description: TerminalText?,
defaultValue: String?,
collapseOnAnswer: Bool,
renderer: Rendering,
validationRules: [ValidatableRule]
Expand Down Expand Up @@ -618,6 +621,7 @@ public final class Noora: Noorable {
title: TerminalText?,
prompt: TerminalText,
description: TerminalText?,
defaultValue: String?,
collapseOnAnswer: Bool,
renderer: Rendering,
validationRules: [ValidatableRule]
Expand All @@ -626,6 +630,7 @@ public final class Noora: Noorable {
title: title,
prompt: prompt,
description: description,
defaultValue: defaultValue,
theme: theme,
content: content,
terminal: terminal,
Expand Down Expand Up @@ -1219,6 +1224,7 @@ extension Noorable {
title: TerminalText? = nil,
prompt: TerminalText,
description: TerminalText? = nil,
defaultValue: String? = nil,
collapseOnAnswer: Bool = true,
renderer: Rendering = Renderer(),
validationRules: [ValidatableRule] = []
Expand All @@ -1227,6 +1233,7 @@ extension Noorable {
title: title,
prompt: prompt,
description: description,
defaultValue: defaultValue,
collapseOnAnswer: collapseOnAnswer,
renderer: renderer,
validationRules: validationRules
Expand Down
2 changes: 2 additions & 0 deletions cli/Sources/Noora/NooraMock.swift
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,7 @@
title: TerminalText?,
prompt: TerminalText,
description: TerminalText?,
defaultValue: String?,
collapseOnAnswer: Bool,
renderer: Rendering,
validationRules: [ValidatableRule]
Expand All @@ -275,6 +276,7 @@
title: title,
prompt: prompt,
description: description,
defaultValue: defaultValue,
collapseOnAnswer: collapseOnAnswer,
renderer: renderer,
validationRules: validationRules
Expand Down
7 changes: 2 additions & 5 deletions cli/Tests/NooraTests/Components/TableTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -128,11 +128,8 @@ struct TableTests {
])

let updates = AsyncStream<TableData> { continuation in
Task {
try await Task.sleep(for: .milliseconds(20))
continuation.yield(updatedData)
continuation.finish()
}
continuation.yield(updatedData)
continuation.finish()
}

let standardOutput = MockStandardPipeline()
Expand Down
67 changes: 67 additions & 0 deletions cli/Tests/NooraTests/Components/TextPromptTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ struct TextPromptTests {
title: "Project",
prompt: "How would you like to name your project?",
description: "The generated project will take this name",
defaultValue: nil,
theme: .test(),
content: .default,
terminal: terminal,
Expand Down Expand Up @@ -87,6 +88,7 @@ struct TextPromptTests {
title: nil,
prompt: "How would you like to name your project?",
description: "The generated project will take this name",
defaultValue: nil,
theme: .test(),
content: .default,
terminal: terminal,
Expand Down Expand Up @@ -146,4 +148,69 @@ struct TextPromptTests {
""")
#expect(validator.invokedValidateInputRulesCount == 1)
}

@Test func returns_default_value_when_input_is_empty() {
// Given
let subject = TextPrompt(
title: "Name",
prompt: "How would you like to name the project?",
description: nil,
defaultValue: "my-project",
theme: .test(),
content: .default,
terminal: terminal,
collapseOnAnswer: true,
renderer: renderer,
standardPipelines: StandardPipelines(),
logger: nil,
validationRules: [],
validator: validator
)
terminal.characters = ["\n"]

// When
let result = subject.run()

// Then
#expect(result == "my-project")
var renders = Array(renderer.renders.reversed())
#expect(renders.popLast() == """
Name
How would you like to name the project? █
Press Enter to use my-project
""")
#expect(renders.popLast() == """
Name
How would you like to name the project? \("")
""")
#expect(renders.popLast() == """
✔︎ Name: my-project\(" ")
""")
}

@Test func uses_typed_input_over_default_value() {
// Given
let subject = TextPrompt(
title: "Name",
prompt: "How would you like to name the project?",
description: nil,
defaultValue: "my-project",
theme: .test(),
content: .default,
terminal: terminal,
collapseOnAnswer: true,
renderer: renderer,
standardPipelines: StandardPipelines(),
logger: nil,
validationRules: [],
validator: validator
)
terminal.characters = ["F", "o", "o", "\n"]

// When
let result = subject.run()

// Then
#expect(result == "Foo")
}
}