diff --git a/Readme.md b/Readme.md index beba688..ab6e8aa 100644 --- a/Readme.md +++ b/Readme.md @@ -99,6 +99,8 @@ This will produce an executable file at `target/release/bluetui` that you can co `e`: Rename the device. +`f`: Favorite/Unfavorite the device. + ### New devices `Space or Enter`: Pair the device. @@ -119,6 +121,7 @@ toggle_discovery = "d" unpair = "u" toggle_trust = "t" rename = "e" +toggle_favorite = "f" ``` ## ⚖️ License diff --git a/src/app.rs b/src/app.rs index 4f71840..34f20e7 100644 --- a/src/app.rs +++ b/src/app.rs @@ -397,6 +397,11 @@ impl App { .iter() .map(|d| { Row::new(vec![ + if d.is_favorite { + "★".to_string() + } else { + " ".to_string() + }, { if let Some(icon) = &d.icon { format!("{} {}", icon, &d.alias) @@ -462,6 +467,7 @@ impl App { .any(|device| device.battery_percentage.is_some()); let mut widths = vec![ + Constraint::Max(1), Constraint::Max(25), Constraint::Length(7), Constraint::Length(9), @@ -476,6 +482,7 @@ impl App { if show_battery_column { if self.focused_block == FocusedBlock::PairedDevices { Row::new(vec![ + Cell::from("★").style(Style::default().fg(Color::Yellow)), Cell::from("Name").style(Style::default().fg(Color::Yellow)), Cell::from("Trusted").style(Style::default().fg(Color::Yellow)), Cell::from("Connected").style(Style::default().fg(Color::Yellow)), @@ -485,6 +492,7 @@ impl App { .bottom_margin(1) } else { Row::new(vec![ + Cell::from("★"), Cell::from("Name"), Cell::from("Trusted"), Cell::from("Connected"), @@ -494,6 +502,7 @@ impl App { } } else if self.focused_block == FocusedBlock::PairedDevices { Row::new(vec![ + Cell::from("★").style(Style::default().fg(Color::Yellow)), Cell::from("Name").style(Style::default().fg(Color::Yellow)), Cell::from("Trusted").style(Style::default().fg(Color::Yellow)), Cell::from("Connected").style(Style::default().fg(Color::Yellow)), @@ -871,7 +880,17 @@ impl App { controller.is_powered = refreshed_controller.is_powered; controller.is_pairable = refreshed_controller.is_pairable; controller.is_discoverable = refreshed_controller.is_discoverable; - controller.paired_devices = refreshed_controller.paired_devices; + + let mut paired_devices_sorted = refreshed_controller.paired_devices; + paired_devices_sorted.sort_by(|a, b| { + use std::cmp::Ordering; + match (a.is_favorite, b.is_favorite) { + (true, false) => Ordering::Less, + (false, true) => Ordering::Greater, + _ => Ordering::Equal, + } + }); + controller.paired_devices = paired_devices_sorted; controller.new_devices = refreshed_controller.new_devices; } else { // Add new detected adapters diff --git a/src/bluetooth.rs b/src/bluetooth.rs index 1911bcc..b8c404d 100644 --- a/src/bluetooth.rs +++ b/src/bluetooth.rs @@ -8,6 +8,7 @@ use bluer::{ use bluer::Device as BTDevice; +use clap::crate_name; use tokio::sync::oneshot; use crate::app::AppResult; @@ -35,6 +36,7 @@ pub struct Device { pub is_trusted: bool, pub is_connected: bool, pub battery_percentage: Option, + pub is_favorite: bool, } impl Device { @@ -58,6 +60,53 @@ impl Device { _ => None, } } + + pub async fn get_if_favorite(addr: Address) -> bool { + let state_dir = dirs::state_dir().expect("Could not find state directory."); + let favorite_addrs_file = state_dir.join(crate_name!()).join("favorite_addrs.txt"); + if !favorite_addrs_file.exists() { + return false; + } + let contents = tokio::fs::read_to_string(favorite_addrs_file).await; + if let Ok(contents) = contents { + for line in contents.lines() { + if line.trim() == addr.to_string() { + return true; + } + } + } + false + } + + pub async fn toggle_favorite(&self) -> AppResult<()> { + let state_dir = dirs::state_dir().expect("Could not find state directory."); + let favorite_addrs_dir = state_dir.join(crate_name!()); + if !favorite_addrs_dir.exists() { + tokio::fs::create_dir_all(&favorite_addrs_dir).await?; + } + let favorite_addrs_file = favorite_addrs_dir.join("favorite_addrs.txt"); + + let mut favorite_addrs: Vec = Vec::new(); + if favorite_addrs_file.exists() { + let contents = tokio::fs::read_to_string(&favorite_addrs_file).await?; + for line in contents.lines() { + favorite_addrs.push(line.trim().to_string()); + } + } + + if self.is_favorite { + // remove from favorites + favorite_addrs.retain(|addr| addr != &self.addr.to_string()); + } else { + // add to favorites + favorite_addrs.push(self.addr.to_string()); + } + + let new_contents = favorite_addrs.join("\n"); + tokio::fs::write(&favorite_addrs_file, new_contents).await?; + + Ok(()) + } } impl Controller { @@ -111,6 +160,7 @@ impl Controller { let is_trusted = device.is_trusted().await?; let is_connected = device.is_connected().await?; let battery_percentage = device.battery_percentage().await?; + let is_favorite = Device::get_if_favorite(addr).await; let dev = Device { device, @@ -121,6 +171,7 @@ impl Controller { is_trusted, is_connected, battery_percentage, + is_favorite, }; if dev.is_paired { diff --git a/src/config.rs b/src/config.rs index 8927b50..446c12b 100644 --- a/src/config.rs +++ b/src/config.rs @@ -47,6 +47,9 @@ pub struct PairedDevice { #[serde(default = "default_set_new_name")] pub rename: char, + + #[serde(default = "default_toggle_device_favorite")] + pub toggle_favorite: char, } impl Default for PairedDevice { @@ -55,6 +58,7 @@ impl Default for PairedDevice { unpair: 'u', toggle_trust: 't', rename: 'e', + toggle_favorite: 'f', } } } @@ -87,6 +91,10 @@ fn default_toggle_device_trust() -> char { 't' } +fn default_toggle_device_favorite() -> char { + 'f' +} + impl Config { pub fn new() -> Self { let conf_path = dirs::config_dir() diff --git a/src/handler.rs b/src/handler.rs index 057354b..1213636 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -545,6 +545,43 @@ pub async fn handle_key_events( app.focused_block = FocusedBlock::SetDeviceAliasBox; } + // Favorite / Unfavorite + KeyCode::Char(c) if c == config.paired_device.toggle_favorite => { + if let Some(selected_controller) = + app.controller_state.selected() + { + let controller = app + .controllers + .get_mut(selected_controller) + .expect("Selected controller should be valid"); + + if let Some(index) = app.paired_devices_state.selected() { + match controller.paired_devices.get_mut(index) { + Some(device) => { + device.toggle_favorite().await?; + Notification::send( + if device.is_favorite { + format!("Device `{}` unfavorited", device.alias) + } else { + format!("Device `{}` favorited", device.alias) + }, + NotificationLevel::Info, + sender.clone(), + )?; + } + None => { + Notification::send( + "Selected device should be valid" + .to_string(), + NotificationLevel::Error, + sender.clone(), + )?; + } + } + } + } + } + _ => {} } }