in cli/azd/pkg/ux/internal/input.go [57:199]
func (i *Input) ReadInput(ctx context.Context, config *InputConfig, handler KeyPressEventHandler) error {
if config == nil {
config = &InputConfig{}
}
// Create a cancellable context to avoid leaking goroutines.
ctx, cancel := context.WithCancel(ctx)
defer cancel()
i.cursor.ShowCursor()
i.value = []rune(config.InitialValue)
// Channel to receive errors from the keyboard input
errChan := make(chan error, 1)
// Channel to receive OS signals (e.g., Ctrl+C)
signalChan := make(chan os.Signal, 1)
// Channel to receive active key press events
inputChan := make(chan *KeyPressEventArgs)
// Signals that we should continue listening for key presses.
receiveChan := make(chan struct{})
// Register for SIGINT (Ctrl+C) signal
signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM)
defer func() {
signal.Stop(signalChan)
}()
// Open the keyboard - sometimes it fails when a keyboard instance in in the process of closing.
tries := 0
for {
if !keyboard.IsStarted(100 * time.Millisecond) {
if err := keyboard.Open(); err != nil {
tries++
continue
}
}
log.Printf("Keyboard opened successfully after %d tries\n", tries)
break
}
// Start listening for key presses
// We need to do this on a separate goroutine to avoid blocking the main thread.
// To ensure we can still handle Ctrl+C or context cancellations.
go func() {
defer func() {
if err := keyboard.Close(); err != nil {
log.Printf("Error closing keyboard: %s\n", err.Error())
}
}()
for {
select {
case <-ctx.Done():
return
case <-receiveChan:
char, key, err := keyboard.GetKey()
if err != nil {
errChan <- err
return
}
eventArgs := KeyPressEventArgs{
Char: char,
Key: key,
}
if len(i.value) > 0 && (key == keyboard.KeyBackspace || key == keyboard.KeyBackspace2) {
i.value = i.value[:len(i.value)-1]
} else if !config.IgnoreHintKeys && char == '?' {
eventArgs.Hint = true
} else if !config.IgnoreHintKeys && key == keyboard.KeyEsc {
eventArgs.Hint = false
} else if key == keyboard.KeySpace {
i.value = append(i.value, ' ')
} else if unicode.IsPrint(char) {
i.value = append(i.value, char)
} else if key == keyboard.KeyCtrlC || key == keyboard.KeyCtrlX || key == keyboard.KeyEsc {
eventArgs.Cancelled = true
cancel()
break
}
eventArgs.Value = string(i.value)
inputChan <- &eventArgs
}
}
}()
// Start the main event loop
receiveChan <- struct{}{}
for {
select {
case err := <-errChan:
return err
case <-ctx.Done():
// If cancellation comes from context, return cancellation error.
allErrors := errors.Join(ErrCancelled, ctx.Err())
args := KeyPressEventArgs{Cancelled: true}
_, err := handler(&args)
if err != nil {
allErrors = errors.Join(allErrors, err)
}
return allErrors
case <-signalChan:
// On OS signal, cancel the context to notify the goroutine.
cancel()
allErrors := errors.Join(ErrCancelled)
if ctx.Err() != nil {
allErrors = errors.Join(allErrors, ctx.Err())
}
args := KeyPressEventArgs{Cancelled: true}
_, err := handler(&args)
if err != nil {
allErrors = errors.Join(allErrors, err)
}
return allErrors
case args, ok := <-inputChan:
if !ok {
return nil
}
keepListening, err := handler(args)
if err != nil {
return err
}
if !keepListening {
return nil
}
receiveChan <- struct{}{}
}
}
}