ci / web (push) Successful in 1m56s
ci / rust (push) Failing after 6m15s
ci / docs-site (push) Successful in 2m1s
android / android (push) Canceled after 11m36s
ci / rust-arm64 (push) Failing after 9m8s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 24s
decky / build-publish (push) Successful in 36s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 22s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 17s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 18s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 14s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 11s
ci / bench (push) Successful in 5m23s
deb / build-publish-client-arm64 (push) Successful in 7m28s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Failing after 6m31s
deb / build-publish (push) Successful in 9m19s
arch / build-publish (push) Successful in 18m42s
flatpak / build-publish (push) Failing after 8m19s
deb / build-publish-host (push) Successful in 10m41s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 18m15s
apple / swift (push) Successful in 5m30s
docker / deploy-docs (push) Successful in 29s
docker / build-push-arm64cross (push) Successful in 11m22s
apple / screenshots (push) Successful in 27m32s
The other half of P1 on GTK (design/client-settings-profiles.md §5.2): until now a
profile could be created and edited but only *used* through `--profile` or a URL. Host
cards get two menus, and they are separate on purpose — the predictability rule is that
connecting with a profile must never change what the card does next time.
- **Connect with ▸** is a one-off. It rides `ConnectRequest.profile` into the session's
`--profile` flag and touches no stored state. "Default settings" in that menu is
`Some("")`, not `None`: on a bound host, asking for the defaults is a real choice and
has to survive as a value rather than degrade into "use the binding".
- **Default profile ▸** is the explicit rebinding act. It writes straight onto the host
record — the binding IS a field there, so there is no map to keep in step — matched by
fingerprint, else by address, like every other per-host lookup in this client.
- The card carries a **chip** naming its bound profile, so what a plain click will do is
visible without opening a menu. A binding whose profile was deleted shows nothing and
connects with the defaults, which is exactly what the resolver does.
A URL's `profile=` now takes the same one-off route, since it is the same kind of thing.
Verified on .21: `punktfunk://connect/Bound%20Box?profile=Work` against a host bound to
"Game" spawned a session that logged `profile=Work id=bbbbbbbbbbbb` — the one-off won,
and the binding on disk was untouched.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
111 lines
4.9 KiB
Rust
111 lines
4.9 KiB
Rust
//! The shell↔session handoff: every stream runs in the spawned `punktfunk-session`
|
|
//! Vulkan binary (the legacy in-process presenter is gone — phase 5 of
|
|
//! punktfunk-planning `linux-client-rearchitecture.md`). What is left here is the
|
|
//! TRANSLATION: a [`ConnectRequest`] becomes a `ConnectPlan`, and the session's typed
|
|
//! lifecycle events become the [`AppMsg`]s the relm4 app consumes — spinner until
|
|
//! `{"ready":true}`, banner from the `{"error"|"ended": …}` line, exit code 3 +
|
|
//! `trust_rejected` routed to the re-pair PIN ceremony.
|
|
//!
|
|
//! Spawning, the argv, the stdout contract and the child handle live in
|
|
//! `pf_client_core::orchestrate` (design/client-architecture-split.md §3) — the WinUI shell
|
|
//! and the coming CLI spawn through the same code, so "what flags does a stream get" and
|
|
//! "what does ready mean" have exactly one answer.
|
|
|
|
use crate::app::AppMsg;
|
|
use crate::ui_hosts::ConnectRequest;
|
|
use pf_client_core::orchestrate::{self, ConnectPlan, HostTarget, SessionEvent};
|
|
use pf_client_core::trust::Settings;
|
|
|
|
/// Spawn tunables beyond a plain connect.
|
|
#[derive(Debug, Default)]
|
|
pub struct SpawnOpts {
|
|
/// Handshake budget override (`--connect-timeout`) — the request-access flow passes
|
|
/// ~185 s because the host PARKS the connection until the operator approves.
|
|
pub connect_timeout_secs: Option<u64>,
|
|
/// Persist the host as *paired* once the child reports ready (request-access: the
|
|
/// operator's approval IS the pairing). Plain TOFU persists unpaired.
|
|
pub persist_paired: bool,
|
|
/// A cancel handle to arm (request-access's waiting dialog): killing the child is
|
|
/// the only abort a parked connect has.
|
|
pub cancel: Option<CancelHandle>,
|
|
}
|
|
|
|
pub use orchestrate::{session_binary, CancelHandle};
|
|
|
|
/// Spawn the session binary for a connect with `fp_hex` pinned and translate its
|
|
/// lifecycle into [`AppMsg`]s. `tofu` = the fingerprint came from the host's advert
|
|
/// rather than the store — the app persists it once the child reports ready (the child
|
|
/// connects pinned to it, so ready proves the host really holds that identity).
|
|
///
|
|
/// The caller has already taken `busy`; [`AppMsg::SessionExited`] releases it. `Err` =
|
|
/// the spawn itself failed (binary missing?) — surfaced as a connect error.
|
|
pub fn spawn_session(
|
|
sender: relm4::Sender<AppMsg>,
|
|
req: ConnectRequest,
|
|
fp_hex: String,
|
|
tofu: bool,
|
|
fullscreen_on_stream: bool,
|
|
opts: SpawnOpts,
|
|
) -> Result<(), String> {
|
|
// The plan this connect resolves to. A plain card click carries no `--profile`: it honors
|
|
// the host's own binding, which the session resolves through the same helper this shell
|
|
// would have used (design/client-settings-profiles.md §4.6) — passing it here would be a
|
|
// second source of truth for one decision. Only a "Connect with ▸" pick (or a URL's
|
|
// `profile=`) sets one, and it applies to this session alone.
|
|
//
|
|
// Two fields are deliberately not the shell's state yet, because nothing in the spawn path
|
|
// reads them: `settings` carries only what the argv needs (the fullscreen flag), and `wake`
|
|
// is false because this shell still runs its own dial-first wake fallback in `app.rs`. Both
|
|
// become real when the GTK connect path moves onto `ConnectOrchestrator` (arch-split C0).
|
|
let plan = ConnectPlan {
|
|
host: HostTarget {
|
|
name: req.name.clone(),
|
|
addr: req.addr.clone(),
|
|
port: req.port,
|
|
fp_hex: Some(fp_hex.clone()),
|
|
mac: req.mac.clone(),
|
|
id: None,
|
|
},
|
|
launch: req.launch.as_ref().map(|(id, _)| id.clone()),
|
|
profile: None,
|
|
// A one-off pick rides the flag; without one the session resolves the host's own
|
|
// binding through the same helper this shell would have used.
|
|
profile_override: req.profile.clone(),
|
|
settings: Settings {
|
|
fullscreen_on_stream,
|
|
..Default::default()
|
|
},
|
|
wake: false,
|
|
connect_timeout_secs: opts.connect_timeout_secs,
|
|
tofu,
|
|
};
|
|
|
|
let persist_paired = opts.persist_paired;
|
|
let (mut error, mut ended) = (None::<(String, bool)>, None::<String>);
|
|
orchestrate::spawn_session(&plan, opts.cancel, move |ev| match ev {
|
|
SessionEvent::Ready => {
|
|
let _ = sender.send(AppMsg::SessionReady {
|
|
req: req.clone(),
|
|
fp_hex: fp_hex.clone(),
|
|
tofu,
|
|
persist_paired,
|
|
});
|
|
}
|
|
SessionEvent::Error {
|
|
msg,
|
|
trust_rejected,
|
|
} => error = Some((msg, trust_rejected)),
|
|
SessionEvent::Ended(msg) => ended = Some(msg),
|
|
SessionEvent::Exited(code) => {
|
|
let _ = sender.send(AppMsg::SessionExited {
|
|
req: req.clone(),
|
|
code,
|
|
error: error.take(),
|
|
ended: ended.take(),
|
|
tofu,
|
|
});
|
|
}
|
|
})?;
|
|
Ok(())
|
|
}
|