diff --git a/downloads/install.sh b/downloads/install.sh index 0b456d3..e700ff3 100644 --- a/downloads/install.sh +++ b/downloads/install.sh @@ -52,9 +52,15 @@ else fi if command -v apt-get >/dev/null 2>&1; then + say "Installing CUPS printer tools..." + $sudo_cmd apt-get update + $sudo_cmd apt-get install -y cups-client policykit-1 || $sudo_cmd apt-get install -y cups-client say "Installing FieldLens Print..." $sudo_cmd apt-get install -y "$package_path" elif command -v apt >/dev/null 2>&1; then + say "Installing CUPS printer tools..." + $sudo_cmd apt update + $sudo_cmd apt install -y cups-client policykit-1 || $sudo_cmd apt install -y cups-client say "Installing FieldLens Print..." $sudo_cmd apt install -y "$package_path" elif command -v dpkg >/dev/null 2>&1; then diff --git a/package.json b/package.json index ecef650..25749a9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@fieldlens/print", - "version": "0.1.2", + "version": "0.1.3", "private": true, "type": "module", "scripts": { diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 2b17c8b..24719ef 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "fieldlens-print" -version = "0.1.2" +version = "0.1.3" description = "FieldLens print-stream barcode capture companion" authors = ["Arrowstack"] edition = "2021" diff --git a/src-tauri/src/capture.rs b/src-tauri/src/capture.rs index 3839d43..94b494a 100644 --- a/src-tauri/src/capture.rs +++ b/src-tauri/src/capture.rs @@ -11,6 +11,9 @@ use std::{ time::Duration, }; +#[cfg(target_os = "linux")] +use std::process::{Command, Stdio}; + use anyhow::{anyhow, Context, Result}; use tauri::AppHandle; use uuid::Uuid; @@ -170,14 +173,25 @@ fn handle_raw_connection( return; } + let tcp_requested = !config.forward_addr.trim().is_empty(); + let (forwarded, final_error) = if forward_error.is_some() { + (false, forward_error) + } else if tcp_requested { + (true, None) + } else if !config.forward_printer.trim().is_empty() { + forward_to_system_printer(&config.forward_printer, &bytes) + } else { + (false, None) + }; + process_payload( state, app, "raw-proxy", label, bytes, - !config.forward_addr.trim().is_empty() && forward_error.is_none(), - forward_error, + forwarded, + final_error, ); } @@ -237,7 +251,7 @@ fn spool_folder_loop( match fs::read(&path) { Ok(bytes) => { - let (forwarded, error) = forward_all(&config.forward_addr, &bytes); + let (forwarded, error) = forward_payload(&config, &bytes); process_payload( &state, &app, @@ -287,6 +301,16 @@ fn unique_processed_path(dir: &std::path::Path, filename: &str) -> PathBuf { )) } +fn forward_payload(config: &AppConfig, bytes: &[u8]) -> (bool, Option) { + if !config.forward_addr.trim().is_empty() { + return forward_all(&config.forward_addr, bytes); + } + if !config.forward_printer.trim().is_empty() { + return forward_to_system_printer(&config.forward_printer, bytes); + } + (false, None) +} + fn forward_all(address: &str, bytes: &[u8]) -> (bool, Option) { if address.trim().is_empty() { return (false, None); @@ -306,6 +330,57 @@ fn forward_all(address: &str, bytes: &[u8]) -> (bool, Option) { } } +#[cfg(target_os = "linux")] +fn forward_to_system_printer(printer: &str, bytes: &[u8]) -> (bool, Option) { + let mut child = match Command::new("lp") + .args(["-d", printer, "-o", "raw", "-t", "FieldLens Print"]) + .stdin(Stdio::piped()) + .stdout(Stdio::null()) + .stderr(Stdio::piped()) + .spawn() + { + Ok(child) => child, + Err(error) if error.kind() == std::io::ErrorKind::NotFound => { + return ( + false, + Some("CUPS client tools are missing; install cups-client".into()), + ) + } + Err(error) => { + return (false, Some(format!("forward to CUPS printer failed: {error}"))) + } + }; + + if let Some(mut stdin) = child.stdin.take() { + if let Err(error) = stdin.write_all(bytes) { + let _ = child.kill(); + return (false, Some(format!("forward to CUPS printer failed: {error}"))); + } + } + + match child.wait_with_output() { + Ok(output) if output.status.success() => (true, None), + Ok(output) => ( + false, + Some(format!( + "CUPS forward failed: {}", + String::from_utf8_lossy(&output.stderr).trim() + )), + ), + Err(error) => (false, Some(format!("CUPS forward failed: {error}"))), + } +} + +#[cfg(not(target_os = "linux"))] +fn forward_to_system_printer(printer: &str, _bytes: &[u8]) -> (bool, Option) { + ( + false, + Some(format!( + "installed-printer RAW forwarding is not available for '{printer}' without a network RAW/TCP address" + )), + ) +} + fn process_payload( state: &SharedState, app: &AppHandle, diff --git a/src-tauri/src/printers.rs b/src-tauri/src/printers.rs index d6dde50..e6dee89 100644 --- a/src-tauri/src/printers.rs +++ b/src-tauri/src/printers.rs @@ -1,10 +1,13 @@ -use std::{ - fs, - process::Command, -}; +use std::process::Command; + +#[cfg(target_os = "windows")] +use std::fs; +#[cfg(target_os = "linux")] +use std::path::PathBuf; use anyhow::{anyhow, Context, Result}; use serde::{Deserialize, Serialize}; +#[cfg(target_os = "windows")] use serde_json::Value; pub const VIRTUAL_PRINTER_NAME: &str = "FieldLens Print"; @@ -77,7 +80,7 @@ pub fn install_virtual_printer(target_printer: &str) -> Result String { #[cfg(target_os = "linux")] fn list_linux_printers() -> Result> { - let output = Command::new("lpstat") - .arg("-v") - .output() - .context("query CUPS printers with lpstat")?; + let output = match Command::new("lpstat").arg("-v").output() { + Ok(output) => output, + Err(error) if error.kind() == std::io::ErrorKind::NotFound => { + return Err(anyhow!( + "CUPS client tools are missing. Install the cups-client package, then reopen FieldLens Print." + )) + } + Err(error) => return Err(error).context("query CUPS printers with lpstat"), + }; if !output.status.success() { return Err(anyhow!( @@ -289,27 +297,42 @@ fn socket_uri_to_address(uri: &str) -> Option { } #[cfg(target_os = "linux")] -fn install_linux_virtual_printer() -> Result<()> { - let args = [ - "-p", - "FieldLens-Print", - "-E", - "-v", - "socket://127.0.0.1:9100", - "-m", - "raw", - "-D", - VIRTUAL_PRINTER_NAME, +fn install_linux_virtual_printer(target_printer: &str) -> Result<()> { + let printers = list_linux_printers()?; + if !printers.iter().any(|printer| printer.name == target_printer && !printer.is_virtual) { + return Err(anyhow!( + "selected printer '{target_printer}' is not installed or available in CUPS" + )); + } + + let ppd_path = PathBuf::from("/etc/cups/ppd").join(format!("{target_printer}.ppd")); + let mut args = vec![ + "-p".to_string(), + "FieldLens-Print".to_string(), + "-E".to_string(), + "-v".to_string(), + "socket://127.0.0.1:9100".to_string(), ]; - let direct = Command::new("lpadmin").args(args).status(); + if ppd_path.exists() { + args.push("-P".to_string()); + args.push(ppd_path.to_string_lossy().into_owned()); + } else { + args.push("-m".to_string()); + args.push("raw".to_string()); + } + + args.push("-D".to_string()); + args.push(VIRTUAL_PRINTER_NAME.to_string()); + + let direct = Command::new("lpadmin").args(&args).status(); if matches!(direct, Ok(status) if status.success()) { return Ok(()); } let status = Command::new("pkexec") .arg("lpadmin") - .args(args) + .args(&args) .status() .context("run elevated CUPS printer setup")?; diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 51f17ff..c8c28f7 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "FieldLens Print", - "version": "0.1.2", + "version": "0.1.3", "identifier": "dev.arrowstack.fieldlens.print", "build": { "beforeDevCommand": "npm run dev", @@ -34,7 +34,17 @@ "installMode": "currentUser" } }, - "createUpdaterArtifacts": true + "createUpdaterArtifacts": true, + "linux": { + "deb": { + "depends": [ + "cups-client" + ], + "recommends": [ + "policykit-1" + ] + } + } }, "plugins": { "updater": { diff --git a/src/main.ts b/src/main.ts index b841d87..a34b537 100644 --- a/src/main.ts +++ b/src/main.ts @@ -69,7 +69,7 @@ let config: AppConfig let jobs: Job[] = [] let status: Status let loginTimer: number | undefined -let currentVersion = '0.1.2' +let currentVersion = '0.1.3' let bundleType: BundleType | null = null let availableUpdate: Update | null = null let updateBusy = false @@ -221,7 +221,8 @@ function renderPrinters() { const current = select.value || config?.forwardPrinter || '' const options = systemPrinters.filter((printer) => !printer.isVirtual) - select.innerHTML = '' + options.map((printer) => { + const emptyLabel = options.length ? 'Choose a printer…' : 'No installed printers found' + select.innerHTML = `` + options.map((printer) => { const suffix = printer.address ? ` · ${printer.address}` : printer.port ? ` · ${printer.port}` : '' return `` }).join('') @@ -234,10 +235,16 @@ function renderPrinters() { help.textContent = `Detected RAW network endpoint: ${selected.address}` } else if (selected) { help.textContent = 'No RAW/TCP address was exposed for this queue. Enter the printer IP:9100 manually.' + } else if (config?.forwardPrinter && !options.some((printer) => printer.name === config.forwardPrinter)) { + help.textContent = `Previously selected printer "${config.forwardPrinter}" is not installed or available. Add it in system printer settings, then Refresh printers.` + } else if (!options.length) { + help.textContent = 'No installed CUPS printers were found. Add the physical printer in system settings, then Refresh printers.' } else { help.textContent = 'Select the physical printer that should receive the original print bytes.' } + el('install-printer').disabled = !options.length + const virtualState = el('virtual-printer-state') virtualState.textContent = virtualPrinterStatus?.detail ?? 'Printer status unavailable.' virtualState.classList.toggle('ready', Boolean(virtualPrinterStatus?.installed))