Skip to content

Add native folder dialog to pending project tabs#177

Merged
astex merged 7 commits into
astex:masterfrom
crdUM:feat/native-folder-dialog
Jun 16, 2026
Merged

Add native folder dialog to pending project tabs#177
astex merged 7 commits into
astex:masterfrom
crdUM:feat/native-folder-dialog

Conversation

@crdUM

@crdUM crdUM commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a clickable [ Browse… ] hint to the pending project tab (Ctrl+T) that opens the native OS folder picker via rfd. Picking a folder spawns the project tab exactly like typing the path and pressing Enter — canonicalize, dedup against already-open projects, spawn, focus. The inline text prompt is unchanged; the dialog is an additional option.

Implementation notes

  • macOS main-thread requirement: the rfd dialog future is built synchronously inside update() (main thread) and only awaited via Task::perform on Iced's executor. Building it inside the async block would spawn the dialog off-main-thread, which rfd warns against on macOS and which has a history of winit event-loop freezes. Validated standalone with a spike before wiring (see branch history).
  • Lifecycle safety: the picker is non-modal, so the pending tab can be closed (Esc) or submitted by typing while it's open. The shared open_project_from_path helper no-ops when the result's tab_id no longer exists; tab IDs are never reused, so a stale result can't alias onto a newer tab.
  • Re-entrancy: an app-level project_dialog_open flag drops Browse clicks while a picker is already in flight.
  • Pending-tab mouse handling: pending tabs now swallow mouse presses so clicks no longer fall through to the text-selection machinery on the empty grid; hovering the hint shows a pointer cursor.
  • Font metrics: pending-tab text renders in the configured terminal font so the drawn label matches the char-metric-based click rect (Font::MONOSPACE has a different advance and clipped the trailing bracket).
  • The Submit arm's canonicalize → dedup → spawn → focus sequence moved into the shared helper; ~ expansion was extracted to a pure expand_tilde function.

Testing

  • New unit tests: expand_tilde (4) and browse_hint_rect geometry/hit-testing (3); full suite passes (85).
  • Manually verified on macOS: hint hover/click, picker open/cancel, spawn-on-pick, dedup-to-existing-project focuses the existing tab, and the re-entrancy guard.

Out of scope (deliberately): parenting the dialog to the main window via set_parent, remembering the last-used directory, and folder drag-and-drop onto the pending tab (the shared helper makes that a cheap follow-up).

🤖 Generated with Claude Code

crdUM and others added 6 commits June 5, 2026 10:24
Augments the existing inline "Project directory:" prompt with a clickable
[ Browse… ] hint that opens a native folder picker (rfd) starting at $HOME,
spawning the project tab immediately on selection.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
- Require the main-thread dialog spawn pattern (build the rfd future in
  update(), await in Task::perform) instead of constructing the dialog
  inside the async block, which Iced polls off-main-thread
- Guard open_project_from_path against stale tab_ids (dialog is
  non-modal; pending tab can be closed or submitted while it is open)
- Add a re-entrancy guard against double-spawned dialogs
- Put the pending check first in the mouse-press arm and swallow other
  mouse handling for pending tabs; add Pointer hover affordance
- Acknowledge the missing App-level test harness and push testable
  logic into pure functions / TabStore methods
- Pin down rfd version/feature notes, $HOME consistency, unparented
  dialog behavior, and a drag-and-drop follow-up enabled by the helper

Co-Authored-By: Claude Fable 5 <[email protected]>
cargo run --example rfd_spike: builds the AsyncFileDialog future
synchronously in update() (main thread, verified by thread-name
logging) and awaits it via Task::perform. On macOS / Iced 0.14 /
Rust 1.94 the picker opens with the app responsive, returns the
picked path, and returns None on cancel — no freezes or panics.

rfd 0.17 lands as a dev-dependency for now; promote to a real
dependency when the feature is wired. Spec's risk section updated
with the validation result.

Co-Authored-By: Claude Fable 5 <[email protected]>
A clickable [ Browse… ] hint below the Project directory: prompt opens
the OS folder picker (rfd::AsyncFileDialog); picking a folder spawns
the project tab exactly like typing the path and pressing Enter. The
inline text prompt is unchanged.

Implementation notes, per the design spec:
- The dialog future is built inside update() (main thread — required
  by rfd on macOS) and only awaited via Task::perform.
- The Submit arm's canonicalize → dedup → spawn → focus sequence moved
  into a shared open_project_from_path helper, which no-ops when the
  pending tab is gone: the dialog is non-modal, so its result can
  arrive after the tab was closed or submitted by typing.
- An app-level project_dialog_open flag drops re-entrant Browse clicks
  while a picker is in flight.
- Pending tabs swallow mouse presses so clicks no longer fall through
  to the selection machinery on the empty grid; hovering the hint
  shows a pointer cursor.
- Pending-tab text renders in the configured terminal font so the
  drawn label matches the char-metric-based click rect (Font::MONOSPACE
  has a different advance and clipped the trailing bracket).
- ~ expansion extracted to expand_tilde and unit-tested, along with
  browse_hint_rect geometry and hit-testing.

Manually verified on macOS: hint hover/click, picker open/cancel,
spawn-on-pick, dedup-to-existing-project, and re-entrancy guard.

Co-Authored-By: Claude Fable 5 <[email protected]>
The spec served its purpose during design, spike, and implementation;
the PR carries the relevant context in its description and commit
messages instead.

Co-Authored-By: Claude Fable 5 <[email protected]>
It validated the macOS main-thread dialog spawn before the feature was
wired; the real Browse… flow now exercises the same pattern, so the
example had no remaining job and would only bitrot.

Co-Authored-By: Claude Fable 5 <[email protected]>
Comment thread src/ui.rs Outdated
/// Open the native folder picker for the pending tab `tab_id`.
OpenProjectDialog(usize),
/// Folder picker resolved; `None` means cancelled.
ProjectDialogResult { tab_id: usize, path: Option<PathBuf> },

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit. This isn't in keeping with the naming convention of the rest of these messages. How about SpawnOrFocusProjectTab? Same with the corresponding handler.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 245bf6b — renamed ProjectDialogResultSpawnOrFocusProjectTab and handle_project_dialog_resulthandle_spawn_or_focus_project_tab. Left OpenProjectDialog as-is since it parallels OpenPr and describes opening the picker rather than the spawn/focus outcome — happy to rename that too if you'd prefer.

Per PR review: the result message and its handler are now named for
the action they perform (spawn a new project tab or focus an existing
one) rather than the dialog mechanism, matching the action-oriented
convention of the other Message variants. OpenProjectDialog keeps its
name — it parallels OpenPr and accurately describes opening the picker.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
@astex astex merged commit 252ad19 into astex:master Jun 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants