launcher: default to GUI and hard-fail unsupported paste chars
This commit is contained in:
parent
7225ed6a1a
commit
fa47fa4e30
@ -12,7 +12,7 @@ pub use diagnostics::{DiagnosticsLog, PerformanceSample, SnapshotReport, quality
|
||||
pub use state::{DeviceSelection, InputRouting, LauncherState, ViewMode};
|
||||
|
||||
pub fn maybe_run_launcher(args: &[String]) -> Result<bool> {
|
||||
if args.iter().any(|arg| arg == "--launcher") {
|
||||
if should_run_launcher(args) {
|
||||
let server_addr = resolve_server_addr(args);
|
||||
ui::run_gui_launcher(server_addr)?;
|
||||
return Ok(true);
|
||||
@ -20,6 +20,14 @@ pub fn maybe_run_launcher(args: &[String]) -> Result<bool> {
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
/// Decides when to present the GUI launcher instead of direct session startup.
|
||||
fn should_run_launcher(args: &[String]) -> bool {
|
||||
if args.iter().any(|arg| arg == "--no-launcher") {
|
||||
return false;
|
||||
}
|
||||
args.iter().any(|arg| arg == "--launcher") || args.is_empty()
|
||||
}
|
||||
|
||||
pub fn runtime_env_vars(state: &LauncherState) -> BTreeMap<String, String> {
|
||||
let mut envs = BTreeMap::new();
|
||||
envs.insert(
|
||||
@ -107,4 +115,35 @@ mod tests {
|
||||
let args = vec!["http://server:50051".to_string()];
|
||||
assert!(!maybe_run_launcher(&args).expect("launcher check"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(coverage)]
|
||||
fn maybe_run_launcher_returns_true_with_launcher_flag() {
|
||||
let args = vec!["--launcher".to_string()];
|
||||
assert!(maybe_run_launcher(&args).expect("launcher should run"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(coverage)]
|
||||
fn maybe_run_launcher_defaults_to_launcher_for_empty_args() {
|
||||
let args: Vec<String> = vec![];
|
||||
assert!(maybe_run_launcher(&args).expect("launcher should run"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_run_launcher_defaults_true_for_empty_args() {
|
||||
assert!(should_run_launcher(&[]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_run_launcher_honors_explicit_opt_out() {
|
||||
let args = vec!["--no-launcher".to_string()];
|
||||
assert!(!should_run_launcher(&args));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_run_launcher_respects_direct_server_args() {
|
||||
let args = vec!["http://server:50051".to_string()];
|
||||
assert!(!should_run_launcher(&args));
|
||||
}
|
||||
}
|
||||
|
||||
@ -63,7 +63,7 @@
|
||||
"client/src/launcher/mod.rs": {
|
||||
"clippy_warnings": 4,
|
||||
"doc_debt": 4,
|
||||
"loc": 110
|
||||
"loc": 149
|
||||
},
|
||||
"client/src/launcher/state.rs": {
|
||||
"clippy_warnings": 8,
|
||||
@ -193,7 +193,7 @@
|
||||
"server/src/paste.rs": {
|
||||
"clippy_warnings": 6,
|
||||
"doc_debt": 3,
|
||||
"loc": 146
|
||||
"loc": 204
|
||||
},
|
||||
"server/src/runtime_support.rs": {
|
||||
"clippy_warnings": 14,
|
||||
|
||||
@ -46,7 +46,7 @@
|
||||
},
|
||||
"client/src/launcher/mod.rs": {
|
||||
"line_percent": 96.15384615384616,
|
||||
"loc": 110
|
||||
"loc": 149
|
||||
},
|
||||
"client/src/launcher/state.rs": {
|
||||
"line_percent": 99.32432432432432,
|
||||
@ -134,7 +134,7 @@
|
||||
},
|
||||
"server/src/paste.rs": {
|
||||
"line_percent": 96.73913043478261,
|
||||
"loc": 146
|
||||
"loc": 204
|
||||
},
|
||||
"server/src/runtime_support.rs": {
|
||||
"line_percent": 96.42857142857143,
|
||||
|
||||
@ -29,7 +29,7 @@ pub fn decrypt(req: &PasteRequest) -> Result<String> {
|
||||
let plaintext = cipher
|
||||
.decrypt(nonce, req.data.as_ref())
|
||||
.map_err(|e| anyhow::anyhow!("paste decrypt failed: {e}"))?;
|
||||
Ok(String::from_utf8(plaintext).context("paste plaintext not UTF-8")?)
|
||||
String::from_utf8(plaintext).context("paste plaintext not UTF-8")
|
||||
}
|
||||
|
||||
/// Type clipboard text into the HID keyboard gadget.
|
||||
@ -51,6 +51,13 @@ pub async fn type_text(kb: &Mutex<File>, text: &str) -> Result<()> {
|
||||
.unwrap_or(8);
|
||||
let delay = std::time::Duration::from_millis(delay_ms);
|
||||
|
||||
let (unsupported_count, unsupported_preview) = unsupported_chars(text.chars().take(max));
|
||||
if unsupported_count > 0 {
|
||||
anyhow::bail!(
|
||||
"paste contains {unsupported_count} unsupported character(s): {unsupported_preview}"
|
||||
);
|
||||
}
|
||||
|
||||
let mut kb = kb.lock().await;
|
||||
for c in text.chars().take(max) {
|
||||
if let Some((usage, mods)) = char_to_usage(c) {
|
||||
@ -65,6 +72,26 @@ pub async fn type_text(kb: &Mutex<File>, text: &str) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Summarize unsupported clipboard characters for explicit operator feedback.
|
||||
fn unsupported_chars(chars: impl Iterator<Item = char>) -> (usize, String) {
|
||||
let mut count = 0usize;
|
||||
let mut preview = Vec::new();
|
||||
for ch in chars {
|
||||
if char_to_usage(ch).is_some() {
|
||||
continue;
|
||||
}
|
||||
count += 1;
|
||||
if preview.len() < 6 {
|
||||
preview.push(format!("{:?}", ch));
|
||||
}
|
||||
}
|
||||
if count == 0 {
|
||||
(0, String::new())
|
||||
} else {
|
||||
(count, preview.join(", "))
|
||||
}
|
||||
}
|
||||
|
||||
fn load_key() -> Result<[u8; 32]> {
|
||||
let raw = std::env::var("LESAVKA_PASTE_KEY")
|
||||
.context("LESAVKA_PASTE_KEY not set (required for PasteText RPC)")?;
|
||||
@ -73,7 +100,7 @@ fn load_key() -> Result<[u8; 32]> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{decrypt, type_text};
|
||||
use super::{decrypt, type_text, unsupported_chars};
|
||||
use chacha20poly1305::aead::{Aead, KeyInit};
|
||||
use chacha20poly1305::{ChaCha20Poly1305, Key, Nonce};
|
||||
use lesavka_common::lesavka::PasteRequest;
|
||||
@ -134,12 +161,43 @@ mod tests {
|
||||
.await
|
||||
.expect("open temp file");
|
||||
let kb = Mutex::new(file);
|
||||
type_text(&kb, "A!🙂").await.expect("type text");
|
||||
type_text(&kb, "A!?").await.expect("type text");
|
||||
|
||||
let mut bytes = Vec::new();
|
||||
let mut file = File::open(&path).await.expect("reopen temp file");
|
||||
file.read_to_end(&mut bytes).await.expect("read reports");
|
||||
assert_eq!(bytes.len(), 32);
|
||||
assert_eq!(bytes.len(), 48);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsupported_chars_reports_count_and_preview() {
|
||||
let (count, preview) = unsupported_chars("ok🙂éx".chars());
|
||||
assert_eq!(count, 2);
|
||||
assert!(preview.contains("'🙂'"));
|
||||
assert!(preview.contains("'é'"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
/// Reject unsupported clipboard characters instead of silently dropping them.
|
||||
fn type_text_rejects_unsupported_chars() {
|
||||
let rt = Runtime::new().expect("runtime");
|
||||
with_var("LESAVKA_PASTE_DELAY_MS", Some("0"), || {
|
||||
rt.block_on(async {
|
||||
let dir = tempdir().expect("tempdir");
|
||||
let path = dir.path().join("hidg0.bin");
|
||||
let file = OpenOptions::new()
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.write(true)
|
||||
.open(&path)
|
||||
.await
|
||||
.expect("open temp file");
|
||||
let kb = Mutex::new(file);
|
||||
let err = type_text(&kb, "pw🙂").await.expect_err("unsupported char should fail");
|
||||
assert!(err.to_string().contains("unsupported character"));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user