From c78e324938f4ec86edff77759c2a25f68ed909ce Mon Sep 17 00:00:00 2001 From: Maxwell Brown Date: Mon, 27 Nov 2023 14:53:53 -0500 Subject: [PATCH] fix wizard mode --- src/internal/args.ts | 2 +- src/internal/cliApp.ts | 3 +- src/internal/command.ts | 159 +++++++++++++++++++++++++------------- src/internal/options.ts | 27 ++++--- src/internal/primitive.ts | 2 +- 5 files changed, 122 insertions(+), 71 deletions(-) diff --git a/src/internal/args.ts b/src/internal/args.ts index 5e60aae..9948567 100644 --- a/src/internal/args.ts +++ b/src/internal/args.ts @@ -734,7 +734,7 @@ const withDescriptionInternal = (self: Instruction, description: string): Args.A } } -const wizardHeader = InternalHelpDoc.p("ARGS WIZARD") +const wizardHeader = InternalHelpDoc.p("ARG WIZARD") const wizardInternal = (self: Instruction, config: CliConfig.CliConfig): Effect.Effect< FileSystem.FileSystem | Terminal.Terminal, diff --git a/src/internal/cliApp.ts b/src/internal/cliApp.ts index e3711fc..8378639 100644 --- a/src/internal/cliApp.ts +++ b/src/internal/cliApp.ts @@ -217,7 +217,8 @@ const handleBuiltInOption = ( ]) ) const help = InternalHelpDoc.sequence(header, description) - return Console.log(InternalHelpDoc.toAnsiText(help)).pipe( + const text = InternalHelpDoc.toAnsiText(help).trimEnd() + return Console.log(text).pipe( Effect.zipRight(InternalCommand.wizard(builtIn.command, config)), Effect.tap((args) => Console.log(InternalHelpDoc.toAnsiText(renderWizardArgs(args)))) ) diff --git a/src/internal/command.ts b/src/internal/command.ts index 964173d..80b2e9c 100644 --- a/src/internal/command.ts +++ b/src/internal/command.ts @@ -841,73 +841,124 @@ const withDescriptionInternal = ( } } -const wizardInternal = (self: Instruction, config: CliConfig.CliConfig): Effect.Effect< +const wizardInternal = ( + self: Instruction, + config: CliConfig.CliConfig +): Effect.Effect< FileSystem.FileSystem | Terminal.Terminal, ValidationError.ValidationError, ReadonlyArray > => { - switch (self._tag) { - case "Standard": { - const message = InternalHelpDoc.p(pipe( - InternalSpan.text("\n"), - InternalSpan.concat(InternalSpan.strong(InternalSpan.code("COMMAND:"))), - InternalSpan.concat(InternalSpan.space), - InternalSpan.concat(InternalSpan.code(self.name)) - )) - return Console.log(InternalHelpDoc.toAnsiText(message)).pipe(Effect.zipRight(Effect.zipWith( - InternalOptions.wizard(self.options, config), - InternalArgs.wizard(self.args, config), - (options, args) => ReadonlyArray.prepend(ReadonlyArray.appendAll(options, args), self.name) - ))) + const loop = (self: WizardCommandSequence): Effect.Effect< + FileSystem.FileSystem | Terminal.Terminal, + ValidationError.ValidationError, + ReadonlyArray + > => { + switch (self._tag) { + case "SingleCommandWizard": { + const optionsWizard = isStandard(self.command) + ? InternalOptions.wizard(self.command.options, config) + : Effect.succeed(ReadonlyArray.empty()) + const argsWizard = isStandard(self.command) + ? InternalArgs.wizard(self.command.args, config) + : Effect.succeed(ReadonlyArray.empty()) + const help = InternalHelpDoc.p(pipe( + InternalSpan.text("\n"), + InternalSpan.concat(InternalSpan.strong(InternalSpan.code("COMMAND:"))), + InternalSpan.concat(InternalSpan.space), + InternalSpan.concat(InternalSpan.code(self.command.name)) + )) + const message = InternalHelpDoc.toAnsiText(help) + return Console.log(message).pipe( + Effect.zipRight(Effect.zipWith(optionsWizard, argsWizard, (options, args) => + pipe( + ReadonlyArray.appendAll(options, args), + ReadonlyArray.prepend(self.command.name) + ))) + ) + } + case "AlternativeCommandWizard": { + const makeChoice = (title: string, value: WizardCommandSequence) => ({ title, value }) + const choices = self.alternatives.map((alternative) => { + switch (alternative._tag) { + case "SingleCommandWizard": { + return makeChoice(alternative.command.name, alternative) + } + case "SubcommandWizard": { + return makeChoice(alternative.names, alternative) + } + } + }) + const description = InternalHelpDoc.p("Select which command you would like to execute") + const message = InternalHelpDoc.toAnsiText(description).trimEnd() + return InternalSelectPrompt.select({ message, choices }).pipe( + Effect.flatMap((nextSequence) => loop(nextSequence)) + ) + } + case "SubcommandWizard": { + return Effect.zipWith( + loop(self.parent), + loop(self.child), + (parent, child) => ReadonlyArray.appendAll(parent, child) + ) + } } + } + return loop(getWizardCommandSequence(self)) +} + +type WizardCommandSequence = SingleCommandWizard | AlternativeCommandWizard | SubcommandWizard + +interface SingleCommandWizard { + readonly _tag: "SingleCommandWizard" + readonly command: GetUserInput | Standard +} + +interface AlternativeCommandWizard { + readonly _tag: "AlternativeCommandWizard" + readonly alternatives: ReadonlyArray +} + +interface SubcommandWizard { + _tag: "SubcommandWizard" + readonly names: string + readonly parent: WizardCommandSequence + readonly child: WizardCommandSequence +} + +/** + * Creates an intermediate data structure that allows commands to be properly + * sequenced by the prompts of Wizard Mode. + */ +const getWizardCommandSequence = (self: Instruction): WizardCommandSequence => { + switch (self._tag) { + case "Standard": case "GetUserInput": { - return Effect.succeed(ReadonlyArray.empty()) + return { _tag: "SingleCommandWizard", command: self } } case "Map": { - return wizardInternal(self.command as Instruction, config) + return getWizardCommandSequence(self.command as Instruction) } case "OrElse": { - const description = InternalHelpDoc.p("Select which command you would like to execute") - const makeChoice = (title: string, value: Instruction) => ({ - title, - value: [title, value] as const - }) - const choices = ReadonlyArray.compact([ - Option.map( - ReadonlyArray.head(Array.from(getNamesInternal(self.left as Instruction))), - (title) => makeChoice(title, self.left as Instruction) - ), - Option.map( - ReadonlyArray.head(Array.from(getNamesInternal(self.right as Instruction))), - (title) => makeChoice(title, self.right as Instruction) - ) - ]) - const message = InternalHelpDoc.toAnsiText(description).trimEnd() - return Console.log().pipe( - Effect.zipRight(InternalSelectPrompt.select({ message, choices })), - Effect.flatMap(([name, command]) => - wizardInternal(command, config).pipe(Effect.map(ReadonlyArray.prepend(name))) - ) - ) + const left = getWizardCommandSequence(self.left as Instruction) + const leftAlternatives = left._tag === "AlternativeCommandWizard" + ? left.alternatives + : ReadonlyArray.of(left) + const right = getWizardCommandSequence(self.right as Instruction) + const rightAlternatives = right._tag === "AlternativeCommandWizard" + ? right.alternatives + : ReadonlyArray.of(right) + const alternatives = ReadonlyArray.appendAll(leftAlternatives, rightAlternatives) + return { _tag: "AlternativeCommandWizard", alternatives } } case "Subcommands": { - const description = InternalHelpDoc.p("Select which command you would like to execute") - const makeChoice = (title: string, value: Instruction) => ({ title, value }) - const parentName = Option.getOrElse( - ReadonlyArray.head(Array.from(getNamesInternal(self))), - () => "" - ) - const parentChoice = makeChoice(parentName, self.parent as Instruction) - const childChoices = ReadonlyArray.map( - Array.from(getSubcommandsInternal(self)), - ([name, command]) => makeChoice(name, command as Instruction) - ) - const choices = ReadonlyArray.prepend(childChoices, parentChoice) - const message = InternalHelpDoc.toAnsiText(description).trimEnd() - return Console.log().pipe( - Effect.zipRight(InternalSelectPrompt.select({ message, choices })), - Effect.flatMap((command) => wizardInternal(command, config)) + const names = pipe( + ReadonlyArray.fromIterable(getNamesInternal(self.parent as Instruction)), + ReadonlyArray.join(" | ") ) + const parent = getWizardCommandSequence(self.parent as Instruction) + const child = getWizardCommandSequence(self.child as Instruction) + return { _tag: "SubcommandWizard", names, parent, child } } } } diff --git a/src/internal/options.ts b/src/internal/options.ts index 4228b34..4c2c788 100644 --- a/src/internal/options.ts +++ b/src/internal/options.ts @@ -1350,7 +1350,7 @@ const parseInternal = ( } } -const wizardHeader = InternalHelpDoc.p("OPTIONS WIZARD") +const wizardHeader = InternalHelpDoc.p("Option Wizard") const wizardInternal = (self: Instruction, config: CliConfig.CliConfig): Effect.Effect< FileSystem.FileSystem | Terminal.Terminal, @@ -1417,12 +1417,13 @@ const wizardInternal = (self: Instruction, config: CliConfig.CliConfig): Effect. (title) => makeChoice(title, self.right as Instruction) ) ]) - return Console.log().pipe(Effect.zipRight( - InternalSelectPrompt.select({ + return Console.log().pipe( + Effect.zipRight(InternalSelectPrompt.select({ message: InternalHelpDoc.toAnsiText(message).trimEnd(), choices - }).pipe(Effect.flatMap((option) => wizardInternal(option, config))) - )) + })), + Effect.flatMap((option) => wizardInternal(option, config)) + ) } case "Variadic": { const repeatHelp = InternalHelpDoc.p( @@ -1460,15 +1461,13 @@ const wizardInternal = (self: Instruction, config: CliConfig.CliConfig): Effect. InternalHelpDoc.sequence(defaultHelp) ) return Console.log().pipe( - Effect.zipRight( - InternalSelectPrompt.select({ - message: InternalHelpDoc.toAnsiText(message).trimEnd(), - choices: [ - { title: `Default ['${JSON.stringify(self.fallback)}']`, value: true }, - { title: "Custom", value: false } - ] - }) - ), + Effect.zipRight(InternalSelectPrompt.select({ + message: InternalHelpDoc.toAnsiText(message).trimEnd(), + choices: [ + { title: `Default ['${JSON.stringify(self.fallback)}']`, value: true }, + { title: "Custom", value: false } + ] + })), Effect.flatMap((useFallback) => useFallback ? Effect.succeed(ReadonlyArray.empty()) diff --git a/src/internal/primitive.ts b/src/internal/primitive.ts index 7f2adef..0cc0462 100644 --- a/src/internal/primitive.ts +++ b/src/internal/primitive.ts @@ -539,7 +539,7 @@ const wizardInternal = (self: Instruction, help: HelpDoc.HelpDoc): Prompt.Prompt case "Integer": { const primitiveHelp = InternalHelpDoc.p("Enter an integer") const message = InternalHelpDoc.sequence(help, primitiveHelp) - return InternalNumberPrompt.float({ + return InternalNumberPrompt.integer({ message: InternalHelpDoc.toAnsiText(message).trimEnd() }).pipe(InternalPrompt.map((value) => `${value}`)) }