first commit

This commit is contained in:
2026-01-02 19:08:56 +01:00
commit fdf37aa7b4
22 changed files with 4000 additions and 0 deletions

View File

@@ -0,0 +1,24 @@
[package]
name = "linux-hello-cli"
version.workspace = true
edition.workspace = true
license.workspace = true
description = "Linux Hello command-line interface"
[[bin]]
name = "linux-hello"
path = "src/main.rs"
[dependencies]
linux-hello-common = { path = "../linux-hello-common" }
linux-hello-daemon = { path = "../linux-hello-daemon" }
thiserror.workspace = true
serde.workspace = true
serde_json.workspace = true
toml.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true
tokio.workspace = true
clap.workspace = true
image.workspace = true

373
linux-hello-cli/src/main.rs Normal file
View File

@@ -0,0 +1,373 @@
//! Linux Hello CLI
//!
//! Command-line interface for enrollment, testing, and diagnostics.
use clap::{Parser, Subcommand};
use linux_hello_common::{Config, Result};
use tracing::{info, Level};
use tracing_subscriber::FmtSubscriber;
#[derive(Parser)]
#[command(name = "linux-hello")]
#[command(author, version, about = "Linux Hello facial authentication", long_about = None)]
struct Cli {
/// Enable verbose output
#[arg(short, long)]
verbose: bool,
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
/// Capture frames from IR camera (for testing)
Capture {
/// Output directory for captured frames
#[arg(short, long, default_value = ".")]
output: String,
/// Number of frames to capture
#[arg(short, long, default_value = "5")]
count: u32,
/// Camera device path (auto-detect if not specified)
#[arg(short, long)]
device: Option<String>,
},
/// Test face detection on camera or image
Detect {
/// Image file to process (use camera if not specified)
#[arg(short, long)]
image: Option<String>,
/// Show detection confidence scores
#[arg(long)]
scores: bool,
/// Save annotated output image
#[arg(short, long)]
output: Option<String>,
},
/// Show system status
Status {
/// Include camera diagnostics
#[arg(long)]
camera: bool,
/// Include daemon status
#[arg(long)]
daemon: bool,
},
/// Enroll a face model
Enroll {
/// Label for this enrollment
#[arg(short, long, default_value = "default")]
label: String,
},
/// List enrolled face models
List,
/// Remove enrolled face models
Remove {
/// Label to remove (or --all for all)
label: Option<String>,
/// Remove all enrolled models
#[arg(long)]
all: bool,
},
/// Test authentication
Test {
/// Show detailed confidence scores
#[arg(long)]
verbose: bool,
/// Save debug information
#[arg(long)]
debug: bool,
},
/// Show or modify configuration
Config {
/// Show config as JSON
#[arg(long)]
json: bool,
/// Set a configuration value (key=value)
#[arg(long)]
set: Option<String>,
},
}
#[tokio::main]
async fn main() -> Result<()> {
let cli = Cli::parse();
// Initialize logging
let log_level = if cli.verbose { Level::DEBUG } else { Level::INFO };
FmtSubscriber::builder()
.with_max_level(log_level)
.with_target(false)
.init();
// Load configuration
let config = Config::load_or_default();
match cli.command {
Commands::Capture { output, count, device } => {
cmd_capture(&config, &output, count, device.as_deref()).await
}
Commands::Detect { image, scores, output } => {
cmd_detect(&config, image.as_deref(), scores, output.as_deref()).await
}
Commands::Status { camera, daemon } => {
cmd_status(&config, camera, daemon).await
}
Commands::Enroll { label } => {
cmd_enroll(&config, &label).await
}
Commands::List => {
cmd_list(&config).await
}
Commands::Remove { label, all } => {
cmd_remove(&config, label.as_deref(), all).await
}
Commands::Test { verbose, debug } => {
cmd_test(&config, verbose, debug).await
}
Commands::Config { json, set } => {
cmd_config(&config, json, set.as_deref()).await
}
}
}
async fn cmd_capture(
_config: &Config,
output: &str,
count: u32,
device: Option<&str>,
) -> Result<()> {
info!("Capturing {} frames to {}", count, output);
#[cfg(target_os = "linux")]
use linux_hello_daemon::camera::{enumerate_cameras, Camera};
#[cfg(not(target_os = "linux"))]
use linux_hello_daemon::camera::{enumerate_cameras, Camera};
use linux_hello_daemon::PixelFormat;
// Find camera
let device_path = match device {
Some(d) => d.to_string(),
None => {
let cameras = enumerate_cameras()?;
let ir_cam = cameras.iter().find(|c| c.is_ir);
let cam = ir_cam.or(cameras.first());
match cam {
Some(c) => {
info!("Using camera: {}", c);
c.device_path.clone()
}
None => {
return Err(linux_hello_common::Error::NoCameraFound);
}
}
}
};
// Open camera and capture
let mut camera = Camera::open(&device_path)?;
info!("Camera opened, resolution: {:?}", camera.resolution());
// Create output directory if it doesn't exist
std::fs::create_dir_all(output)?;
for i in 0..count {
let frame = camera.capture_frame()?;
match frame.format {
PixelFormat::Grey => {
let filename = format!("{}/frame_{:03}.png", output, i);
if let Some(img) = image::GrayImage::from_raw(frame.width, frame.height, frame.data) {
img.save(&filename).map_err(|e| linux_hello_common::Error::Io(std::io::Error::new(std::io::ErrorKind::Other, e)))?;
info!("Saved frame {} (Permissions: Grey)", filename);
}
}
PixelFormat::Yuyv => {
// TODO: Convert YUYV to RGB/Gray
let filename = format!("{}/frame_{:03}.raw", output, i);
std::fs::write(&filename, &frame.data)?;
info!("Saved frame {} ({} bytes - Raw YUYV)", filename, frame.data.len());
}
_ => {
let filename = format!("{}/frame_{:03}.raw", output, i);
std::fs::write(&filename, &frame.data)?;
info!("Saved frame {} ({} bytes - Unknown format)", filename, frame.data.len());
}
}
}
info!("Capture complete");
Ok(())
}
async fn cmd_detect(
_config: &Config,
image: Option<&str>,
show_scores: bool,
output: Option<&str>,
) -> Result<()> {
match image {
Some(path) => {
info!("Detecting faces in: {}", path);
// Load image
let img = image::open(path)
.map_err(|e| linux_hello_common::Error::Detection(e.to_string()))?;
let gray = img.to_luma8();
let (width, height) = gray.dimensions();
// Run simple detection (placeholder)
use linux_hello_daemon::detection::detect_face_simple;
let detection = detect_face_simple(gray.as_raw(), width, height);
match detection {
Some(det) => {
let (x, y, w, h) = det.to_pixels(width, height);
println!("Face detected:");
println!(" Position: ({}, {})", x, y);
println!(" Size: {}x{}", w, h);
if show_scores {
println!(" Confidence: {:.2}%", det.confidence * 100.0);
}
if let Some(out_path) = output {
info!("Saving annotated image to: {}", out_path);
// TODO: Draw bounding box and save
}
}
None => {
println!("No face detected");
}
}
}
None => {
println!("Live camera detection not yet implemented");
println!("Use --image to detect in an image file");
}
}
Ok(())
}
async fn cmd_status(config: &Config, show_camera: bool, show_daemon: bool) -> Result<()> {
println!("Linux Hello Status");
println!("==================");
if show_camera || (!show_camera && !show_daemon) {
println!("\nCamera:");
#[cfg(target_os = "linux")]
{
use linux_hello_daemon::camera::enumerate_cameras;
match enumerate_cameras() {
Ok(cameras) => {
if cameras.is_empty() {
println!(" No cameras found");
} else {
for cam in cameras {
println!(" - {}", cam);
if !cam.resolutions.is_empty() {
println!(
" Resolutions: {:?}",
cam.resolutions.iter().take(3).collect::<Vec<_>>()
);
}
}
}
}
Err(e) => {
println!(" Error: {}", e);
}
}
}
#[cfg(not(target_os = "linux"))]
println!(" Camera detection requires Linux");
}
if show_daemon || (!show_camera && !show_daemon) {
println!("\nDaemon:");
// TODO: Check daemon via D-Bus
println!(" Status: Unknown (D-Bus not implemented)");
}
println!("\nConfiguration:");
println!(" Detection model: {}", config.detection.model);
println!(" Anti-spoofing: {}", if config.anti_spoofing.enabled { "enabled" } else { "disabled" });
println!(" TPM: {}", if config.tpm.enabled { "enabled" } else { "disabled" });
Ok(())
}
async fn cmd_enroll(_config: &Config, label: &str) -> Result<()> {
println!("Enrollment not yet implemented");
println!("Would enroll with label: {}", label);
Ok(())
}
async fn cmd_list(_config: &Config) -> Result<()> {
println!("Enrolled models:");
println!(" (none - enrollment not yet implemented)");
Ok(())
}
async fn cmd_remove(_config: &Config, label: Option<&str>, all: bool) -> Result<()> {
if all {
println!("Would remove ALL enrolled models");
} else if let Some(l) = label {
println!("Would remove model: {}", l);
} else {
println!("Specify a label or --all");
}
Ok(())
}
async fn cmd_test(_config: &Config, verbose: bool, debug: bool) -> Result<()> {
println!("Authentication test not yet implemented");
if verbose {
println!(" (would show confidence scores)");
}
if debug {
println!(" (would save debug frames)");
}
Ok(())
}
async fn cmd_config(config: &Config, json: bool, set: Option<&str>) -> Result<()> {
if let Some(kv) = set {
println!("Setting config values not yet implemented: {}", kv);
return Ok(());
}
if json {
let json = serde_json::to_string_pretty(config)
.map_err(|e| linux_hello_common::Error::Serialization(e.to_string()))?;
println!("{}", json);
} else {
let toml = toml::to_string_pretty(config)
.map_err(|e| linux_hello_common::Error::Serialization(e.to_string()))?;
println!("{}", toml);
}
Ok(())
}