feat(clients/windows): screen-module restructure + parity features (speed test, native mode, capture UX)
Structure: split the 1400-line app.rs into per-screen app/ modules (mod=root/ router, hosts, connect, pair, speed, settings, licenses, stream, style) with shared card/header/busy-page builders and setting_combo/toggle helpers; the re-render rule (thread-driven state lives in root use_async_state, flows down as props) is now documented at the module root. Parity features the other clients already had: - "Native display" resolves the real monitor mode at connect (MonitorFromWindow -> EnumDisplaySettingsW; was a hardcoded 1080p60) - per-host network speed test: saved-host card button + a results screen (probe burst -> goodput/loss -> ~70% recommended bitrate applied in one tap; stale runs invalidated by generation) and `--headless --speed-test`; the bitrate setting becomes a free-form NumberBox so the recommendation round-trips - forget host (ContentDialog confirm -> KnownHosts::remove_by_fp) - settings: forwarded-controller picker (pads/pinned/set_pinned now wired), gamepad type, host compositor, capture-system-shortcuts; the previously dead Settings.compositor / inhibit_shortcuts are honored (shortcuts off = Alt+Tab/Alt+Esc/Ctrl+Esc/Win act locally) - click-to-recapture after a Ctrl+Alt+Shift+Q release; the HUD hint tracks the live capture state Perf: the input hook caches lock geometry (clip rect + contain-fit scale) at engage instead of GetClientRect per WM_MOUSEMOVE; the audio jitter ring trims via drain() and reuses the render scratch buffer. Validated on the bare-metal box: --discover, synthetic-host loopback E2E (TOFU -> clock skew -> HEVC negotiate -> D3D11VA init -> session end), speed-test E2E, and the WinUI shell rendering in the console session via PsExec (SSH/session-0 cannot create windows, pre-existing 0x80070005). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
//! The SPAKE2 PIN pairing screen: the host is armed and displays a 4-digit PIN; proving
|
||||
//! knowledge of it pins the host's certificate (and registers ours) with no offline-guessable
|
||||
//! transcript. Also offers the no-PIN "request access" (delegated-approval) alternative.
|
||||
|
||||
use super::connect::{connect, request_access};
|
||||
use super::style::*;
|
||||
use super::{Screen, Svc};
|
||||
use crate::trust::{self, KnownHost, KnownHosts};
|
||||
use punktfunk_core::client::NativeClient;
|
||||
use windows_reactor::*;
|
||||
|
||||
pub(crate) fn pair_page(props: &Svc, cx: &mut RenderCx) -> Element {
|
||||
let ctx = &props.ctx;
|
||||
let set_screen = &props.set_screen;
|
||||
let set_status = &props.set_status;
|
||||
let (code, set_code) = cx.use_state(String::new());
|
||||
let target = ctx.shared.target.lock().unwrap().clone();
|
||||
|
||||
let pair_btn = {
|
||||
let (ctx2, ss, st, code2, target2) = (
|
||||
ctx.clone(),
|
||||
set_screen.clone(),
|
||||
set_status.clone(),
|
||||
code.clone(),
|
||||
target.clone(),
|
||||
);
|
||||
button("Pair & Connect")
|
||||
.accent()
|
||||
.icon(SymbolGlyph::Accept)
|
||||
.on_click(move || {
|
||||
let pin = code2.trim().to_string();
|
||||
let (ctx3, ss, st, target3) =
|
||||
(ctx2.clone(), ss.clone(), st.clone(), target2.clone());
|
||||
std::thread::spawn(move || {
|
||||
let name =
|
||||
std::env::var("COMPUTERNAME").unwrap_or_else(|_| "windows-client".into());
|
||||
match NativeClient::pair(
|
||||
&target3.addr,
|
||||
target3.port,
|
||||
(&ctx3.identity.0, &ctx3.identity.1),
|
||||
&pin,
|
||||
&name,
|
||||
std::time::Duration::from_secs(90),
|
||||
) {
|
||||
Ok(fp) => {
|
||||
let mut k = KnownHosts::load();
|
||||
k.upsert(KnownHost {
|
||||
name: target3.name.clone(),
|
||||
addr: target3.addr.clone(),
|
||||
port: target3.port,
|
||||
fp_hex: trust::hex(&fp),
|
||||
paired: true,
|
||||
});
|
||||
let _ = k.save();
|
||||
connect(&ctx3, &target3, Some(fp), &ss, &st);
|
||||
}
|
||||
Err(e) => {
|
||||
st.call(format!("Pairing failed: {e:?} (wrong PIN, or not armed?)"));
|
||||
ss.call(Screen::Hosts);
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
};
|
||||
let cancel_btn = {
|
||||
let ss = set_screen.clone();
|
||||
button("Cancel")
|
||||
.icon(SymbolGlyph::Cancel)
|
||||
.on_click(move || ss.call(Screen::Hosts))
|
||||
};
|
||||
// The no-PIN alternative offered alongside the PIN ceremony: open an identified connect that
|
||||
// the host parks until the operator approves this device in its console (delegated approval).
|
||||
let request_btn = {
|
||||
let (svc, target2) = (props.clone(), target.clone());
|
||||
button("Request access without a PIN")
|
||||
.icon(SymbolGlyph::Send)
|
||||
.on_click(move || request_access(&svc, &target2))
|
||||
.horizontal_alignment(HorizontalAlignment::Stretch)
|
||||
};
|
||||
|
||||
let content = card(vstack((
|
||||
grid((
|
||||
avatar(&target.name)
|
||||
.grid_column(0)
|
||||
.vertical_alignment(VerticalAlignment::Center),
|
||||
vstack((
|
||||
text_block(format!("Pair with {}", target.name))
|
||||
.font_size(20.0)
|
||||
.semibold(),
|
||||
text_block(format!("{}:{}", target.addr, target.port))
|
||||
.font_size(12.0)
|
||||
.foreground(ThemeRef::SecondaryText),
|
||||
))
|
||||
.spacing(2.0)
|
||||
.grid_column(1)
|
||||
.vertical_alignment(VerticalAlignment::Center)
|
||||
.margin(edges(12.0, 0.0, 0.0, 0.0)),
|
||||
))
|
||||
.columns([GridLength::Auto, GridLength::Star(1.0)]),
|
||||
InfoBar::new("Arm pairing on the host")
|
||||
.message(
|
||||
"On the host's console or web console, start pairing — it shows a 4-digit PIN. \
|
||||
Enter it below within 90 seconds.",
|
||||
)
|
||||
.informational()
|
||||
.is_closable(false),
|
||||
text_box(code)
|
||||
.placeholder("PIN")
|
||||
.font_size(28.0)
|
||||
.on_changed(move |s| set_code.call(s)),
|
||||
hstack((pair_btn, cancel_btn)).spacing(8.0),
|
||||
text_block(
|
||||
"Don\u{2019}t have a PIN? Request access instead and approve this device on the host \
|
||||
(its console or web UI) \u{2014} no PIN needed.",
|
||||
)
|
||||
.font_size(12.0)
|
||||
.foreground(ThemeRef::SecondaryText),
|
||||
request_btn,
|
||||
))
|
||||
.spacing(16.0))
|
||||
.max_width(480.0)
|
||||
.horizontal_alignment(HorizontalAlignment::Center)
|
||||
.margin(edges(0.0, 60.0, 0.0, 0.0));
|
||||
|
||||
page(vec![content.into()])
|
||||
}
|
||||
Reference in New Issue
Block a user