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,
|
paired: false,
|
||||||
last_used: None,
|
last_used: None,
|
||||||
mac: Vec::new(),
|
mac: Vec::new(),
|
||||||
|
clipboard_sync: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
match known.save() {
|
match known.save() {
|
||||||
|
|||||||
@@ -453,6 +453,7 @@ impl ServiceState {
|
|||||||
paired: false,
|
paired: false,
|
||||||
last_used: None,
|
last_used: None,
|
||||||
mac: Vec::new(),
|
mac: Vec::new(),
|
||||||
|
clipboard_sync: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if let Err(e) = known.save() {
|
if let Err(e) = known.save() {
|
||||||
|
|||||||
@@ -103,6 +103,14 @@ mod session_main {
|
|||||||
force_software: Arc<AtomicBool>,
|
force_software: Arc<AtomicBool>,
|
||||||
vulkan: Option<pf_client_core::video::VulkanDecodeDevice>,
|
vulkan: Option<pf_client_core::video::VulkanDecodeDevice>,
|
||||||
) -> SessionParams {
|
) -> 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`
|
// 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
|
// 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
|
// process. Applied per params-build (idempotent; browse re-launches included) so
|
||||||
@@ -165,6 +173,7 @@ mod session_main {
|
|||||||
// pump) pins one manually.
|
// pump) pins one manually.
|
||||||
display_hdr: None,
|
display_hdr: None,
|
||||||
mic_enabled: settings.mic_enabled,
|
mic_enabled: settings.mic_enabled,
|
||||||
|
clipboard,
|
||||||
// The Settings preference (auto → VAAPI where it exists; the presenter
|
// The Settings preference (auto → VAAPI where it exists; the presenter
|
||||||
// demotes to software on boxes whose Vulkan can't import the dmabufs).
|
// demotes to software on boxes whose Vulkan can't import the dmabufs).
|
||||||
// PUNKTFUNK_DECODER still overrides inside the decoder for bisects.
|
// PUNKTFUNK_DECODER still overrides inside the decoder for bisects.
|
||||||
|
|||||||
@@ -274,6 +274,7 @@ fn connect_spawn(
|
|||||||
paired: persist_paired,
|
paired: persist_paired,
|
||||||
last_used: None,
|
last_used: None,
|
||||||
mac: target.mac.clone(),
|
mac: target.mac.clone(),
|
||||||
|
clipboard_sync: false,
|
||||||
});
|
});
|
||||||
let _ = k.save();
|
let _ = k.save();
|
||||||
}
|
}
|
||||||
@@ -473,6 +474,7 @@ fn wake_and_connect(
|
|||||||
paired: false,
|
paired: false,
|
||||||
last_used: None,
|
last_used: None,
|
||||||
mac: target.mac.clone(),
|
mac: target.mac.clone(),
|
||||||
|
clipboard_sync: false,
|
||||||
});
|
});
|
||||||
let _ = k.save();
|
let _ = k.save();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,11 @@ const MENU_LIBRARY: &str = "Browse library\u{2026}";
|
|||||||
const MENU_CONSOLE: &str = "Open console UI";
|
const MENU_CONSOLE: &str = "Open console UI";
|
||||||
const MENU_SPEED: &str = "Test network speed\u{2026}";
|
const MENU_SPEED: &str = "Test network speed\u{2026}";
|
||||||
const MENU_WAKE: &str = "Wake host";
|
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_RENAME: &str = "Rename\u{2026}";
|
||||||
const MENU_FORGET: &str = "Forget\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 menu = {
|
||||||
let (svc, target) = (props.svc.clone(), target.clone());
|
let (svc, target) = (props.svc.clone(), target.clone());
|
||||||
let (sf, sr) = (set_forget.clone(), set_rename.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());
|
let (fp, name) = (k.fp_hex.clone(), k.name.clone());
|
||||||
button("")
|
button("")
|
||||||
.icon(Symbol::More)
|
.icon(Symbol::More)
|
||||||
@@ -474,6 +483,13 @@ pub(crate) fn hosts_page(props: &HostsProps, cx: &mut RenderCx) -> Element {
|
|||||||
if CONSOLE_UI_AVAILABLE && k.paired {
|
if CONSOLE_UI_AVAILABLE && k.paired {
|
||||||
items.push(menu_item(MENU_CONSOLE));
|
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));
|
items.push(menu_item(MENU_SPEED));
|
||||||
// Offer an explicit wake only when the host is offline and we have a MAC.
|
// Offer an explicit wake only when the host is offline and we have a MAC.
|
||||||
if can_wake {
|
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)
|
open_console(&svc.ctx, target.clone(), &svc.set_screen, &svc.set_status)
|
||||||
}
|
}
|
||||||
MENU_WAKE => crate::wol::wake(&target.mac, target.addr.parse().ok()),
|
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 => {
|
MENU_SPEED => {
|
||||||
*svc.ctx.shared.target.lock().unwrap() = target.clone();
|
*svc.ctx.shared.target.lock().unwrap() = target.clone();
|
||||||
// New run: invalidate any still-in-flight probe, reset the screen.
|
// 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,
|
paired: true,
|
||||||
last_used: None,
|
last_used: None,
|
||||||
mac: target3.mac.clone(),
|
mac: target3.mac.clone(),
|
||||||
|
clipboard_sync: false,
|
||||||
});
|
});
|
||||||
let _ = k.save();
|
let _ = k.save();
|
||||||
connect(&ctx3, &target3, Some(fp), &ss, &st);
|
connect(&ctx3, &target3, Some(fp), &ss, &st);
|
||||||
|
|||||||
@@ -41,6 +41,9 @@ pub struct SessionParams {
|
|||||||
pub display_hdr: Option<punktfunk_core::quic::HdrMeta>,
|
pub display_hdr: Option<punktfunk_core::quic::HdrMeta>,
|
||||||
/// Stream the default microphone to the host's virtual mic source.
|
/// Stream the default microphone to the host's virtual mic source.
|
||||||
pub mic_enabled: bool,
|
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 preference (Settings; `PUNKTFUNK_DECODER` overrides — see
|
||||||
/// `video::Decoder::new`).
|
/// `video::Decoder::new`).
|
||||||
pub decoder: String,
|
pub decoder: String,
|
||||||
@@ -342,14 +345,17 @@ fn pump(
|
|||||||
// The shared clipboard (design/clipboard-and-file-transfer.md §5): its own thread, since
|
// 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
|
// `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.
|
// away when the host has no clipboard capability, so spawning is unconditional.
|
||||||
let clipboard_thread = {
|
let clipboard_thread = params
|
||||||
let c = connector.clone();
|
.clipboard
|
||||||
let s = stop.clone();
|
.then(|| {
|
||||||
std::thread::Builder::new()
|
let c = connector.clone();
|
||||||
.name("pf-clipboard".into())
|
let s = stop.clone();
|
||||||
.spawn(move || crate::clipboard::run(c, s))
|
std::thread::Builder::new()
|
||||||
.ok()
|
.name("pf-clipboard".into())
|
||||||
};
|
.spawn(move || crate::clipboard::run(c, s))
|
||||||
|
.ok()
|
||||||
|
})
|
||||||
|
.flatten();
|
||||||
let _mic = params
|
let _mic = params
|
||||||
.mic_enabled
|
.mic_enabled
|
||||||
.then(|| {
|
.then(|| {
|
||||||
|
|||||||
@@ -124,6 +124,12 @@ pub struct KnownHost {
|
|||||||
/// pre-existing stores load; empty until first learned.
|
/// pre-existing stores load; empty until first learned.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub mac: Vec<String>,
|
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)]
|
#[derive(Default, Serialize, Deserialize)]
|
||||||
@@ -201,6 +207,7 @@ pub fn persist_host(name: &str, addr: &str, port: u16, fp_hex: &str, paired: boo
|
|||||||
paired,
|
paired,
|
||||||
last_used: None,
|
last_used: None,
|
||||||
mac: Vec::new(),
|
mac: Vec::new(),
|
||||||
|
clipboard_sync: false,
|
||||||
});
|
});
|
||||||
let _ = known.save();
|
let _ = known.save();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user