feat(cli)!: replace the one-shot commands with a scriptable surface
The one-shot half of the binary could not finish its headline task without a human at the keyboard: `search` opened a full-screen picker, panicked with exit 101 when there was no terminal, and no command could fetch a known file at all. Everything exited 0 whatever happened, wrote emoji prose to stdout, and ignored --download-dir, SOULSEEK_DOWNLOAD_DIR and config.toml. The commands are now built for scripts and agents: search download get browse room list|say|listen message send|read stdout carries records only (tab-separated, or NDJSON under --json), stderr carries progress, and the exit code carries the verdict: 2 usage, 3 connect, 4 no results, 5 timed out, 6 transfer failed. `get` does search, rank and fetch in one step, and `search --json | jq ... | download --stdin` composes. Global flags parse in any position and every one has a SOULSEEK_ variable and a config.toml key resolved through a single precedence chain. Defects found and fixed along the way, each covered by a test: - a download with an unknown size was finalized against zero bytes and saved as an empty file; the size now comes from the peer's share listing - transfers are keyed by a hash of the remote path, so the same path from two peers shadowed itself and two files sharing a basename overwrote each other; both collisions are now dropped with a warning - `message send` and `room say` slept 500ms and claimed success; they now wait for a server reply queued behind the message before reporting - Client::login waited on an unbounded channel and never returned when the server went quiet Deleted the TUI picker the old `search` drove (download_selector, MultiDownloadProgress, SearchConfig): 1,573 lines out, 926 in. The interactive TUI is otherwise untouched. BREAKING CHANGE: `rooms` and `chat` become `room list|say|listen` and `message send|read`; `--disable-listener` becomes `--no-listener`; the unprefixed DISABLE_LISTENER, LISTENER_PORT, LOG_FILE and MAX_CONCURRENT_DOWNLOADS variables are now SOULSEEK_-prefixed; `search` no longer opens a picker or downloads anything.
This commit is contained in:
@@ -78,9 +78,9 @@ pub const DEFAULT_SEARCH_TIMEOUT: u64 = 10;
|
||||
|
||||
/// Layer CLI/env values over the config file over defaults.
|
||||
///
|
||||
/// The `--disable-listener` flag can only enable the disable (a bare flag
|
||||
/// has no "explicitly off" form), so file `disable_listener = true` wins
|
||||
/// unless the flag is passed.
|
||||
/// The listener has an explicit form in both directions: `--listener` beats
|
||||
/// `--no-listener` (and `SOULSEEK_NO_LISTENER`), which beats the file's
|
||||
/// `disable_listener`, so a config file can always be overridden per run.
|
||||
#[must_use]
|
||||
pub fn resolve(cli: &crate::cli::Cli, file: &FileConfig) -> Resolved {
|
||||
let download_dir = cli
|
||||
@@ -99,8 +99,11 @@ pub fn resolve(cli: &crate::cli::Cli, file: &FileConfig) -> Resolved {
|
||||
.listener_port
|
||||
.or(file.listener_port)
|
||||
.unwrap_or(DEFAULT_LISTENER_PORT),
|
||||
disable_listener: cli.disable_listener
|
||||
|| file.disable_listener.unwrap_or(false),
|
||||
disable_listener: if cli.listener {
|
||||
false
|
||||
} else {
|
||||
cli.no_listener || file.disable_listener.unwrap_or(false)
|
||||
},
|
||||
download_dir: download_dir.clone(),
|
||||
shared_dirs: resolve_shared_dirs(cli, file, &download_dir),
|
||||
max_concurrent_downloads: cli
|
||||
@@ -111,7 +114,10 @@ pub fn resolve(cli: &crate::cli::Cli, file: &FileConfig) -> Resolved {
|
||||
.search_timeout
|
||||
.or(file.search_timeout)
|
||||
.unwrap_or(DEFAULT_SEARCH_TIMEOUT),
|
||||
password_cmd: file.password_cmd.clone(),
|
||||
password_cmd: cli
|
||||
.password_cmd
|
||||
.clone()
|
||||
.or_else(|| file.password_cmd.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,7 +131,7 @@ fn resolve_shared_dirs(
|
||||
file: &FileConfig,
|
||||
download_dir: &str,
|
||||
) -> Vec<String> {
|
||||
if cli.shared_dir.is_none()
|
||||
if cli.shared_dir.is_empty()
|
||||
&& file.shared_dir.is_none()
|
||||
&& file.shared_dirs.is_none()
|
||||
{
|
||||
@@ -148,20 +154,7 @@ mod tests {
|
||||
use crate::cli::Cli;
|
||||
|
||||
fn bare_cli() -> Cli {
|
||||
Cli {
|
||||
username: None,
|
||||
password: None,
|
||||
server: None,
|
||||
disable_listener: false,
|
||||
listener_port: None,
|
||||
verbose: 0,
|
||||
log_file: None,
|
||||
command: None,
|
||||
download_dir: None,
|
||||
shared_dir: None,
|
||||
max_concurrent_downloads: None,
|
||||
search_timeout: None,
|
||||
}
|
||||
Cli::default()
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -244,7 +237,7 @@ mod tests {
|
||||
#[test]
|
||||
fn cli_shared_dir_combines_with_file_list_and_dedupes() {
|
||||
let mut cli = bare_cli();
|
||||
cli.shared_dir = Some("/a".into());
|
||||
cli.shared_dir = vec!["/a".into()];
|
||||
let file = FileConfig {
|
||||
shared_dirs: Some(vec!["/a".into(), "/b".into()]),
|
||||
..FileConfig::default()
|
||||
@@ -256,6 +249,53 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repeated_cli_shared_dirs_are_all_kept() {
|
||||
let mut cli = bare_cli();
|
||||
cli.shared_dir = vec!["/a".into(), "/b".into()];
|
||||
let resolved = resolve(&cli, &FileConfig::default());
|
||||
assert_eq!(
|
||||
resolved.shared_dirs,
|
||||
vec!["/a".to_string(), "/b".to_string()]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_empty_cli_shared_dir_disables_sharing() {
|
||||
let mut cli = bare_cli();
|
||||
cli.shared_dir = vec![String::new()];
|
||||
let resolved = resolve(&cli, &FileConfig::default());
|
||||
assert!(resolved.shared_dirs.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_listener_flag_disables_the_listener() {
|
||||
let mut cli = bare_cli();
|
||||
cli.no_listener = true;
|
||||
assert!(resolve(&cli, &FileConfig::default()).disable_listener);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn listener_flag_overrides_the_config_file() {
|
||||
let file = FileConfig {
|
||||
disable_listener: Some(true),
|
||||
..FileConfig::default()
|
||||
};
|
||||
assert!(resolve(&bare_cli(), &file).disable_listener);
|
||||
|
||||
let mut cli = bare_cli();
|
||||
cli.listener = true;
|
||||
assert!(!resolve(&cli, &file).disable_listener);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn listener_flag_wins_over_no_listener() {
|
||||
let mut cli = bare_cli();
|
||||
cli.listener = true;
|
||||
cli.no_listener = true;
|
||||
assert!(!resolve(&cli, &FileConfig::default()).disable_listener);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn file_values_override_defaults() {
|
||||
let file = FileConfig {
|
||||
|
||||
Reference in New Issue
Block a user