feat(layout): add FilePickerNode for file/folder selection#284
Merged
Conversation
Interactive terminal file picker with breadcrumb navigation, fuzzy filtering, and configurable selection modes (Files, Directories, All). Supports single and multi-select, hidden file toggling, and glob-based file filters via FileSystemName.MatchesSimpleExpression. Key design decisions: - Enter always navigates into directories; Space selects in Directories/All modes (avoids blocking subdirectory browsal) - IFileSystemProvider abstraction enables deterministic testing - ASCII icons avoid surrogate-pair/column-width issues with emoji - Filter state properly resets on blur to prevent stale filtered views Includes Gallery demo page with side-by-side file and folder pickers, and 26 unit tests covering navigation, filtering, selection modes, glob patterns, scroll behavior, and lifecycle.
- Fix Multi+Directories mode: Enter now confirms toggled selections instead of always navigating into the highlighted directory - Clear _selectedPaths on directory navigation to prevent stale cross-directory paths from being emitted on confirmation - Auto-include highlighted file in multi-select Enter confirmation - Fix breadcrumb truncation crash (ArgumentOutOfRangeException) when render width is 4 or less - Fix '/' then Backspace leaving user stuck in filter mode by moving empty-text check outside the handled guard - Allow space characters in filter text by removing Spacebar interception in filter mode - Fix entry truncation crash when contentWidth is 0 - Clamp availableForList to prevent negative listHeight in render - Use SetFocus instead of PushFocus in gallery page to prevent unbounded focus stack growth - Replace _entries.Count==0 sentinel with _isLoaded flag so empty directories don't re-trigger LoadDirectory on every focus - Catch IOException alongside UnauthorizedAccessException in DefaultFileSystemProvider for network/device errors - Add !_disposed guards on _selectionConfirmed and _cancelled Subject emissions to match Invalidate/EmitDirectoryChanged pattern - Deactivate filter input before completing subjects in Dispose - Align x-offset: breadcrumb, filter bar, and footer now render at x=0 consistently with file list entries - Fix Slash_ActivatesFilterMode test to verify filter mode was actually entered before asserting cancellation - Add tests for backspace-exits-empty-filter and Multi+Directories toggle-then-confirm workflows
- Add docs/components/file-picker-node.md covering usage, picker and selection modes, keyboard shortcuts, filtering, glob filters, styling, IFileSystemProvider testing, and full API reference - Register FilePickerNode in the VitePress sidebar and components index - Add gallery-file-picker.tape VHS script and recorded GIF showing directory navigation, type-to-filter, file and folder selection - Lazy-load the directory on first render so unfocused pickers show content instead of "Empty directory" before receiving focus
- Add Layouts.FilePicker(startPath) factory matching the established pattern for SelectionList, Modal, Wizard, and other interactive nodes - Switch docs examples and gallery demo page to use the factory - Add unit test covering both the explicit-path and default overloads
Load lifecycle: - Latch _isLoaded on failed loads so a nonexistent start path no longer retries filesystem I/O on every render frame; WithStartPath resets the latch to allow reloading - Collapse three lazy-load sites into EnsureLoaded(); initial lazy loads no longer emit DirectoryChanged (navigation-only signal) Filter input: - Only ApplyFilter when the filter text actually changes — cursor movement (Left/Right) no longer resets the list highlight and scroll - Unhandled keys (Tab, F-keys, Ctrl chords) now bubble instead of being swallowed and silently closing an empty filter; Backspace on an empty filter still exits filter mode explicitly - Alt/Ctrl chords no longer hijacked into filter activation - Filter input now receives OnFocused/OnBlurred so the cursor blink timer starts and stops correctly; its Invalidated is bridged to the picker's so blink frames repaint; TimeProvider is threaded through for testability - LoadDirectory exits filter mode with full deactivation instead of just flipping the flag (no more orphaned blink timer) Multi-select model: - Selections persist across directory navigation; footer shows a running count (e.g. "3 selected") - Enter on an untoggled directory always navigates (drill-in restored); Enter on a toggled directory confirms — behavior is predictable from the visible [x] on the highlighted row - Confirming no longer mutates the toggle set; the highlighted file is appended to the emitted list only Scroll math: - EnsureVisible now tracks the rows actually rendered (_effectiveRows), fixing blind navigation when the parent allocates less space than the configured visible rows - _scrollOffset re-clamped in Render so enlarging the terminal no longer strands the view past the end of the list - Measure reserves a row for the empty-state message so it no longer overwrites the footer Misc: - DefaultFileSystemProvider.GetParentDirectory trims trailing separators and guards ArgumentException/IOException - EmitSelectionConfirmed helper for consistent disposed-guarding - Filter bar width off-by-one and empty-state x-offset cleanup - Stale Cancelled XML doc corrected - Gallery menu hint updated to [1-6] Quick Select; GIF re-recorded - Docs updated for the new multi-select semantics; doc fake provider example now sorts per the GetEntries contract - 9 new tests: cursor-move highlight preservation, unhandled-key bubbling, space-in-filter, cross-directory selection persistence, confirm non-mutation, All+Multi mode, untoggled-dir navigation, WithStartPath reload, bad-path no-retry; hidden-file tests made discriminating
The OnActivate/OnDeactivate tree-walk pattern-matched on the concrete LayoutNode class at every dispatch site, so components implementing ILayoutNode interfaces directly (FilePickerNode, SelectionListNode) never received lifecycle calls. Their OnActivate/OnDeactivate methods were dead code, which is why FilePickerNode had to lazy-load in Render() as a workaround. - LayoutNode now implements the existing IActivatableNode interface (its virtual OnActivate/OnDeactivate already satisfy it) - All 36 activation dispatch sites across ContainerNode, GridNode, PanelNode, ModalNode, ConditionalNode, DynamicLayoutNode, KeyedDynamicLayoutNode, ReactiveLayoutNode, and ReactivePage now pattern-match on IActivatableNode instead of LayoutNode — behavior for LayoutNode subclasses is unchanged, and bare-interface nodes now participate in the page lifecycle - FilePickerNode and SelectionListNode implement IActivatableNode, so directory loading happens at page activation instead of first render, and embedded input timers pause on navigation away - Traversal-only LayoutNode checks (FocusManager child collection, WizardNode tree walks) are intentionally unchanged - Render-time EnsureLoaded() retained as a fallback for nodes attached outside the activation flow Adds tests verifying activation propagates through VerticalLayout and GridNode to an interface-based FilePickerNode child.
"[BS] Up" was ambiguous — use "[Backspace] Up" in all four footer hint variants. Re-record the gallery GIF to match.
- Custom-components guide now explains that lifecycle dispatch goes through IActivatableNode (LayoutNode implements it; interface-based components implement it directly) and the container propagation example matches on the interface instead of the concrete class — the old sample taught the exact gap fixed in d390749 - Replace the Timer/.Elapsed event example with Observable.Interval + TimeProvider, and the System.Reactive types (IObservable, BooleanDisposable) with R3 equivalents, per project conventions - Add missing TextAreaNode and CopyableTextNode entries to the docs sidebar Input Components section
Merged
This was referenced Jun 10, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
FilePickerNode— an interactive file/folder picker with breadcrumb navigation, keyboard-driven directory traversal, scrolling, fuzzy filtering, and single/multi-select supportFilePickerMode(Files/Directories/All),FilePickerSelectionMode(Single/Multi),FileSystemEntryrecord,IFileSystemProviderabstraction withDefaultFileSystemProviderGridNode50/50 layoutKey design decisions
[D]/ spaces) avoid surrogate-pair and column-width issues with emojiTextInputNodefor filter bar (reuses existing component, not registered with focus manager)IFileSystemProviderabstraction enables deterministic testing withFakeFileSystemProviderTest plan
dotnet test— all 1402 tests pass, 0 regressionsdotnet buildon library, demo, and test projects — clean