← Back to Home • Next: TreeSelect — Operations →
Every fluent method on ITreeSelectControl<T>. Each returns the same control instance, so calls chain
in any order — except AddLast / AddFirst / AddAfter /
AddBefore, which return the new ITreeNode<T> so you can attach
children to it. Call Run last.
The factory is
PromptPlus.Controls.TreeSelect<T>(string prompt = "", string? description = null), which returnsITreeSelectControl<T>.
Required before
Run:Root,TextSelector, andDefaultMatchBy.
Quick jump: Root · AddLast · AddFirst · AddAfter · AddBefore · ITreeNode<T> · Interaction · InteractionAsync · TextSelector · ExtraInfo · ExtraInfoAsync · PathSeparator · ShowFullPath · PageSize · Filter · SelectLeafOnly · Default · DefaultMatchBy · PredicateSelected · PredicateSelectedAsync · ViewOnly · ChangeDescription · ChangeDescriptionAsync · EnableHistory · Styles · Options · Run
RootITreeSelectControl<T> Root(T value)
Sets the top-level node shown at the top of the tree. Required — call it before adding any children.
PromptPlus.Controls.TreeSelect<string>("Folders")
.Root("Company")
.TextSelector(n => n)
.DefaultMatchBy((a, b) => a == b)
.Run();
Throws
ArgumentNullExceptionifvalueisnull.
AddLastITreeNode<T> AddLast(T value)
Adds a first-level node (child of the root) at the end and returns it so children can be attached.
var eng = tree.AddLast("Engineering");
eng.AddLast("Backend"); // nested child
Throws
InvalidOperationExceptionif the root has not been set yet.
AddFirstITreeNode<T> AddFirst(T value)
Adds a first-level node at the beginning so it appears at the very top of the child list.
Throws
InvalidOperationExceptionif the root has not been set yet.
AddAfterITreeNode<T> AddAfter(ITreeNode<T> node, T value)
Inserts a sibling immediately after node and returns the new node.
var eng = tree.AddLast("Engineering");
tree.AddAfter(eng, "Sales"); // sibling right after Engineering
Throws
ArgumentNullExceptionifnodeisnull, orInvalidOperationExceptionifnodedoes not belong to this tree.
AddBeforeITreeNode<T> AddBefore(ITreeNode<T> node, T value)
Inserts a sibling immediately before node and returns the new node.
var sales = tree.AddLast("Sales");
tree.AddBefore(sales, "HR"); // → [HR, Sales]
Throws
ArgumentNullExceptionifnodeisnull, orInvalidOperationExceptionifnodedoes not belong to this tree.
ITreeNode<T>The object returned by the Add* methods. Use it to read the node and attach children.
public interface ITreeNode<T>
{
T Value { get; } // the user value on this node
ITreeNode<T>? Parent { get; } // parent node, or null for the root
ITreeNode<T> AddLast(T value); // append a child
ITreeNode<T> AddFirst(T value); // prepend a child
}
var backend = eng.AddLast("Backend");
backend.AddLast("API");
backend.AddFirst("Database"); // Database appears before API
A node with at least one child renders as a container; a node with none renders as a leaf.
InteractionITreeSelectControl<T> Interaction<T1>(IEnumerable<T1> items, Action<T1, ITreeSelectControl<T>> interactionAction)
Iterates a source collection and lets you add first-level nodes (and their descendants)
programmatically — equivalent to calling AddLast inside the loop.
PromptPlus.Controls.TreeSelect<Node>("Departments")
.Root(new Node { Id = 0, Name = "Company" })
.TextSelector(n => n.Name)
.DefaultMatchBy((a, b) => a.Id == b.Id)
.Interaction(flatDepts, (dept, ctrl) =>
{
var deptNode = ctrl.AddLast(new Node { Name = dept.Dept });
foreach (var team in dept.Teams)
deptNode.AddLast(new Node { Name = team });
})
.Run();
Throws
ArgumentNullExceptionifitemsorinteractionActionisnull.
InteractionAsyncITreeSelectControl<T> InteractionAsync<T1>(IEnumerable<T1> items, Func<T1, ITreeSelectControl<T>, Task> interactionAction)
Asynchronous version of Interaction. Callbacks are awaited sequentially
(blocking) so tree construction stays deterministic.
TextSelectorITreeSelectControl<T> TextSelector(Func<T, string> selector)
Sets how each node is rendered as text. Required.
PromptPlus.Controls.TreeSelect<Node>("Nodes")
.Root(company)
.TextSelector(n => n.Name)
.DefaultMatchBy((a, b) => a.Id == b.Id)
.Run();
Throws
ArgumentNullExceptionifselectorisnull.
ExtraInfoITreeSelectControl<T> ExtraInfo(Func<T, string?> extraInfoNode)
Shows a secondary piece of text next to each node label (return null to show nothing for that node).
The focused node’s ExtraInfo also appears in the live answer line while navigating (not in the
final answer shown after Enter) — see Operations.
PromptPlus.Controls.TreeSelect<Node>("Nodes")
.Root(company).TextSelector(n => n.Name).DefaultMatchBy((a, b) => a.Id == b.Id)
.ExtraInfo(n => n.Info)
.Run();
Throws
ArgumentNullExceptionifextraInfoNodeisnull.
ExtraInfoAsyncITreeSelectControl<T> ExtraInfoAsync(Func<T, Task<string?>> extraInfoNode)
Asynchronous version of ExtraInfo.
⚠️ The task is awaited synchronously (blocking) once per node, per render frame — keep it fast.
PathSeparatorITreeSelectControl<T> PathSeparator(char value)
Sets the character that joins the parent chain when a full path is shown. Default is '/'.
PromptPlus.Controls.TreeSelect<Node>("Nodes")
.Root(company).TextSelector(n => n.Name).DefaultMatchBy((a, b) => a.Id == b.Id)
.PathSeparator('.') // Company.Engineering.Backend.API
.ShowFullPath()
.Run();
The separator is also used by Filter, which matches against the joined full path.
ShowFullPathITreeSelectControl<T> ShowFullPath(bool value = true)
Shows the full path (parent chain) instead of only the entry name in the answer line. Default false.
PageSizeITreeSelectControl<T> PageSize(byte value)
Rows visible at once (valid range 0–255). 0 (default) auto-fits from terminal height. Only the
visible slice is materialized, so large trees stay cheap.
PromptPlus.Controls.TreeSelect<Node>("Nodes")
.Root(company).TextSelector(n => n.Name).DefaultMatchBy((a, b) => a.Id == b.Id)
.PageSize(15)
.Run();
FilterITreeSelectControl<T> Filter(FilterMode value)
Enables interactive filtering. Typing a printable character switches the tree to filter mode: the
whole tree is flattened once and the chosen FilterMode is applied against each node’s full path
(the parent chain joined by PathSeparator). Clearing the filter restores the lazy
tree view, preserving the previous expand/collapse state.
FilterMode |
Behavior |
|---|---|
Disabled |
No filtering (default) |
Contains |
Match nodes whose full path contains the typed text |
StartsWith |
Match nodes whose full path starts with the typed text |
PromptPlus.Controls.TreeSelect<Node>("Nodes")
.Root(company).TextSelector(n => n.Name).DefaultMatchBy((a, b) => a.Id == b.Id)
.Filter(FilterMode.Contains)
.Run();
SelectLeafOnlyITreeSelectControl<T> SelectLeafOnly(bool value = true)
When enabled, blocks confirmation of container nodes — only leaves (nodes without children) can be
confirmed with Enter. Default false.
PromptPlus.Controls.TreeSelect<Node>("Pick a leaf")
.Root(company).TextSelector(n => n.Name).DefaultMatchBy((a, b) => a.Id == b.Id)
.SelectLeafOnly()
.Run();
PredicateSelectedITreeSelectControl<T> PredicateSelected(Func<T, bool> validselect)
ITreeSelectControl<T> PredicateSelected(Func<T, (bool, string?)> validselect)
Validation evaluated when the user presses Enter. On failure the tree stays open and shows an error.
| Overload | Return | Behavior |
|---|---|---|
Func<T, bool> |
true = valid |
Generic error on failure |
Func<T, (bool, string?)> |
(isValid, message) |
Custom message on failure |
PromptPlus.Controls.TreeSelect<Node>("Pick a service")
.Root(company).TextSelector(n => n.Name).DefaultMatchBy((a, b) => a.Id == b.Id)
.ExtraInfo(n => n.Info)
.PredicateSelected(n => n.Info == "service"
? (true, null)
: (false, $"'{n.Name}' is a {n.Info}, not a service."))
.Run();
PredicateSelectedAsyncITreeSelectControl<T> PredicateSelectedAsync(Func<T, Task<bool>> validselect)
ITreeSelectControl<T> PredicateSelectedAsync(Func<T, Task<(bool, string?)>> validselect)
Asynchronous counterparts.
⚠️ The async predicate is awaited synchronously (blocking) on the UI thread — keep it fast.
DefaultITreeSelectControl<T> Default(T value, bool useDefaultHistory = true)
Pre-selects value, expanding the tree down to it when it is reachable from the root. When
useDefaultHistory is true and history is enabled, the restored history value
is preferred. Matching uses DefaultMatchBy.
PromptPlus.Controls.TreeSelect<Node>("Nodes")
.Root(company).TextSelector(n => n.Name).DefaultMatchBy((a, b) => a.Id == b.Id)
.Default(database) // tree auto-expands to reveal it
.Run();
Throws
ArgumentNullExceptionifvalueisnull.
DefaultMatchByITreeSelectControl<T> DefaultMatchBy(Func<T, T, bool> comparer)
Custom equality used to locate the Default value and any value restored from history.
Required — essential for records/classes where reference equality is not meaningful.
PromptPlus.Controls.TreeSelect<Node>("Nodes")
.Root(company).TextSelector(n => n.Name)
.DefaultMatchBy((a, b) => a.Id == b.Id)
.Run();
Throws
ArgumentNullExceptionifcomparerisnull.
ViewOnlyITreeSelectControl<T> ViewOnly(bool value = true)
Renders the tree for navigation only — nodes can be expanded/collapsed but not selected. Enter always
returns the node the tree started on — the Default target if one was set, otherwise the
root node (never null, since a root is mandatory) — regardless of where the user navigated to.
PromptPlus.Controls.TreeSelect<Node>("Read-only tree")
.Root(company).TextSelector(n => n.Name).DefaultMatchBy((a, b) => a.Id == b.Id)
.ViewOnly()
.Run();
ChangeDescriptionITreeSelectControl<T> ChangeDescription(Func<T, string> value)
Recomputes the description from the currently focused node as the user navigates.
PromptPlus.Controls.TreeSelect<Node>("Nodes")
.Root(company).TextSelector(n => n.Name).DefaultMatchBy((a, b) => a.Id == b.Id)
.ChangeDescription(n => $"[Id={n.Id}] {n.Name}")
.Run();
Throws
ArgumentNullExceptionifvalueisnull.
ChangeDescriptionAsyncITreeSelectControl<T> ChangeDescriptionAsync(Func<T, Task<string>> value)
Asynchronous version of ChangeDescription, awaited synchronously (blocking)
each frame.
Throws
ArgumentNullExceptionifvalueisnull.
EnableHistoryITreeSelectControl<T> EnableHistory(string filename, Action<IHistoryOptions>? options = null)
Persists the confirmed value (serialized as JSON) to filename. On the next run the tree is searched
— using DefaultMatchBy — for the restored value so it can be pre-selected. The
IHistoryOptions builder is the same one documented for
Input → EnableHistory.
PromptPlus.Controls.TreeSelect<Node>("Nodes")
.Root(company).TextSelector(n => n.Name).DefaultMatchBy((a, b) => a.Id == b.Id)
.EnableHistory("tree-history")
.Run();
Throws
ArgumentNullExceptioniffilenameisnull,ArgumentExceptionif it is empty/whitespace.
StylesITreeSelectControl<T> Styles(TreeSelectStyles styleType, Style style)
Recolors one visual region of this control. See the region list and examples on the Styles page.
using PromptPlusLibrary;
using ConsolePlusLibrary; // Color, Style live here
PromptPlus.Controls.TreeSelect<Node>("Nodes")
.Root(company).TextSelector(n => n.Name).DefaultMatchBy((a, b) => a.Id == b.Id)
.Styles(TreeSelectStyles.Selected, new Style(Color.Black, Color.Gray))
.Run();
OptionsITreeSelectControl<T> Options(Action<IControlOptions> options)
Overrides global behaviors for this one control (prompt/description text, abort key, tooltip, hide-after-finish, extra-info affixes). See Global Behaviors → Per-Control Override.
Throws
ArgumentNullExceptionifoptionsisnull.
RunResultPrompt<T?> Run(CancellationToken token = default)
Renders the tree and blocks until the user confirms (Enter) or aborts (Esc). Returns
ResultPrompt<T?> — the Content is nullable.
var result = PromptPlus.Controls.TreeSelect<Node>("Nodes")
.Root(company).TextSelector(n => n.Name).DefaultMatchBy((a, b) => a.Id == b.Id)
.Run();
TreeSelectStyles regions