diff --git a/crates/rds-cli/src/viewer_main.rs b/crates/rds-cli/src/viewer_main.rs index 5dca607..7801cf0 100644 --- a/crates/rds-cli/src/viewer_main.rs +++ b/crates/rds-cli/src/viewer_main.rs @@ -65,6 +65,7 @@ async fn main() -> std::process::ExitCode { } async fn run(mut cli: Cli, matches: &clap::ArgMatches) -> anyhow::Result<()> { + let mut configured_resolution = None; let key_path = rds_net::default_key_path(); let directory = match cli.control_dir.take() { Some(directory) => directory, @@ -130,18 +131,13 @@ async fn run(mut cli: Cli, matches: &clap::ArgMatches) -> anyhow::Result<()> { if cli.grant_file.is_none() { cli.grant_file = config.grant_file; } - if let Some(resolution) = config.resolution - && matches.value_source("resolution") != Some(ValueSource::CommandLine) - { - cli.options.resolution = resolution; - } + configured_resolution = config.resolution; } Err(error) if error.kind() == std::io::ErrorKind::NotFound => {} Err(error) => return Err(error.into()), } } - if !cli.options.headless && matches.value_source("resolution") != Some(ValueSource::CommandLine) - { + if configure_resolution(&mut cli.options, matches, configured_resolution) { let Some(height) = rds_desktop::render::choose_resolution() else { return Ok(()); }; @@ -160,3 +156,59 @@ async fn run(mut cli: Cli, matches: &clap::ArgMatches) -> anyhow::Result<()> { rds_cli::desktop::managed(&client, session, grant, cli.options).await } } + +/// Apply explicit quality choices before deciding whether native selection is +/// needed. Command-line values override the persisted profile. +fn configure_resolution( + options: &mut rds_cli::desktop::Options, + matches: &clap::ArgMatches, + configured: Option, +) -> bool { + let explicit_cli = matches.value_source("resolution") == Some(ValueSource::CommandLine); + if !explicit_cli && let Some(resolution) = configured { + options.resolution = resolution; + } + !options.headless && !explicit_cli && configured.is_none() +} + +#[cfg(test)] +mod tests { + use super::*; + + fn configured_cli(args: &[&str], configured: Option<&str>) -> (Cli, bool) { + let matches = Cli::command().try_get_matches_from(args).unwrap(); + let mut cli = Cli::from_arg_matches(&matches).unwrap(); + let config: Config = serde_json::from_str(configured.unwrap_or("{}")).unwrap(); + let picker = configure_resolution(&mut cli.options, &matches, config.resolution); + (cli, picker) + } + + #[test] + fn persisted_quality_is_applied_without_reopening_the_picker() { + for (profile, height) in [("hd", 720), ("full-hd", 1080), ("native", 0)] { + let json = format!(r#"{{"resolution":"{profile}"}}"#); + let (cli, picker) = configured_cli(&["rds-viewer"], Some(&json)); + assert_eq!(cli.options.resolution.height(), height); + assert!( + !picker, + "a persisted profile must not be overwritten by a picker" + ); + } + } + + #[test] + fn command_line_quality_overrides_the_persisted_profile() { + let (cli, picker) = configured_cli( + &["rds-viewer", "--resolution", "native"], + Some(r#"{"resolution":"hd"}"#), + ); + assert_eq!(cli.options.resolution.height(), 0); + assert!(!picker); + } + + #[test] + fn only_unconfigured_native_launches_need_the_picker() { + assert!(configured_cli(&["rds-viewer"], None).1); + assert!(!configured_cli(&["rds-viewer", "--headless"], None).1); + } +} diff --git a/docs/native-viewer.md b/docs/native-viewer.md index fe16407..bfaa542 100644 --- a/docs/native-viewer.md +++ b/docs/native-viewer.md @@ -346,10 +346,11 @@ contains no endpoint identity; the normal private viewer configuration applies. ## Quality selection -The macOS application presents a native quality chooser before opening its -remote session. Full HD (1080p) is the default; HD (720p) and Original are also -available. `--resolution full-hd|hd|native` skips the chooser for explicit CLI -or automated launches. Output preserves aspect ratio and does not upscale a +The macOS application presents a native quality chooser when no explicit +profile is configured. Full HD (1080p) is the default; HD (720p) and Original are +also available. `--resolution full-hd|hd|native` or a `resolution` value in +`viewer.json` skips the chooser; the command-line value overrides the file. +Headless runs never open the chooser. Output preserves aspect ratio and does not upscale a smaller source. This is encoded video geometry, not the local window's size. The additive `DesktopV3` greeting and `DesktopProfile` manager command carry the diff --git a/docs/reports/rds-persisted-viewer-quality-20261007.md b/docs/reports/rds-persisted-viewer-quality-20261007.md new file mode 100644 index 0000000..89908ce --- /dev/null +++ b/docs/reports/rds-persisted-viewer-quality-20261007.md @@ -0,0 +1,21 @@ +# Persisted native viewer quality — 2026-10-07 + +`viewer.json` already supported a typed `resolution` value. Startup applied it, +then still opened the native picker because only the CLI value source was +checked. A subsequent picker response could overwrite the persisted choice. + +Apply the CLI/file precedence before deciding whether a picker is needed. +An explicit CLI profile wins, a persisted profile bypasses the picker, and +headless execution never opens native UI. An unconfigured native launch keeps +the existing chooser. No protocol, permission or remote-process behavior changes. + +The regression with the former picker condition failed for the intended reason. +Tests cover every persisted profile, an explicit CLI override, an unconfigured +native launch and headless execution. The public software CI remains required; +no native pixel or WAN acceptance is implied by configuration tests. + +The existing [`ValueSource` API](https://docs.rs/clap/latest/clap/parser/enum.ValueSource.html) +distinguishes a defaulted argument from a command-line choice. The JSON choice +is an application source tracked separately; no new configuration mechanism is +introduced. The API distinction predates the requested research boundary; +this implementation observation is dated 2026-10-07.