This document specifies a new control for XenoAtom.Terminal.UI: PromptEditor.
PromptEditor is a prompt-like text editor inspired by XenoAtom.Terminal’s rich ReadLine editor, but implemented as
a retained-mode UI control built on top of the existing TextEditorBase/TextEditorCore infrastructure.
Why a dedicated control?
TextBox and TextArea are general-purpose editors.Name: PromptEditor reflects the control’s multi-line capable, programmable “editor” nature, while keeping a
prompt-oriented UX (prefix, history, completion).
TextEditorBase editing features (caret, selection, word wrap, clipboard, undo/redo).Command (discoverable via CommandBar),Terminal.ReadLineAsync(...) and related types:
TerminalReadLineOptions (prompt markup, completion handler, history, key bindings)TerminalReadLineController (imperative editing API)TerminalTextUtility (word boundaries, cell widths, grapheme-aware navigation)TextBox / TextArea (built on TextEditorBase)TextEditorBase / TextEditorCore (selection/caret/search/scrolling/clipboard/undo)OptionList and popup infrastructure (completion list presentation)PromptEditor SHOULD reuse as much of the existing editor core as possible, and rely on TerminalTextUtility for
text-width/word-boundary correctness.
> , cwd, counter).PromptEditor is implemented as a specialized TextEditorBase-derived control:
TextEditorCore (caret, selection, editing, scrolling, search popup).The goal is to avoid re-implementing a second text editor; PromptEditor is primarily:
namespace XenoAtom.Terminal.UI.Controls;
public enum PromptEditorEnterMode
{
/// <summary>
/// Enter accepts the prompt, and Ctrl+J inserts a newline.
/// </summary>
/// <remarks>
/// In terminals, Enter is commonly received as <c>TerminalKey.Enter</c> and/or <c>TerminalChar.CtrlM</c> (CR, <c>\r</c>),
/// while Ctrl+J is <c>TerminalChar.CtrlJ</c> (LF, <c>\n</c>).
/// </remarks>
EnterAccepts,
/// <summary>
/// Enter inserts a newline, and Ctrl+J accepts the prompt.
/// </summary>
EnterInsertsNewLine,
}
public enum PromptEditorCompletionPresentation
{
/// <summary>Do not show UI; still raise CompletionRequested.</summary>
None,
/// <summary>Cycle candidates inline (readline-like).</summary>
InlineCycle,
/// <summary>Show a popup list of candidates (OptionList).</summary>
PopupList,
}
/// <summary>
/// Represents a completion request computed for the current text and caret/selection.
/// </summary>
public readonly record struct PromptEditorCompletion(
bool Handled,
IReadOnlyList<string>? Candidates,
int ReplaceStart,
int ReplaceLength,
int SelectedIndex = 0,
string? GhostText = null);
public readonly record struct PromptEditorCompletionRequest(
ITextSnapshot Snapshot,
int CaretIndex,
int SelectionStart,
int SelectionLength,
TerminalModifiers Modifiers);
public delegate PromptEditorCompletion PromptEditorCompletionHandler(in PromptEditorCompletionRequest request);
public readonly record struct PromptEditorHighlightRequest(
ITextSnapshot Snapshot,
Theme Theme,
int CaretIndex,
int SelectionStart,
int SelectionLength);
public delegate void PromptEditorHighlighter(in PromptEditorHighlightRequest request, List<StyledRun> runs);
public partial class PromptEditor : TextEditorBase
{
// Prompt prefix
[Bindable(NoVisualAttach = true)] public partial Visual? Prompt { get; set; }
[Bindable] public partial string? PromptMarkup { get; set; }
[Bindable] public partial string? ContinuationPromptMarkup { get; set; }
// Accept/cancel and behavior
[Bindable] public partial PromptEditorEnterMode EnterMode { get; set; } = PromptEditorEnterMode.EnterAccepts;
[Bindable] public partial bool AcceptOnBlur { get; set; }
// Optional suggestion line (ghost completion)
[Bindable] public partial bool EnableGhostCompletion { get; set; } = true;
// Completion + history
[Bindable] public partial PromptEditorCompletionPresentation CompletionPresentation { get; set; } = PromptEditorCompletionPresentation.PopupList;
[Bindable] public partial Delegator<PromptEditorCompletionHandler> CompletionHandler { get; set; }
// Highlighting + word hints
[Bindable] public partial bool EnableWordHints { get; set; }
[Bindable] public partial Delegator<PromptEditorHighlighter> Highlighter { get; set; }
// Events
[RoutedEvent(RoutingStrategy.Bubble)]
protected virtual void OnAccepted(PromptEditorAcceptedEventArgs e) { }
[RoutedEvent(RoutingStrategy.Bubble)]
protected virtual void OnCanceled(PromptEditorCanceledEventArgs e) { }
}
Notes:
Prompt is optional. When set, it takes precedence over PromptMarkup for the first visual row.PromptMarkup uses markup syntax (same as Markup control / XenoAtom.Ansi) and SHOULD support theme custom tags
(primary, success, etc.) like the rest of Terminal.UI.Highlighter is optional. When empty, the editor renders with its default text style.Delegate-valued bindables MUST use Delegator<TDelegate> for fluent support (see Control Development Guide).
PromptEditor layout is conceptually:
[prompt prefix][ editable region (scrolling, selection, caret) ]
TextEditorBase behavior (single-line vs multi-line).PromptMarkup into plain text + runs (via MarkupTextParser).TerminalTextUtility.GetWidth.PromptMarkup, the prompt prefix is rendered by PromptEditor itself (like TextBox renders its chrome).Prompt, the prompt is a child visual arranged in the left prompt column (first visual row only).Bounds.Width - prefixWidth (minus style padding).ContinuationPromptMarkup when set,This preserves the “prompt column” look and prevents wrapped text from jumping under the prompt.
PromptEditor rendering consists of:
TextArea or prompt-style surface).TextEditorCore), with optional:
MarkupTextParser.If Highlighter is set:
MarkupTextParser can be used by implementers to easily produce style runs from markup without building visuals.
Ghost completion is a dim suggestion that does not alter the document:
EnableGhostCompletion=true,GhostText.Ghost completion should never interfere with:
PromptEditor inherits the editor input model from TextEditorCore and adds prompt-specific actions.
Accepted.
\r), and Ctrl+J inserts a newline (LF, \n).EnterMode allows swapping the meaning of Enter and Ctrl+J.Canceled.
Esc cancels completion if active; otherwise cancels the prompt.The accept/cancel design must avoid breaking multi-line editing.
The editor document uses \n for line breaks internally. Even when input arrives as CR (\r) from Enter, PromptEditor
should insert \n when it needs to create a new line.
Completion is triggered by a command (default gesture is typically Tab):
AcceptTab=true, Tab inserts a tab character and completion should be triggered via a separate gesture
(e.g. Ctrl+Space).PromptEditor MUST expose completion as both:
CompletionHandler callback (easy for simple use), andDefault completion behavior:
InlineCycle:
[ReplaceStart..ReplaceStart+ReplaceLength) in the document,PopupList:
Popup containing an OptionList,PromptEditor history navigation should not be hard-coded to Up/Down in multi-line mode.
Suggested defaults:
Alt+Up / Alt+Down (or Ctrl+Up / Ctrl+Down depending on terminal support).Ctrl+R (optional; matches Terminal.ReadLine defaults).History should be user-provided (like Terminal.ReadLine options) so it can be scoped and shared.
PromptEditor SHOULD register commands so CommandBar / CommandPalette can discover them, similar to TextEditorBase.
Required commands (v1):
PromptEditor.AcceptPromptEditor.CancelPromptEditor.InsertNewLinePromptEditor.Complete (request completion)PromptEditor.CompletionNext / PromptEditor.CompletionPrevious (when cycling)PromptEditor.HistoryPrevious / PromptEditor.HistoryNext (when history enabled)TextEditor.* commands inherited from TextEditorBase (undo/redo/copy/paste/select-all/search)Default gestures (v1 suggested):
PromptEditor.Accept: Enter (CR, TerminalKey.Enter and/or TerminalChar.CtrlM)PromptEditor.InsertNewLine: Ctrl+J (LF, TerminalChar.CtrlJ)Users can swap the behavior by either:
EnterMode, orEach command should have:
IdCanExecute behavior (e.g. completion commands disabled when no candidates)PromptEditor has its own style record (v1), but should reuse familiar input styling:
public readonly record struct PromptEditorStyle : IStyle<PromptEditorStyle>
{
public Thickness Padding { get; init; }
public Func<Theme, bool, Style> BackgroundStyle { get; init; }
public Func<Theme, Style> TextStyle { get; init; }
public Func<Theme, Style> SelectionStyle { get; init; }
public Func<Theme, Style> PlaceholderStyle { get; init; }
public Func<Theme, Style> PromptStyle { get; init; }
public Func<Theme, Style> GhostStyle { get; init; }
public Func<Theme, Style> WordHintStyle { get; init; } // underline/dim, optional
}
Guidance:
Add tests in src/XenoAtom.Terminal.UI.Tests (rendering-focused) to cover:
EnterMode swap.PromptEditorHighlighter styles a token range (e.g. underline a word).Add a demo that mirrors the feel of samples/HelloReadLine from XenoAtom.Terminal:
> glyph, theme colors.Ctrl+L clears the document,Esc cancels completion or the prompt (depending on state),Visual slot (not only markup) for richer composition (icons, small status widgets).