Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: sort the peripherals list #16

Merged
merged 5 commits into from
Nov 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use clap::CommandFactory;

#[path = "src/cli_args.rs"]
#[allow(dead_code)]
mod cli_args;

fn main() -> std::io::Result<()> {
Expand Down
84 changes: 79 additions & 5 deletions src/bluetooth.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::cli_args::{GeneralSort, GeneralSortable};
use crate::error::{Error, Result};
use crate::tui::ui::StableListItem;
use crate::Ctx;
Expand All @@ -13,6 +14,7 @@ use tokio::time::{self, sleep, timeout};

pub mod ble_default_services;

const DEFAULT_DEVICE_NAME: &str = "Unknown device";
const TIMEOUT: Duration = Duration::from_secs(10);

pub async fn disconnect_with_timeout(peripheral: &btleplug::platform::Peripheral) {
Expand Down Expand Up @@ -49,6 +51,24 @@ pub struct HandledPeripheral<TPer: Peripheral = btleplug::platform::Peripheral>
pub services_names: Vec<Cow<'static, str>>,
}

impl GeneralSortable for HandledPeripheral {
const AVAILABLE_SORTS: &'static [GeneralSort] = &[GeneralSort::Name, GeneralSort::DefaultSort];

fn cmp(&self, sort: &GeneralSort, a: &Self, b: &Self) -> std::cmp::Ordering {
match sort {
// Specifically put all the "unknown devices" to the end of the list.
GeneralSort::Name if a.name == b.name && a.name == DEFAULT_DEVICE_NAME => {
std::cmp::Ordering::Equal
}
GeneralSort::Name if b.name == DEFAULT_DEVICE_NAME => std::cmp::Ordering::Less,
GeneralSort::Name if a.name == DEFAULT_DEVICE_NAME => std::cmp::Ordering::Greater,

GeneralSort::Name => a.name.to_lowercase().cmp(&b.name.to_lowercase()),
GeneralSort::DefaultSort => a.rssi.cmp(&b.rssi),
}
}
}

impl StableListItem<PeripheralId> for HandledPeripheral {
fn id(&self) -> PeripheralId {
self.ble_peripheral.id()
Expand All @@ -67,13 +87,53 @@ pub struct ConnectedCharacteristic {
pub service_uuid: uuid::Uuid,
}

impl GeneralSortable for ConnectedCharacteristic {
const AVAILABLE_SORTS: &'static [GeneralSort] = &[GeneralSort::Name, GeneralSort::DefaultSort];

fn cmp(&self, sort: &GeneralSort, a: &Self, b: &Self) -> std::cmp::Ordering {
match sort {
GeneralSort::Name => {
if a.service_name() == b.service_name() && a.char_name() == b.char_name() {
return std::cmp::Ordering::Equal;
}

if a.has_readable_char_name() && !b.has_readable_char_name() {
return std::cmp::Ordering::Less;
}

if !a.has_readable_char_name() && b.has_readable_char_name() {
return std::cmp::Ordering::Greater;
}

(
a.service_name().to_lowercase(),
a.char_name().to_lowercase(),
)
.cmp(&(
b.service_name().to_lowercase(),
b.char_name().to_lowercase(),
))
}
GeneralSort::DefaultSort => (a.service_uuid, a.uuid).cmp(&(b.service_uuid, b.uuid)),
}
}
}

impl StableListItem<uuid::Uuid> for ConnectedCharacteristic {
fn id(&self) -> uuid::Uuid {
self.uuid
}
}

impl ConnectedCharacteristic {
pub fn has_readable_char_name(&self) -> bool {
self.custom_char_name.is_some() || self.standard_gatt_char_name.is_some()
}

pub fn has_readable_service_name(&self) -> bool {
self.custom_service_name.is_some() || self.standard_gatt_service_name.is_some()
}

pub fn char_name(&self) -> Cow<'_, str> {
if let Some(custom_name) = &self.custom_char_name {
return Cow::from(format!("{} ({})", custom_name, self.uuid));
Expand Down Expand Up @@ -110,9 +170,17 @@ pub struct ConnectedPeripheral {
}

impl ConnectedPeripheral {
pub fn apply_sort(&mut self, ctx: &Ctx) {
let options = ctx.general_options.read();

if let Ok(options) = options.as_ref() {
options.sort.sort(&mut self.characteristics)
}
}

pub fn new(ctx: &Ctx, peripheral: HandledPeripheral) -> Self {
let chars = peripheral.ble_peripheral.characteristics();
let characteristics = chars
let characteristics: Vec<_> = chars
.into_iter()
.map(|char| ConnectedCharacteristic {
custom_char_name: ctx
Expand All @@ -137,10 +205,13 @@ impl ConnectedPeripheral {
})
.collect();

Self {
let mut view = Self {
peripheral,
characteristics,
}
};

view.apply_sort(ctx);
view
}
}

Expand Down Expand Up @@ -170,7 +241,7 @@ pub async fn start_scan(context: Arc<Ctx>) -> Result<()> {
.map(Peripheral::properties)
.collect::<Vec<_>>();

let peripherals = try_join_all(properties_futures)
let mut peripherals = try_join_all(properties_futures)
.await?
.into_iter()
.zip(peripherals.into_iter())
Expand All @@ -179,7 +250,7 @@ pub async fn start_scan(context: Arc<Ctx>) -> Result<()> {
let name_unset = properties.local_name.is_none();
let name = properties
.local_name
.unwrap_or_else(|| "Unknown device".to_string());
.unwrap_or_else(|| DEFAULT_DEVICE_NAME.to_string());

HandledPeripheral {
ble_peripheral: peripheral,
Expand Down Expand Up @@ -215,6 +286,9 @@ pub async fn start_scan(context: Arc<Ctx>) -> Result<()> {
})
.collect::<Vec<_>>();

let sort = context.general_options.read()?.sort;
sort.sort(&mut peripherals);

context.latest_scan.write()?.replace(BleScan {
peripherals,
sync_time: chrono::Local::now(),
Expand Down
36 changes: 35 additions & 1 deletion src/cli_args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,35 @@
std::fs::remove_file(test_path).expect("Unable to delete file");
}

#[derive(Default, PartialEq, Eq, Debug, Clone, Copy, clap::ValueEnum)]
pub enum GeneralSort {
Name,
#[default]
/// The default sort based on the trait implementer.
DefaultSort,
}

impl GeneralSort {
pub fn sort<T: GeneralSortable>(&self, data: &mut [T]) {
let should_sort = T::AVAILABLE_SORTS.contains(&self);

if should_sort {
data.sort_by(|a, b| a.cmp(self, a, b));
}
}
}

pub trait GeneralSortable {
const AVAILABLE_SORTS: &'static [GeneralSort];
fn cmp(&self, sort: &GeneralSort, a: &Self, b: &Self) -> std::cmp::Ordering;
}

impl GeneralSort {
pub fn apply_sort<T: GeneralSortable>(&self, a: &T, b: &T) -> std::cmp::Ordering {

Check warning on line 92 in src/cli_args.rs

View workflow job for this annotation

GitHub Actions / Test (stable)

method `apply_sort` is never used

Check warning on line 92 in src/cli_args.rs

View workflow job for this annotation

GitHub Actions / Test (stable)

method `apply_sort` is never used

Check warning on line 92 in src/cli_args.rs

View workflow job for this annotation

GitHub Actions / Test (beta)

method `apply_sort` is never used

Check warning on line 92 in src/cli_args.rs

View workflow job for this annotation

GitHub Actions / Test (beta)

method `apply_sort` is never used

Check warning on line 92 in src/cli_args.rs

View workflow job for this annotation

GitHub Actions / Test (nightly)

method `apply_sort` is never used

Check warning on line 92 in src/cli_args.rs

View workflow job for this annotation

GitHub Actions / Test (nightly)

method `apply_sort` is never used

Check warning on line 92 in src/cli_args.rs

View workflow job for this annotation

GitHub Actions / Test (nightly)

method `apply_sort` is never used
a.cmp(self, a, b)
}
}

#[derive(Debug, Parser)]
#[command(
version=env!("CARGO_PKG_VERSION"),
Expand All @@ -79,7 +108,7 @@
#[clap(default_value_t = 0)]
pub adapter_index: usize,

#[clap(long, short)]
#[clap(long, short = 'i')]
/// Scan interval in milliseconds.
#[clap(default_value_t = 1000)]
pub scan_interval: u64,
Expand Down Expand Up @@ -109,4 +138,9 @@
/// ```
#[clap(long, value_parser = clap::builder::ValueParser::new(parse_name_map))]
pub names_map_file: Option<HashMap<uuid::Uuid, String>>,

/// Default sort type for all the views and lists.
#[clap(long)]
#[arg(value_enum)]
pub sort: Option<GeneralSort>,
}
37 changes: 37 additions & 0 deletions src/general_options.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
use std::sync::Arc;

use cli_args::GeneralSort;
use crossterm::event::KeyCode;

use crate::{
cli_args::{self, Args},
Ctx,
};

#[derive(Default, Debug)]
pub struct GeneralOptions {
pub sort: GeneralSort,
}

impl GeneralOptions {
pub fn new(args: &Args) -> Self {
Self {
sort: args.sort.unwrap_or_default(),
}
}

pub fn handle_keystroke(keycode: &KeyCode, ctx: &Arc<Ctx>) -> bool {
match keycode {
KeyCode::Char('n') => {
let mut general_options = ctx.general_options.write().unwrap();
general_options.sort = match general_options.sort {
GeneralSort::Name => GeneralSort::DefaultSort,
GeneralSort::DefaultSort => GeneralSort::Name,
};

true
}
_ => false,
}
}
}
6 changes: 5 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@
mod bluetooth;
mod cli_args;
mod error;
mod general_options;
mod route;
mod tui;

use crate::{bluetooth::BleScan, tui::run_tui_app};
use btleplug::platform::Manager;
use clap::Parser;
use cli_args::Args;
use general_options::GeneralOptions;
use std::sync::RwLock;
use std::sync::{Arc, Mutex, RwLockReadGuard};

Expand All @@ -21,6 +23,7 @@ pub struct Ctx {
active_side_effect_handle: Mutex<Option<tokio::task::JoinHandle<()>>>,
request_scan_restart: Mutex<bool>,
global_error: Mutex<Option<crate::error::Error>>,
general_options: RwLock<general_options::GeneralOptions>,
}

impl Ctx {
Expand All @@ -37,7 +40,6 @@ impl Ctx {
async fn main() {
let args = Args::parse();
let ctx = Arc::new(Ctx {
args,
latest_scan: RwLock::new(None),
active_route: RwLock::new(route::Route::PeripheralList),
active_side_effect_handle: Mutex::new(None),
Expand All @@ -46,6 +48,8 @@ async fn main() {
.expect("Can not establish BLE connection."),
request_scan_restart: Mutex::new(false),
global_error: Mutex::new(None),
general_options: RwLock::new(GeneralOptions::new(&args)),
args,
});

let ctx_clone = Arc::clone(&ctx);
Expand Down
61 changes: 32 additions & 29 deletions src/tui/connection_view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ impl AppRoute for ConnectionView {

use ansi_to_tui::IntoText;
if let Ok(output) = hexyl_output_buf.into_text() {
text.extend(output.into_iter());
text.extend(output);
} else {
tracing::error!(
?hexyl_output_buf,
Expand Down Expand Up @@ -331,34 +331,37 @@ impl AppRoute for ConnectionView {
f.render_widget(paragraph, chunks[0]);
if chunks[1].height > 0 {
f.render_widget(
block::render_help([
Some(("<-", "Previous value", false)),
Some(("->", "Next value", false)),
Some(("d", "[D]isconnect from device", false)),
Some(("u", "Parse numeric as [u]nsigned", self.unsigned_numbers)),
Some(("f", "Parse numeric as [f]loats", self.float_numbers)),
historical_index.map(|_| {
(
"l",
"Go to the [l]atest values",
self.highlight_copy_char_renders_delay_stack > 0,
)
}),
self.clipboard.as_ref().map(|_| {
(
"c",
"Copy [c]haracteristic UUID",
self.highlight_copy_char_renders_delay_stack > 0,
)
}),
self.clipboard.as_ref().map(|_| {
(
"s",
"Copy [s]ervice UUID",
self.highlight_copy_service_renders_delay_stack > 0,
)
}),
]),
block::render_help(
Arc::clone(&self.ctx),
[
Some(("<-", "Previous value", false)),
Some(("->", "Next value", false)),
Some(("d", "[D]isconnect from device", false)),
Some(("u", "Parse numeric as [u]nsigned", self.unsigned_numbers)),
Some(("f", "Parse numeric as [f]loats", self.float_numbers)),
historical_index.map(|_| {
(
"l",
"Go to the [l]atest values",
self.highlight_copy_char_renders_delay_stack > 0,
)
}),
self.clipboard.as_ref().map(|_| {
(
"c",
"Copy [c]haracteristic UUID",
self.highlight_copy_char_renders_delay_stack > 0,
)
}),
self.clipboard.as_ref().map(|_| {
(
"s",
"Copy [s]ervice UUID",
self.highlight_copy_service_renders_delay_stack > 0,
)
}),
],
),
chunks[1],
);
}
Expand Down
Loading
Loading