feat(cli): --file mode
CI / format (push) Has been cancelled
CI / test (push) Has been cancelled
CI / packaging (push) Has been cancelled

This commit is contained in:
Tien Do Nam
2026-07-31 04:23:30 +02:00
parent 78445157ae
commit f99c71e89d
4 changed files with 42 additions and 10 deletions
+12 -4
View File
@@ -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) {
+17 -5
View File
@@ -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<Picker>,
device_list: Option<DeviceList>,
/// Files given via `-f`/`--file`; sending uses these instead of the picker.
preselected: Vec<PathBuf>,
events_tx: mpsc::Sender<AppEvent>,
}
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::<AppEvent>(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() {
+9 -1
View File
@@ -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<PathBuf>) {
pub(super) fn start_send(&mut self, fingerprint: &str, picked: Vec<PathBuf>) {
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;
};
+4
View File
@@ -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<PathBuf>,
/// 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<PathBuf>,
}
const HELP_SECTIONS: &str = "Events:\n \