From f99c71e89dfc501525eb1e964db620af53bf86cc Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Fri, 31 Jul 2026 04:23:30 +0200 Subject: [PATCH] feat(cli): --file mode --- cli/src/app/devices.rs | 16 ++++++++++++---- cli/src/app/mod.rs | 22 +++++++++++++++++----- cli/src/app/sending.rs | 10 +++++++++- cli/src/main.rs | 4 ++++ 4 files changed, 42 insertions(+), 10 deletions(-) diff --git a/cli/src/app/devices.rs b/cli/src/app/devices.rs index 7556a1d5..19bff8a2 100644 --- a/cli/src/app/devices.rs +++ b/cli/src/app/devices.rs @@ -65,22 +65,30 @@ impl App { rows } - pub(super) fn handle_device_list_key(&mut self, key: KeyEvent) { + /// Handles a key while the list is open; returns `true` when the + /// application should quit (the list was cancelled in `--file` mode). + pub(super) fn handle_device_list_key(&mut self, key: KeyEvent) -> bool { let Some(list) = &mut self.device_list else { - return; + return false; }; match list.handle_key(key) { DeviceListOutcome::Open => {} - DeviceListOutcome::Closed => self.close_device_list(), + DeviceListOutcome::Closed => { + self.close_device_list(); + return !self.preselected.is_empty(); + } DeviceListOutcome::Send { fingerprint } => { self.close_device_list(); - if let Some(device) = self.registry.by_fingerprint(&fingerprint).cloned() { + if !self.preselected.is_empty() { + self.start_send(&fingerprint, self.preselected.clone()); + } else if let Some(device) = self.registry.by_fingerprint(&fingerprint).cloned() { self.open_picker(device); } } DeviceListOutcome::Pair { fingerprint } => self.pair(&fingerprint), DeviceListOutcome::Unpair { fingerprint } => self.unpair(&fingerprint), } + false } pub(super) fn close_device_list(&mut self) { diff --git a/cli/src/app/mod.rs b/cli/src/app/mod.rs index 22fb253b..c76e5fe5 100644 --- a/cli/src/app/mod.rs +++ b/cli/src/app/mod.rs @@ -20,6 +20,7 @@ use localsend::multicast::{ }; use receive::{Answer, PendingReceive, ReceiveSession}; use sending::SendState; +use std::path::PathBuf; use std::sync::Arc; use std::time::Duration; use tokio::sync::{mpsc, oneshot}; @@ -73,10 +74,16 @@ struct App { picker: Option, device_list: Option, + /// Files given via `-f`/`--file`; sending uses these instead of the picker. + preselected: Vec, + events_tx: mpsc::Sender, } pub async fn run(args: Args) -> anyhow::Result<()> { + for path in &args.file { + anyhow::ensure!(path.is_file(), "Not a file: {}", path.display()); + } let storage = storage::Repository::load(&args)?; let identity = storage.identity.clone(); let (events_tx, mut events_rx) = mpsc::channel::(64); @@ -159,10 +166,14 @@ pub async fn run(args: Args) -> anyhow::Result<()> { send: None, picker: None, device_list: None, + preselected: args.file, events_tx: events_tx.clone(), }; - app.ui.log_plain(&crate::banner::render(&app.storage)); + match app.preselected.is_empty() { + true => app.ui.log_plain(&crate::banner::render(&app.storage)), + false => app.open_device_list(), + } let mut tick = tokio::time::interval(Duration::from_millis(250)); tick.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); @@ -200,7 +211,6 @@ pub async fn run(args: Args) -> anyhow::Result<()> { if let Some(multicast) = &multicast { let _ = tokio::time::timeout(Duration::from_secs(1), multicast.wait_stopped()).await; } - println!("Bye!"); Ok(()) } @@ -241,6 +251,8 @@ impl App { AppEvent::SendEnded => { self.send = None; self.render_status(); + // In `--file` mode the transfer is the whole program. + return !self.preselected.is_empty(); } AppEvent::Log { category, text } => self.ui.log(category, &text), } @@ -258,8 +270,7 @@ impl App { return false; } if self.device_list.is_some() { - self.handle_device_list_key(key); - return false; + return self.handle_device_list_key(key); } if let KeyCode::Char(c) = key.code { @@ -286,7 +297,8 @@ impl App { } if self.device_list.is_some() { self.close_device_list(); - return false; + // In `--file` mode the device list is the whole program. + return !self.preselected.is_empty(); } let mut cancelled = false; if self.pending.is_some() { diff --git a/cli/src/app/sending.rs b/cli/src/app/sending.rs index 09c77bf5..b3fcb456 100644 --- a/cli/src/app/sending.rs +++ b/cli/src/app/sending.rs @@ -33,6 +33,10 @@ impl App { .log(Category::Send, &format!("No device on [{slot}]")); return; }; + if !self.preselected.is_empty() { + self.start_send(&device.fingerprint, self.preselected.clone()); + return; + } self.open_picker(device); } @@ -76,7 +80,11 @@ impl App { } } - fn start_send(&mut self, fingerprint: &str, picked: Vec) { + pub(super) fn start_send(&mut self, fingerprint: &str, picked: Vec) { + if self.send.is_some() { + self.ui.log(Category::Send, "A send is already in progress"); + return; + } let Some(device) = self.registry.by_fingerprint(fingerprint).cloned() else { return; }; diff --git a/cli/src/main.rs b/cli/src/main.rs index ae0f7ba1..521367e6 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -26,6 +26,10 @@ pub struct Args { /// Directory where received files are saved [default: config.toml, else the Downloads folder] #[arg(long, env = "LOCALSEND_DESTINATION")] pub destination: Option, + + /// File to send: opens the device list on start, selecting a device starts the transfer (repeatable) + #[arg(short, long = "file", value_name = "PATH")] + pub file: Vec, } const HELP_SECTIONS: &str = "Events:\n \