feat: browse your own shares as a peer receives them

The window could show any peer's share tree but never this account's, so
"am I actually sharing that?" had no answer short of asking a friend to
browse you. The tree is already there; what was missing was feeding it
from our own index.

B opens it — the same collapsible tree, filter, and navigation the peer
browse offers, built from `Shares::directories()`, which is the listing
GetShareFileList answers with. So what it shows is what the network sees,
not what the config meant to share. r re-scans the disk rather than
re-asking a peer, o steps across to the folders that feed it, and nothing
here queues a download from ourselves.

Attached to a daemon the index lives on the daemon's host, so it comes
back over a new `shares.files` method rather than being scanned locally;
`shares files [--filter]` prints the same listing headless, through the
`emit_listing` that `browse` now shares with it — the two are only
comparable while they are built the same way.

Two allocation fixes came out of the review, both on paths this reaches:
the daemon hands its listing to the DTO instead of deep-copying one it
already owns, and `Shares::directories()` allocates its map key once per
folder rather than once per file, which the peer-facing serve path has
been paying for all along.
This commit is contained in:
Michel de Graaf
2026-09-10 11:44:57 +02:00
parent d6d870b39f
commit 26b551d997
24 changed files with 606 additions and 57 deletions
+43 -1
View File
@@ -54,6 +54,14 @@ impl MainTui {
self.sync_browse_selection();
return;
}
KeyCode::Char('B') => {
self.show_own_shares();
return;
}
KeyCode::Char('r') if self.state.browse.active_is_own() => {
self.reindex_shares();
return;
}
KeyCode::Char('r') => {
// Retry a timed-out browse.
if let Some(username) = self.state.browse.retry_active() {
@@ -61,6 +69,13 @@ impl MainTui {
}
return;
}
// The shared folders themselves are the settings pane's business,
// so adding or dropping one is a keystroke away from seeing it.
KeyCode::Char('o') if self.state.browse.active_is_own() => {
self.state.show_browse = false;
self.open_settings();
return;
}
_ => {}
}
@@ -192,9 +207,11 @@ impl MainTui {
}
/// Queue downloads of `files` (path, size) from the active browse tab's user.
///
/// Our own shares are already on this disk, so the own tab queues nothing.
fn queue_browse_files(&mut self, files: Vec<(String, u64)>) {
let Some(username) =
self.state.browse.active_tab().map(|b| b.username.clone())
self.state.browse.download_target().map(str::to_string)
else {
return;
};
@@ -240,6 +257,31 @@ impl MainTui {
self.sync_browse_selection();
}
/// Open (or focus) the view of what this session shares, filled from the
/// index the network is served from.
pub(super) fn show_own_shares(&mut self) {
if self.state.browse.open_own(&self.client.username()) {
self.load_own_shares();
}
self.state.show_browse = true;
self.sync_browse_selection();
}
/// Put a fresh copy of the share index into the own tab, if it is open.
/// A re-index or an added folder would otherwise leave it showing what
/// the network saw a scan ago.
pub(super) fn load_own_shares(&mut self) {
// The early return matters: without an own tab there is nothing to
// fill, and against a daemon asking anyway is a whole index over the
// socket for nobody.
let Some(idx) = self.state.browse.own_index() else {
return;
};
let listing = self.client.shared_listing();
self.state.browse.tabs[idx].load(&listing);
self.sync_browse_selection();
}
/// The username of the highlighted search result (filter-aware).
pub(super) fn highlighted_result_owner(&self) -> Option<String> {
self.highlighted_result().map(|f| f.username.clone())