Skip to content

Commit 094a70a

Browse files
committed
fix(clean): single-keypress prompts with descriptive option labels
1 parent ca38ec6 commit 094a70a

1 file changed

Lines changed: 49 additions & 12 deletions

File tree

cmd/clean.go

Lines changed: 49 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"github.com/cego/gitte/config"
1515
"github.com/cego/gitte/executor"
1616
"github.com/spf13/cobra"
17+
"golang.org/x/term"
1718
)
1819

1920
func newCleanCmd() *cobra.Command {
@@ -203,28 +204,27 @@ func runCleanLocalChanges(ctx context.Context, cfg *config.GitteConfig, cwd stri
203204
fmt.Printf(" %s\n", r.name)
204205
}
205206

206-
scanner := bufio.NewScanner(stdin)
207-
208-
fmt.Print("\nReset all, handle individually, or cancel? [all/individually/cancel]: ")
209-
scanner.Scan()
210-
choice := strings.TrimSpace(strings.ToLower(scanner.Text()))
211-
if err := scanner.Err(); err != nil {
207+
fmt.Print("\nReset [a]ll / [i]ndividually / [C]ancel? ")
208+
choice, err := readKey(stdin)
209+
if err != nil {
212210
return fmt.Errorf("reading stdin: %w", err)
213211
}
214212

215213
switch choice {
216-
case "all":
214+
case "a":
217215
for _, r := range dirty {
218216
if err := hardReset(ctx, r.path); err != nil {
219217
fmt.Fprintf(os.Stderr, "warning: [%s] %v\n", r.name, err)
220218
}
221219
}
222-
case "individually":
220+
case "i":
223221
for _, r := range dirty {
224-
fmt.Printf("Reset %s? [y/N]: ", r.name)
225-
scanner.Scan()
226-
answer := strings.TrimSpace(strings.ToLower(scanner.Text()))
227-
if answer == "y" || answer == "yes" {
222+
fmt.Printf("Reset %s? [y/N] ", r.name)
223+
answer, err := readKey(stdin)
224+
if err != nil {
225+
return fmt.Errorf("reading stdin: %w", err)
226+
}
227+
if answer == "y" {
228228
if err := hardReset(ctx, r.path); err != nil {
229229
fmt.Fprintf(os.Stderr, "warning: [%s] %v\n", r.name, err)
230230
}
@@ -303,6 +303,43 @@ func hardReset(ctx context.Context, dir string) error {
303303
return nil
304304
}
305305

306+
// readKey reads a single keypress and returns it as a lowercase string.
307+
// When stdin is a terminal it switches to raw mode so the user does not need
308+
// to press Enter; the character is echoed followed by a newline.
309+
// Falls back to line-reading for non-TTY stdin (pipes, tests).
310+
func readKey(stdin io.Reader) (string, error) {
311+
if f, ok := stdin.(*os.File); ok {
312+
if fd := int(f.Fd()); term.IsTerminal(fd) {
313+
old, err := term.MakeRaw(fd)
314+
if err == nil {
315+
defer term.Restore(fd, old) //nolint:errcheck
316+
buf := make([]byte, 1)
317+
if _, err := f.Read(buf); err != nil {
318+
return "", err
319+
}
320+
ch := buf[0]
321+
switch ch {
322+
case 3: // Ctrl+C — treat as cancel
323+
fmt.Println()
324+
return "", nil
325+
case '\r', '\n': // Enter — default (empty → caller uses default)
326+
fmt.Println()
327+
return "", nil
328+
}
329+
fmt.Printf("%c\n", ch)
330+
return strings.ToLower(string(ch)), nil
331+
}
332+
}
333+
}
334+
// non-TTY fallback: read a full line
335+
scanner := bufio.NewScanner(stdin)
336+
scanner.Scan()
337+
if err := scanner.Err(); err != nil {
338+
return "", err
339+
}
340+
return strings.TrimSpace(strings.ToLower(scanner.Text())), nil
341+
}
342+
306343
// sortedProjectNames returns project names in alphabetical order.
307344
func sortedProjectNames(cfg *config.GitteConfig) []string {
308345
names := make([]string, 0, len(cfg.Projects))

0 commit comments

Comments
 (0)