1- import { isCancel } from '@clack/core' ;
1+ import { isBack , isCancel , settings } from '@clack/core' ;
2+
3+ /** Shift+Tab. Bound to the `back` action while a navigable group is running. */
4+ const SHIFT_TAB_SEQUENCE = '\x1b[Z' ;
25
36type Prettify < T > = {
47 [ P in keyof T ] : T [ P ] ;
@@ -20,6 +23,26 @@ export interface PromptGroupOptions<T> {
2023 * Called when any one of the prompts is canceled.
2124 */
2225 onCancel ?: ( opts : { results : Prettify < Partial < PromptGroupAwaitedReturn < T > > > } ) => void ;
26+
27+ /**
28+ * Enable back navigation. When `true` (the default), pressing `Shift+Tab`
29+ * during a prompt returns to the previous step. Answers are kept, and each
30+ * step receives its previous answer as `previous` so it can be restored. Set
31+ * to `false` to keep the group strictly forward-only.
32+ *
33+ * @default true
34+ */
35+ navigate ?: boolean ;
36+
37+ /**
38+ * Called when the user returns to a previous step, with the name of the step
39+ * being left and the step being returned to.
40+ */
41+ onBack ?: ( opts : {
42+ from : keyof T ;
43+ to : keyof T ;
44+ results : Prettify < Partial < PromptGroupAwaitedReturn < T > > > ;
45+ } ) => void ;
2346}
2447
2548/**
@@ -29,6 +52,12 @@ export interface PromptGroupOptions<T> {
2952export type PromptGroup < T > = {
3053 [ P in keyof T ] : ( opts : {
3154 results : Prettify < Partial < PromptGroupAwaitedReturn < Omit < T , P > > > > ;
55+ /**
56+ * This step's previously-submitted answer, if the user has visited it
57+ * before (e.g. after going back). Pass it to the prompt's `initialValue`
58+ * to restore what they had entered.
59+ */
60+ previous ?: PromptGroupAwaitedReturn < T > [ P ] ;
3261 } ) => undefined | Promise < T [ P ] | undefined > ;
3362} ;
3463
@@ -44,11 +73,14 @@ export type PromptGroup<T> = {
4473 * import { group, text, password } from '@clack/prompts';
4574 *
4675 * const account = await group({
47- * email: () => text({
76+ * // `Shift+Tab` goes back; pass `previous` to `initialValue` to restore it.
77+ * email: ({ previous }) => text({
4878 * message: 'What is your email address?',
79+ * initialValue: previous,
4980 * }),
50- * username: ({ results }) => text({
81+ * username: ({ results, previous }) => text({
5182 * message: 'What is your username?',
83+ * initialValue: previous,
5284 * placeholder: results.email?.replace(/@.+$/, '').toLowerCase() ?? '',
5385 * }),
5486 * password: () => password({
@@ -63,23 +95,61 @@ export const group = async <T>(
6395) : Promise < Prettify < PromptGroupAwaitedReturn < T > > > => {
6496 const results = { } as any ;
6597 const promptNames = Object . keys ( prompts ) ;
98+ const navigate = opts ?. navigate !== false ;
6699
67- for ( const name of promptNames ) {
68- const prompt = prompts [ name as keyof T ] ;
69- const result = await prompt ( { results } ) ?. catch ( ( e ) => {
70- throw e ;
71- } ) ;
100+ // We bind Shift+Tab -> `back` only while a step that *can* go back is showing,
101+ // and restore the previous alias state afterwards so we never clobber a user's
102+ // own binding. We only manage the alias when the user hasn't bound it already.
103+ const manageAlias = navigate && ! settings . aliases . has ( SHIFT_TAB_SEQUENCE ) ;
104+ const setBackEnabled = ( enabled : boolean ) => {
105+ if ( ! manageAlias ) return ;
106+ if ( enabled ) settings . aliases . set ( SHIFT_TAB_SEQUENCE , 'back' ) ;
107+ else settings . aliases . delete ( SHIFT_TAB_SEQUENCE ) ;
108+ } ;
72109
73- // Pass the results to the onCancel function
74- // so the user can decide what to do with the results
75- // TODO: Switch to callback within core to avoid isCancel Fn
76- if ( typeof opts ?. onCancel === 'function' && isCancel ( result ) ) {
77- results [ name ] = 'canceled' ;
78- opts . onCancel ( { results } ) ;
79- continue ;
80- }
110+ try {
111+ let index = 0 ;
112+ while ( index < promptNames . length ) {
113+ // There is nowhere to go back to from the first step, so `back` is
114+ // disabled there (Shift+Tab does nothing); enabled from the second on.
115+ setBackEnabled ( index > 0 ) ;
116+
117+ // Safe: the loop condition guarantees `index` is in range.
118+ const name = promptNames [ index ] as string ;
119+ const prompt = prompts [ name as keyof T ] ;
120+ // `previous` lets a revisited step restore what the user had entered.
121+ const result = await prompt ( { results, previous : results [ name ] } ) ?. catch ( ( e ) => {
122+ throw e ;
123+ } ) ;
81124
82- results [ name ] = result ;
125+ // `back`: return to the previous step. Answers are kept (not discarded),
126+ // so each step can offer them back via `previous`. Guarded against the
127+ // first step in case a caller forces `back` there programmatically.
128+ if ( navigate && isBack ( result ) ) {
129+ if ( index > 0 ) {
130+ const from = name as keyof T ;
131+ index -- ;
132+ const to = promptNames [ index ] as keyof T ;
133+ opts ?. onBack ?.( { from, to, results } ) ;
134+ }
135+ continue ;
136+ }
137+
138+ // Pass the results to the onCancel function
139+ // so the user can decide what to do with the results
140+ // TODO: Switch to callback within core to avoid isCancel Fn
141+ if ( typeof opts ?. onCancel === 'function' && isCancel ( result ) ) {
142+ results [ name ] = 'canceled' ;
143+ opts . onCancel ( { results } ) ;
144+ index ++ ;
145+ continue ;
146+ }
147+
148+ results [ name ] = result ;
149+ index ++ ;
150+ }
151+ } finally {
152+ setBackEnabled ( false ) ;
83153 }
84154
85155 return results ;
0 commit comments