feat(cli): simpler pairing in device list

This commit is contained in:
Tien Do Nam
2026-07-31 02:31:45 +02:00
parent 00f9bad89b
commit 6e990a6f42
2 changed files with 43 additions and 56 deletions
+30
View File
@@ -78,6 +78,7 @@ impl App {
self.open_picker(device);
}
}
DeviceListOutcome::Pair { fingerprint } => self.pair(&fingerprint),
DeviceListOutcome::Unpair { fingerprint } => self.unpair(&fingerprint),
}
}
@@ -89,6 +90,35 @@ impl App {
}
}
/// Pairs a discovered device; the list stays open and refreshes (the
/// device moves up into "Paired"). The log line shows up once the list
/// is closed.
fn pair(&mut self, fingerprint: &str) {
let Some(device) = self.registry.by_fingerprint(fingerprint) else {
return;
};
let alias = device.alias.clone();
match self
.storage
.paired
.insert(fingerprint.to_string(), alias.clone())
{
Ok(()) => self.ui.log(
Category::Discovery,
&format!("{alias}: Paired. Future requests are auto-accepted."),
),
Err(err) => self.ui.log(
Category::Discovery,
&format!("{alias}: Paired for this run, but saving failed: {err:#}"),
),
}
let rows = self.device_rows();
if let Some(list) = &mut self.device_list {
list.set_rows(rows);
list.draw();
}
}
/// Removes a paired device; the list stays open and refreshes (the
/// device reappears under "Discovered" when it is still around). The
/// log line shows up once the list is closed.
+13 -56
View File
@@ -34,7 +34,7 @@ pub struct DeviceRow {
/// not been discovered in this run and therefore cannot be sent to.
pub host: Option<String>,
/// Paired devices offer the remove action.
/// Whether the device is paired; the pair hotkey toggles it.
pub paired: bool,
}
@@ -49,27 +49,19 @@ pub enum DeviceListOutcome {
/// The user wants to send files to this device.
Send { fingerprint: String },
/// The user confirmed removing this paired device.
Unpair { fingerprint: String },
}
/// The user wants to pair this discovered device.
Pair { fingerprint: String },
/// A pending remove confirmation, answered with y/n.
struct Confirm {
fingerprint: String,
alias: String,
/// The user wants to remove this paired device.
Unpair { fingerprint: String },
}
/// A modal list of the paired and discovered devices, rendered on the
/// alternate screen while the log UI is suspended.
///
/// Up/Down navigates, Enter sends to the highlighted device, Delete (or
/// Backspace) removes a paired device after a y/n confirmation, Esc or D
/// closes the list.
pub struct DeviceList {
terminal: Terminal<CrosstermBackend<Stdout>>,
rows: Vec<Row>,
list_state: ListState,
confirm: Option<Confirm>,
}
impl DeviceList {
@@ -82,7 +74,6 @@ impl DeviceList {
terminal,
rows,
list_state: ListState::default(),
confirm: None,
};
list.select_first_device();
list.draw();
@@ -101,13 +92,6 @@ impl DeviceList {
.selected_device()
.map(|device| device.fingerprint.clone());
self.rows = rows;
if let Some(confirm) = &self.confirm
&& !self
.device_rows()
.any(|(_, device)| device.paired && device.fingerprint == confirm.fingerprint)
{
self.confirm = None;
}
let index = highlighted.and_then(|fingerprint| {
self.device_rows()
.find(|(_, device)| device.fingerprint == fingerprint)
@@ -120,23 +104,6 @@ impl DeviceList {
}
pub fn handle_key(&mut self, key: KeyEvent) -> DeviceListOutcome {
// The confirmation prompt consumes every key.
if self.confirm.is_some() {
match key.code {
KeyCode::Char(c) if c.eq_ignore_ascii_case(&'y') => {
let confirm = self.confirm.take().unwrap();
return DeviceListOutcome::Unpair {
fingerprint: confirm.fingerprint,
};
}
KeyCode::Esc => self.confirm = None,
KeyCode::Char(c) if c.eq_ignore_ascii_case(&'n') => self.confirm = None,
_ => {}
}
self.draw();
return DeviceListOutcome::Open;
}
match key.code {
KeyCode::Esc => return DeviceListOutcome::Closed,
KeyCode::Char(c) if c.eq_ignore_ascii_case(&'d') => return DeviceListOutcome::Closed,
@@ -152,16 +119,13 @@ impl DeviceList {
};
}
}
// Backspace, because that is what the key labeled "delete"
// sends on macOS.
KeyCode::Delete | KeyCode::Backspace => {
if let Some(device) = self.selected_device()
&& device.paired
{
self.confirm = Some(Confirm {
fingerprint: device.fingerprint.clone(),
alias: device.alias.clone(),
});
KeyCode::Char(c) if c.eq_ignore_ascii_case(&'p') => {
if let Some(device) = self.selected_device() {
let fingerprint = device.fingerprint.clone();
return match device.paired {
true => DeviceListOutcome::Unpair { fingerprint },
false => DeviceListOutcome::Pair { fingerprint },
};
}
}
_ => {}
@@ -217,16 +181,9 @@ impl DeviceList {
terminal,
rows,
list_state,
confirm,
} = self;
let help = match confirm {
Some(confirm) => Span::styled(
format!(" Remove {}? y/n", confirm.alias),
Style::default().add_modifier(Modifier::BOLD),
),
None => Span::raw(" ↑/↓: navigate Enter: send Del: remove (paired only) Esc: close"),
};
let help = Span::raw(" ↑/↓: navigate Enter: send P: pair/unpair Esc: close");
let _ = terminal.draw(|frame| {
let [main_area, help_area] =
Layout::vertical([Constraint::Fill(1), Constraint::Length(1)]).areas(frame.area());