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

Commit

Permalink
add Prompt.password and Prompt.hidden
Browse files Browse the repository at this point in the history
  • Loading branch information
IMax153 committed Dec 7, 2023
1 parent 73a6f30 commit 82db706
Show file tree
Hide file tree
Showing 9 changed files with 64 additions and 30 deletions.
5 changes: 5 additions & 0 deletions .changeset/neat-ears-kneel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@effect/cli": patch
---

remove `"type"` option from `Prompt.text` and add `Prompt.password` and `Prompt.hidden` which return `Secret`
5 changes: 2 additions & 3 deletions examples/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,8 @@ const numberPrompt = Prompt.float({
validate: (n) => n > 0 ? Effect.succeed(n) : Effect.fail("must be greater than 0")
})

const textPrompt = Prompt.text({
const passwordPrompt = Prompt.password({
message: "Enter your password: ",
type: "password",
validate: (value) =>
value.length === 0
? Effect.fail("Password cannot be empty")
Expand All @@ -52,7 +51,7 @@ const prompt = Prompt.all([
confirmPrompt,
datePrompt,
numberPrompt,
textPrompt,
passwordPrompt,
togglePrompt
])

Expand Down
4 changes: 2 additions & 2 deletions src/Args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@
import type { FileSystem } from "@effect/platform/FileSystem"
import type { QuitException, Terminal } from "@effect/platform/Terminal"
import type { Config } from "effect/Config"
import type { ConfigSecret } from "effect/ConfigSecret"
import type { Effect } from "effect/Effect"
import type { Either } from "effect/Either"
import type { Option } from "effect/Option"
import type { Pipeable } from "effect/Pipeable"
import type { NonEmptyReadonlyArray } from "effect/ReadonlyArray"
import type { Secret } from "effect/Secret"
import type { CliConfig } from "./CliConfig.js"
import type { HelpDoc } from "./HelpDoc.js"
import * as InternalArgs from "./internal/args.js"
Expand Down Expand Up @@ -328,7 +328,7 @@ export const repeated: <A>(self: Args<A>) => Args<ReadonlyArray<A>> = InternalAr
* @since 1.0.0
* @category constructors
*/
export const secret: (config?: Args.BaseArgsConfig) => Args<ConfigSecret> = InternalArgs.secret
export const secret: (config?: Args.BaseArgsConfig) => Args<Secret> = InternalArgs.secret

/**
* Creates a text argument.
Expand Down
4 changes: 2 additions & 2 deletions src/Options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@
import type { FileSystem } from "@effect/platform/FileSystem"
import type { QuitException, Terminal } from "@effect/platform/Terminal"
import type { Config } from "effect/Config"
import type { ConfigSecret } from "effect/ConfigSecret"
import type { Effect } from "effect/Effect"
import type { Either } from "effect/Either"
import type { HashMap } from "effect/HashMap"
import type { Option } from "effect/Option"
import type { Pipeable } from "effect/Pipeable"
import type { NonEmptyReadonlyArray } from "effect/ReadonlyArray"
import type { Secret } from "effect/Secret"
import type { CliConfig } from "./CliConfig.js"
import type { HelpDoc } from "./HelpDoc.js"
import * as InternalOptions from "./internal/options.js"
Expand Down Expand Up @@ -271,7 +271,7 @@ export const none: Options<void> = InternalOptions.none
* @since 1.0.0
* @category constructors
*/
export const secret: (name: string) => Options<ConfigSecret> = InternalOptions.secret
export const secret: (name: string) => Options<Secret> = InternalOptions.secret

/**
* @since 1.0.0
Expand Down
17 changes: 13 additions & 4 deletions src/Prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { QuitException, Terminal, UserInput } from "@effect/platform/Termin
import type { Effect } from "effect/Effect"
import type { Option } from "effect/Option"
import type { Pipeable } from "effect/Pipeable"
import type { Secret } from "effect/Secret"
import * as InternalPrompt from "./internal/prompt.js"
import * as InternalConfirmPrompt from "./internal/prompt/confirm.js"
import * as InternalDatePrompt from "./internal/prompt/date.js"
Expand Down Expand Up @@ -284,10 +285,6 @@ export declare namespace Prompt {
* The message to display in the prompt.
*/
readonly message: string
/**
* The type of the text option.
*/
readonly type?: "hidden" | "password" | "text"
/**
* The default value of the text option.
*/
Expand Down Expand Up @@ -426,6 +423,12 @@ export const flatMap: {
*/
export const float: (options: Prompt.FloatOptions) => Prompt<number> = InternalNumberPrompt.float

/**
* @since 1.0.0
* @category constructors
*/
export const hidden: (options: Prompt.TextOptions) => Prompt<Secret> = InternalTextPrompt.hidden

/**
* @since 1.0.0
* @category constructors
Expand All @@ -449,6 +452,12 @@ export const map: {
<Output, Output2>(self: Prompt<Output>, f: (output: Output) => Output2): Prompt<Output2>
} = InternalPrompt.map

/**
* @since 1.0.0
* @category constructors
*/
export const password: (options: Prompt.TextOptions) => Prompt<Secret> = InternalTextPrompt.password

/**
* Executes the specified `Prompt`.
*
Expand Down
4 changes: 2 additions & 2 deletions src/internal/args.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import type * as FileSystem from "@effect/platform/FileSystem"
import type * as Terminal from "@effect/platform/Terminal"
import type * as Config from "effect/Config"
import type * as ConfigSecret from "effect/ConfigSecret"
import * as Console from "effect/Console"
import * as Effect from "effect/Effect"
import * as Either from "effect/Either"
Expand All @@ -10,6 +9,7 @@ import * as Option from "effect/Option"
import { pipeArguments } from "effect/Pipeable"
import * as ReadonlyArray from "effect/ReadonlyArray"
import * as Ref from "effect/Ref"
import type * as Secret from "effect/Secret"
import type * as Args from "../Args.js"
import type * as CliConfig from "../CliConfig.js"
import type * as HelpDoc from "../HelpDoc.js"
Expand Down Expand Up @@ -227,7 +227,7 @@ export const path = (config: Args.Args.PathArgsConfig = {}): Args.Args<string> =
/** @internal */
export const secret = (
config: Args.Args.BaseArgsConfig = {}
): Args.Args<ConfigSecret.ConfigSecret> =>
): Args.Args<Secret.Secret> =>
makeSingle(Option.fromNullable(config.name), InternalPrimitive.secret)

/** @internal */
Expand Down
4 changes: 2 additions & 2 deletions src/internal/options.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import type * as FileSystem from "@effect/platform/FileSystem"
import type * as Terminal from "@effect/platform/Terminal"
import type * as Config from "effect/Config"
import type * as ConfigSecret from "effect/ConfigSecret"
import * as Console from "effect/Console"
import * as Effect from "effect/Effect"
import * as Either from "effect/Either"
Expand All @@ -12,6 +11,7 @@ import * as Order from "effect/Order"
import { pipeArguments } from "effect/Pipeable"
import * as ReadonlyArray from "effect/ReadonlyArray"
import * as Ref from "effect/Ref"
import type * as Secret from "effect/Secret"
import type * as CliConfig from "../CliConfig.js"
import type * as HelpDoc from "../HelpDoc.js"
import type * as Options from "../Options.js"
Expand Down Expand Up @@ -333,7 +333,7 @@ export const none: Options.Options<void> = (() => {
})()

/** @internal */
export const secret = (name: string): Options.Options<ConfigSecret.ConfigSecret> =>
export const secret = (name: string): Options.Options<Secret.Secret> =>
makeSingle(name, ReadonlyArray.empty(), InternalPrimitive.secret)

/** @internal */
Expand Down
11 changes: 5 additions & 6 deletions src/internal/primitive.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import * as FileSystem from "@effect/platform/FileSystem"
import * as Schema from "@effect/schema/Schema"
import * as ConfigSecret from "effect/ConfigSecret"
import * as Effect from "effect/Effect"
import { dual, pipe } from "effect/Function"
import * as Option from "effect/Option"
import { pipeArguments } from "effect/Pipeable"
import * as ReadonlyArray from "effect/ReadonlyArray"
import * as EffectSecret from "effect/Secret"
import type * as CliConfig from "../CliConfig.js"
import type * as HelpDoc from "../HelpDoc.js"
import type * as Span from "../HelpDoc/Span.js"
Expand Down Expand Up @@ -194,7 +194,7 @@ export const path = (
}

/** @internal */
export const secret: Primitive.Primitive<ConfigSecret.ConfigSecret> = (() => {
export const secret: Primitive.Primitive<EffectSecret.Secret> = (() => {
const op = Object.create(proto)
op._tag = "Secret"
return op
Expand Down Expand Up @@ -452,7 +452,7 @@ const validateInternal = (
}
case "Secret": {
return attempt(value, getTypeNameInternal(self), Schema.parse(Schema.string)).pipe(
Effect.map((value) => ConfigSecret.fromString(value))
Effect.map((value) => EffectSecret.fromString(value))
)
}
case "Text": {
Expand Down Expand Up @@ -579,10 +579,9 @@ const wizardInternal = (self: Instruction, help: HelpDoc.HelpDoc): Prompt.Prompt
case "Secret": {
const primitiveHelp = InternalHelpDoc.p("Enter some text (value will be hidden)")
const message = InternalHelpDoc.sequence(help, primitiveHelp)
return InternalTextPrompt.text({
type: "password",
return InternalTextPrompt.hidden({
message: InternalHelpDoc.toAnsiText(message).trimEnd()
}).pipe(InternalPrompt.map((value) => ConfigSecret.fromString(value)))
})
}
case "Text": {
const primitiveHelp = InternalHelpDoc.p("Enter some text")
Expand Down
40 changes: 31 additions & 9 deletions src/internal/prompt/text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,19 @@ import * as Effect from "effect/Effect"
import { pipe } from "effect/Function"
import * as Option from "effect/Option"
import * as ReadonlyArray from "effect/ReadonlyArray"
import * as Secret from "effect/Secret"
import type * as Prompt from "../../Prompt.js"
import * as InternalPrompt from "../prompt.js"
import * as InternalPromptAction from "./action.js"
import * as InternalAnsiUtils from "./ansi-utils.js"

interface Options extends Required<Prompt.Prompt.TextOptions> {
/**
* The type of the text option.
*/
readonly type: "hidden" | "password" | "text"
}

interface State {
readonly cursor: number
readonly offset: number
Expand All @@ -22,7 +30,7 @@ const renderBeep = Doc.render(Doc.beep, { style: "pretty" })

const renderClearScreen = (
prevState: Option.Option<State>,
options: Required<Prompt.Prompt.TextOptions>,
options: Options,
columns: number
): Doc.AnsiDoc => {
// Erase the current line and place the cursor in column one
Expand Down Expand Up @@ -54,7 +62,7 @@ const renderClearScreen = (

const renderInput = (
nextState: State,
options: Required<Prompt.Prompt.TextOptions>,
options: Options,
submitted: boolean
): Doc.AnsiDoc => {
const annotation = Option.match(nextState.error, {
Expand Down Expand Up @@ -103,7 +111,7 @@ const renderOutput = (
nextState: State,
leadingSymbol: Doc.AnsiDoc,
trailingSymbol: Doc.AnsiDoc,
options: Required<Prompt.Prompt.TextOptions>,
options: Options,
submitted: boolean = false
): Doc.AnsiDoc => {
const annotateLine = (line: string): Doc.AnsiDoc => pipe(Doc.text(line), Doc.annotate(Ansi.bold))
Expand All @@ -126,7 +134,7 @@ const renderOutput = (
const renderNextFrame = (
prevState: Option.Option<State>,
nextState: State,
options: Required<Prompt.Prompt.TextOptions>
options: Options
): Effect.Effect<Terminal.Terminal, never, string> =>
Effect.gen(function*(_) {
const terminal = yield* _(Terminal.Terminal)
Expand All @@ -149,7 +157,7 @@ const renderNextFrame = (

const renderSubmission = (
nextState: State,
options: Required<Prompt.Prompt.TextOptions>
options: Options
) =>
Effect.gen(function*(_) {
const terminal = yield* _(Terminal.Terminal)
Expand Down Expand Up @@ -232,11 +240,13 @@ const initialState: State = {
error: Option.none()
}

/** @internal */
export const text = (options: Prompt.Prompt.TextOptions): Prompt.Prompt<string> => {
const opts: Required<Prompt.Prompt.TextOptions> = {
const basePrompt = (
options: Prompt.Prompt.TextOptions,
type: Options["type"]
): Prompt.Prompt<string> => {
const opts: Options = {
default: "",
type: "text",
type,
validate: Effect.succeed,
...options
}
Expand Down Expand Up @@ -285,3 +295,15 @@ export const text = (options: Prompt.Prompt.TextOptions): Prompt.Prompt<string>
}
)
}

/** @internal */
export const hidden = (options: Prompt.Prompt.TextOptions): Prompt.Prompt<Secret.Secret> =>
basePrompt(options, "hidden").pipe(InternalPrompt.map(Secret.fromString))

/** @internal */
export const password = (options: Prompt.Prompt.TextOptions): Prompt.Prompt<Secret.Secret> =>
basePrompt(options, "password").pipe(InternalPrompt.map(Secret.fromString))

/** @internal */
export const text = (options: Prompt.Prompt.TextOptions): Prompt.Prompt<string> =>
basePrompt(options, "text")

0 comments on commit 82db706

Please sign in to comment.