1- import type { Key , ReadLine } from 'node:readline' ;
2-
31import { stdin , stdout } from 'node:process' ;
4- import readline from 'node:readline' ;
2+ import readline , { type Key , type ReadLine } from 'node:readline' ;
53import { Readable , Writable } from 'node:stream' ;
64import { WriteStream } from 'node:tty' ;
75import { cursor , erase } from 'sisteransi' ;
86import wrap from 'wrap-ansi' ;
97
10- function diffLines ( a : string , b : string ) {
11- if ( a === b ) return ;
12-
13- const aLines = a . split ( '\n' ) ;
14- const bLines = b . split ( '\n' ) ;
15- const diff : number [ ] = [ ] ;
16-
17- for ( let i = 0 ; i < Math . max ( aLines . length , bLines . length ) ; i ++ ) {
18- if ( aLines [ i ] !== bLines [ i ] ) diff . push ( i ) ;
19- }
20-
21- return diff ;
22- }
23-
24- const cancel = Symbol ( 'clack:cancel' ) ;
25- export function isCancel ( value : unknown ) : value is symbol {
26- return value === cancel ;
27- }
28-
29- function setRawMode ( input : Readable , value : boolean ) {
30- if ( ( input as typeof stdin ) . isTTY ) ( input as typeof stdin ) . setRawMode ( value ) ;
31- }
8+ import { ALIASES , CANCEL_SYMBOL , diffLines , hasAliasKey , KEYS , setRawMode } from '../utils' ;
329
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' ] ) ;
10+ import type { ClackEvents , ClackState , InferSetType } from '../types' ;
4011
4112export interface PromptOptions < Self extends Prompt > {
4213 render ( this : Omit < Self , 'prompt' > ) : string | void ;
@@ -48,25 +19,25 @@ export interface PromptOptions<Self extends Prompt> {
4819 debug ?: boolean ;
4920}
5021
51- export type State = 'initial' | 'active' | 'cancel' | 'submit' | 'error' ;
52-
5322export default class Prompt {
5423 protected input : Readable ;
5524 protected output : Writable ;
25+
5626 private rl ! : ReadLine ;
5727 private opts : Omit < PromptOptions < Prompt > , 'render' | 'input' | 'output' > ;
58- private _track : boolean = false ;
5928 private _render : ( context : Omit < Prompt , 'prompt' > ) => string | void ;
60- protected _cursor : number = 0 ;
29+ private _track = false ;
30+ private _prevFrame = '' ;
31+ private _subscribers = new Map < string , { cb : ( ...args : any ) => any ; once ?: boolean } [ ] > ( ) ;
32+ protected _cursor = 0 ;
6133
62- public state : State = 'initial' ;
34+ public state : ClackState = 'initial' ;
35+ public error = '' ;
6336 public value : any ;
64- public error : string = '' ;
6537
66- constructor (
67- { render, input = stdin , output = stdout , ...opts } : PromptOptions < Prompt > ,
68- trackValue : boolean = true
69- ) {
38+ constructor ( options : PromptOptions < Prompt > , trackValue : boolean = true ) {
39+ const { input = stdin , output = stdout , render, ...opts } = options ;
40+
7041 this . opts = opts ;
7142 this . onKeypress = this . onKeypress . bind ( this ) ;
7243 this . close = this . close . bind ( this ) ;
@@ -78,6 +49,66 @@ export default class Prompt {
7849 this . output = output ;
7950 }
8051
52+ /**
53+ * Unsubscribe all listeners
54+ */
55+ protected unsubscribe ( ) {
56+ this . _subscribers . clear ( ) ;
57+ }
58+
59+ /**
60+ * Set a subscriber with opts
61+ * @param event - The event name
62+ */
63+ private setSubscriber < T extends keyof ClackEvents > (
64+ event : T ,
65+ opts : { cb : ClackEvents [ T ] ; once ?: boolean }
66+ ) {
67+ const params = this . _subscribers . get ( event ) ?? [ ] ;
68+ params . push ( opts ) ;
69+ this . _subscribers . set ( event , params ) ;
70+ }
71+
72+ /**
73+ * Subscribe to an event
74+ * @param event - The event name
75+ * @param cb - The callback
76+ */
77+ public on < T extends keyof ClackEvents > ( event : T , cb : ClackEvents [ T ] ) {
78+ this . setSubscriber ( event , { cb } ) ;
79+ }
80+
81+ /**
82+ * Subscribe to an event once
83+ * @param event - The event name
84+ * @param cb - The callback
85+ */
86+ public once < T extends keyof ClackEvents > ( event : T , cb : ClackEvents [ T ] ) {
87+ this . setSubscriber ( event , { cb, once : true } ) ;
88+ }
89+
90+ /**
91+ * Emit an event with data
92+ * @param event - The event name
93+ * @param data - The data to pass to the callback
94+ */
95+ public emit < T extends keyof ClackEvents > ( event : T , ...data : Parameters < ClackEvents [ T ] > ) {
96+ const cbs = this . _subscribers . get ( event ) ?? [ ] ;
97+ const cleanup : ( ( ) => void ) [ ] = [ ] ;
98+
99+ for ( const subscriber of cbs ) {
100+ subscriber . cb ( ...data ) ;
101+
102+ if ( subscriber . once ) {
103+ cleanup . push ( ( ) => cbs . splice ( cbs . indexOf ( subscriber ) , 1 ) ) ;
104+ }
105+ }
106+
107+ for ( const cb of cleanup ) {
108+ cb ( ) ;
109+ }
110+ }
111+
81112 public prompt ( ) {
82113 const sink = new WriteStream ( 0 ) ;
83114 sink . _write = ( chunk , encoding , done ) => {
@@ -120,48 +151,20 @@ export default class Prompt {
120151 this . output . write ( cursor . show ) ;
121152 this . output . off ( 'resize' , this . render ) ;
122153 setRawMode ( this . input , false ) ;
123- resolve ( cancel ) ;
154+ resolve ( CANCEL_SYMBOL ) ;
124155 } ) ;
125156 } ) ;
126157 }
127158
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-
156159 private onKeypress ( char : string , key ?: Key ) {
157160 if ( this . state === 'error' ) {
158161 this . state = 'active' ;
159162 }
160- if ( key ?. name && ! this . _track && aliases . has ( key . name ) ) {
161- this . emit ( 'cursor' , aliases . get ( key . name ) ) ;
163+ if ( key ?. name && ! this . _track && ALIASES . has ( key . name ) ) {
164+ this . emit ( 'cursor' , ALIASES . get ( key . name ) ) ;
162165 }
163- if ( key ?. name && keys . has ( key . name ) ) {
164- this . emit ( 'cursor' , key . name ) ;
166+ if ( key ?. name && KEYS . has ( key . name as InferSetType < typeof KEYS > ) ) {
167+ this . emit ( 'cursor' , key . name as InferSetType < typeof KEYS > ) ;
165168 }
166169 if ( char && ( char . toLowerCase ( ) === 'y' || char . toLowerCase ( ) === 'n' ) ) {
167170 this . emit ( 'confirm' , char . toLowerCase ( ) === 'y' ) ;
@@ -189,7 +192,8 @@ export default class Prompt {
189192 this . state = 'submit' ;
190193 }
191194 }
192- if ( char === '\x03' ) {
195+
196+ if ( hasAliasKey ( [ key ?. name , key ?. sequence ] , 'cancel' ) ) {
193197 this . state = 'cancel' ;
194198 }
195199 if ( this . state === 'submit' || this . state === 'cancel' ) {
@@ -217,7 +221,6 @@ export default class Prompt {
217221 this . output . write ( cursor . move ( - 999 , lines * - 1 ) ) ;
218222 }
219223
220- private _prevFrame = '' ;
221224 private render ( ) {
222225 const frame = wrap ( this . _render ( this ) ?? '' , process . stdout . columns , { hard : true } ) ;
223226 if ( frame === this . _prevFrame ) return ;
0 commit comments