feat(tui): say so in the status bar when the listener took another port

With the login screen's own attempts no longer holding the port, a
session listens somewhere other than the configured port mainly when
another program already holds it. soulseek-rs still falls back to an
ephemeral port there, which is what lets several clients on one machine
share a configured port, and it advertises the port it really bound. A
manual port forward on the router then points at nothing, though, and
the TUI never said so; the CLI already warns.

After login the TUI compares the bound port with the configured one,
using the CLI's rule, and the status bar's title reads "port X in use,
listening on Y" for the rest of the session. A window attached to a
daemon shows nothing: the daemon owns its listener.

Nicotine+ keeps retrying the configured port every five seconds and
Soulseek.NET refuses it; neither falls back. Keeping the fallback and
making it visible is a deliberate difference.
This commit is contained in:
Michel de Graaf
2026-09-14 13:44:57 +02:00
parent bd502c216a
commit deb846be14
5 changed files with 35 additions and 6 deletions
+9 -4
View File
@@ -502,14 +502,17 @@ fn run_default_tui(
// Best-effort: make ourselves reachable behind a home router so // Best-effort: make ourselves reachable behind a home router so
// firewalled peers can connect back. Mapped only once the listener is // firewalled peers can connect back. Mapped only once the listener is
// up, and for the port it really bound. Kept alive for the session. // up, and for the port it really bound. Kept alive for the session.
let _port_mapper = outcome let bound_port = outcome.client.listen_port();
.client let _port_mapper =
.listen_port() bound_port.map(crate::port_mapping::PortMapper::spawn);
.map(crate::port_mapping::PortMapper::spawn);
let store = crate::persist::paths::state_dir() let store = crate::persist::paths::state_dir()
.map(crate::persist::state::StateStore::new); .map(crate::persist::state::StateStore::new);
let listener_fallback = bound_port
.filter(|&bound| listen_port != 0 && bound != listen_port)
.map(|bound| (listen_port, bound));
let exit = launch_main_tui( let exit = launch_main_tui(
terminal, terminal,
Arc::new(outcome.client), Arc::new(outcome.client),
@@ -517,6 +520,7 @@ fn run_default_tui(
Duration::from_secs(resolved.search_timeout), Duration::from_secs(resolved.search_timeout),
store, store,
config_path.clone(), config_path.clone(),
listener_fallback,
) )
.map_err(|e| CliError::new(Exit::Failure, e.to_string()))?; .map_err(|e| CliError::new(Exit::Failure, e.to_string()))?;
@@ -592,6 +596,7 @@ fn run_attached_tui(
Duration::from_secs(resolved.search_timeout), Duration::from_secs(resolved.search_timeout),
None, None,
config_path, config_path,
None,
) )
.map(|_| ()) .map(|_| ())
.map_err(|e| CliError::new(Exit::Failure, e.to_string())) .map_err(|e| CliError::new(Exit::Failure, e.to_string()))
+7 -1
View File
@@ -12,6 +12,7 @@ use ratatui::{
widgets::Paragraph, widgets::Paragraph,
}; };
use soulseek_rs::DownloadStatus; use soulseek_rs::DownloadStatus;
use std::fmt::Write as _;
const VERSION: &str = env!("CARGO_PKG_VERSION"); const VERSION: &str = env!("CARGO_PKG_VERSION");
@@ -54,6 +55,7 @@ pub fn render_download_stats(
downloads: &[DownloadEntry], downloads: &[DownloadEntry],
active_count: usize, active_count: usize,
daemon: Option<&str>, daemon: Option<&str>,
listener_fallback: Option<(u16, u16)>,
) { ) {
let completed = downloads let completed = downloads
.iter() .iter()
@@ -79,10 +81,14 @@ pub fn render_download_stats(
let active = active_progress(downloads); let active = active_progress(downloads);
let title = match daemon { let mut title = match daemon {
Some(endpoint) => format!("Status · daemon {endpoint}"), Some(endpoint) => format!("Status · daemon {endpoint}"),
None => "Status".to_string(), None => "Status".to_string(),
}; };
if let Some((configured, bound)) = listener_fallback {
let _ =
write!(title, " · port {configured} in use, listening on {bound}");
}
let block = pane_block(false).title(plain_title(&title, false)); let block = pane_block(false).title(plain_title(&title, false));
let inner_area = block.inner(area); let inner_area = block.inner(area);
+5 -1
View File
@@ -33,6 +33,7 @@ pub struct MainTui {
saved_snapshot: Snapshot, saved_snapshot: Snapshot,
/// Which divider the left mouse button is holding, if any. /// Which divider the left mouse button is holding, if any.
resize_drag: Option<input::ResizeDrag>, resize_drag: Option<input::ResizeDrag>,
listener_fallback: Option<(u16, u16)>,
} }
impl MainTui { impl MainTui {
@@ -53,6 +54,7 @@ impl MainTui {
config_path, config_path,
saved_snapshot: Snapshot::default(), saved_snapshot: Snapshot::default(),
resize_drag: None, resize_drag: None,
listener_fallback: None,
}; };
tui.restore_persisted_state(); tui.restore_persisted_state();
tui tui
@@ -257,9 +259,11 @@ pub fn launch_main_tui(
search_timeout: Duration, search_timeout: Duration,
store: Option<StateStore>, store: Option<StateStore>,
config_path: Option<std::path::PathBuf>, config_path: Option<std::path::PathBuf>,
listener_fallback: Option<(u16, u16)>,
) -> Result<TuiExit> { ) -> Result<TuiExit> {
let tui = let mut tui =
MainTui::new(client, download_dir, search_timeout, store, config_path); MainTui::new(client, download_dir, search_timeout, store, config_path);
tui.listener_fallback = listener_fallback;
tui.run(terminal) tui.run(terminal)
} }
@@ -40,6 +40,7 @@ impl MainTui {
&self.state.downloads, &self.state.downloads,
self.state.active_downloads_count, self.state.active_downloads_count,
daemon.as_deref(), daemon.as_deref(),
self.listener_fallback,
); );
self.render_content(frame, main_chunks[1]); self.render_content(frame, main_chunks[1]);
@@ -21,6 +21,19 @@ fn the_results_pane_has_the_whole_width_to_itself() {
assert!(searches < downloads && downloads < info, "{row}"); assert!(searches < downloads && downloads < info, "{row}");
} }
#[test]
fn a_listener_that_fell_back_says_so_in_the_status_bar() {
let mut tui = furnished_tui();
tui.listener_fallback = Some((2234, 50123));
let screen = screen_of(&mut tui);
assert!(
screen.contains("port 2234 in use, listening on 50123"),
"{screen}"
);
}
#[test] #[test]
fn tab_and_shift_tab_walk_the_panes_in_legend_order() { fn tab_and_shift_tab_walk_the_panes_in_legend_order() {
let mut tui = furnished_tui(); let mut tui = furnished_tui();