Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cli/azd/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

### Bugs Fixed

- [[#8648]](https://github.com/Azure/azure-dev/pull/8648) Fix interactive prompts (for example the `azd init` environment-name prompt) rendering twice on Windows terminals by rendering prompts with azd's own UX components instead of the archived survey library.

### Other Changes

## 1.25.6 (2026-06-12)
Expand Down
16 changes: 16 additions & 0 deletions cli/azd/pkg/input/console.go
Original file line number Diff line number Diff line change
Expand Up @@ -719,6 +719,10 @@ func (c *AskerConsole) Prompt(ctx context.Context, options ConsoleOptions) (stri
return response, nil
}

if c.isTerminal {
return c.promptUx(ctx, options)
}
Comment on lines +722 to +724

err := c.doInteraction(func(c *AskerConsole) error {
return c.asker(promptFromOptions(options), &response)
})
Expand Down Expand Up @@ -781,6 +785,10 @@ func (c *AskerConsole) Select(ctx context.Context, options ConsoleOptions) (int,
return res, nil
}

if c.isTerminal {
return c.selectUx(ctx, options)
}
Comment on lines +788 to +790

surveyOptions := make([]string, len(options.Options))
surveyDefault := options.DefaultValue
surveyDefaultAsString, surveyDefaultIsString := surveyDefault.(string)
Expand Down Expand Up @@ -854,6 +862,10 @@ func (c *AskerConsole) MultiSelect(ctx context.Context, options ConsoleOptions)
return response, nil
}

if c.isTerminal {
return c.multiSelectUx(ctx, options)
}
Comment on lines +865 to +867

surveyOptions := make([]string, len(options.Options))
surveyDefault := options.DefaultValue
surveyDefaultAsArr, surveyDefaultIsArr := surveyDefault.([]string)
Expand Down Expand Up @@ -931,6 +943,10 @@ func (c *AskerConsole) Confirm(ctx context.Context, options ConsoleOptions) (boo
}
}

if c.isTerminal {
return c.confirmUx(ctx, options)
}
Comment on lines +946 to +948

var defaultValue bool
if value, ok := options.DefaultValue.(bool); ok {
defaultValue = value
Expand Down
182 changes: 182 additions & 0 deletions cli/azd/pkg/input/console_ux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package input

import (
"context"
"errors"
"fmt"
"slices"

surveyterm "github.com/AlecAivazis/survey/v2/terminal"
"github.com/azure/azure-dev/cli/azd/pkg/output"
uxlib "github.com/azure/azure-dev/cli/azd/pkg/ux"
)

// The interactive prompts below are rendered with the in-repo ux package instead
// of AlecAivazis/survey. survey has a Windows-specific double-render bug (the
// prompt and its answer are redrawn above the next prompt) that was never fixed
// upstream and the library is archived. See Azure/azure-dev#435.
//
// Only the terminal path is routed through ux. The non-terminal / no-prompt paths
// continue to use the lightweight asker implementation so machine-friendly input
// behavior (used by tests, CI, and piped stdin) is unchanged.

// mapUxCancel converts a ux cancellation error into the survey interrupt error
// that the rest of azd already recognizes, preserving existing error handling.
func mapUxCancel(err error) error {
if err != nil && errors.Is(err, uxlib.ErrCancelled) {
return surveyterm.InterruptErr
}

return err
}

// optionLabel returns the display label for an option, appending the gray detail
// text (when present) the same way the previous survey-based rendering did.
func optionLabel(options ConsoleOptions, index int) string {
Comment on lines +36 to +38
option := options.Options[index]
if index < len(options.OptionDetails) && options.OptionDetails[index] != "" {
return fmt.Sprintf("%s %s", option, output.WithGrayFormat("(%s)", options.OptionDetails[index]))
}

return option
}

func (c *AskerConsole) promptUx(ctx context.Context, options ConsoleOptions) (string, error) {
var defaultValue string
if value, ok := options.DefaultValue.(string); ok {
defaultValue = value
}

prompt := uxlib.NewPrompt(&uxlib.PromptOptions{
Writer: c.writer,
Message: options.Message,
HelpMessage: options.Help,
DefaultValue: defaultValue,
Secret: options.IsPassword,
})

var response string
err := c.doInteraction(func(c *AskerConsole) error {
var askErr error
response, askErr = prompt.Ask(ctx)
return askErr
})
if err != nil {
return "", mapUxCancel(err)
}

c.updateLastBytes(afterIoSentinel)
return response, nil
}

func (c *AskerConsole) selectUx(ctx context.Context, options ConsoleOptions) (int, error) {
choices := make([]*uxlib.SelectChoice, len(options.Options))
for i, option := range options.Options {
choices[i] = &uxlib.SelectChoice{
Value: option,
Label: optionLabel(options, i),
}
}
Comment on lines +75 to +82

selectedIndex := 0
if value, ok := options.DefaultValue.(string); ok {
if idx := slices.Index(options.Options, value); idx >= 0 {
selectedIndex = idx
}
}

component := uxlib.NewSelect(&uxlib.SelectOptions{
Writer: c.writer,
Message: options.Message,
HelpMessage: options.Help,
Choices: choices,
SelectedIndex: &selectedIndex,
})

var result *int
err := c.doInteraction(func(c *AskerConsole) error {
var askErr error
result, askErr = component.Ask(ctx)
return askErr
})
if err != nil {
return -1, mapUxCancel(err)
}
if result == nil {
return -1, surveyterm.InterruptErr
}

c.updateLastBytes(afterIoSentinel)
return *result, nil
}

func (c *AskerConsole) confirmUx(ctx context.Context, options ConsoleOptions) (bool, error) {
defaultValue := false
if value, ok := options.DefaultValue.(bool); ok {
defaultValue = value
}

component := uxlib.NewConfirm(&uxlib.ConfirmOptions{
Writer: c.writer,
Message: options.Message,
HelpMessage: options.Help,
DefaultValue: &defaultValue,
})

var result *bool
err := c.doInteraction(func(c *AskerConsole) error {
var askErr error
result, askErr = component.Ask(ctx)
return askErr
})
if err != nil {
return false, mapUxCancel(err)
}
if result == nil {
return false, surveyterm.InterruptErr
}

c.updateLastBytes(afterIoSentinel)
return *result, nil
}

func (c *AskerConsole) multiSelectUx(ctx context.Context, options ConsoleOptions) ([]string, error) {
defaultValues, _ := options.DefaultValue.([]string)

choices := make([]*uxlib.MultiSelectChoice, len(options.Options))
Comment on lines +146 to +149
for i, option := range options.Options {
choices[i] = &uxlib.MultiSelectChoice{
Value: option,
Label: optionLabel(options, i),
Selected: slices.Contains(defaultValues, option),
}
}

component := uxlib.NewMultiSelect(&uxlib.MultiSelectOptions{
Writer: c.writer,
Message: options.Message,
HelpMessage: options.Help,
Choices: choices,
})
Comment on lines +158 to +163

var selected []*uxlib.MultiSelectChoice
err := c.doInteraction(func(c *AskerConsole) error {
var askErr error
selected, askErr = component.Ask(ctx)
return askErr
})
if err != nil {
return nil, mapUxCancel(err)
}

response := make([]string, len(selected))
for i, choice := range selected {
response[i] = choice.Value
}

c.updateLastBytes(afterIoSentinel)
return response, nil
}
Loading