Skip to content

Escape always shuts the application down and never reaches an element, so Esc-to-cancel dialogs and edits are impossible #186

Description

@matt-edmondson

What's wrong

SpectreConsoleProvider.ReadInputAsync turns every Escape keypress into InputResult.Exit() before the application or any element sees it (TUI/Services/SpectreConsoleProvider.cs:89-93):

if (keyInfo.Key == ConsoleKey.Escape ||
    (keyInfo.Key == ConsoleKey.C && keyInfo.Modifiers.HasFlag(ConsoleModifiers.Control)))
{
    return InputResult.Exit();
}

UIApplication.ProcessInputAsync checks input.IsExit first. If it is set, it calls Shutdown() and breaks without ever calling RootElement.HandleInput (TUI/Services/UIApplication.cs:402-414). The demo works around this in TUI.App/InteractiveDemo.cs:197,226: "Escape reaches ProcessInputAsync as an exit" and never reaches an element.

Failure scenario

  1. Build a UI with a modal panel or text-entry element whose OnHandleInput returns true for ConsoleKey.Escape, meaning "close the modal" or "cancel the edit".
  2. Run the app, open the modal, and press Esc.
  3. The whole application exits. The element's handler is never called. The only way to change this is to write a custom IConsoleProvider.

Why it matters

Esc-to-cancel is one of the most common TUI conventions (dialogs, menus, search boxes, inline edits). The library currently cannot support it. The exit policy also sits in the console provider, which should only translate keys, rather than in the application.

Suggested fix / acceptance criteria

  • SpectreConsoleProvider reports Escape as ordinary keyboard input (InputResult.FromKey(ConsoleKey.Escape, ...)).
  • UIApplication offers the key to RootElement.HandleInput first, and treats it as exit only if nothing handled it. Alternatively, the exit key comes from a configurable option, for example UIApplicationBuilder.UseExitKey(ConsoleKey?), where null disables it.
  • Escape remains the default exit key, so existing apps behave the same.
  • Tests:
    • An element that handles Escape receives it, and the app keeps running.
    • With no handler, Escape still ends RunAsync.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions