Skip to content

Commit 2a30c3f

Browse files
committed
feat: Changed to custom component
1 parent f9d342c commit 2a30c3f

4 files changed

Lines changed: 80 additions & 32 deletions

File tree

src/ui/components/default-component.tsx

Lines changed: 9 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
import { Form, FormProps } from '@codifycli/ink-form';
2-
import { PasswordInput, TextInput } from '@inkjs/ui';
2+
import { TextInput } from '@inkjs/ui';
33
import chalk from 'chalk';
44
import { Box, Static, Text } from 'ink';
55
import SelectInput from 'ink-select-input';
66
import { useAtom } from 'jotai';
7-
import { selectAtom } from 'jotai/utils';
87
import { EventEmitter } from 'node:events';
9-
import React, { useLayoutEffect, useState } from 'react';
8+
import React, { useLayoutEffect } from 'react';
109

1110
import { Plan } from '../../entities/plan.js';
1211
import { FileModificationResult } from '../../generators/index.js';
@@ -21,12 +20,12 @@ import { MultiSelect } from './multi-select/MultiSelect.js';
2120
import { PlanComponent } from './plan/plan.js';
2221
import { ProgressDisplay } from './progress/progress-display.js';
2322
import { PromptPressKeyToContinue } from './widgets/PromptPressKeyToContinue.js';
23+
import { SudoPasswordInput } from './widgets/SudoPasswordInput.js';
2424

2525
export function DefaultComponent(props: {
2626
emitter: EventEmitter
2727
}) {
2828
const { emitter } = props
29-
const [disableSudoPrompt, setDisableSudoPrompt] = useState(false);
3029
const [{ status: renderStatus, data: renderData }] = useAtom(store.renderState);
3130

3231
// Use layoutEffect runs before the first render, whereas useEffect runs after
@@ -37,15 +36,8 @@ export function DefaultComponent(props: {
3736

3837
emitter.on(RenderEvent.LOG, logListener);
3938

40-
const disableSudoPrompt = (isDisabled: boolean) => {
41-
setDisableSudoPrompt(isDisabled);
42-
}
43-
44-
emitter.on(RenderEvent.DISABLE_SUDO_PROMPT, disableSudoPrompt)
45-
4639
return () => {
4740
emitter.off(RenderEvent.LOG, logListener);
48-
emitter.off(RenderEvent.DISABLE_SUDO_PROMPT, disableSudoPrompt);
4941
}
5042
}, []);
5143

@@ -91,13 +83,12 @@ export function DefaultComponent(props: {
9183
}
9284
{
9385
renderStatus === RenderStatus.SUDO_PROMPT && (
94-
<Box flexDirection="column">
95-
<Text>Password:</Text>
96-
{/* Use sudoAttemptCount as a hack to reset password input between attempts */}
97-
<PasswordInput isDisabled={disableSudoPrompt} key={renderData as number} onSubmit={(password) => {
98-
emitter.emit(RenderEvent.SUDO_PROMPT_RESULT, password);
99-
}}/>
100-
</Box>
86+
<SudoPasswordInput
87+
key={renderData as number}
88+
hasError={(renderData as number) > 0}
89+
onSubmit={(password) => emitter.emit(RenderEvent.SUDO_PROMPT_RESULT, password)}
90+
onCancel={() => emitter.emit(RenderEvent.SUDO_PASSWORD_CANCEL)}
91+
/>
10192
)
10293
}
10394
{
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { Box, Text, useInput } from 'ink';
2+
import React, { useState } from 'react';
3+
4+
export function SudoPasswordInput(props: {
5+
hasError: boolean;
6+
onSubmit: (password: string) => void;
7+
onCancel: () => void;
8+
}) {
9+
const { hasError, onSubmit, onCancel } = props;
10+
const [value, setValue] = useState('');
11+
12+
const borderColor = hasError ? 'red' : 'cyan';
13+
14+
useInput((input, key) => {
15+
if (key.escape) {
16+
onCancel();
17+
return;
18+
}
19+
20+
if (key.return) {
21+
onSubmit(value);
22+
setValue('');
23+
return;
24+
}
25+
26+
if (key.backspace || key.delete) {
27+
setValue((prev) => prev.slice(0, -1));
28+
return;
29+
}
30+
31+
// Ignore non-printable characters
32+
if (input && !key.ctrl && !key.meta) {
33+
setValue((prev) => prev + input);
34+
}
35+
});
36+
37+
return (
38+
<Box
39+
flexDirection="column"
40+
borderStyle="single"
41+
borderTop={true}
42+
borderBottom={true}
43+
borderLeft={false}
44+
borderRight={false}
45+
borderColor={borderColor}
46+
marginTop={1}
47+
>
48+
<Box gap={1}>
49+
<Text bold color={borderColor}>Sudo Password:</Text>
50+
<Text>{value.replace(/./g, '*')}</Text>
51+
<Text inverse> </Text>
52+
</Box>
53+
{hasError && <Text color="red">Incorrect password, try again</Text>}
54+
<Text dimColor>Enter to confirm · Esc to cancel</Text>
55+
</Box>
56+
);
57+
}

src/ui/reporters/default-reporter.tsx

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -327,14 +327,19 @@ export class DefaultReporter implements Reporter {
327327
let attemptCount = 0;
328328

329329
while (attemptCount < 3) {
330-
this.renderEmitter.emit(RenderEvent.DISABLE_SUDO_PROMPT, false);
331-
const passwordAttempt = await this.updateStateAndAwaitEvent<string>(
332-
() => this.updateRenderState(RenderStatus.SUDO_PROMPT, attemptCount),
333-
RenderEvent.SUDO_PROMPT_RESULT,
334-
);
335-
this.renderEmitter.emit(RenderEvent.DISABLE_SUDO_PROMPT, true);
330+
const result = await (await Promise.all([
331+
this.updateRenderState(RenderStatus.SUDO_PROMPT, attemptCount),
332+
Promise.race([
333+
this.awaitEvent<string>(RenderEvent.SUDO_PROMPT_RESULT),
334+
this.awaitEvent<'cancel'>(RenderEvent.SUDO_PASSWORD_CANCEL),
335+
]),
336+
])).at(1) as string | 'cancel';
337+
338+
if (result === 'cancel') {
339+
break;
340+
}
336341

337-
const isValid = this.sudoPasswordSubmittedCallback?.(passwordAttempt) ?? false;
342+
const isValid = this.sudoPasswordSubmittedCallback?.(result) ?? false;
338343
if (isValid) {
339344
await sleep(50);
340345
this.updateRenderState(RenderStatus.NOTHING, null);
@@ -344,14 +349,10 @@ export class DefaultReporter implements Reporter {
344349
return;
345350
}
346351

347-
if (attemptCount + 1 < 3) {
348-
ctx.log(chalk.red(`Sorry, try again. (${attemptCount + 1}/3)`));
349-
}
350-
351352
attemptCount++;
352353
}
353354

354-
// All attempts exhausted — restore progress display without marking saved
355+
// Cancelled or all attempts exhausted — restore progress display
355356
await sleep(50);
356357
this.updateRenderState(RenderStatus.NOTHING, null);
357358
await sleep(50);
@@ -362,12 +363,10 @@ export class DefaultReporter implements Reporter {
362363
let attemptCount = 0;
363364

364365
while (attemptCount < 3) {
365-
this.renderEmitter.emit(RenderEvent.DISABLE_SUDO_PROMPT, false);
366366
const passwordAttempt = await this.updateStateAndAwaitEvent<string>(
367367
() => this.updateRenderState(RenderStatus.SUDO_PROMPT, attemptCount),
368368
RenderEvent.SUDO_PROMPT_RESULT,
369369
);
370-
this.renderEmitter.emit(RenderEvent.DISABLE_SUDO_PROMPT, true);
371370

372371
// Validates that the password works
373372
if (SudoUtils.validate(passwordAttempt)) {

src/ui/reporters/reporter.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export enum RenderEvent {
2525
STATE_TRANSITION = 'stateTransition',
2626
TOGGLE_VERBOSITY = 'toggleVerbosity',
2727
SUDO_PASSWORD_TOGGLE = 'sudoPasswordToggle',
28+
SUDO_PASSWORD_CANCEL = 'sudoPasswordCancel',
2829
SUDO_PASSWORD_PRE_SUPPLIED = 'sudoPasswordPreSupplied',
2930
}
3031

0 commit comments

Comments
 (0)