Skip to content

Commit 6f5ca3f

Browse files
authored
Merge pull request #31 from constructive-io/fixes/cga-cli
fix(inquirerer): implement stack-based ownership for keypress handling
2 parents 264e551 + 1c57b2f commit 6f5ca3f

File tree

2 files changed

+66
-27
lines changed

2 files changed

+66
-27
lines changed

packages/create-gen-app/src/template/replace.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -127,9 +127,6 @@ async function ensureLicenseFile(
127127
const licensePath = path.join(outputDir, 'LICENSE');
128128
fs.mkdirSync(path.dirname(licensePath), { recursive: true });
129129
fs.writeFileSync(licensePath, content.trimEnd() + '\n', 'utf8');
130-
console.log(
131-
`[create-gen-app] LICENSE updated with ${selectedLicense} template.`
132-
);
133130
}
134131

135132
/**

packages/inquirerer/src/keypress.ts

Lines changed: 66 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -25,32 +25,35 @@ export const KEY_CODES = {
2525
interface SharedInputState {
2626
dataHandler: (key: string) => void;
2727
instances: Set<TerminalKeypress>;
28+
activeStack: TerminalKeypress[]; // Stack of active instances, top is current owner
2829
rawModeSet: boolean;
2930
}
3031

3132
const sharedInputStates = new WeakMap<Readable, SharedInputState>();
3233

33-
function getOrCreateSharedState(input: Readable, proc: ProcessWrapper): SharedInputState {
34+
function getOrCreateSharedState(input: Readable): SharedInputState {
3435
let state = sharedInputStates.get(input);
3536
if (!state) {
3637
const dataHandler = (key: string) => {
3738
const currentState = sharedInputStates.get(input);
3839
if (!currentState) return;
3940

40-
for (const instance of currentState.instances) {
41-
if (instance.isActive()) {
42-
instance.handleKey(key);
41+
// Only dispatch to the top of the active stack (current owner)
42+
const owner = currentState.activeStack[currentState.activeStack.length - 1];
43+
if (owner) {
44+
owner.handleKey(key);
45+
46+
// Handle Ctrl+C via the current owner's process wrapper
47+
if (key === KEY_CODES.CTRL_C) {
48+
owner.exitProcess(0);
4349
}
4450
}
45-
46-
if (key === KEY_CODES.CTRL_C) {
47-
proc.exit(0);
48-
}
4951
};
5052

5153
state = {
5254
dataHandler,
5355
instances: new Set(),
56+
activeStack: [],
5457
rawModeSet: false
5558
};
5659

@@ -66,19 +69,28 @@ function removeFromSharedState(input: Readable, instance: TerminalKeypress): voi
6669

6770
state.instances.delete(instance);
6871

72+
// Remove from active stack as well
73+
const stackIndex = state.activeStack.indexOf(instance);
74+
if (stackIndex !== -1) {
75+
state.activeStack.splice(stackIndex, 1);
76+
}
77+
78+
// If stack is now empty, disable raw mode
79+
if (state.activeStack.length === 0 && state.rawModeSet) {
80+
if (typeof (input as any).setRawMode === 'function') {
81+
(input as any).setRawMode(false);
82+
}
83+
state.rawModeSet = false;
84+
}
85+
6986
if (state.instances.size === 0) {
7087
input.removeListener('data', state.dataHandler);
7188
sharedInputStates.delete(input);
72-
73-
if (state.rawModeSet && typeof (input as any).setRawMode === 'function') {
74-
(input as any).setRawMode(false);
75-
}
7689
}
7790
}
7891

7992
export class TerminalKeypress {
8093
private listeners: Record<string, KeyHandler[]> = {};
81-
private active: boolean = true;
8294
private noTty: boolean;
8395
private input: Readable;
8496
private proc: ProcessWrapper;
@@ -101,7 +113,7 @@ export class TerminalKeypress {
101113
}
102114

103115
private registerWithSharedState(): void {
104-
const state = getOrCreateSharedState(this.input, this.proc);
116+
const state = getOrCreateSharedState(this.input);
105117
state.instances.add(this);
106118
}
107119

@@ -110,11 +122,19 @@ export class TerminalKeypress {
110122
}
111123

112124
isActive(): boolean {
113-
return this.active && !this.destroyed;
125+
if (this.destroyed) return false;
126+
const state = sharedInputStates.get(this.input);
127+
if (!state) return false;
128+
// Active only if this instance is the current owner (top of stack)
129+
return state.activeStack[state.activeStack.length - 1] === this;
130+
}
131+
132+
exitProcess(code?: number): void {
133+
this.proc.exit(code);
114134
}
115135

116136
handleKey(key: string): void {
117-
if (!this.active || this.destroyed) return;
137+
if (this.destroyed) return;
118138
const handlers = this.listeners[key];
119139
handlers?.forEach(handler => handler());
120140
}
@@ -140,25 +160,47 @@ export class TerminalKeypress {
140160
}
141161

142162
pause(): void {
143-
this.active = false;
144-
this.clearHandlers();
163+
const state = sharedInputStates.get(this.input);
164+
if (!state) return;
165+
166+
// Remove from active stack (from anywhere, not just top)
167+
const stackIndex = state.activeStack.indexOf(this);
168+
if (stackIndex !== -1) {
169+
state.activeStack.splice(stackIndex, 1);
170+
}
171+
172+
// If stack is now empty, disable raw mode
173+
if (state.activeStack.length === 0 && state.rawModeSet) {
174+
if (this.isTTY() && typeof (this.input as any).setRawMode === 'function') {
175+
(this.input as any).setRawMode(false);
176+
}
177+
state.rawModeSet = false;
178+
}
145179
}
146180

147181
resume(): void {
148-
this.active = true;
182+
if (this.destroyed) return;
183+
184+
const state = sharedInputStates.get(this.input);
185+
if (!state) return;
186+
187+
// Move-to-top semantics: remove from anywhere in stack, then push to top
188+
const existingIndex = state.activeStack.indexOf(this);
189+
if (existingIndex !== -1) {
190+
state.activeStack.splice(existingIndex, 1);
191+
}
192+
state.activeStack.push(this);
193+
194+
// Enable raw mode if TTY
149195
if (this.isTTY() && typeof (this.input as any).setRawMode === 'function') {
150196
(this.input as any).setRawMode(true);
151-
const state = sharedInputStates.get(this.input);
152-
if (state) {
153-
state.rawModeSet = true;
154-
}
197+
state.rawModeSet = true;
155198
}
156199
}
157200

158201
destroy(): void {
159202
if (this.destroyed) return;
160203
this.destroyed = true;
161-
this.active = false;
162204
this.clearHandlers();
163205

164206
removeFromSharedState(this.input, this);

0 commit comments

Comments
 (0)