Skip to content

Commit 8aa4e17

Browse files
wbrezaCopilot
andcommitted
refactor: migrate azd tool interactive components to uxlib
Replace legacy console.MultiSelect/Confirm/Spinner (survey.v2) with the newer pkg/ux/ components that use canvas-based rendering. This fixes escape code artifacts on arrow key presses in terminal. Changes: - cmd/tool.go: 2x console.MultiSelect -> uxlib.NewMultiSelect - cmd/middleware/tool_first_run.go: console.Confirm -> uxlib.NewConfirm, console.MultiSelect -> uxlib.NewMultiSelect, console.ShowSpinner/StopSpinner -> uxlib.NewSpinner.Run Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 331c7a6 commit 8aa4e17

File tree

2 files changed

+95
-60
lines changed

2 files changed

+95
-60
lines changed

cli/azd/cmd/middleware/tool_first_run.go

Lines changed: 47 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package middleware
55

66
import (
77
"context"
8+
"errors"
89
"fmt"
910
"log"
1011
"os"
@@ -17,6 +18,7 @@ import (
1718
"github.com/azure/azure-dev/cli/azd/pkg/input"
1819
"github.com/azure/azure-dev/cli/azd/pkg/output"
1920
"github.com/azure/azure-dev/cli/azd/pkg/tool"
21+
uxlib "github.com/azure/azure-dev/cli/azd/pkg/ux"
2022
)
2123

2224
// configKeyFirstRunCompleted is the user-config path that records
@@ -124,18 +126,22 @@ func (m *ToolFirstRunMiddleware) runFirstRunExperience(ctx context.Context) erro
124126
// ---------------------------------------------------------------
125127
// Opt-in prompt
126128
// ---------------------------------------------------------------
127-
runCheck, err := m.console.Confirm(ctx, input.ConsoleOptions{
129+
confirm := uxlib.NewConfirm(&uxlib.ConfirmOptions{
128130
Message: "Would you like to check your Azure development tools?",
129-
DefaultValue: true,
131+
DefaultValue: new(true),
130132
})
133+
runCheck, err := confirm.Ask(ctx)
131134
if err != nil {
132135
// Confirm can fail on interrupt/cancel — mark completed so
133136
// we don't pester the user again.
134137
m.markCompleted()
138+
if errors.Is(err, uxlib.ErrCancelled) {
139+
return nil
140+
}
135141
return fmt.Errorf("prompting for tool check: %w", err)
136142
}
137143

138-
if !runCheck {
144+
if runCheck == nil || !*runCheck {
139145
m.markCompleted()
140146
return nil
141147
}
@@ -144,13 +150,17 @@ func (m *ToolFirstRunMiddleware) runFirstRunExperience(ctx context.Context) erro
144150
// Tool detection
145151
// ---------------------------------------------------------------
146152
m.console.Message(ctx, "")
147-
m.console.ShowSpinner(ctx, "Detecting tools...", input.Step)
148-
149-
statuses, err := m.manager.DetectAll(ctx)
150153

151-
m.console.StopSpinner(ctx, "", input.StepDone)
152-
153-
if err != nil {
154+
var statuses []*tool.ToolStatus
155+
detectSpinner := uxlib.NewSpinner(&uxlib.SpinnerOptions{
156+
Text: "Detecting tools...",
157+
ClearOnStop: true,
158+
})
159+
if err := detectSpinner.Run(ctx, func(ctx context.Context) error {
160+
var detectErr error
161+
statuses, detectErr = m.manager.DetectAll(ctx)
162+
return detectErr
163+
}); err != nil {
154164
m.markCompleted()
155165
return fmt.Errorf("detecting tools: %w", err)
156166
}
@@ -213,19 +223,25 @@ func (m *ToolFirstRunMiddleware) offerInstall(
213223
ctx context.Context,
214224
missing []*tool.ToolStatus,
215225
) error {
216-
options := make([]string, 0, len(missing))
217-
toolIDs := make([]string, 0, len(missing))
218-
for _, s := range missing {
219-
options = append(options, fmt.Sprintf("%s — %s", s.Tool.Name, s.Tool.Description))
220-
toolIDs = append(toolIDs, s.Tool.Id)
226+
choices := make([]*uxlib.MultiSelectChoice, len(missing))
227+
for i, s := range missing {
228+
choices[i] = &uxlib.MultiSelectChoice{
229+
Value: s.Tool.Id,
230+
Label: fmt.Sprintf("%s — %s", s.Tool.Name, s.Tool.Description),
231+
Selected: true, // pre-select all recommended
232+
}
221233
}
222234

223-
selected, err := m.console.MultiSelect(ctx, input.ConsoleOptions{
224-
Message: "Select recommended tools to install:",
225-
Options: options,
226-
DefaultValue: options, // pre-select all by default
235+
multiSelect := uxlib.NewMultiSelect(&uxlib.MultiSelectOptions{
236+
Message: "Select recommended tools to install:",
237+
Choices: choices,
227238
})
239+
240+
selected, err := multiSelect.Ask(ctx)
228241
if err != nil {
242+
if errors.Is(err, uxlib.ErrCancelled) {
243+
return nil
244+
}
229245
return fmt.Errorf("prompting for tool selection: %w", err)
230246
}
231247

@@ -235,26 +251,25 @@ func (m *ToolFirstRunMiddleware) offerInstall(
235251
return nil
236252
}
237253

238-
// Map selected display strings back to tool IDs.
254+
// Extract selected tool IDs.
239255
selectedIDs := make([]string, 0, len(selected))
240-
for _, sel := range selected {
241-
for i, opt := range options {
242-
if sel == opt {
243-
selectedIDs = append(selectedIDs, toolIDs[i])
244-
break
245-
}
246-
}
256+
for _, choice := range selected {
257+
selectedIDs = append(selectedIDs, choice.Value)
247258
}
248259

249260
// Install selected tools.
250261
m.console.Message(ctx, "")
251-
m.console.ShowSpinner(ctx, "Installing tools...", input.Step)
252-
253-
results, err := m.manager.InstallTools(ctx, selectedIDs)
254262

255-
m.console.StopSpinner(ctx, "", input.StepDone)
256-
257-
if err != nil {
263+
var results []*tool.InstallResult
264+
installSpinner := uxlib.NewSpinner(&uxlib.SpinnerOptions{
265+
Text: "Installing tools...",
266+
ClearOnStop: true,
267+
})
268+
if err := installSpinner.Run(ctx, func(ctx context.Context) error {
269+
var installErr error
270+
results, installErr = m.manager.InstallTools(ctx, selectedIDs)
271+
return installErr
272+
}); err != nil {
258273
return fmt.Errorf("installing tools: %w", err)
259274
}
260275

cli/azd/cmd/tool.go

Lines changed: 48 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -151,17 +151,14 @@ func (a *toolAction) Run(ctx context.Context) (*actions.ActionResult, error) {
151151
a.console.Message(ctx, "")
152152

153153
// 3. Collect uninstalled tools for interactive selection.
154-
var uninstalledNames []string
155-
uninstalledByName := map[string]*tool.ToolStatus{}
156-
154+
var uninstalled []*tool.ToolStatus
157155
for _, s := range statuses {
158156
if !s.Installed {
159-
uninstalledNames = append(uninstalledNames, s.Tool.Name)
160-
uninstalledByName[s.Tool.Name] = s
157+
uninstalled = append(uninstalled, s)
161158
}
162159
}
163160

164-
if len(uninstalledNames) == 0 {
161+
if len(uninstalled) == 0 {
165162
a.console.Message(ctx, output.WithSuccessFormat("All tools are installed!"))
166163
return &actions.ActionResult{
167164
Message: &actions.ResultMessage{
@@ -171,26 +168,39 @@ func (a *toolAction) Run(ctx context.Context) (*actions.ActionResult, error) {
171168
}
172169

173170
// 4. MultiSelect uninstalled tools.
174-
selected, err := a.console.MultiSelect(ctx, input.ConsoleOptions{
171+
choices := make([]*uxlib.MultiSelectChoice, len(uninstalled))
172+
for i, s := range uninstalled {
173+
choices[i] = &uxlib.MultiSelectChoice{
174+
Value: s.Tool.Id,
175+
Label: s.Tool.Name,
176+
Selected: s.Tool.Priority == tool.ToolPriorityRecommended,
177+
}
178+
}
179+
180+
multiSelect := uxlib.NewMultiSelect(&uxlib.MultiSelectOptions{
181+
Writer: a.console.Handles().Stdout,
182+
Reader: a.console.Handles().Stdin,
175183
Message: "Select tools to install",
176-
Options: uninstalledNames,
184+
Choices: choices,
177185
})
186+
187+
selected, err := multiSelect.Ask(ctx)
178188
if err != nil {
179189
return nil, fmt.Errorf("selecting tools: %w", err)
180190
}
181191

182-
if len(selected) == 0 {
183-
return nil, nil
184-
}
185-
186192
// 5. Install selected tools using TaskList.
187-
ids := make([]string, 0, len(selected))
188-
for _, name := range selected {
189-
if s, ok := uninstalledByName[name]; ok {
190-
ids = append(ids, s.Tool.Id)
193+
var ids []string
194+
for _, choice := range selected {
195+
if choice.Selected {
196+
ids = append(ids, choice.Value)
191197
}
192198
}
193199

200+
if len(ids) == 0 {
201+
return nil, nil
202+
}
203+
194204
taskList := uxlib.NewTaskList(
195205
&uxlib.TaskListOptions{ContinueOnError: true},
196206
)
@@ -437,32 +447,42 @@ func (a *toolInstallAction) resolveToolIds(ctx context.Context) ([]string, error
437447
return nil, fmt.Errorf("detecting tools: %w", err)
438448
}
439449

440-
var uninstalledNames []string
441-
nameToID := map[string]string{}
442-
450+
var uninstalled []*tool.ToolStatus
443451
for _, s := range statuses {
444452
if !s.Installed {
445-
uninstalledNames = append(uninstalledNames, s.Tool.Name)
446-
nameToID[s.Tool.Name] = s.Tool.Id
453+
uninstalled = append(uninstalled, s)
447454
}
448455
}
449456

450-
if len(uninstalledNames) == 0 {
457+
if len(uninstalled) == 0 {
451458
return nil, nil
452459
}
453460

454-
selected, err := a.console.MultiSelect(ctx, input.ConsoleOptions{
461+
choices := make([]*uxlib.MultiSelectChoice, len(uninstalled))
462+
for i, s := range uninstalled {
463+
choices[i] = &uxlib.MultiSelectChoice{
464+
Value: s.Tool.Id,
465+
Label: s.Tool.Name,
466+
Selected: s.Tool.Priority == tool.ToolPriorityRecommended,
467+
}
468+
}
469+
470+
multiSelect := uxlib.NewMultiSelect(&uxlib.MultiSelectOptions{
471+
Writer: a.console.Handles().Stdout,
472+
Reader: a.console.Handles().Stdin,
455473
Message: "Select tools to install",
456-
Options: uninstalledNames,
474+
Choices: choices,
457475
})
476+
477+
selected, err := multiSelect.Ask(ctx)
458478
if err != nil {
459479
return nil, fmt.Errorf("selecting tools: %w", err)
460480
}
461481

462-
ids := make([]string, 0, len(selected))
463-
for _, name := range selected {
464-
if id, ok := nameToID[name]; ok {
465-
ids = append(ids, id)
482+
var ids []string
483+
for _, choice := range selected {
484+
if choice.Selected {
485+
ids = append(ids, choice.Value)
466486
}
467487
}
468488
return ids, nil

0 commit comments

Comments
 (0)