diff --git a/cli/azd/pkg/ux/internal/console_other.go b/cli/azd/pkg/ux/internal/console_other.go new file mode 100644 index 00000000000..2a32269acfa --- /dev/null +++ b/cli/azd/pkg/ux/internal/console_other.go @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +//go:build !windows + +package internal + +import "os" + +// disableVirtualTerminalInput is a no-op on non-Windows platforms. +// The ENABLE_VIRTUAL_TERMINAL_INPUT console flag only exists on Windows. +func disableVirtualTerminalInput(_ *os.File) error { + return nil +} diff --git a/cli/azd/pkg/ux/internal/console_windows.go b/cli/azd/pkg/ux/internal/console_windows.go new file mode 100644 index 00000000000..2cb4b8c1691 --- /dev/null +++ b/cli/azd/pkg/ux/internal/console_windows.go @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +//go:build windows + +package internal + +import ( + "os" + "syscall" + "unsafe" +) + +var ( + kernel32 = syscall.NewLazyDLL("kernel32.dll") + getConsoleMode = kernel32.NewProc("GetConsoleMode") + setConsoleMode = kernel32.NewProc("SetConsoleMode") +) + +const enableVirtualTerminalInput uint32 = 0x0200 + +// disableVirtualTerminalInput clears the ENABLE_VIRTUAL_TERMINAL_INPUT flag +// on the console input handle. When this flag is set (e.g. by PowerShell), +// Windows translates arrow key presses into ANSI escape sequences instead of +// delivering them as native virtual key codes. The survey library's Windows +// ReadRune only handles native VK codes, so leaving VTI enabled causes +// escape sequence fragments to leak through as individual runes. +// +// This is called after survey's SetTermMode (which saves/restores the original +// console state), so RestoreTermMode will re-enable VTI if it was originally set. +func disableVirtualTerminalInput(f *os.File) error { + handle := f.Fd() + + var mode uint32 + //nolint:gosec // Win32 API requires unsafe pointer + r, _, err := getConsoleMode.Call(uintptr(handle), uintptr(unsafe.Pointer(&mode))) + if r == 0 { + return err + } + + if mode&enableVirtualTerminalInput == 0 { + return nil // VTI not enabled, nothing to do + } + + newMode := mode &^ enableVirtualTerminalInput + r, _, err = setConsoleMode.Call(uintptr(handle), uintptr(newMode)) + if r == 0 { + return err + } + + return nil +} diff --git a/cli/azd/pkg/ux/internal/input.go b/cli/azd/pkg/ux/internal/input.go index d41279bd163..20428d01f91 100644 --- a/cli/azd/pkg/ux/internal/input.go +++ b/cli/azd/pkg/ux/internal/input.go @@ -97,6 +97,12 @@ func (i *Input) ReadInput(ctx context.Context, config *InputConfig, handler KeyP errChan <- err return } + // On Windows, clear ENABLE_VIRTUAL_TERMINAL_INPUT so the console + // delivers native virtual key codes instead of ANSI escape sequences. + // survey's RestoreTermMode will restore the original console state. + if err := disableVirtualTerminalInput(os.Stdin); err != nil { + log.Printf("Warning: could not disable virtual terminal input: %v\n", err) + } defer func() { if err := rr.RestoreTermMode(); err != nil { log.Printf("Error restoring terminal mode: %v\n", err)