feat(console): touch-mode setting + request-access pairing + polish
Extend the gamepad/console shell (pf-console-ui) to parity with the other clients: - Settings gain a Touchscreen → Touch mode row (Trackpad / Direct pointer / Touch passthrough), the one couch-relevant Settings field the screen lacked. - The pair screen adds the no-PIN delegated-approval path: a "Request access" action (only when the host advertises a fingerprint to pin) opens a connect the host PARKS until the operator approves this device, then persists it as paired. A role-based row model keeps the cursor off stale indices; manual hosts stay PIN-only, matching the desktop shells. - Threads request_access through OverlayAction::Launch and ConnectIntent; the shell shows a "Waiting for approval…" takeover, and the session binary parks on a 185 s budget (PendingApproval → persist-as-paired via on_connected). Auto-wake (WoL) was already implemented end-to-end and is left as-is. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -29,6 +29,15 @@ use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
/// A request-access connect awaiting the operator's approval on the host: stamped by the
|
||||
/// launch handler and consumed by `on_connected`, which persists the host as paired.
|
||||
struct PendingApproval {
|
||||
name: String,
|
||||
addr: String,
|
||||
port: u16,
|
||||
fp_hex: String,
|
||||
}
|
||||
|
||||
pub fn run(target: Option<&str>) -> u8 {
|
||||
let identity = match trust::load_or_create_identity() {
|
||||
Ok(i) => i,
|
||||
@@ -128,6 +137,14 @@ pub fn run(target: Option<&str>) -> u8 {
|
||||
// `{"ready":true}` and restores on exit) — plain CLI/gamescope runs stay silent.
|
||||
let json_status = arg_flag("--json-status");
|
||||
let settings_at_start = trust::Settings::load();
|
||||
|
||||
// Request-access hand-off: the launch handler stamps this when it starts a delegated-approval
|
||||
// connect; `on_connected` reads it once the host lets us in and persists the host as PAIRED,
|
||||
// so the next connect is an ordinary one. `None` for every normal launch, so `on_connected`
|
||||
// then only touches last-used.
|
||||
let pending_approval: Arc<Mutex<Option<PendingApproval>>> = Arc::new(Mutex::new(None));
|
||||
let pending_cb = pending_approval.clone();
|
||||
|
||||
let opts = pf_presenter::SessionOpts {
|
||||
window_title: window_label.map_or_else(
|
||||
|| "Punktfunk".to_string(),
|
||||
@@ -142,8 +159,16 @@ pub fn run(target: Option<&str>) -> u8 {
|
||||
},
|
||||
touch_mode: settings_at_start.touch_mode(),
|
||||
json_status,
|
||||
on_connected: Some(Box::new(|fingerprint: [u8; 32]| {
|
||||
trust::touch_last_used(&trust::hex(&fingerprint));
|
||||
on_connected: Some(Box::new(move |fingerprint: [u8; 32]| {
|
||||
let fp_hex = trust::hex(&fingerprint);
|
||||
trust::touch_last_used(&fp_hex);
|
||||
// A request-access connect just succeeded → the operator approved us. Save the
|
||||
// host as paired (it was unsaved/discovered), keyed to the fingerprint we pinned.
|
||||
if let Some(p) = pending_cb.lock().unwrap().take() {
|
||||
if p.fp_hex == fp_hex {
|
||||
trust::persist_host(&p.name, &p.addr, p.port, &fp_hex, true);
|
||||
}
|
||||
}
|
||||
})),
|
||||
overlay: Some(Box::new(overlay)),
|
||||
window_size: crate::session_main::window_size(&settings_at_start),
|
||||
@@ -161,21 +186,23 @@ pub fn run(target: Option<&str>) -> u8 {
|
||||
fp_hex,
|
||||
launch,
|
||||
title,
|
||||
request_access,
|
||||
} => {
|
||||
let Some(pin) = trust::parse_hex32(&fp_hex) else {
|
||||
// The console only offers Connect on paired rows; a pinless
|
||||
// launch is a logic slip, never a silent TOFU.
|
||||
// Connect (and request-access) pin the host's advertised fingerprint;
|
||||
// a pinless launch is a logic slip, never a silent TOFU.
|
||||
tracing::warn!(%addr, "launch without a stored pin — refusing");
|
||||
return ActionOutcome::Handled;
|
||||
};
|
||||
tracing::info!(%addr, %title, launch = launch.as_deref().unwrap_or("desktop"),
|
||||
tracing::info!(%addr, %title, request_access,
|
||||
launch = launch.as_deref().unwrap_or("desktop"),
|
||||
"launching from the console");
|
||||
// Settings re-load per launch: the console's own settings screen
|
||||
// may have changed them since the last stream.
|
||||
let settings = trust::Settings::load();
|
||||
ActionOutcome::Start(Box::new(session_params(
|
||||
let mut params = session_params(
|
||||
&settings,
|
||||
addr,
|
||||
addr.clone(),
|
||||
port,
|
||||
pin,
|
||||
identity.clone(),
|
||||
@@ -184,7 +211,20 @@ pub fn run(target: Option<&str>) -> u8 {
|
||||
native,
|
||||
force_software,
|
||||
vulkan,
|
||||
)))
|
||||
);
|
||||
if request_access {
|
||||
// The host PARKS the connect until the operator approves — outlast its
|
||||
// approval window (host `PENDING_APPROVAL_WAIT`), matching the desktop
|
||||
// shells' 185 s. On success `on_connected` persists the host as paired.
|
||||
params.connect_timeout = Duration::from_secs(185);
|
||||
*pending_approval.lock().unwrap() = Some(PendingApproval {
|
||||
name: title.clone(),
|
||||
addr,
|
||||
port,
|
||||
fp_hex: fp_hex.clone(),
|
||||
});
|
||||
}
|
||||
ActionOutcome::Start(Box::new(params))
|
||||
}
|
||||
OverlayAction::CancelConnect => ActionOutcome::Handled, // run-loop-side
|
||||
OverlayAction::Quit => ActionOutcome::Quit,
|
||||
|
||||
@@ -53,6 +53,10 @@ pub(crate) struct ConnectIntent {
|
||||
pub launch: Option<String>,
|
||||
/// What the connecting card says (host or game title).
|
||||
pub title: String,
|
||||
/// The no-PIN delegated-approval connect (the pair screen's "Request access"): the
|
||||
/// shell shows a "waiting for approval" takeover instead of "connecting", and the
|
||||
/// binary parks on a long budget and persists the host as paired once let in.
|
||||
pub request_access: bool,
|
||||
}
|
||||
|
||||
pub(crate) enum Nav {
|
||||
|
||||
@@ -100,6 +100,7 @@ impl HomeScreen {
|
||||
fp_hex: h.fp_hex.clone(),
|
||||
launch: None,
|
||||
title: h.name.clone(),
|
||||
request_access: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,6 +119,7 @@ impl LibraryScreen {
|
||||
fp_hex: self.fp_hex.clone(),
|
||||
launch: Some(g.id.clone()),
|
||||
title: g.title.clone(),
|
||||
request_access: false,
|
||||
});
|
||||
Some(MenuPulse::Confirm)
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
use crate::glyphs::{Hint, HintKey};
|
||||
use crate::model::{ConsoleCmd, HostRow, PairPhase};
|
||||
use crate::screens::{Ctx, Outbox};
|
||||
use crate::screens::{ConnectIntent, Ctx, Outbox};
|
||||
use crate::theme::{Fonts, DIM, ERROR, W};
|
||||
use crate::widgets::{permits, Charset, KeyMsg, Keyboard, ListMsg, MenuList, RowSpec};
|
||||
use pf_client_core::gamepad::{MenuEvent, MenuPulse};
|
||||
@@ -18,10 +18,24 @@ enum Field {
|
||||
Device,
|
||||
}
|
||||
|
||||
/// The ordered actions a pair screen presents. `RequestAccess` leads only when the host
|
||||
/// has an advertised fingerprint to pin (a discovered host); a manually-typed host with
|
||||
/// no advert is PIN-only, exactly like the desktop shells.
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
enum Role {
|
||||
RequestAccess,
|
||||
Pin,
|
||||
Device,
|
||||
Pair,
|
||||
}
|
||||
|
||||
pub(crate) struct PairScreen {
|
||||
host_name: String,
|
||||
addr: String,
|
||||
port: u16,
|
||||
/// The host's advertised certificate fingerprint (lowercase hex); empty = a manual
|
||||
/// entry with no advert → no request-access path.
|
||||
fp_hex: String,
|
||||
list: MenuList,
|
||||
keyboard: Keyboard,
|
||||
pin: String,
|
||||
@@ -39,6 +53,7 @@ impl PairScreen {
|
||||
host_name: host.name.clone(),
|
||||
addr: host.addr.clone(),
|
||||
port: host.port,
|
||||
fp_hex: host.fp_hex.clone(),
|
||||
list: MenuList::new(),
|
||||
keyboard: Keyboard::new(),
|
||||
pin: String::new(),
|
||||
@@ -49,6 +64,25 @@ impl PairScreen {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the no-PIN "request access" action is offered (host advertises an identity
|
||||
/// to pin). Stable across `busy` so the row list never reshuffles mid-ceremony.
|
||||
fn can_request(&self) -> bool {
|
||||
!self.fp_hex.is_empty()
|
||||
}
|
||||
|
||||
/// The ordered roles for the current host — the single source both `rows` (render) and
|
||||
/// `menu` (activate dispatch) index, so a cursor never acts on a stale row.
|
||||
fn roles(&self) -> Vec<Role> {
|
||||
let mut roles = Vec::with_capacity(4);
|
||||
if self.can_request() {
|
||||
roles.push(Role::RequestAccess);
|
||||
}
|
||||
roles.push(Role::Pin);
|
||||
roles.push(Role::Device);
|
||||
roles.push(Role::Pair);
|
||||
roles
|
||||
}
|
||||
|
||||
pub(crate) fn host_name(&self) -> &str {
|
||||
&self.host_name
|
||||
}
|
||||
@@ -170,13 +204,29 @@ impl PairScreen {
|
||||
fx.pop();
|
||||
return None;
|
||||
}
|
||||
let (msg, pulse) = self.list.menu(ev, 3);
|
||||
let roles = self.roles();
|
||||
let (msg, pulse) = self.list.menu(ev, roles.len());
|
||||
match msg {
|
||||
ListMsg::Activate => {
|
||||
match self.list.cursor {
|
||||
0 => self.editing = Some(Field::Pin),
|
||||
1 => self.editing = Some(Field::Device),
|
||||
_ if self.can_pair() => {
|
||||
match roles.get(self.list.cursor) {
|
||||
Some(Role::RequestAccess) if !self.busy => {
|
||||
// The no-PIN path: connect and park until the operator approves this
|
||||
// device on the host. The shell shows the approval takeover; on
|
||||
// success the binary persists the host as paired. Leave the pair
|
||||
// screen so a canceled or finished session returns to Home.
|
||||
fx.connect = Some(ConnectIntent {
|
||||
addr: self.addr.clone(),
|
||||
port: self.port,
|
||||
fp_hex: self.fp_hex.clone(),
|
||||
launch: None,
|
||||
title: self.host_name.clone(),
|
||||
request_access: true,
|
||||
});
|
||||
fx.pop();
|
||||
}
|
||||
Some(Role::Pin) => self.editing = Some(Field::Pin),
|
||||
Some(Role::Device) => self.editing = Some(Field::Device),
|
||||
Some(Role::Pair) if self.can_pair() => {
|
||||
self.busy = true;
|
||||
self.error = None;
|
||||
fx.cmds.push(ConsoleCmd::Pair {
|
||||
@@ -191,8 +241,11 @@ impl PairScreen {
|
||||
});
|
||||
}
|
||||
_ => {
|
||||
// No PIN yet — jump into the PIN field instead of a dead press.
|
||||
self.list.cursor = 0;
|
||||
// Pair with no PIN yet (or a request while busy) — jump into the
|
||||
// PIN field instead of a dead press.
|
||||
if let Some(i) = roles.iter().position(|r| *r == Role::Pin) {
|
||||
self.list.cursor = i;
|
||||
}
|
||||
self.editing = Some(Field::Pin);
|
||||
}
|
||||
}
|
||||
@@ -233,9 +286,14 @@ impl PairScreen {
|
||||
ctx: &mut Ctx,
|
||||
) {
|
||||
let cx = f64::from(rect.left) + f64::from(rect.width()) / 2.0;
|
||||
let intro = if self.can_request() {
|
||||
"Request access and approve this device on the host, or enter the PIN it shows."
|
||||
} else {
|
||||
"Enter the PIN from the host's web console (Pairing page) or its log."
|
||||
};
|
||||
fonts.centered(
|
||||
canvas,
|
||||
"Enter the PIN from the host's web console (Pairing page) or its log.",
|
||||
intro,
|
||||
W::Regular,
|
||||
13.0 * k,
|
||||
DIM,
|
||||
@@ -317,19 +375,39 @@ impl PairScreen {
|
||||
.collect::<String>()
|
||||
.trim_end()
|
||||
.to_string();
|
||||
let mut pin = RowSpec::field("PIN", pin_display, "From the host");
|
||||
pin.caret = self.editing == Some(Field::Pin);
|
||||
let mut device = RowSpec::field(
|
||||
"Device name",
|
||||
self.device.clone(),
|
||||
"How the host lists this device",
|
||||
);
|
||||
device.caret = self.editing == Some(Field::Device);
|
||||
vec![
|
||||
pin,
|
||||
device,
|
||||
RowSpec::action(if self.busy { "Pairing…" } else { "Pair" }, self.can_pair()),
|
||||
]
|
||||
let has_request = self.can_request();
|
||||
self.roles()
|
||||
.into_iter()
|
||||
.map(|role| match role {
|
||||
Role::RequestAccess => {
|
||||
let mut r = RowSpec::action("Request access — approve on the host", !self.busy);
|
||||
r.header = Some("No PIN needed");
|
||||
r
|
||||
}
|
||||
Role::Pin => {
|
||||
let mut pin = RowSpec::field("PIN", pin_display.clone(), "From the host");
|
||||
pin.caret = self.editing == Some(Field::Pin);
|
||||
// When a request-access path is offered above, head the PIN group so
|
||||
// the two ways to pair read as alternatives.
|
||||
if has_request {
|
||||
pin.header = Some("Or pair with a PIN");
|
||||
}
|
||||
pin
|
||||
}
|
||||
Role::Device => {
|
||||
let mut device = RowSpec::field(
|
||||
"Device name",
|
||||
self.device.clone(),
|
||||
"How the host lists this device",
|
||||
);
|
||||
device.caret = self.editing == Some(Field::Device);
|
||||
device
|
||||
}
|
||||
Role::Pair => {
|
||||
RowSpec::action(if self.busy { "Pairing…" } else { "Pair" }, self.can_pair())
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -388,6 +466,43 @@ mod tests {
|
||||
assert!(fx.cmds.is_empty());
|
||||
}
|
||||
|
||||
/// A host with an advertised fingerprint offers Request Access as the first row; A on
|
||||
/// it raises a request-access connect intent (pinning the advert) and leaves the screen.
|
||||
#[test]
|
||||
fn request_access_connects_and_leaves() {
|
||||
let mut host = host();
|
||||
host.fp_hex = "abcd".into();
|
||||
let mut settings = Settings::default();
|
||||
let pads = Vec::new();
|
||||
let library = crate::library::LibraryShared::default();
|
||||
let mut ctx = Ctx {
|
||||
hosts: &[],
|
||||
library: &library,
|
||||
settings: &mut settings,
|
||||
pads: &pads,
|
||||
deck: false,
|
||||
device_name: "deck",
|
||||
t: 0.0,
|
||||
};
|
||||
let mut s = PairScreen::new(&host, "deck");
|
||||
assert_eq!(s.roles().len(), 4, "Request Access + PIN + Device + Pair");
|
||||
s.list.cursor = 0; // the Request Access row leads
|
||||
let mut fx = Outbox::default();
|
||||
s.menu(MenuEvent::Confirm, &mut ctx, &mut fx);
|
||||
let intent = fx.connect.expect("request-access raises a connect intent");
|
||||
assert!(intent.request_access);
|
||||
assert_eq!(intent.fp_hex, "abcd");
|
||||
assert!(matches!(fx.nav, Some(crate::screens::Nav::Pop)));
|
||||
}
|
||||
|
||||
/// A manual host (no advert) is PIN-only — the Request Access row never appears.
|
||||
#[test]
|
||||
fn no_request_access_without_an_advert() {
|
||||
let s = PairScreen::new(&host(), "deck"); // host() has an empty fp_hex
|
||||
assert!(!s.can_request());
|
||||
assert_eq!(s.roles().len(), 3, "PIN + Device + Pair only");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pin_is_digits_only() {
|
||||
let mut s = PairScreen::new(&host(), "d");
|
||||
|
||||
@@ -10,7 +10,7 @@ use crate::screens::{Ctx, Outbox};
|
||||
use crate::theme::{Fonts, DIM, W};
|
||||
use crate::widgets::{ListMsg, MenuList, RowSpec};
|
||||
use pf_client_core::gamepad::{MenuEvent, MenuPulse};
|
||||
use pf_client_core::trust::StatsVerbosity;
|
||||
use pf_client_core::trust::{StatsVerbosity, TouchMode};
|
||||
use skia_safe::{Canvas, Rect};
|
||||
|
||||
/// Stable row identity — adjust/activate dispatch by id so nothing acts on a stale
|
||||
@@ -28,10 +28,11 @@ enum RowId {
|
||||
Mic,
|
||||
Pad,
|
||||
PadType,
|
||||
Touch,
|
||||
Stats,
|
||||
}
|
||||
|
||||
const ROWS: [RowId; 12] = [
|
||||
const ROWS: [RowId; 13] = [
|
||||
RowId::Resolution,
|
||||
RowId::Refresh,
|
||||
RowId::Bitrate,
|
||||
@@ -43,6 +44,7 @@ const ROWS: [RowId; 12] = [
|
||||
RowId::Mic,
|
||||
RowId::Pad,
|
||||
RowId::PadType,
|
||||
RowId::Touch,
|
||||
RowId::Stats,
|
||||
];
|
||||
|
||||
@@ -241,6 +243,11 @@ fn row_spec(id: RowId, ctx: &Ctx) -> RowSpec {
|
||||
"Controller type",
|
||||
label_for(&PAD_TYPES, &s.gamepad).into(),
|
||||
),
|
||||
RowId::Touch => (
|
||||
Some("Touchscreen"),
|
||||
"Touch mode",
|
||||
s.touch_mode().label().into(),
|
||||
),
|
||||
RowId::Stats => (
|
||||
Some("Interface"),
|
||||
"Statistics overlay",
|
||||
@@ -278,6 +285,10 @@ fn detail(id: RowId) -> &'static str {
|
||||
RowId::Mic => "Send this device's microphone to the host's virtual mic.",
|
||||
RowId::Pad => "Which pad is forwarded to the host, as player 1.",
|
||||
RowId::PadType => "The virtual pad the host creates — Automatic matches this controller.",
|
||||
RowId::Touch => {
|
||||
"How the touchscreen drives the host: Trackpad (relative cursor), \
|
||||
Direct pointer (cursor jumps to your finger), or Touch passthrough (raw contacts)."
|
||||
}
|
||||
RowId::Stats => {
|
||||
"How much the overlay shows: Compact (one line) → Normal → Detailed. \
|
||||
Ctrl+Alt+Shift+S cycles it live while streaming."
|
||||
@@ -348,6 +359,11 @@ fn adjust(id: RowId, delta: i32, wrap: bool, ctx: &mut Ctx) -> bool {
|
||||
step_option(cur, keys.len(), delta, wrap).map(|i| s.forward_pad = keys[i].clone())
|
||||
}
|
||||
RowId::PadType => step_str(&PAD_TYPES, &mut s.gamepad, delta, wrap),
|
||||
RowId::Touch => {
|
||||
let cur = TouchMode::ALL.iter().position(|m| *m == s.touch_mode());
|
||||
step_option(cur, TouchMode::ALL.len(), delta, wrap)
|
||||
.map(|i| s.touch_mode = TouchMode::ALL[i].as_name().to_string())
|
||||
}
|
||||
RowId::Stats => {
|
||||
let cur = StatsVerbosity::ALL
|
||||
.iter()
|
||||
@@ -462,6 +478,35 @@ mod tests {
|
||||
assert!(!ctx.settings.mic_enabled);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn touch_mode_steps_and_wraps() {
|
||||
let (mut settings, pads) = ctx_parts();
|
||||
assert_eq!(settings.touch_mode, "trackpad");
|
||||
let library = crate::library::LibraryShared::default();
|
||||
let mut ctx = Ctx {
|
||||
hosts: &[],
|
||||
library: &library,
|
||||
settings: &mut settings,
|
||||
pads: &pads,
|
||||
deck: false,
|
||||
device_name: "t",
|
||||
t: 0.0,
|
||||
};
|
||||
// Trackpad → Pointer → Touch, then a step past the end is a boundary.
|
||||
assert!(
|
||||
!adjust(RowId::Touch, -1, false, &mut ctx),
|
||||
"already first = thud"
|
||||
);
|
||||
assert!(adjust(RowId::Touch, 1, false, &mut ctx));
|
||||
assert_eq!(ctx.settings.touch_mode, "pointer");
|
||||
assert!(adjust(RowId::Touch, 1, false, &mut ctx));
|
||||
assert_eq!(ctx.settings.touch_mode, "touch");
|
||||
assert!(!adjust(RowId::Touch, 1, false, &mut ctx), "last = thud");
|
||||
// A wraps back to the first.
|
||||
assert!(adjust(RowId::Touch, 1, true, &mut ctx));
|
||||
assert_eq!(ctx.settings.touch_mode, "trackpad");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_value_snaps_to_first() {
|
||||
let (mut settings, pads) = ctx_parts();
|
||||
|
||||
@@ -45,6 +45,9 @@ struct Connecting {
|
||||
title: String,
|
||||
canceling: bool,
|
||||
appear: f64,
|
||||
/// A request-access wait (parked on the host until the operator approves) — the
|
||||
/// takeover reads "Waiting for approval" rather than "Connecting".
|
||||
request_access: bool,
|
||||
}
|
||||
|
||||
/// What the session binary hands the shell at construction.
|
||||
@@ -152,6 +155,7 @@ impl Shell {
|
||||
title,
|
||||
canceling: false,
|
||||
appear: 0.0,
|
||||
request_access: false,
|
||||
})
|
||||
}
|
||||
None => self.connecting = None,
|
||||
@@ -236,6 +240,7 @@ impl Shell {
|
||||
fp_hex: h.fp_hex.clone(),
|
||||
launch: None,
|
||||
title: h.name.clone(),
|
||||
request_access: false,
|
||||
})
|
||||
});
|
||||
self.bus.send(ConsoleCmd::CancelWake);
|
||||
@@ -254,12 +259,16 @@ impl Shell {
|
||||
|
||||
fn start_connect(&mut self, intent: ConnectIntent) {
|
||||
self.set_connecting(Some(intent.title.clone()));
|
||||
if let Some(c) = &mut self.connecting {
|
||||
c.request_access = intent.request_access;
|
||||
}
|
||||
self.actions.push_back(OverlayAction::Launch {
|
||||
addr: intent.addr,
|
||||
port: intent.port,
|
||||
fp_hex: intent.fp_hex,
|
||||
launch: intent.launch,
|
||||
title: intent.title,
|
||||
request_access: intent.request_access,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -629,6 +638,17 @@ impl Shell {
|
||||
String::new(),
|
||||
vec![],
|
||||
))
|
||||
} else if c.request_access {
|
||||
Some((
|
||||
c.appear,
|
||||
true,
|
||||
"Waiting for approval…".to_string(),
|
||||
format!(
|
||||
"Approve this device in {}'s console or web UI — no PIN needed.",
|
||||
c.title
|
||||
),
|
||||
vec![Hint::new(HintKey::Back, "Cancel")],
|
||||
))
|
||||
} else {
|
||||
Some((
|
||||
c.appear,
|
||||
|
||||
@@ -73,6 +73,11 @@ pub enum OverlayAction {
|
||||
fp_hex: String,
|
||||
launch: Option<String>,
|
||||
title: String,
|
||||
/// The no-PIN delegated-approval path: pin the host's advertised fingerprint and
|
||||
/// open a connect the host PARKS until the operator approves this device in its
|
||||
/// console (a long connect budget), then persist it as paired. `false` = an
|
||||
/// ordinary connect to an already-paired host.
|
||||
request_access: bool,
|
||||
},
|
||||
/// Abort an in-flight connect (B while Connecting) — the console keeps browsing.
|
||||
/// The run loop stops the pump; a dial that already won the race is quit-closed.
|
||||
|
||||
Reference in New Issue
Block a user