Skip to content

Commit 516d3bd

Browse files
committed
custom prompter with colored input text
1 parent a813158 commit 516d3bd

5 files changed

Lines changed: 58 additions & 50 deletions

File tree

cmd/add.go

Lines changed: 1 addition & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,11 @@ package cmd
33
import (
44
"fmt"
55

6-
"github.com/AlecAivazis/survey/v2/terminal"
76
"github.com/github/gh-stack/internal/branch"
87
"github.com/github/gh-stack/internal/config"
98
"github.com/github/gh-stack/internal/git"
109
"github.com/github/gh-stack/internal/modify"
1110
"github.com/github/gh-stack/internal/stack"
12-
"github.com/mgutz/ansi"
1311
"github.com/spf13/cobra"
1412
)
1513

@@ -154,7 +152,7 @@ func runAdd(cfg *config.Config, opts *addOptions, args []string) error {
154152
prefill = s.Prefix + "/"
155153
}
156154
for {
157-
input, err := inputWithPrefill(cfg, "Enter a name for the new branch", prefill)
155+
input, err := inputWithPrefill(cfg, "Enter a name for the new branch:", prefill)
158156
if err != nil {
159157
if isInterruptError(err) {
160158
printInterrupt(cfg)
@@ -282,34 +280,3 @@ func applyPrefix(cfg *config.Config, prefix, name string) string {
282280
}
283281
return name
284282
}
285-
286-
// inputWithPrefill prompts the user for text input with the given prefill
287-
// already editable in the input field. Unlike survey.Input's Default (which
288-
// shows in parentheses), this places the prefill text directly in the
289-
// editable line so the user can append to or modify it.
290-
func inputWithPrefill(cfg *config.Config, prompt, prefill string) (string, error) {
291-
if cfg.InputFn != nil {
292-
return cfg.InputFn(prompt, prefill)
293-
}
294-
295-
stdio := terminal.Stdio{In: cfg.In, Out: cfg.Out, Err: cfg.Err}
296-
rr := terminal.NewRuneReader(stdio)
297-
_ = rr.SetTermMode()
298-
defer func() { _ = rr.RestoreTermMode() }()
299-
300-
// Render the prompt in survey style: green bold "?" + bold message
301-
icon := "?"
302-
if cfg.Terminal.IsColorEnabled() {
303-
icon = ansi.Color("?", "green+hb")
304-
}
305-
fmt.Fprintf(cfg.Out, "%s %s ", icon, prompt)
306-
307-
line, err := rr.ReadLineWithDefault(0, []rune(prefill))
308-
// Move to a new line after the input
309-
fmt.Fprintln(cfg.Out)
310-
311-
if err != nil {
312-
return "", err
313-
}
314-
return string(line), nil
315-
}

cmd/add_test.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -344,8 +344,9 @@ func TestAdd_PromptPrefillsPrefix(t *testing.T) {
344344

345345
cfg, outR, errR := config.NewTestConfig()
346346

347-
var gotDefault string
347+
var gotPrompt, gotDefault string
348348
cfg.InputFn = func(prompt, defaultValue string) (string, error) {
349+
gotPrompt = prompt
349350
gotDefault = defaultValue
350351
return "feat/my-branch", nil
351352
}
@@ -355,6 +356,7 @@ func TestAdd_PromptPrefillsPrefix(t *testing.T) {
355356

356357
require.NoError(t, err)
357358
require.NotContains(t, output, "\u2717", "unexpected error")
359+
assert.Contains(t, gotPrompt, ":", "prompt should end with a colon")
358360
assert.Equal(t, "feat/", gotDefault, "prompt should pre-fill prefix/")
359361
assert.Equal(t, "feat/my-branch", createdBranch, "full input should be used as branch name")
360362
}

cmd/init.go

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -159,8 +159,7 @@ func runInit(cfg *config.Config, opts *initOptions) error {
159159
} else if opts.numbered {
160160
// === NUMBERED PATH (unchanged) ===
161161
if opts.prefix == "" && cfg.IsInteractive() {
162-
p := prompter.New(cfg.In, cfg.Out, cfg.Err)
163-
prefixInput, err := p.Input("Enter a branch prefix (required for --numbered)", "")
162+
prefixInput, err := inputWithPrefill(cfg, "Enter a branch prefix (required for --numbered):", "")
164163
if err != nil {
165164
if isInterruptError(err) {
166165
printInterrupt(cfg)
@@ -377,15 +376,15 @@ func runInteractiveInit(cfg *config.Config, sf *stack.StackFile, trunk, currentB
377376
branchName = currentBranch
378377
} else {
379378
// Create a new branch — fall through to input prompt
380-
name, err := promptBranchName(cfg, p, opts.prefix)
379+
name, err := promptBranchName(cfg, opts.prefix)
381380
if err != nil {
382381
return nil, false, err
383382
}
384383
branchName = name
385384
}
386385
} else {
387386
// On trunk or detached HEAD — prompt for name directly
388-
name, err := promptBranchName(cfg, p, opts.prefix)
387+
name, err := promptBranchName(cfg, opts.prefix)
389388
if err != nil {
390389
return nil, false, err
391390
}
@@ -430,14 +429,16 @@ func runInteractiveInit(cfg *config.Config, sf *stack.StackFile, trunk, currentB
430429
return []string{branchName}, wasAdopted, nil
431430
}
432431

433-
// promptBranchName prompts the user for a branch name, applying the
434-
// explicit --prefix if set.
435-
func promptBranchName(cfg *config.Config, p *prompter.Prompter, prefix string) (string, error) {
436-
prompt := "What's the name of the first branch?"
432+
// promptBranchName prompts the user for a branch name, pre-filling the
433+
// prefix in the input when set so the user can see and edit the full name.
434+
func promptBranchName(cfg *config.Config, prefix string) (string, error) {
435+
prefill := ""
436+
prompt := "What's the name of the first branch:"
437437
if prefix != "" {
438-
prompt = fmt.Sprintf("Enter a name for the first branch (will be prefixed with %s/)", prefix)
438+
prompt = "Enter a name for the first branch:"
439+
prefill = prefix + "/"
439440
}
440-
branchName, err := p.Input(prompt, "")
441+
branchName, err := inputWithPrefill(cfg, prompt, prefill)
441442
if err != nil {
442443
if isInterruptError(err) {
443444
printInterrupt(cfg)
@@ -451,9 +452,6 @@ func promptBranchName(cfg *config.Config, p *prompter.Prompter, prefix string) (
451452
cfg.Errorf("branch name cannot be empty")
452453
return "", ErrInvalidArgs
453454
}
454-
if prefix != "" {
455-
branchName = prefix + "/" + branchName
456-
}
457455
return branchName, nil
458456
}
459457

cmd/submit.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -281,8 +281,7 @@ func createPR(cfg *config.Config, client github.ClientOps, s *stack.Stack, i int
281281
title, commitBody := defaultPRTitleBody(baseBranch, b.Branch)
282282
originalTitle := title
283283
if !opts.auto && cfg.IsInteractive() {
284-
p := prompter.New(cfg.In, cfg.Out, cfg.Err)
285-
input, err := p.Input(fmt.Sprintf("Title for PR (branch %s):", b.Branch), title)
284+
input, err := inputWithPrefill(cfg, fmt.Sprintf("Title for PR (branch %s):", b.Branch), title)
286285
if err != nil {
287286
if isInterruptError(err) {
288287
return errInterrupt

cmd/utils.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"github.com/github/gh-stack/internal/git"
1515
"github.com/github/gh-stack/internal/github"
1616
"github.com/github/gh-stack/internal/stack"
17+
"github.com/mgutz/ansi"
1718
)
1819

1920
// ErrSilent indicates the error has already been printed to the user.
@@ -69,6 +70,47 @@ func printInterrupt(cfg *config.Config) {
6970
cfg.Infof("Received interrupt, aborting operation")
7071
}
7172

73+
// inputWithPrefill prompts the user for text input with the given prefill
74+
// already editable in the input field. Unlike survey.Input's Default (which
75+
// shows in parentheses), this places the prefill text directly in the
76+
// editable line so the user can append to or modify it. The user's input
77+
// is rendered in cyan for visual distinction from the prompt message.
78+
func inputWithPrefill(cfg *config.Config, prompt, prefill string) (string, error) {
79+
if cfg.InputFn != nil {
80+
return cfg.InputFn(prompt, prefill)
81+
}
82+
83+
stdio := terminal.Stdio{In: cfg.In, Out: cfg.Out, Err: cfg.Err}
84+
rr := terminal.NewRuneReader(stdio)
85+
_ = rr.SetTermMode()
86+
defer func() { _ = rr.RestoreTermMode() }()
87+
88+
// Render the prompt in survey style: green bold "?" + bold message
89+
icon := "?"
90+
useColor := cfg.Terminal.IsColorEnabled()
91+
if useColor {
92+
icon = ansi.Color("?", "green+hb")
93+
}
94+
fmt.Fprintf(cfg.Out, "%s %s ", icon, prompt)
95+
96+
// Set cyan color for the user's input text
97+
if useColor {
98+
fmt.Fprint(cfg.Out, ansi.ColorCode("cyan"))
99+
}
100+
101+
line, err := rr.ReadLineWithDefault(0, []rune(prefill))
102+
103+
// Reset color after input
104+
if useColor {
105+
fmt.Fprint(cfg.Out, ansi.ColorCode("reset"))
106+
}
107+
108+
if err != nil {
109+
return "", err
110+
}
111+
return string(line), nil
112+
}
113+
72114
// selectPromptPageSize matches the PageSize used by the go-gh prompter.
73115
const selectPromptPageSize = 20
74116

0 commit comments

Comments
 (0)