@@ -7,6 +7,8 @@ import { WriteStream } from 'node:tty';
77import { cursor , erase } from 'sisteransi' ;
88import wrap from 'wrap-ansi' ;
99
10+ import { type InferSetType , aliases , keys , hasAliasKey } from '../utils' ;
11+
1012function diffLines ( a : string , b : string ) {
1113 if ( a === b ) return ;
1214
@@ -30,14 +32,6 @@ function setRawMode(input: Readable, value: boolean) {
3032 if ( ( input as typeof stdin ) . isTTY ) ( input as typeof stdin ) . setRawMode ( value ) ;
3133}
3234
33- const aliases = new Map ( [
34- [ 'k' , 'up' ] ,
35- [ 'j' , 'down' ] ,
36- [ 'h' , 'left' ] ,
37- [ 'l' , 'right' ] ,
38- ] ) ;
39- const keys = new Set ( [ 'up' , 'down' , 'left' , 'right' , 'space' , 'enter' ] ) ;
40-
4135export interface PromptOptions < Self extends Prompt > {
4236 render ( this : Omit < Self , 'prompt' > ) : string | void ;
4337 placeholder ?: string ;
@@ -50,23 +44,49 @@ export interface PromptOptions<Self extends Prompt> {
5044
5145export type State = 'initial' | 'active' | 'cancel' | 'submit' | 'error' ;
5246
47+ /**
48+ * Typed event emitter for clack
49+ */
50+ interface ClackHooks {
51+ 'initial' : ( value ?: any ) => void ;
52+ 'active' : ( value ?: any ) => void ;
53+ 'cancel' : ( value ?: any ) => void ;
54+ 'submit' : ( value ?: any ) => void ;
55+ 'error' : ( value ?: any ) => void ;
56+ 'cursor' : ( key ?: InferSetType < typeof keys > ) => void ;
57+ 'key' : ( key ?: string ) => void ;
58+ 'value' : ( value ?: string ) => void ;
59+ 'confirm' : ( value ?: boolean ) => void ;
60+ 'finalize' : ( ) => void ;
61+ }
62+
5363export default class Prompt {
5464 protected input : Readable ;
5565 protected output : Writable ;
66+
5667 private rl ! : ReadLine ;
5768 private opts : Omit < PromptOptions < Prompt > , 'render' | 'input' | 'output' > ;
58- private _track : boolean = false ;
5969 private _render : ( context : Omit < Prompt , 'prompt' > ) => string | void ;
60- protected _cursor : number = 0 ;
70+ private _track = false ;
71+ private _prevFrame = '' ;
72+ private _subscribers = new Map < string , { cb : ( ...args : any ) => any ; once ?: boolean } [ ] > ( ) ;
73+ protected _cursor = 0 ;
6174
6275 public state : State = 'initial' ;
76+ public error = '' ;
6377 public value : any ;
64- public error : string = '' ;
6578
6679 constructor (
67- { render , input = stdin , output = stdout , ... opts } : PromptOptions < Prompt > ,
80+ options : PromptOptions < Prompt > ,
6881 trackValue : boolean = true
6982 ) {
83+ const {
84+ input = stdin ,
85+ output = stdout ,
86+ render,
87+ ...opts
88+ } = options ;
89+
7090 this . opts = opts ;
7191 this . onKeypress = this . onKeypress . bind ( this ) ;
7292 this . close = this . close . bind ( this ) ;
@@ -78,6 +98,63 @@ export default class Prompt {
7898 this . output = output ;
7999 }
80100
101+ /**
102+ * Unsubscribe all listeners
103+ */
104+ protected unsubscribe ( ) {
105+ this . _subscribers . clear ( ) ;
106+ }
107+
108+ /**
109+ * Set a subscriber with opts
110+ * @param event - The event name
111+ */
112+ private setSubscriber < T extends keyof ClackHooks > ( event : T , opts : { cb : ClackHooks [ T ] ; once ?: boolean } ) {
113+ const params = this . _subscribers . get ( event ) ?? [ ] ;
114+ params . push ( opts ) ;
115+ this . _subscribers . set ( event , params ) ;
116+ }
117+
118+ /**
119+ * Subscribe to an event
120+ * @param event - The event name
121+ * @param cb - The callback
122+ */
123+ public on < T extends keyof ClackHooks > ( event : T , cb : ClackHooks [ T ] ) {
124+ this . setSubscriber ( event , { cb } ) ;
125+ }
126+
127+ /**
128+ * Subscribe to an event once
129+ * @param event - The event name
130+ * @param cb - The callback
131+ */
132+ public once < T extends keyof ClackHooks > ( event : T , cb : ClackHooks [ T ] ) {
133+ this . setSubscriber ( event , { cb, once : true } ) ;
134+ }
135+
136+ /**
137+ * Emit an event with data
138+ * @param event - The event name
139+ * @param data - The data to pass to the callback
140+ */
141+ public emit < T extends keyof ClackHooks > ( event : T , ...data : Parameters < ClackHooks [ T ] > ) {
142+ const cbs = this . _subscribers . get ( event ) ?? [ ] ;
143+ const cleanup : ( ( ) => void ) [ ] = [ ] ;
144+
145+ for ( const subscriber of cbs ) {
146+ subscriber . cb ( ...data ) ;
147+
148+ if ( subscriber . once ) {
149+ cleanup . push ( ( ) => cbs . splice ( cbs . indexOf ( subscriber ) , 1 ) ) ;
150+ }
151+ }
152+
153+ for ( const cb of cleanup ) {
154+ cb ( ) ;
155+ }
156+ }
157+
81158 public prompt ( ) {
82159 const sink = new WriteStream ( 0 ) ;
83160 sink . _write = ( chunk , encoding , done ) => {
@@ -125,43 +202,15 @@ export default class Prompt {
125202 } ) ;
126203 }
127204
128- private subscribers = new Map < string , { cb : ( ...args : any ) => any ; once ?: boolean } [ ] > ( ) ;
129- public on ( event : string , cb : ( ...args : any ) => any ) {
130- const arr = this . subscribers . get ( event ) ?? [ ] ;
131- arr . push ( { cb } ) ;
132- this . subscribers . set ( event , arr ) ;
133- }
134- public once ( event : string , cb : ( ...args : any ) => any ) {
135- const arr = this . subscribers . get ( event ) ?? [ ] ;
136- arr . push ( { cb, once : true } ) ;
137- this . subscribers . set ( event , arr ) ;
138- }
139- public emit ( event : string , ...data : any [ ] ) {
140- const cbs = this . subscribers . get ( event ) ?? [ ] ;
141- const cleanup : ( ( ) => void ) [ ] = [ ] ;
142- for ( const subscriber of cbs ) {
143- subscriber . cb ( ...data ) ;
144- if ( subscriber . once ) {
145- cleanup . push ( ( ) => cbs . splice ( cbs . indexOf ( subscriber ) , 1 ) ) ;
146- }
147- }
148- for ( const cb of cleanup ) {
149- cb ( ) ;
150- }
151- }
152- private unsubscribe ( ) {
153- this . subscribers . clear ( ) ;
154- }
155-
156205 private onKeypress ( char : string , key ?: Key ) {
157206 if ( this . state === 'error' ) {
158207 this . state = 'active' ;
159208 }
160209 if ( key ?. name && ! this . _track && aliases . has ( key . name ) ) {
161210 this . emit ( 'cursor' , aliases . get ( key . name ) ) ;
162211 }
163- if ( key ?. name && keys . has ( key . name ) ) {
164- this . emit ( 'cursor' , key . name ) ;
212+ if ( key ?. name && keys . has ( key . name as InferSetType < typeof keys > ) ) {
213+ this . emit ( 'cursor' , key . name as InferSetType < typeof keys > ) ;
165214 }
166215 if ( char && ( char . toLowerCase ( ) === 'y' || char . toLowerCase ( ) === 'n' ) ) {
167216 this . emit ( 'confirm' , char . toLowerCase ( ) === 'y' ) ;
@@ -189,7 +238,8 @@ export default class Prompt {
189238 this . state = 'submit' ;
190239 }
191240 }
192- if ( char === '\x03' ) {
241+
242+ if ( hasAliasKey ( [ key ?. name , key ?. sequence ] , 'cancel' ) ) {
193243 this . state = 'cancel' ;
194244 }
195245 if ( this . state === 'submit' || this . state === 'cancel' ) {
@@ -217,7 +267,6 @@ export default class Prompt {
217267 this . output . write ( cursor . move ( - 999 , lines * - 1 ) ) ;
218268 }
219269
220- private _prevFrame = '' ;
221270 private render ( ) {
222271 const frame = wrap ( this . _render ( this ) ?? '' , process . stdout . columns , { hard : true } ) ;
223272 if ( frame === this . _prevFrame ) return ;
0 commit comments