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,25 @@ 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, discarding its answer. Set
30+ * to `false` to keep the group strictly forward-only.
31+ *
32+ * @default true
33+ */
34+ navigate ?: boolean ;
35+
36+ /**
37+ * Called when the user returns to a previous step, with the name of the step
38+ * being left and the step being returned to.
39+ */
40+ onBack ?: ( opts : {
41+ from : keyof T ;
42+ to : keyof T ;
43+ results : Prettify < Partial < PromptGroupAwaitedReturn < T > > > ;
44+ } ) => void ;
2345}
2446
2547/**
@@ -63,23 +85,55 @@ export const group = async <T>(
6385) : Promise < Prettify < PromptGroupAwaitedReturn < T > > > => {
6486 const results = { } as any ;
6587 const promptNames = Object . keys ( prompts ) ;
88+ const navigate = opts ?. navigate !== false ;
6689
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- } ) ;
90+ // Bind Shift+Tab -> `back` for the duration of a navigable group, and restore
91+ // the previous alias state afterwards so we never clobber a user's binding.
92+ const hadBackAlias = settings . aliases . has ( SHIFT_TAB_SEQUENCE ) ;
93+ if ( navigate && ! hadBackAlias ) {
94+ settings . aliases . set ( SHIFT_TAB_SEQUENCE , 'back' ) ;
95+ }
7296
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- }
97+ try {
98+ let index = 0 ;
99+ while ( index < promptNames . length ) {
100+ // Safe: the loop condition guarantees `index` is in range.
101+ const name = promptNames [ index ] as string ;
102+ const prompt = prompts [ name as keyof T ] ;
103+ const result = await prompt ( { results } ) ?. catch ( ( e ) => {
104+ throw e ;
105+ } ) ;
106+
107+ // `back`: discard this answer and return to the previous step. On the
108+ // first step there is nowhere to go, so we simply re-run it.
109+ if ( navigate && isBack ( result ) ) {
110+ if ( index > 0 ) {
111+ const from = name as keyof T ;
112+ index -- ;
113+ const to = promptNames [ index ] as keyof T ;
114+ delete results [ to as keyof typeof results ] ;
115+ opts ?. onBack ?.( { from, to, results } ) ;
116+ }
117+ continue ;
118+ }
81119
82- results [ name ] = result ;
120+ // Pass the results to the onCancel function
121+ // so the user can decide what to do with the results
122+ // TODO: Switch to callback within core to avoid isCancel Fn
123+ if ( typeof opts ?. onCancel === 'function' && isCancel ( result ) ) {
124+ results [ name ] = 'canceled' ;
125+ opts . onCancel ( { results } ) ;
126+ index ++ ;
127+ continue ;
128+ }
129+
130+ results [ name ] = result ;
131+ index ++ ;
132+ }
133+ } finally {
134+ if ( navigate && ! hadBackAlias ) {
135+ settings . aliases . delete ( SHIFT_TAB_SEQUENCE ) ;
136+ }
83137 }
84138
85139 return results ;
0 commit comments