feat(client/windows): bind a host to a profile, connect with one, pin one, copy its link
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 2m8s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 3m37s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 1m25s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 3m43s
apple / swift (push) Successful in 5m17s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Canceled after 0s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Canceled after 0s
flatpak / build-publish (push) Canceled after 0s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Canceled after 0s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Canceled after 0s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Canceled after 0s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Canceled after 0s
docker / build-push-arm64cross (push) Canceled after 0s
docker / deploy-docs (push) Canceled after 0s
decky / build-publish (push) Canceled after 0s
deb / build-publish (push) Canceled after 0s
deb / build-publish-host (push) Canceled after 0s
deb / build-publish-client-arm64 (push) Canceled after 0s
apple / screenshots (push) Canceled after 0s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Canceled after 0s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Canceled after 0s
android / android (push) Canceled after 2m34s
ci / rust-arm64 (push) Canceled after 0s
ci / web (push) Canceled after 0s
ci / docs-site (push) Canceled after 0s
ci / bench (push) Canceled after 0s
ci / rust (push) Canceled after 2m30s
arch / build-publish (push) Canceled after 2m39s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 2m8s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 3m37s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 1m25s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 3m43s
apple / swift (push) Successful in 5m17s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Canceled after 0s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Canceled after 0s
flatpak / build-publish (push) Canceled after 0s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Canceled after 0s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Canceled after 0s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Canceled after 0s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Canceled after 0s
docker / build-push-arm64cross (push) Canceled after 0s
docker / deploy-docs (push) Canceled after 0s
decky / build-publish (push) Canceled after 0s
deb / build-publish (push) Canceled after 0s
deb / build-publish-host (push) Canceled after 0s
deb / build-publish-client-arm64 (push) Canceled after 0s
apple / screenshots (push) Canceled after 0s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Canceled after 0s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Canceled after 0s
android / android (push) Canceled after 2m34s
ci / rust-arm64 (push) Canceled after 0s
ci / web (push) Canceled after 0s
ci / docs-site (push) Canceled after 0s
ci / bench (push) Canceled after 0s
ci / rust (push) Canceled after 2m30s
arch / build-publish (push) Canceled after 2m39s
The WinUI half of §5.2: `Target` carries a one-off profile into the session's `--profile`,
the host editor gains the binding picker, and the tile names its bound profile on a chip so
what a plain click will do is visible without opening anything.
Shape notes, both forced by this toolkit rather than chosen:
- The flyout has no submenus, so "Connect with", "Pin as card" and "Unpin card" are flat
items, one per profile, matched by prefix. The prefixes end in ": " so a profile named
"Copy link" can't collide with a fixed entry.
- The binding picker commits on change rather than at Save. The rest of that sheet is text
boxes with draft refs, which need a Save; a ComboBox doesn't, and making it wait would be
the odd one out.
Semantics stay identical to the Linux client, because they are the design's, not the
platform's: "Connect with" never rebinds, "Default settings" is `Some("")` so it really
does override a bound host for one session, and a binding whose profile was deleted reads
as Default settings and resolves to it.
`pf_client_core::clipboard::set_text` is the small addition the crate needed for "Copy
link" — the OS clipboard plumbing was already there, just private to the streaming bridge.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -239,6 +239,7 @@ fn connect_spawn(
|
|||||||
let target = target.clone();
|
let target = target.clone();
|
||||||
// The closure owns `target`/`fp_hex`; the call itself borrows copies.
|
// The closure owns `target`/`fp_hex`; the call itself borrows copies.
|
||||||
let (addr, port, fp_arg) = (target.addr.clone(), target.port, fp_hex.clone());
|
let (addr, port, fp_arg) = (target.addr.clone(), target.port, fp_hex.clone());
|
||||||
|
let profile_arg = target.profile.clone();
|
||||||
let spawned = crate::spawn::spawn_session(
|
let spawned = crate::spawn::spawn_session(
|
||||||
&addr,
|
&addr,
|
||||||
port,
|
port,
|
||||||
@@ -246,6 +247,7 @@ fn connect_spawn(
|
|||||||
opts.connect_timeout.as_secs(),
|
opts.connect_timeout.as_secs(),
|
||||||
fullscreen,
|
fullscreen,
|
||||||
opts.launch.as_deref(),
|
opts.launch.as_deref(),
|
||||||
|
profile_arg.as_deref(),
|
||||||
child,
|
child,
|
||||||
move |event| {
|
move |event| {
|
||||||
use crate::spawn::SpawnEvent;
|
use crate::spawn::SpawnEvent;
|
||||||
|
|||||||
@@ -20,6 +20,13 @@ const MENU_WAKE: &str = "Wake host";
|
|||||||
/// Apple client's add/edit sheet. A menu item per field read as clutter and buried the ones
|
/// Apple client's add/edit sheet. A menu item per field read as clutter and buried the ones
|
||||||
/// that matter.
|
/// that matter.
|
||||||
const MENU_EDIT: &str = "Edit\u{2026}";
|
const MENU_EDIT: &str = "Edit\u{2026}";
|
||||||
|
/// Dynamic menu-item prefixes: this flyout has no submenus, so the profile entries are flat
|
||||||
|
/// items matched by prefix. The trailing space keeps them readable AND keeps a profile named
|
||||||
|
/// e.g. "Copy link" from colliding with a fixed entry.
|
||||||
|
const MENU_WITH: &str = "Connect with: ";
|
||||||
|
const MENU_PIN: &str = "Pin as card: ";
|
||||||
|
const MENU_UNPIN: &str = "Unpin card: ";
|
||||||
|
const MENU_COPY_LINK: &str = "Copy link";
|
||||||
const MENU_FORGET: &str = "Forget\u{2026}";
|
const MENU_FORGET: &str = "Forget\u{2026}";
|
||||||
|
|
||||||
/// Whether the console (gamepad) UI is available in this build: the session binary ships
|
/// Whether the console (gamepad) UI is available in this build: the session binary ships
|
||||||
@@ -163,6 +170,19 @@ pub(crate) struct Hover {
|
|||||||
|
|
||||||
/// The status row at the bottom of a tile: presence dot + Online/Offline, plus the trust chip.
|
/// The status row at the bottom of a tile: presence dot + Online/Offline, plus the trust chip.
|
||||||
fn status_row(online: Option<bool>, badge: &str, kind: Pill) -> Element {
|
fn status_row(online: Option<bool>, badge: &str, kind: Pill) -> Element {
|
||||||
|
status_row_with(online, badge, kind, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`status_row`] plus the profile chip: what a plain click on THIS tile will use — its own
|
||||||
|
/// profile on a pinned tile, the host's binding on the primary one. A binding whose profile
|
||||||
|
/// was deleted shows nothing and resolves as the defaults, which is what will happen on
|
||||||
|
/// connect (design §6).
|
||||||
|
fn status_row_with(
|
||||||
|
online: Option<bool>,
|
||||||
|
badge: &str,
|
||||||
|
kind: Pill,
|
||||||
|
profile: Option<(&str, Pill)>,
|
||||||
|
) -> Element {
|
||||||
let mut items: Vec<Element> = Vec::new();
|
let mut items: Vec<Element> = Vec::new();
|
||||||
if let Some(online) = online {
|
if let Some(online) = online {
|
||||||
items.push(
|
items.push(
|
||||||
@@ -183,6 +203,13 @@ fn status_row(online: Option<bool>, badge: &str, kind: Pill) -> Element {
|
|||||||
.vertical_alignment(VerticalAlignment::Center)
|
.vertical_alignment(VerticalAlignment::Center)
|
||||||
.into(),
|
.into(),
|
||||||
);
|
);
|
||||||
|
if let Some((name, kind)) = profile {
|
||||||
|
items.push(
|
||||||
|
pill(name, kind)
|
||||||
|
.vertical_alignment(VerticalAlignment::Center)
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
hstack(items)
|
hstack(items)
|
||||||
.spacing(6.0)
|
.spacing(6.0)
|
||||||
.margin(edges(0.0, 12.0, 0.0, 0.0))
|
.margin(edges(0.0, 12.0, 0.0, 0.0))
|
||||||
@@ -250,6 +277,43 @@ fn edit_editor(
|
|||||||
se.call(None);
|
se.call(None);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
// The profile binding: what a plain click on this tile will use. It commits on change
|
||||||
|
// rather than at Save — it is a picker with no draft ref, and the rest of the sheet's
|
||||||
|
// fields are text boxes that genuinely need one.
|
||||||
|
let profile_picker = {
|
||||||
|
let catalog = pf_client_core::profiles::ProfilesFile::load();
|
||||||
|
let stored = KnownHosts::load()
|
||||||
|
.hosts
|
||||||
|
.iter()
|
||||||
|
.find(|h| h.fp_hex == fp)
|
||||||
|
.and_then(|h| h.profile_id.clone());
|
||||||
|
let mut names = vec!["Default settings".to_string()];
|
||||||
|
let mut ids: Vec<String> = vec![String::new()];
|
||||||
|
for p in &catalog.profiles {
|
||||||
|
names.push(p.name.clone());
|
||||||
|
ids.push(p.id.clone());
|
||||||
|
}
|
||||||
|
// A binding whose profile is gone reads as Default settings — the same "dangling
|
||||||
|
// resolves as none" rule the connect path follows — and is cleaned up on the next pick.
|
||||||
|
let current = stored
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|id| ids.iter().position(|i| i == id))
|
||||||
|
.unwrap_or(0);
|
||||||
|
let fp = fp.to_string();
|
||||||
|
ComboBox::new(names)
|
||||||
|
.header("Profile")
|
||||||
|
.selected_index(current as i32)
|
||||||
|
.on_selection_changed(move |i: i32| {
|
||||||
|
let Some(id) = ids.get(i.max(0) as usize) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let mut known = KnownHosts::load();
|
||||||
|
if let Some(h) = known.hosts.iter_mut().find(|h| h.fp_hex == fp) {
|
||||||
|
h.profile_id = (!id.is_empty()).then(|| id.clone());
|
||||||
|
let _ = known.save();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
let field = |label: &str, value: String, placeholder: &str, draft: HookRef<String>| {
|
let field = |label: &str, value: String, placeholder: &str, draft: HookRef<String>| {
|
||||||
vstack((
|
vstack((
|
||||||
text_block(label)
|
text_block(label)
|
||||||
@@ -281,6 +345,18 @@ fn edit_editor(
|
|||||||
"auto-filled when known",
|
"auto-filled when known",
|
||||||
mac_draft,
|
mac_draft,
|
||||||
),
|
),
|
||||||
|
vstack((
|
||||||
|
profile_picker,
|
||||||
|
text_block(
|
||||||
|
"The settings a plain click on this host uses. \u{201c}Connect with\u{201d} \
|
||||||
|
in the tile\u{2019}s menu overrides it for one session without changing it.",
|
||||||
|
)
|
||||||
|
.font_size(12.0)
|
||||||
|
.foreground(ThemeRef::SecondaryText)
|
||||||
|
.wrap()
|
||||||
|
.horizontal_alignment(HorizontalAlignment::Left),
|
||||||
|
))
|
||||||
|
.spacing(4.0),
|
||||||
vstack((
|
vstack((
|
||||||
ToggleSwitch::new(clip0)
|
ToggleSwitch::new(clip0)
|
||||||
.header("Share clipboard with this host")
|
.header("Share clipboard with this host")
|
||||||
@@ -481,6 +557,12 @@ pub(crate) fn hosts_page(props: &HostsProps, cx: &mut RenderCx) -> Element {
|
|||||||
if !known.hosts.is_empty() {
|
if !known.hosts.is_empty() {
|
||||||
body.push(section("SAVED HOSTS"));
|
body.push(section("SAVED HOSTS"));
|
||||||
let mut tiles: Vec<Element> = Vec::new();
|
let mut tiles: Vec<Element> = Vec::new();
|
||||||
|
// One catalog read per render, shared by every tile's menu and chip.
|
||||||
|
let profiles: Vec<(String, String)> = pf_client_core::profiles::ProfilesFile::load()
|
||||||
|
.profiles
|
||||||
|
.into_iter()
|
||||||
|
.map(|p| (p.id, p.name))
|
||||||
|
.collect();
|
||||||
for k in &known.hosts {
|
for k in &known.hosts {
|
||||||
// Rust 2021 (no let-chains): match the "this tile is being edited" case explicitly.
|
// Rust 2021 (no let-chains): match the "this tile is being edited" case explicitly.
|
||||||
if matches!(&rename, Some((fp, _)) if fp == &k.fp_hex) {
|
if matches!(&rename, Some((fp, _)) if fp == &k.fp_hex) {
|
||||||
@@ -504,6 +586,7 @@ pub(crate) fn hosts_page(props: &HostsProps, cx: &mut RenderCx) -> Element {
|
|||||||
fp_hex: Some(k.fp_hex.clone()),
|
fp_hex: Some(k.fp_hex.clone()),
|
||||||
pair_optional: false,
|
pair_optional: false,
|
||||||
mac: k.mac.clone(),
|
mac: k.mac.clone(),
|
||||||
|
profile: None,
|
||||||
};
|
};
|
||||||
// Online = advertising on mDNS OR proven reachable by the last probe sweep (the latter
|
// Online = advertising on mDNS OR proven reachable by the last probe sweep (the latter
|
||||||
// covers a routed/Tailscale host that never advertises — the display companion to
|
// covers a routed/Tailscale host that never advertises — the display companion to
|
||||||
@@ -525,6 +608,8 @@ pub(crate) fn hosts_page(props: &HostsProps, cx: &mut RenderCx) -> Element {
|
|||||||
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());
|
||||||
let (fp, name) = (k.fp_hex.clone(), k.name.clone());
|
let (fp, name) = (k.fp_hex.clone(), k.name.clone());
|
||||||
|
let menu_profiles = profiles.clone();
|
||||||
|
let (link_host, link_profile) = (k.clone(), None::<String>);
|
||||||
button("")
|
button("")
|
||||||
.icon(Symbol::More)
|
.icon(Symbol::More)
|
||||||
.subtle()
|
.subtle()
|
||||||
@@ -532,6 +617,24 @@ pub(crate) fn hosts_page(props: &HostsProps, cx: &mut RenderCx) -> Element {
|
|||||||
.automation_name("More options")
|
.automation_name("More options")
|
||||||
.menu_flyout({
|
.menu_flyout({
|
||||||
let mut items = vec![menu_item(MENU_CONNECT)];
|
let mut items = vec![menu_item(MENU_CONNECT)];
|
||||||
|
// One-off connects, flat: this flyout has no submenus, so each
|
||||||
|
// profile is its own item. "Connect with" NEVER rebinds — the default
|
||||||
|
// is changed in the host editor (design §5.2).
|
||||||
|
for (_, name) in profiles.iter() {
|
||||||
|
items.push(menu_item(format!("{MENU_WITH}{name}")));
|
||||||
|
}
|
||||||
|
if !profiles.is_empty() {
|
||||||
|
items.push(menu_item(format!("{MENU_WITH}Default settings")));
|
||||||
|
for (id, name) in profiles.iter() {
|
||||||
|
let label = if k.pinned_profiles.iter().any(|p| p == id) {
|
||||||
|
format!("{MENU_UNPIN}{name}")
|
||||||
|
} else {
|
||||||
|
format!("{MENU_PIN}{name}")
|
||||||
|
};
|
||||||
|
items.push(menu_item(label));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
items.push(menu_item(MENU_COPY_LINK));
|
||||||
// The library surfaces — mouse/KB page and the gamepad console UI —
|
// The library surfaces — mouse/KB page and the gamepad console UI —
|
||||||
// for paired hosts only (the mgmt API needs the paired identity);
|
// for paired hosts only (the mgmt API needs the paired identity);
|
||||||
// the page additionally sits behind the experimental toggle, the
|
// the page additionally sits behind the experimental toggle, the
|
||||||
@@ -550,6 +653,52 @@ pub(crate) fn hosts_page(props: &HostsProps, cx: &mut RenderCx) -> Element {
|
|||||||
items
|
items
|
||||||
})
|
})
|
||||||
.on_item_clicked(move |item: String| match item.as_str() {
|
.on_item_clicked(move |item: String| match item.as_str() {
|
||||||
|
// The profile items are dynamic, so they are matched by prefix before
|
||||||
|
// the fixed ones.
|
||||||
|
_ if item.starts_with(MENU_WITH) => {
|
||||||
|
let name = item.trim_start_matches(MENU_WITH);
|
||||||
|
let mut target = target.clone();
|
||||||
|
// `Some("")` — not `None` — so "Default settings" really does
|
||||||
|
// override a bound host for this one connect.
|
||||||
|
target.profile = Some(
|
||||||
|
menu_profiles
|
||||||
|
.iter()
|
||||||
|
.find(|(_, n)| n == name)
|
||||||
|
.map(|(id, _)| id.clone())
|
||||||
|
.unwrap_or_default(),
|
||||||
|
);
|
||||||
|
initiate(&svc.ctx, target, &svc.set_screen, &svc.set_status)
|
||||||
|
}
|
||||||
|
_ if item.starts_with(MENU_PIN) || item.starts_with(MENU_UNPIN) => {
|
||||||
|
let pin = item.starts_with(MENU_PIN);
|
||||||
|
let name = item
|
||||||
|
.trim_start_matches(MENU_PIN)
|
||||||
|
.trim_start_matches(MENU_UNPIN);
|
||||||
|
let Some((id, _)) =
|
||||||
|
menu_profiles.iter().find(|(_, n)| n == name).cloned()
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let mut known = KnownHosts::load();
|
||||||
|
if let Some(h) = known.hosts.iter_mut().find(|h| h.fp_hex == fp) {
|
||||||
|
h.pinned_profiles.retain(|p| p != &id);
|
||||||
|
if pin {
|
||||||
|
h.pinned_profiles.push(id);
|
||||||
|
}
|
||||||
|
let _ = known.save();
|
||||||
|
}
|
||||||
|
// The tile list re-reads the store on the next render; nudge it.
|
||||||
|
sr.call(None);
|
||||||
|
}
|
||||||
|
MENU_COPY_LINK => {
|
||||||
|
let url = pf_client_core::deeplink::DeepLink::for_host(
|
||||||
|
&link_host,
|
||||||
|
None,
|
||||||
|
link_profile.as_deref(),
|
||||||
|
)
|
||||||
|
.to_url();
|
||||||
|
pf_client_core::clipboard::set_text(&url);
|
||||||
|
}
|
||||||
MENU_CONNECT => {
|
MENU_CONNECT => {
|
||||||
initiate(&svc.ctx, target.clone(), &svc.set_screen, &svc.set_status)
|
initiate(&svc.ctx, target.clone(), &svc.set_screen, &svc.set_status)
|
||||||
}
|
}
|
||||||
@@ -580,10 +729,14 @@ pub(crate) fn hosts_page(props: &HostsProps, cx: &mut RenderCx) -> Element {
|
|||||||
&hover,
|
&hover,
|
||||||
&k.name,
|
&k.name,
|
||||||
&format!("{}:{}", k.addr, k.port),
|
&format!("{}:{}", k.addr, k.port),
|
||||||
status_row(
|
status_row_with(
|
||||||
Some(online),
|
Some(online),
|
||||||
if k.paired { "Paired" } else { "Trusted" },
|
if k.paired { "Paired" } else { "Trusted" },
|
||||||
if k.paired { Pill::Good } else { Pill::Info },
|
if k.paired { Pill::Good } else { Pill::Info },
|
||||||
|
k.profile_id
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|id| profiles.iter().find(|(pid, _)| pid == id))
|
||||||
|
.map(|(_, name)| (name.as_str(), Pill::Neutral)),
|
||||||
),
|
),
|
||||||
Some(menu),
|
Some(menu),
|
||||||
Some(Box::new(move || {
|
Some(Box::new(move || {
|
||||||
@@ -634,6 +787,7 @@ pub(crate) fn hosts_page(props: &HostsProps, cx: &mut RenderCx) -> Element {
|
|||||||
fp_hex: (!h.fp_hex.is_empty()).then(|| h.fp_hex.clone()),
|
fp_hex: (!h.fp_hex.is_empty()).then(|| h.fp_hex.clone()),
|
||||||
pair_optional: h.pair == "optional",
|
pair_optional: h.pair == "optional",
|
||||||
mac: h.mac.clone(),
|
mac: h.mac.clone(),
|
||||||
|
profile: None,
|
||||||
};
|
};
|
||||||
let (ctx2, ss, st) = (ctx.clone(), set_screen.clone(), set_status.clone());
|
let (ctx2, ss, st) = (ctx.clone(), set_screen.clone(), set_status.clone());
|
||||||
let (badge, kind) = if h.pair == "required" {
|
let (badge, kind) = if h.pair == "required" {
|
||||||
@@ -716,6 +870,7 @@ pub(crate) fn hosts_page(props: &HostsProps, cx: &mut RenderCx) -> Element {
|
|||||||
fp_hex: None,
|
fp_hex: None,
|
||||||
pair_optional: false,
|
pair_optional: false,
|
||||||
mac: Vec::new(),
|
mac: Vec::new(),
|
||||||
|
profile: None,
|
||||||
},
|
},
|
||||||
&ss,
|
&ss,
|
||||||
&st,
|
&st,
|
||||||
|
|||||||
@@ -80,6 +80,11 @@ pub(crate) struct Target {
|
|||||||
/// Wake-on-LAN MAC(s) for this host (from the saved store or the live advert) — used to send a
|
/// Wake-on-LAN MAC(s) for this host (from the saved store or the live advert) — used to send a
|
||||||
/// magic packet before connecting to an offline host. Empty when none is known.
|
/// magic packet before connecting to an offline host. Empty when none is known.
|
||||||
pub(crate) mac: Vec<String>,
|
pub(crate) mac: Vec<String>,
|
||||||
|
/// A ONE-OFF settings profile for this connect ("Connect with"): `Some(id)` overrides the
|
||||||
|
/// host's binding for this launch, `Some("")` forces the global defaults on a bound host,
|
||||||
|
/// `None` honors the binding. It never rebinds anything — the default changes only through
|
||||||
|
/// the picker in the host editor (design/client-settings-profiles.md §5.2).
|
||||||
|
pub(crate) profile: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stable app services handed to the page components as props. Each routed screen that uses
|
/// Stable app services handed to the page components as props. Each routed screen that uses
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ pub(crate) fn spawn_session(
|
|||||||
connect_timeout_secs: u64,
|
connect_timeout_secs: u64,
|
||||||
fullscreen: bool,
|
fullscreen: bool,
|
||||||
launch: Option<&str>,
|
launch: Option<&str>,
|
||||||
|
profile: Option<&str>,
|
||||||
slot: SessionChild,
|
slot: SessionChild,
|
||||||
on_event: impl FnMut(SpawnEvent) + Send + 'static,
|
on_event: impl FnMut(SpawnEvent) + Send + 'static,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
@@ -117,6 +118,11 @@ pub(crate) fn spawn_session(
|
|||||||
if let Some(id) = launch {
|
if let Some(id) = launch {
|
||||||
cmd.arg("--launch").arg(id);
|
cmd.arg("--launch").arg(id);
|
||||||
}
|
}
|
||||||
|
// Only a ONE-OFF pick rides the flag: without it the session resolves the host's own
|
||||||
|
// binding through the same helper this shell would have used, so the two can't disagree.
|
||||||
|
if let Some(reference) = profile {
|
||||||
|
cmd.arg("--profile").arg(reference);
|
||||||
|
}
|
||||||
add_window_pos(&mut cmd);
|
add_window_pos(&mut cmd);
|
||||||
spawn_with(cmd, &format!("{addr}:{port}"), slot, on_event)
|
spawn_with(cmd, &format!("{addr}:{port}"), slot, on_event)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -260,6 +260,16 @@ fn handle_event(client: &NativeClient, state: &mut State, ev: ClipEventCore) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Put plain text on this machine's clipboard — the "Copy link" affordance, which has nothing
|
||||||
|
/// to do with the streaming bridge above but wants the same OS plumbing. Best-effort: a
|
||||||
|
/// clipboard another process is holding open is a transient nuisance, never worth failing a
|
||||||
|
/// user action over. A no-op where this build has no OS clipboard.
|
||||||
|
pub fn set_text(text: &str) {
|
||||||
|
if let Err(e) = os::set(MIME_TEXT, text.as_bytes()) {
|
||||||
|
tracing::warn!(error = %format!("{e:#}"), "copying to the clipboard");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
mod os {
|
mod os {
|
||||||
//! The Win32 clipboard seam. Every entry point opens the clipboard, does one thing and
|
//! The Win32 clipboard seam. Every entry point opens the clipboard, does one thing and
|
||||||
|
|||||||
Reference in New Issue
Block a user