Skip to content
This repository has been archived by the owner on Jul 16, 2024. It is now read-only.

Commit

Permalink
Add final confirmation to run command during wizard mode (#395)
Browse files Browse the repository at this point in the history
  • Loading branch information
IMax153 authored Nov 30, 2023
1 parent 6d6259e commit ca45e4f
Show file tree
Hide file tree
Showing 7 changed files with 137 additions and 57 deletions.
16 changes: 9 additions & 7 deletions examples/naval-fate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ const speedOption = Options.integer("speed").pipe(

const shipCommand = Command.make("ship", {
verbose: Options.boolean("verbose")
})
}).pipe(Command.withDescription("Controls a ship in Naval Fate"))

const newShipCommand = Command.make("new", {
name: nameArg
Expand All @@ -46,7 +46,7 @@ const newShipCommand = Command.make("new", {
if (verbose) {
yield* _(Console.log(`Verbose mode enabled`))
}
}))
})).pipe(Command.withDescription("Create a new ship"))

const moveShipCommand = Command.make("move", {
...nameAndCoordinatesArg,
Expand All @@ -55,7 +55,7 @@ const moveShipCommand = Command.make("move", {
Effect.gen(function*(_) {
yield* _(moveShip(name, x, y))
yield* _(Console.log(`Moving ship '${name}' to coordinates (${x}, ${y}) at ${speed} knots`))
}))
})).pipe(Command.withDescription("Move a ship"))

const shootShipCommand = Command.make(
"shoot",
Expand All @@ -65,9 +65,11 @@ const shootShipCommand = Command.make(
yield* _(shoot(x, y))
yield* _(Console.log(`Shot cannons at coordinates (${x}, ${y})`))
})
)
).pipe(Command.withDescription("Shoot from a ship"))

const mineCommand = Command.make("mine")
const mineCommand = Command.make("mine").pipe(
Command.withDescription("Controls mines in Naval Fate")
)

const setMineCommand = Command.make("set", {
...coordinatesArg,
Expand All @@ -78,15 +80,15 @@ const setMineCommand = Command.make("set", {
yield* _(
Console.log(`Set ${moored ? "moored" : "drifting"} mine at coordinates (${x}, ${y})`)
)
}))
})).pipe(Command.withDescription("Set a mine at specific coordinates"))

const removeMineCommand = Command.make("remove", {
...coordinatesArg
}, ({ x, y }) =>
Effect.gen(function*(_) {
yield* _(removeMine(x, y))
yield* _(Console.log(`Removing mine at coordinates (${x}, ${y}), if present`))
}))
})).pipe(Command.withDescription("Remove a mine at specific coordinates"))

const run = Command.make("naval_fate").pipe(
Command.withDescription("An implementation of the Naval Fate CLI application."),
Expand Down
108 changes: 77 additions & 31 deletions src/internal/args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -610,37 +610,39 @@ const validateInternal = (
}
case "Single": {
return Effect.suspend(() => {
if (ReadonlyArray.isNonEmptyReadonlyArray(args)) {
const head = ReadonlyArray.headNonEmpty(args)
const tail = ReadonlyArray.tailNonEmpty(args)
return InternalPrimitive.validate(self.primitiveType, Option.some(head), config).pipe(
Effect.mapBoth({
onFailure: (text) => InternalValidationError.invalidArgument(InternalHelpDoc.p(text)),
onSuccess: (a) => [tail, a] as [ReadonlyArray<string>, any]
})
)
}
const choices = InternalPrimitive.getChoices(self.primitiveType)
if (Option.isSome(self.pseudoName) && Option.isSome(choices)) {
return Effect.fail(InternalValidationError.missingValue(InternalHelpDoc.p(
`Missing argument <${self.pseudoName.value}> with choices ${choices.value}`
)))
}
if (Option.isSome(self.pseudoName)) {
return Effect.fail(InternalValidationError.missingValue(InternalHelpDoc.p(
`Missing argument <${self.pseudoName.value}>`
)))
}
if (Option.isSome(choices)) {
return Effect.fail(InternalValidationError.missingValue(InternalHelpDoc.p(
`Missing argument ${
InternalPrimitive.getTypeName(self.primitiveType)
} with choices ${choices.value}`
)))
}
return Effect.fail(InternalValidationError.missingValue(InternalHelpDoc.p(
`Missing argument ${InternalPrimitive.getTypeName(self.primitiveType)}`
)))
return ReadonlyArray.matchLeft(args, {
onEmpty: () => {
const choices = InternalPrimitive.getChoices(self.primitiveType)
if (Option.isSome(self.pseudoName) && Option.isSome(choices)) {
return Effect.fail(InternalValidationError.missingValue(InternalHelpDoc.p(
`Missing argument <${self.pseudoName.value}> with choices ${choices.value}`
)))
}
if (Option.isSome(self.pseudoName)) {
return Effect.fail(InternalValidationError.missingValue(InternalHelpDoc.p(
`Missing argument <${self.pseudoName.value}>`
)))
}
if (Option.isSome(choices)) {
return Effect.fail(InternalValidationError.missingValue(InternalHelpDoc.p(
`Missing argument ${
InternalPrimitive.getTypeName(self.primitiveType)
} with choices ${choices.value}`
)))
}
return Effect.fail(InternalValidationError.missingValue(InternalHelpDoc.p(
`Missing argument ${InternalPrimitive.getTypeName(self.primitiveType)}`
)))
},
onNonEmpty: (head, tail) =>
InternalPrimitive.validate(self.primitiveType, Option.some(head), config).pipe(
Effect.mapBoth({
onFailure: (text) =>
InternalValidationError.invalidArgument(InternalHelpDoc.p(text)),
onSuccess: (a) => [tail, a] as [ReadonlyArray<string>, any]
})
)
})
})
}
case "Map": {
Expand Down Expand Up @@ -872,3 +874,47 @@ export const getFishCompletions = (self: Instruction): ReadonlyArray<string> =>
}
}
}

interface ZshCompletionState {
readonly multiple: boolean
readonly optional: boolean
}

export const getZshCompletions = (
self: Instruction,
state: ZshCompletionState = { multiple: false, optional: false }
): ReadonlyArray<string> => {
switch (self._tag) {
case "Empty": {
return ReadonlyArray.empty()
}
case "Single": {
const multiple = state.multiple ? "*" : ""
const optional = state.optional ? "::" : ":"
const shortDescription = getShortDescription(self)
const description = shortDescription.length > 0 ? ` -- ${shortDescription}` : ""
const possibleValues = InternalPrimitive.getZshCompletions(
self.primitiveType as InternalPrimitive.Instruction
)
return possibleValues.length === 0
? ReadonlyArray.empty()
: ReadonlyArray.of(`${multiple}${optional}${self.name}${description}${possibleValues}`)
}
case "Map": {
return getZshCompletions(self.args as Instruction, state)
}
case "Both": {
const left = getZshCompletions(self.left as Instruction, state)
const right = getZshCompletions(self.right as Instruction, state)
return ReadonlyArray.appendAll(left, right)
}
case "Variadic": {
return Option.isSome(self.max) && self.max.value > 1
? getZshCompletions(self.args as Instruction, { ...state, multiple: true })
: getZshCompletions(self.args as Instruction, state)
}
case "WithDefault": {
return getZshCompletions(self.args as Instruction, { ...state, optional: true })
}
}
}
5 changes: 4 additions & 1 deletion src/internal/builtInOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,10 @@ export const completionsOptions: Options.Options<
["bash", "bash" as const],
["fish", "fish" as const],
["zsh", "zsh" as const]
]).pipe(InternalOptions.optional)
]).pipe(
InternalOptions.optional,
InternalOptions.withDescription("Generate a completion script for a specific shell")
)

/** @internal */
export const helpOptions: Options.Options<boolean> = InternalOptions.boolean("help").pipe(
Expand Down
37 changes: 29 additions & 8 deletions src/internal/cliApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import * as InternalCliConfig from "./cliConfig.js"
import * as InternalCommand from "./commandDescriptor.js"
import * as InternalHelpDoc from "./helpDoc.js"
import * as InternalSpan from "./helpDoc/span.js"
import * as InternalTogglePrompt from "./prompt/toggle.js"
import * as InternalUsage from "./usage.js"
import * as InternalValidationError from "./validationError.js"

Expand Down Expand Up @@ -89,13 +90,13 @@ export const run = dual<
Effect.catchSome((e) =>
InternalValidationError.isValidationError(e) &&
InternalValidationError.isHelpRequested(e)
? Option.some(handleBuiltInOption(self, e.showHelp, config))
? Option.some(handleBuiltInOption(self, e.showHelp, execute, config))
: Option.none()
)
)
}
case "BuiltIn": {
return handleBuiltInOption(self, directive.option, config).pipe(
return handleBuiltInOption(self, directive.option, execute, config).pipe(
Effect.catchSome((e) =>
InternalValidationError.isValidationError(e)
? Option.some(Effect.zipRight(printDocs(e.error), Effect.fail(e)))
Expand All @@ -115,13 +116,18 @@ export const run = dual<
const printDocs = (error: HelpDoc.HelpDoc): Effect.Effect<never, never, void> =>
Console.log(InternalHelpDoc.toAnsiText(error))

const handleBuiltInOption = <A>(
// TODO: move to `/platform`
const isQuitException = (u: unknown): u is Terminal.QuitException =>
typeof u === "object" && u != null && "_tag" in u && u._tag === "QuitException"

const handleBuiltInOption = <R, E, A>(
self: CliApp.CliApp<A>,
builtIn: BuiltInOptions.BuiltInOptions,
execute: (a: A) => Effect.Effect<R, E, void>,
config: CliConfig.CliConfig
): Effect.Effect<
CliApp.CliApp.Environment | Terminal.Terminal,
ValidationError.ValidationError,
R | CliApp.CliApp.Environment | Terminal.Terminal,
E | ValidationError.ValidationError,
void
> => {
switch (builtIn._tag) {
Expand Down Expand Up @@ -225,9 +231,24 @@ const handleBuiltInOption = <A>(
return Console.log(text).pipe(
Effect.zipRight(InternalCommand.wizard(builtIn.command, programName, config)),
Effect.tap((args) => Console.log(InternalHelpDoc.toAnsiText(renderWizardArgs(args)))),
Effect.catchTag("QuitException", () => {
const message = InternalHelpDoc.p(InternalSpan.error("\n\nQuitting wizard mode..."))
return Console.log(InternalHelpDoc.toAnsiText(message))
Effect.flatMap((args) =>
InternalTogglePrompt.toggle({
message: "Would you like to run the command?",
initial: true,
active: "yes",
inactive: "no"
}).pipe(Effect.flatMap((shouldRunCommand) =>
shouldRunCommand
? Console.log().pipe(Effect.zipRight(run(self, args.slice(1), execute)))
: Effect.unit
))
),
Effect.catchAll((e) => {
if (isQuitException(e)) {
const message = InternalHelpDoc.p(InternalSpan.error("\n\nQuitting wizard mode..."))
return Console.log(InternalHelpDoc.toAnsiText(message))
}
return Effect.fail(e)
})
)
}
Expand Down
8 changes: 8 additions & 0 deletions src/internal/commandDescriptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1382,14 +1382,20 @@ const getZshSubcommandCases = (
const options = isStandard(self)
? InternalOptions.all([InternalBuiltInOptions.builtIns, self.options])
: InternalBuiltInOptions.builtIns
const args = isStandard(self) ? self.args : InternalArgs.none
const optionCompletions = pipe(
InternalOptions.getZshCompletions(options as InternalOptions.Instruction),
ReadonlyArray.map((completion) => `'${completion}' \\`)
)
const argCompletions = pipe(
InternalArgs.getZshCompletions(args as InternalArgs.Instruction),
ReadonlyArray.map((completion) => `'${completion}' \\`)
)
if (ReadonlyArray.isEmptyReadonlyArray(parentCommands)) {
return [
"_arguments \"${_arguments_options[@]}\" \\",
...indentAll(optionCompletions, 4),
...indentAll(argCompletions, 4),
` ":: :_${self.name}_commands" \\`,
` "*::: :->${self.name}" \\`,
" && ret=0"
Expand All @@ -1400,6 +1406,7 @@ const getZshSubcommandCases = (
`(${self.name})`,
"_arguments \"${_arguments_options[@]}\" \\",
...indentAll(optionCompletions, 4),
...indentAll(argCompletions, 4),
" && ret=0",
";;"
]
Expand All @@ -1408,6 +1415,7 @@ const getZshSubcommandCases = (
`(${self.name})`,
"_arguments \"${_arguments_options[@]}\" \\",
...indentAll(optionCompletions, 4),
...indentAll(argCompletions, 4),
` ":: :_${ReadonlyArray.append(parentCommands, self.name).join("__")}_commands" \\`,
` "*::: :->${self.name}" \\`,
" && ret=0"
Expand Down
10 changes: 5 additions & 5 deletions test/snapshots/fish-completions
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
[
"complete -c forge -n \"__fish_use_subcommand\" -l completions -r -f -a \"{sh'',bash'',fish'',zsh''}\"",
"complete -c forge -n \"__fish_use_subcommand\" -l completions -r -f -a \"{sh'',bash'',fish'',zsh''}\" -d 'Generate a completion script for a specific shell'",
"complete -c forge -n \"__fish_use_subcommand\" -s h -l help -d 'Show the help documentation for a command'",
"complete -c forge -n \"__fish_use_subcommand\" -l wizard -d 'Start wizard mode for a command'",
"complete -c forge -n \"__fish_use_subcommand\" -l version -d 'Show the version of the application'",
"complete -c forge -n \"__fish_use_subcommand\" -f -a \"cache\" -d 'The cache command does cache things'",
"complete -c forge -n \"__fish_seen_subcommand_from cache; and not __fish_seen_subcommand_from clean; and not __fish_seen_subcommand_from ls\" -l completions -r -f -a \"{sh'',bash'',fish'',zsh''}\"",
"complete -c forge -n \"__fish_seen_subcommand_from cache; and not __fish_seen_subcommand_from clean; and not __fish_seen_subcommand_from ls\" -l completions -r -f -a \"{sh'',bash'',fish'',zsh''}\" -d 'Generate a completion script for a specific shell'",
"complete -c forge -n \"__fish_seen_subcommand_from cache; and not __fish_seen_subcommand_from clean; and not __fish_seen_subcommand_from ls\" -s h -l help -d 'Show the help documentation for a command'",
"complete -c forge -n \"__fish_seen_subcommand_from cache; and not __fish_seen_subcommand_from clean; and not __fish_seen_subcommand_from ls\" -l wizard -d 'Start wizard mode for a command'",
"complete -c forge -n \"__fish_seen_subcommand_from cache; and not __fish_seen_subcommand_from clean; and not __fish_seen_subcommand_from ls\" -l version -d 'Show the version of the application'",
"complete -c forge -n \"__fish_seen_subcommand_from cache; and not __fish_seen_subcommand_from clean; and not __fish_seen_subcommand_from ls\" -l verbose -d 'Output in verbose mode'",
"complete -c forge -n \"__fish_seen_subcommand_from cache; and not __fish_seen_subcommand_from clean; and not __fish_seen_subcommand_from ls\" -f -a \"clean\"",
"complete -c forge -n \"__fish_seen_subcommand_from cache; and not __fish_seen_subcommand_from clean; and not __fish_seen_subcommand_from ls\" -f -a \"ls\"",
"complete -c forge -n \"__fish_seen_subcommand_from cache; and __fish_seen_subcommand_from clean\" -l completions -r -f -a \"{sh'',bash'',fish'',zsh''}\"",
"complete -c forge -n \"__fish_seen_subcommand_from cache; and __fish_seen_subcommand_from clean\" -l completions -r -f -a \"{sh'',bash'',fish'',zsh''}\" -d 'Generate a completion script for a specific shell'",
"complete -c forge -n \"__fish_seen_subcommand_from cache; and __fish_seen_subcommand_from clean\" -s h -l help -d 'Show the help documentation for a command'",
"complete -c forge -n \"__fish_seen_subcommand_from cache; and __fish_seen_subcommand_from clean\" -l wizard -d 'Start wizard mode for a command'",
"complete -c forge -n \"__fish_seen_subcommand_from cache; and __fish_seen_subcommand_from clean\" -l version -d 'Show the version of the application'",
"complete -c forge -n \"__fish_seen_subcommand_from cache; and __fish_seen_subcommand_from ls\" -l completions -r -f -a \"{sh'',bash'',fish'',zsh''}\"",
"complete -c forge -n \"__fish_seen_subcommand_from cache; and __fish_seen_subcommand_from ls\" -l completions -r -f -a \"{sh'',bash'',fish'',zsh''}\" -d 'Generate a completion script for a specific shell'",
"complete -c forge -n \"__fish_seen_subcommand_from cache; and __fish_seen_subcommand_from ls\" -s h -l help -d 'Show the help documentation for a command'",
"complete -c forge -n \"__fish_seen_subcommand_from cache; and __fish_seen_subcommand_from ls\" -l wizard -d 'Start wizard mode for a command'",
"complete -c forge -n \"__fish_seen_subcommand_from cache; and __fish_seen_subcommand_from ls\" -l version -d 'Show the version of the application'",
]
]
10 changes: 5 additions & 5 deletions test/snapshots/zsh-completions
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"",
" local context curcontext=\"$curcontext\" state line",
" _arguments \"${_arguments_options[@]}\" \\",
" '--completions[]:CHOICE:(sh bash fish zsh)' \\",
" '--completions[Generate a completion script for a specific shell]:CHOICE:(sh bash fish zsh)' \\",
" '-h[Show the help documentation for a command]' \\",
" '--help[Show the help documentation for a command]' \\",
" '--wizard[Start wizard mode for a command]' \\",
Expand All @@ -32,7 +32,7 @@
" case $line[1] in",
" (cache)",
" _arguments \"${_arguments_options[@]}\" \\",
" '--completions[]:CHOICE:(sh bash fish zsh)' \\",
" '--completions[Generate a completion script for a specific shell]:CHOICE:(sh bash fish zsh)' \\",
" '-h[Show the help documentation for a command]' \\",
" '--help[Show the help documentation for a command]' \\",
" '--wizard[Start wizard mode for a command]' \\",
Expand All @@ -49,7 +49,7 @@
" case $line[1] in",
" (clean)",
" _arguments \"${_arguments_options[@]}\" \\",
" '--completions[]:CHOICE:(sh bash fish zsh)' \\",
" '--completions[Generate a completion script for a specific shell]:CHOICE:(sh bash fish zsh)' \\",
" '-h[Show the help documentation for a command]' \\",
" '--help[Show the help documentation for a command]' \\",
" '--wizard[Start wizard mode for a command]' \\",
Expand All @@ -58,7 +58,7 @@
" ;;",
" (ls)",
" _arguments \"${_arguments_options[@]}\" \\",
" '--completions[]:CHOICE:(sh bash fish zsh)' \\",
" '--completions[Generate a completion script for a specific shell]:CHOICE:(sh bash fish zsh)' \\",
" '-h[Show the help documentation for a command]' \\",
" '--help[Show the help documentation for a command]' \\",
" '--wizard[Start wizard mode for a command]' \\",
Expand Down Expand Up @@ -105,4 +105,4 @@
"else",
" compdef _forge_zsh_completions forge",
"fi",
]
]

0 comments on commit ca45e4f

Please sign in to comment.