feat(client/windows): per-host clipboard toggle
The bridge landed always-on whenever the host permitted it; sharing a clipboard is a trust decision about a specific host, so it needs to be opted into. Mirrors the Apple client's per-host model (StoredHost.clipboardSync, "Share clipboard with this host") rather than a global switch: KnownHost::clipboard_sync, toggled from the host card's overflow menu, default off. The session binary resolves the stored flag itself in session_params, so a direct connect and the console's own launches honor the same decision without every caller having to remember to pass it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -352,6 +352,7 @@ pub fn headless_add_host(target: &str) -> glib::ExitCode {
|
||||
paired: false,
|
||||
last_used: None,
|
||||
mac: Vec::new(),
|
||||
clipboard_sync: false,
|
||||
});
|
||||
}
|
||||
match known.save() {
|
||||
|
||||
@@ -453,6 +453,7 @@ impl ServiceState {
|
||||
paired: false,
|
||||
last_used: None,
|
||||
mac: Vec::new(),
|
||||
clipboard_sync: false,
|
||||
});
|
||||
}
|
||||
if let Err(e) = known.save() {
|
||||
|
||||
@@ -103,6 +103,14 @@ mod session_main {
|
||||
force_software: Arc<AtomicBool>,
|
||||
vulkan: Option<pf_client_core::video::VulkanDecodeDevice>,
|
||||
) -> SessionParams {
|
||||
// Per-host clipboard opt-in (design/clipboard-and-file-transfer.md §5.3), resolved
|
||||
// here rather than passed in so every caller — a direct connect and the console's
|
||||
// own launches — honors the same stored decision. `addr` is moved into the struct
|
||||
// below, so read it first.
|
||||
let clipboard = trust::KnownHosts::load()
|
||||
.hosts
|
||||
.iter()
|
||||
.any(|h| h.addr == addr && h.port == port && h.clipboard_sync);
|
||||
// Re-apply the shell-persisted forwarded-controller pin (stable `vid:pid:name`
|
||||
// key) to OUR gamepad service — the shells' in-process services can't reach this
|
||||
// process. Applied per params-build (idempotent; browse re-launches included) so
|
||||
@@ -165,6 +173,7 @@ mod session_main {
|
||||
// pump) pins one manually.
|
||||
display_hdr: None,
|
||||
mic_enabled: settings.mic_enabled,
|
||||
clipboard,
|
||||
// The Settings preference (auto → VAAPI where it exists; the presenter
|
||||
// demotes to software on boxes whose Vulkan can't import the dmabufs).
|
||||
// PUNKTFUNK_DECODER still overrides inside the decoder for bisects.
|
||||
|
||||
@@ -274,6 +274,7 @@ fn connect_spawn(
|
||||
paired: persist_paired,
|
||||
last_used: None,
|
||||
mac: target.mac.clone(),
|
||||
clipboard_sync: false,
|
||||
});
|
||||
let _ = k.save();
|
||||
}
|
||||
@@ -473,6 +474,7 @@ fn wake_and_connect(
|
||||
paired: false,
|
||||
last_used: None,
|
||||
mac: target.mac.clone(),
|
||||
clipboard_sync: false,
|
||||
});
|
||||
let _ = k.save();
|
||||
}
|
||||
|
||||
@@ -17,6 +17,11 @@ const MENU_LIBRARY: &str = "Browse library\u{2026}";
|
||||
const MENU_CONSOLE: &str = "Open console UI";
|
||||
const MENU_SPEED: &str = "Test network speed\u{2026}";
|
||||
const MENU_WAKE: &str = "Wake host";
|
||||
/// The per-host clipboard opt-in (design/clipboard-and-file-transfer.md §5.3 — the Apple
|
||||
/// client's "Share clipboard with this host"). Two labels rather than a checkable item: the
|
||||
/// flyout reports the clicked item BY TEXT, so the item has to say which way it's going.
|
||||
const MENU_CLIP_ON: &str = "Share clipboard with this host";
|
||||
const MENU_CLIP_OFF: &str = "Stop sharing clipboard";
|
||||
const MENU_RENAME: &str = "Rename\u{2026}";
|
||||
const MENU_FORGET: &str = "Forget\u{2026}";
|
||||
|
||||
@@ -456,6 +461,10 @@ pub(crate) fn hosts_page(props: &HostsProps, cx: &mut RenderCx) -> Element {
|
||||
let menu = {
|
||||
let (svc, target) = (props.svc.clone(), target.clone());
|
||||
let (sf, sr) = (set_forget.clone(), set_rename.clone());
|
||||
// Re-render lever for the clipboard toggle: `hover` is a value field, so
|
||||
// clearing it flips the menu label to its opposite immediately (and dropping
|
||||
// the hover highlight after a menu action is right regardless).
|
||||
let sh = props.set_hover.clone();
|
||||
let (fp, name) = (k.fp_hex.clone(), k.name.clone());
|
||||
button("")
|
||||
.icon(Symbol::More)
|
||||
@@ -474,6 +483,13 @@ pub(crate) fn hosts_page(props: &HostsProps, cx: &mut RenderCx) -> Element {
|
||||
if CONSOLE_UI_AVAILABLE && k.paired {
|
||||
items.push(menu_item(MENU_CONSOLE));
|
||||
}
|
||||
if k.paired {
|
||||
items.push(menu_item(if k.clipboard_sync {
|
||||
MENU_CLIP_OFF
|
||||
} else {
|
||||
MENU_CLIP_ON
|
||||
}));
|
||||
}
|
||||
items.push(menu_item(MENU_SPEED));
|
||||
// Offer an explicit wake only when the host is offline and we have a MAC.
|
||||
if can_wake {
|
||||
@@ -497,6 +513,19 @@ pub(crate) fn hosts_page(props: &HostsProps, cx: &mut RenderCx) -> Element {
|
||||
open_console(&svc.ctx, target.clone(), &svc.set_screen, &svc.set_status)
|
||||
}
|
||||
MENU_WAKE => crate::wol::wake(&target.mac, target.addr.parse().ok()),
|
||||
MENU_CLIP_ON | MENU_CLIP_OFF => {
|
||||
let mut known = KnownHosts::load();
|
||||
if let Some(h) = known.hosts.iter_mut().find(|h| h.fp_hex == fp) {
|
||||
h.clipboard_sync = !h.clipboard_sync;
|
||||
tracing::info!(
|
||||
host = %h.name,
|
||||
on = h.clipboard_sync,
|
||||
"clipboard sharing toggled for host"
|
||||
);
|
||||
}
|
||||
known.save();
|
||||
sh.call(None);
|
||||
}
|
||||
MENU_SPEED => {
|
||||
*svc.ctx.shared.target.lock().unwrap() = target.clone();
|
||||
// New run: invalidate any still-in-flight probe, reset the screen.
|
||||
|
||||
@@ -59,6 +59,7 @@ pub(crate) fn pair_page(props: &Svc, cx: &mut RenderCx) -> Element {
|
||||
paired: true,
|
||||
last_used: None,
|
||||
mac: target3.mac.clone(),
|
||||
clipboard_sync: false,
|
||||
});
|
||||
let _ = k.save();
|
||||
connect(&ctx3, &target3, Some(fp), &ss, &st);
|
||||
|
||||
@@ -41,6 +41,9 @@ pub struct SessionParams {
|
||||
pub display_hdr: Option<punktfunk_core::quic::HdrMeta>,
|
||||
/// Stream the default microphone to the host's virtual mic source.
|
||||
pub mic_enabled: bool,
|
||||
/// Share the clipboard with this host (the per-host `KnownHost::clipboard_sync`). The
|
||||
/// bridge additionally needs the host to advertise `HOST_CAP_CLIPBOARD`.
|
||||
pub clipboard: bool,
|
||||
/// Video decoder preference (Settings; `PUNKTFUNK_DECODER` overrides — see
|
||||
/// `video::Decoder::new`).
|
||||
pub decoder: String,
|
||||
@@ -342,14 +345,17 @@ fn pump(
|
||||
// The shared clipboard (design/clipboard-and-file-transfer.md §5): its own thread, since
|
||||
// `next_clip` blocks and the OS clipboard calls can wait on other apps. Returns straight
|
||||
// away when the host has no clipboard capability, so spawning is unconditional.
|
||||
let clipboard_thread = {
|
||||
let c = connector.clone();
|
||||
let s = stop.clone();
|
||||
std::thread::Builder::new()
|
||||
.name("pf-clipboard".into())
|
||||
.spawn(move || crate::clipboard::run(c, s))
|
||||
.ok()
|
||||
};
|
||||
let clipboard_thread = params
|
||||
.clipboard
|
||||
.then(|| {
|
||||
let c = connector.clone();
|
||||
let s = stop.clone();
|
||||
std::thread::Builder::new()
|
||||
.name("pf-clipboard".into())
|
||||
.spawn(move || crate::clipboard::run(c, s))
|
||||
.ok()
|
||||
})
|
||||
.flatten();
|
||||
let _mic = params
|
||||
.mic_enabled
|
||||
.then(|| {
|
||||
|
||||
@@ -124,6 +124,12 @@ pub struct KnownHost {
|
||||
/// pre-existing stores load; empty until first learned.
|
||||
#[serde(default)]
|
||||
pub mac: Vec<String>,
|
||||
/// Share this machine's clipboard with THIS host (design/clipboard-and-file-transfer.md
|
||||
/// §5.3 — the Apple client's `StoredHost.clipboardSync`). Per-host, not global: handing a
|
||||
/// host your clipboard is a trust decision about that host. Default off; the host must
|
||||
/// also advertise `HOST_CAP_CLIPBOARD` and have its own policy enabled.
|
||||
#[serde(default)]
|
||||
pub clipboard_sync: bool,
|
||||
}
|
||||
|
||||
#[derive(Default, Serialize, Deserialize)]
|
||||
@@ -201,6 +207,7 @@ pub fn persist_host(name: &str, addr: &str, port: u16, fp_hex: &str, paired: boo
|
||||
paired,
|
||||
last_used: None,
|
||||
mac: Vec::new(),
|
||||
clipboard_sync: false,
|
||||
});
|
||||
let _ = known.save();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user