From 706e3e1468908383ced27609b62584620e2a5f2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C3=85kesson?= Date: Wed, 17 Sep 2025 20:37:36 +0200 Subject: [PATCH 1/5] Begin implementing. But we need mut ref... --- src/app.rs | 12 ++++++++++-- src/bluetooth.rs | 5 +++++ src/config.rs | 8 ++++++++ src/handler.rs | 13 +++++++++++++ 4 files changed, 36 insertions(+), 2 deletions(-) diff --git a/src/app.rs b/src/app.rs index 4f71840..de9881e 100644 --- a/src/app.rs +++ b/src/app.rs @@ -392,8 +392,16 @@ impl App { } //Paired devices - let rows: Vec = selected_controller - .paired_devices + let mut paired_devices_sorted = selected_controller.paired_devices.clone(); + 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, + } + }); + let rows: Vec = paired_devices_sorted .iter() .map(|d| { Row::new(vec![ diff --git a/src/bluetooth.rs b/src/bluetooth.rs index 1911bcc..480a48a 100644 --- a/src/bluetooth.rs +++ b/src/bluetooth.rs @@ -34,6 +34,7 @@ pub struct Device { pub is_paired: bool, pub is_trusted: bool, pub is_connected: bool, + pub is_favorite: bool, pub battery_percentage: Option, } @@ -43,6 +44,10 @@ impl Device { Ok(()) } + pub fn toggle_favorite(&mut self) -> () { + self.is_favorite = !self.is_favorite; + } + // https://specifications.freedesktop.org/icon-naming-spec/icon-naming-spec-latest.html pub fn get_icon(name: &str) -> Option { match name { 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..50b7da8 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -545,6 +545,19 @@ 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[selected_controller]; + if let Some(index) = app.paired_devices_state.selected() { + let device = &controller.paired_devices[index]; + device.toggle_favorite(); + } + } + } + _ => {} } } From adbbef75cd15464afb20880ad669c70fc5f24bfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C3=85kesson?= Date: Thu, 18 Sep 2025 10:54:47 +0200 Subject: [PATCH 2/5] Add keybind to Readme --- Readme.md | 3 +++ 1 file changed, 3 insertions(+) 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 From b2256432b85b549180a781ae99d04ba042f4bc6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C3=85kesson?= Date: Thu, 18 Sep 2025 10:55:14 +0200 Subject: [PATCH 3/5] Try using get_mut() to get mutable reference. --- src/bluetooth.rs | 4 ---- src/handler.rs | 21 ++++++++++++++++++--- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/bluetooth.rs b/src/bluetooth.rs index 480a48a..b67cac3 100644 --- a/src/bluetooth.rs +++ b/src/bluetooth.rs @@ -44,10 +44,6 @@ impl Device { Ok(()) } - pub fn toggle_favorite(&mut self) -> () { - self.is_favorite = !self.is_favorite; - } - // https://specifications.freedesktop.org/icon-naming-spec/icon-naming-spec-latest.html pub fn get_icon(name: &str) -> Option { match name { diff --git a/src/handler.rs b/src/handler.rs index 50b7da8..d44809f 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -550,10 +550,25 @@ pub async fn handle_key_events( if let Some(selected_controller) = app.controller_state.selected() { - let controller = &app.controllers[selected_controller]; + let controller = app + .controllers + .get_mut(selected_controller) + .expect("Selected controller should be valid"); + if let Some(index) = app.paired_devices_state.selected() { - let device = &controller.paired_devices[index]; - device.toggle_favorite(); + match controller.paired_devices.get_mut(index) { + Some(device) => { + device.is_favorite = !device.is_favorite; + } + None => { + Notification::send( + "Selected device should be valid" + .to_string(), + NotificationLevel::Error, + sender.clone(), + )?; + } + } } } } From 34cb92cad698c7d054a77433ab48a3ce9118477f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C3=85kesson?= Date: Sat, 25 Oct 2025 22:03:14 +0200 Subject: [PATCH 4/5] Almost working, suboptimal and broken. - We should stop reading from disk everytime. - Only visually changes order. This MUST BE FIXED. - Need to add keybind to hint in footer. --- src/app.rs | 9 +++++++++ src/bluetooth.rs | 52 +++++++++++++++++++++++++++++++++++++++++++++++- src/handler.rs | 11 +++++++++- 3 files changed, 70 insertions(+), 2 deletions(-) diff --git a/src/app.rs b/src/app.rs index de9881e..a7d9f24 100644 --- a/src/app.rs +++ b/src/app.rs @@ -405,6 +405,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) @@ -470,6 +475,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), @@ -484,6 +490,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)), @@ -493,6 +500,7 @@ impl App { .bottom_margin(1) } else { Row::new(vec![ + Cell::from("★"), Cell::from("Name"), Cell::from("Trusted"), Cell::from("Connected"), @@ -502,6 +510,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)), diff --git a/src/bluetooth.rs b/src/bluetooth.rs index b67cac3..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; @@ -34,8 +35,8 @@ pub struct Device { pub is_paired: bool, pub is_trusted: bool, pub is_connected: bool, - pub is_favorite: bool, pub battery_percentage: Option, + pub is_favorite: bool, } impl Device { @@ -59,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 { @@ -112,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, @@ -122,6 +171,7 @@ impl Controller { is_trusted, is_connected, battery_percentage, + is_favorite, }; if dev.is_paired { diff --git a/src/handler.rs b/src/handler.rs index d44809f..1213636 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -558,7 +558,16 @@ pub async fn handle_key_events( if let Some(index) = app.paired_devices_state.selected() { match controller.paired_devices.get_mut(index) { Some(device) => { - device.is_favorite = !device.is_favorite; + 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( From ec432f9e1b7b59940fa74edb9f49865afc472936 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C3=85kesson?= Date: Sat, 25 Oct 2025 22:10:13 +0200 Subject: [PATCH 5/5] Move sorting to a better place. Fix bug where visual was not in sync. --- src/app.rs | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/app.rs b/src/app.rs index a7d9f24..34f20e7 100644 --- a/src/app.rs +++ b/src/app.rs @@ -392,16 +392,8 @@ impl App { } //Paired devices - let mut paired_devices_sorted = selected_controller.paired_devices.clone(); - 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, - } - }); - let rows: Vec = paired_devices_sorted + let rows: Vec = selected_controller + .paired_devices .iter() .map(|d| { Row::new(vec![ @@ -888,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