Skip to content

Commit

Permalink
Implement general purpose settings for sort
Browse files Browse the repository at this point in the history
Make sort enum and share it with other views that we want to search
  • Loading branch information
dmtrKovalenko committed Nov 26, 2023
1 parent 9dc7635 commit 1003a61
Show file tree
Hide file tree
Showing 9 changed files with 229 additions and 62 deletions.
59 changes: 52 additions & 7 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,22 @@ pub struct HandledPeripheral<TPer: Peripheral = btleplug::platform::Peripheral>
pub services_names: Vec<Cow<'static, str>>,
}

impl GeneralSortable for HandledPeripheral {
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,6 +85,22 @@ pub struct ConnectedCharacteristic {
pub service_uuid: uuid::Uuid,
}

impl GeneralSortable for ConnectedCharacteristic {
fn cmp(&self, sort: &GeneralSort, a: &Self, b: &Self) -> std::cmp::Ordering {
match sort {
GeneralSort::Name => (
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
Expand Down Expand Up @@ -110,9 +144,18 @@ 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() {
self.characteristics
.sort_by(|a, b| options.sort.apply_sort(a, b))
}
}

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 +180,13 @@ impl ConnectedPeripheral {
})
.collect();

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

view.apply_sort(&ctx);
view
}
}

Expand Down Expand Up @@ -179,7 +225,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,9 +261,8 @@ pub async fn start_scan(context: Arc<Ctx>) -> Result<()> {
})
.collect::<Vec<_>>();

if *context.sort_by_name.lock().unwrap() {
peripherals.sort_by(|p1, p2| (&p1.name, p1.address).cmp(&(&p2.name, p2.address)));
}
let sort = context.general_options.read()?.sort;
peripherals.sort_by(|p1, p2| sort.apply_sort(p1, p2));

context.latest_scan.write()?.replace(BleScan {
peripherals,
Expand Down
26 changes: 23 additions & 3 deletions src/cli_args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,25 @@ fn test_parse_name_map() {
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,
}

pub trait GeneralSortable {
fn cmp(&self, sort: &GeneralSort, a: &Self, b: &Self) -> std::cmp::Ordering;
}

impl GeneralSort {
#[allow(dead_code)] // ? It is actually used in the code, some clippy bug
pub fn apply_sort<T: GeneralSortable>(&self, a: &T, b: &T) -> std::cmp::Ordering {
a.cmp(self, a, b)
}
}

#[derive(Debug, Parser)]
#[command(
version=env!("CARGO_PKG_VERSION"),
Expand All @@ -79,7 +98,7 @@ pub struct Args {
#[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 @@ -110,7 +129,8 @@ pub struct Args {
#[clap(long, value_parser = clap::builder::ValueParser::new(parse_name_map))]
pub names_map_file: Option<HashMap<uuid::Uuid, String>>,

/// Sort peripherals by name
/// Default sort type for all the views and lists.
#[clap(long)]
pub sort_by_name: bool,
#[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,
};

return true;
}
_ => false,
}
}
}
9 changes: 5 additions & 4 deletions 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,7 +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>>,
sort_by_name: Mutex<bool>,
general_options: RwLock<general_options::GeneralOptions>,
}

impl Ctx {
Expand All @@ -37,9 +39,7 @@ impl Ctx {
#[tokio::main]
async fn main() {
let args = Args::parse();
let sort_by_name = args.sort_by_name;
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 @@ -48,7 +48,8 @@ async fn main() {
.expect("Can not establish BLE connection."),
request_scan_restart: Mutex::new(false),
global_error: Mutex::new(None),
sort_by_name: Mutex::new(sort_by_name),
general_options: RwLock::new(GeneralOptions::new(&args)),
args,
});

let ctx_clone = Arc::clone(&ctx);
Expand Down
59 changes: 31 additions & 28 deletions src/tui/connection_view.rs
Original file line number Diff line number Diff line change
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
29 changes: 19 additions & 10 deletions src/tui/peripheral_list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use crate::error::Result;
use crate::tui::ui::{block, list::StableListState, search_input, BlendrBlock, ShouldUpdate};
use crate::tui::ui::{HandleInputResult, StableIndexList};
use crate::tui::{AppRoute, HandleKeydownResult};
use crate::GeneralOptions;
use crate::{route::Route, Ctx};
use btleplug::platform::PeripheralId;
use crossterm::event::{KeyCode, KeyEvent};
Expand Down Expand Up @@ -95,6 +96,10 @@ impl AppRoute for PeripheralList {
self.list_state
.stabilize_selected_index(&filtered_peripherals);

if GeneralOptions::handle_keystroke(&key.code, &self.ctx) {
return HandleKeydownResult::Handled;
}

match self.focus {
Focus::Search => {
search_input::handle_search_input(&mut self.search, key);
Expand Down Expand Up @@ -140,8 +145,8 @@ impl AppRoute for PeripheralList {
}
KeyCode::Char('u') => self.to_remove_unknowns = !self.to_remove_unknowns,
KeyCode::Char('s') => {
let mut sort_by_name = self.ctx.sort_by_name.lock().unwrap();
*sort_by_name = !*sort_by_name;
// let mut sort_by_name = self.ctx.sort_by_name.lock().unwrap();
// *sort_by_name = !*sort_by_name;
}
_ => {}
}
Expand Down Expand Up @@ -269,14 +274,18 @@ impl AppRoute for PeripheralList {
f.render_stateful_widget(items, chunks[1], self.list_state.get_ratatui_state());
if chunks[2].height > 0 {
f.render_widget(
block::render_help([
Some(("q", "Quit", false)),
Some(("u", "Hide unknown devices", self.to_remove_unknowns)),
Some(("->", "Connect to device", false)),
Some(("r", "Restart scan", false)),
Some(("s", "Sort by name", *self.ctx.sort_by_name.lock().unwrap())),
Some(("h/j or arrows", "Navigate", false)),
]),
block::render_help(
Arc::clone(&self.ctx),
[
Some(("q", "Quit", false)),
Some(("u", "Hide unknown devices", self.to_remove_unknowns)),
Some(("->", "Connect to device", false)),
Some(("r", "Restart scan", false)),
// FIXME genearl sort
// Some(("s", "Sort by name", *self.ctx.sort_by_name.lock().unwrap())),
Some(("h/j or arrows", "Navigate", false)),
],
),
chunks[2],
);
}
Expand Down
Loading

0 comments on commit 1003a61

Please sign in to comment.