From 292a97232b9b785c9fe79506f40f918b5f2f8466 Mon Sep 17 00:00:00 2001 From: Admin Date: Thu, 24 Sep 2026 05:20:35 +0000 Subject: [PATCH] feat(linux): add no-paper capture pipeline self-test --- src-tauri/src/lib.rs | 77 ++++++++++++++++++++++++++++++++++++++++++++ src/main.ts | 17 ++++++++++ src/styles.css | 5 +++ 3 files changed, 99 insertions(+) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 25242e9..be02b41 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -96,6 +96,82 @@ fn poll_login(state: State<'_, AppState>) -> Result { fieldlens::poll_login(&state.shared).map_err(|error| error.to_string()) } +#[cfg(target_os = "linux")] +fn build_self_test_pdf() -> Vec { + let objects = [ + "1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n".to_string(), + "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n".to_string(), + "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Resources << /Font << /F1 5 0 R >> >> /Contents 4 0 R >>\nendobj\n".to_string(), + { + let stream = "BT /F1 18 Tf 72 720 Td (FieldLens capture self-test) Tj ET\n"; + format!("4 0 obj\n<< /Length {} >>\nstream\n{}endstream\nendobj\n", stream.len(), stream) + }, + "5 0 obj\n<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>\nendobj\n".to_string(), + ]; + + let mut pdf = b"%PDF-1.4\n".to_vec(); + let mut offsets = Vec::new(); + for object in &objects { + offsets.push(pdf.len()); + pdf.extend_from_slice(object.as_bytes()); + } + + let xref = pdf.len(); + pdf.extend_from_slice(format!("xref\n0 {}\n", objects.len() + 1).as_bytes()); + pdf.extend_from_slice(b"0000000000 65535 f \n"); + for offset in offsets { + pdf.extend_from_slice(format!("{offset:010} 00000 n \n").as_bytes()); + } + pdf.extend_from_slice( + format!( + "trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{}\n%%EOF\n", + objects.len() + 1, + xref + ) + .as_bytes(), + ); + pdf +} + +#[tauri::command] +fn run_capture_self_test() -> Result { + #[cfg(target_os = "linux")] + { + let path = std::env::temp_dir().join(format!( + "fieldlens-capture-self-test-{}.pdf", + uuid::Uuid::new_v4() + )); + std::fs::write(&path, build_self_test_pdf()).map_err(|error| error.to_string())?; + + let output = std::process::Command::new("lp") + .args(["-d", "FieldLens-Test-Capture"]) + .arg(&path) + .output() + .map_err(|error| error.to_string()); + let _ = std::fs::remove_file(&path); + let output = output?; + + if !output.status.success() { + return Err(format!( + "CUPS self-test submission failed: {}", + String::from_utf8_lossy(&output.stderr).trim() + )); + } + + let detail = String::from_utf8_lossy(&output.stdout).trim().to_string(); + return Ok(if detail.is_empty() { + "Self-test submitted to FieldLens Test Capture.".into() + } else { + detail + }); + } + + #[cfg(not(target_os = "linux"))] + { + Err("Capture self-test is currently available on Linux only.".into()) + } +} + #[tauri::command] fn sync_pending(state: State<'_, AppState>) -> Result { fieldlens::sync_pending(&state.shared).map_err(|error| error.to_string()) @@ -127,6 +203,7 @@ pub fn run() { get_virtual_printer_status, install_virtual_printer, remove_virtual_printer, + run_capture_self_test, begin_login, poll_login, sync_pending diff --git a/src/main.ts b/src/main.ts index f42ffea..147373d 100644 --- a/src/main.ts +++ b/src/main.ts @@ -152,6 +152,7 @@ app.innerHTML = `
+
@@ -471,6 +472,22 @@ el('install-printer').addEventListener('click', async () => { } }) +el('self-test').addEventListener('click', async () => { + const button = el('self-test') + const state = el('virtual-printer-state') + button.disabled = true + state.textContent = 'Submitting a no-paper capture self-test…' + try { + const result = await invoke('run_capture_self_test') + state.textContent = result + window.setTimeout(() => { void refreshStatus() }, 1200) + } catch (error) { + state.textContent = `Self-test failed: ${String(error)}` + } finally { + button.disabled = false + } +}) + el('remove-printer').addEventListener('click', async () => { const button = el('remove-printer') const state = el('virtual-printer-state') diff --git a/src/styles.css b/src/styles.css index 8849fa2..b9d8d78 100644 --- a/src/styles.css +++ b/src/styles.css @@ -532,3 +532,8 @@ select:focus { .job { grid-template-columns: 1fr; } .job-meta { justify-content: flex-start; text-align: left; } } + +#self-test { + color: #9fcef5; + border-color: #294865; +}