feat(client): settings profiles — the override catalog, the one resolver, and the session flag
A client setting is global today: pick 4K@120 HDR and the retro box gets it too.
The one escape hatch (per-host `clipboard_sync`) proves the shape works but covers a
single field. This is P0 of design/client-settings-profiles.md — the headless core the
shells, Apple, Android and the deep-link grammar all build against. No UI yet.
A profile is a NAMED BUNDLE OF OVERRIDES, not a snapshot: `SettingsOverlay` is sparse
`Option`s, so a field the user never touched keeps following the global value live, and
fixing a global once fixes it everywhere. `Some(x)` equal to today's global is still
meaningful — it pins x against a later global change — which is why the UI will write
`Some` on touch and `None` on an explicit reset, never by diffing.
The catalog is its own `client-profiles.json`, deliberately not part of the settings
file: that file has five whole-file load-modify-save writers with no merge, so a profile
written by one would be dropped by the next. Which host uses which profile is a field on
the host record instead of a map keyed by host — that is what dissolves the client's
long-standing "what identifies a host record" question for this feature.
Resolution has exactly one implementation, `trust::effective_settings()`:
effective = overlay(profile).apply(global)
profile = one-off pick ?? host binding ?? none
Both session construction sites go through it (`main.rs` AND `console.rs` — touching one
and not the other is a Windows-only build break), so the console, and therefore Decky,
honor host bindings with no work of their own. Nothing here can fail a connect: a
deleted binding resolves as "no profile", and a one-off that can't be honored falls back
to the DEFAULTS rather than to the host's own profile — "connect with Work" must never
quietly stream "Game". `--profile <id|name>` is the one-off door for the shells' coming
"Connect with ▸" and for `punktfunk://…&profile=`; `--profile ""` forces the defaults on
a bound host. The active profile's name closes the stats overlay's first line, so
"which profile am I on?" is answerable without leaving the stream.
Also here, because this effort touches every one of these anyway:
- `KnownHost` gains `profile_id`, `pinned_profiles` and a lazily minted stable `id`. The
id is groundwork (design §4.5) — no lookup is re-keyed, `fp_hex`/`addr:port` stay.
- `upsert` now preserves the state the USER set on a record — the profile binding, its
pins, the clipboard decision, the id — against refreshes that carry none of it.
`clipboard_sync` survived that only because the function happened not to mention it.
- `KnownHost::default()` exists so construction sites read `..Default::default()`; five
hand-written literals were exactly how a new field gets silently dropped on re-pair.
- All three client stores now write temp+rename. A torn settings file loads as `Default`
— i.e. silently resets every setting the user has.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -201,11 +201,16 @@ pub fn run(target: Option<&str>) -> u8 {
|
||||
tracing::info!(%addr, %title, request_access,
|
||||
launch = launch.as_deref().unwrap_or("desktop"),
|
||||
"launching from the console");
|
||||
// Settings re-load per launch: the console's own settings screen
|
||||
// may have changed them since the last stream.
|
||||
let settings = trust::Settings::load();
|
||||
// Settings re-resolve per launch: the console's own settings screen may
|
||||
// have changed the defaults since the last stream, and the host may carry
|
||||
// a profile binding. Console (and therefore Decky, which spawns this
|
||||
// binary) honors bindings with no console-side work — the resolver is the
|
||||
// same one `--connect` goes through. No one-off here: picking a profile is
|
||||
// a desktop-shell affordance in v1, pinned cards are the console's.
|
||||
let (settings, profile) = trust::effective_settings(&addr, port, None);
|
||||
let mut params = session_params(
|
||||
&settings,
|
||||
profile.map(|p| p.name),
|
||||
addr.clone(),
|
||||
port,
|
||||
pin,
|
||||
@@ -434,11 +439,7 @@ impl ServiceState {
|
||||
name: if name.is_empty() { addr.clone() } else { name },
|
||||
addr,
|
||||
port,
|
||||
fp_hex: String::new(),
|
||||
paired: false,
|
||||
last_used: None,
|
||||
mac: Vec::new(),
|
||||
clipboard_sync: false,
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
if let Err(e) = known.save() {
|
||||
|
||||
@@ -124,6 +124,15 @@ mod session_main {
|
||||
}
|
||||
}
|
||||
|
||||
/// `--profile <id|name>` — the settings profile this one session runs with, overriding the
|
||||
/// host's own binding for this launch only (never rebinding it): the shells' "Connect
|
||||
/// with ▸ X" and a `punktfunk://…&profile=` link both land here. Absent = honor the host's
|
||||
/// binding; `--profile ""` (or a bare `--profile`) forces the global defaults, which is
|
||||
/// how "Connect with ▸ Default settings" reaches a bound host.
|
||||
fn profile_arg() -> Option<String> {
|
||||
arg_flag("--profile").then(|| arg_value("--profile").unwrap_or_default())
|
||||
}
|
||||
|
||||
/// The connect budget: 15 s normally; `--connect-timeout SECS` overrides — the
|
||||
/// shell's request-access flow passes ~185 s because the host PARKS the connection
|
||||
/// until the operator clicks Approve.
|
||||
@@ -135,13 +144,20 @@ mod session_main {
|
||||
)
|
||||
}
|
||||
|
||||
/// One session's pump parameters from the Settings store — shared by `--connect`
|
||||
/// One session's pump parameters from the EFFECTIVE settings — shared by `--connect`
|
||||
/// and every `--browse` launch. Explicit settings, `0` fields resolved to the
|
||||
/// window's display (the GTK client reads the monitor under its window — same
|
||||
/// contract).
|
||||
///
|
||||
/// `settings` is what [`trust::effective_settings`] returned, never a raw
|
||||
/// `Settings::load()`: both callers resolve the host's profile first, so the two
|
||||
/// construction sites cannot drift (they historically did — touching one and not the
|
||||
/// other is a Windows-only build break). `profile` is that profile's name, for the
|
||||
/// stats overlay's first line.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn session_params(
|
||||
settings: &trust::Settings,
|
||||
profile: Option<String>,
|
||||
addr: String,
|
||||
port: u16,
|
||||
pin: [u8; 32],
|
||||
@@ -246,6 +262,7 @@ mod session_main {
|
||||
identity,
|
||||
connect_timeout: connect_timeout(),
|
||||
force_software,
|
||||
profile,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -431,14 +448,16 @@ mod session_main {
|
||||
}
|
||||
let Some(target) = arg_value("--connect") else {
|
||||
eprintln!(
|
||||
"usage: punktfunk-session --connect host[:port] [--fp HEX] [--launch id] [--fullscreen]\n\
|
||||
"usage: punktfunk-session --connect host[:port] [--fp HEX] [--launch id] [--profile REF] [--fullscreen]\n\
|
||||
\x20 punktfunk-session --browse [host[:port]] [--mgmt PORT] [--fullscreen] [--json-status]\n\
|
||||
\x20 punktfunk-session --pair <PIN> --connect host[:port] [--name LABEL]\n\
|
||||
\n\
|
||||
Streams from a paired punktfunk host in a Vulkan window. --browse opens the\n\
|
||||
gamepad console instead: bare --browse is the host list (discovery, PIN\n\
|
||||
pairing, settings, wake-on-LAN); with a target it opens that host's game\n\
|
||||
library. --connect never dials a host it has no pinned fingerprint for —\n\
|
||||
library. --profile picks a settings profile by id or name for this session\n\
|
||||
only (\"\" = the global defaults); without it the host's own profile applies.\n\
|
||||
--connect never dials a host it has no pinned fingerprint for —\n\
|
||||
enrol with --pair (no display needed), in the console, or from the desktop\n\
|
||||
client."
|
||||
);
|
||||
@@ -453,7 +472,13 @@ mod session_main {
|
||||
return EXIT_CONNECT_FAILED;
|
||||
}
|
||||
};
|
||||
let settings = trust::Settings::load();
|
||||
// Global defaults with this host's settings profile overlaid — the binding on the
|
||||
// host record, or `--profile <id|name>` for a one-off (`--profile ""` forces the
|
||||
// defaults). Resolved through the shared helper, exactly like the console's launches.
|
||||
let (settings, profile) = trust::effective_settings(&addr, port, profile_arg().as_deref());
|
||||
if let Some(p) = &profile {
|
||||
tracing::info!(profile = %p.name, id = %p.id, "streaming with a settings profile");
|
||||
}
|
||||
|
||||
// Trust follows the GTK client's `--connect` rules: a stored (or `--fp`) pin
|
||||
// connects silently; an unknown host is REFUSED — there is no dialog here, and a
|
||||
@@ -523,6 +548,7 @@ mod session_main {
|
||||
pf_presenter::run_session(opts, move |gamepad, native, force_software, vulkan| {
|
||||
session_params(
|
||||
&settings,
|
||||
profile.map(|p| p.name),
|
||||
addr,
|
||||
port,
|
||||
pin,
|
||||
|
||||
Reference in New Issue
Block a user