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:
2026-07-19 18:41:02 +02:00
parent cab6350723
commit aa45757a72
8 changed files with 64 additions and 8 deletions
+2
View File
@@ -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();
}
+29
View File
@@ -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.
+1
View File
@@ -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);