Skip to content
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ toml = "0.8"
serde = { version = "1", features = ["derive"] }
clap = { version = "4", features = ["derive", "cargo"] }
terminal-light = "1"
tui-input = "0.11"
tui-input = "0.11.1"

[profile.release]
strip = true
Expand Down
104 changes: 104 additions & 0 deletions src/alias_filter.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
use std::sync::Arc;
use std::time::Instant;

use ratatui::{
layout::{Alignment, Constraint, Direction, Layout},
style::{Color, Style, Stylize},
widgets::{Block, BorderType, Borders, Clear, Padding, Row, Table, TableState},
Frame,
};
use tui_input::{Input, InputRequest};

use crate::config::Config;

#[derive(Debug)]
pub struct AliasFilter {
pub filter: Option<String>,
input: Input,
state: TableState,
start: Instant,
}

impl AliasFilter {
pub fn new(_config: Arc<Config>) -> Self {
let mut state = TableState::new().with_offset(0);
state.select(Some(0));

let input = Input::new("".to_string());

let start = Instant::now();

Self {
state,
input,
filter: None,
start,
}
}

pub fn insert_char(&mut self, c: char) {
let req = InputRequest::InsertChar(c);
self.input.handle(req);
self.filter = Some(self.input.value().to_string());
}

pub fn delete_char(&mut self) {
let req = InputRequest::DeletePrevChar;
self.input.handle(req);
self.filter = Some(self.input.value().to_string());
}

pub fn render(&mut self, frame: &mut Frame) {
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Fill(1),
Constraint::Fill(1),
Constraint::Length(5),
])
.flex(ratatui::layout::Flex::SpaceBetween)
.split(frame.area());

let block = Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Fill(1),
Constraint::Min(100),
Constraint::Fill(1),
])
.flex(ratatui::layout::Flex::SpaceBetween)
.split(layout[2])[1];

let mut text = match &self.filter {
Some(f) => f.to_string(),
None => "".to_string(),
};

self.insert_cursor(&mut text);

let row = Row::new(vec![text]);

let table = Table::new([row], [Constraint::Length(20)]).block(
Block::default()
.padding(Padding::uniform(1))
.title(" Filter Device Names ")
.title_style(Style::default().bold().fg(Color::Green))
.title_alignment(Alignment::Center)
.borders(Borders::ALL)
.style(Style::default())
.border_type(BorderType::Thick)
.border_style(Style::default().fg(Color::Green)),
);

frame.render_widget(Clear, block);
frame.render_stateful_widget(table, block, &mut self.state);
}

fn insert_cursor(&self, s: &mut String) {
let time = Instant::now().duration_since(self.start).as_secs();

let c = if time % 2 == 0 { '_' } else { ' ' };

s.insert(self.input.cursor(), c);
}
}
35 changes: 31 additions & 4 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ use ratatui::{
use tui_input::Input;

use crate::{
alias_filter::AliasFilter,
bluetooth::{request_confirmation, Controller},
config::Config,
confirmation::PairingConfirmation,
Expand All @@ -38,6 +39,7 @@ pub enum FocusedBlock {
Help,
PassKeyConfirmation,
SetDeviceAliasBox,
AliasFilterPopup,
}

#[derive(Debug, Clone, Copy, PartialEq)]
Expand All @@ -52,6 +54,7 @@ pub struct App {
pub session: Arc<Session>,
pub agent: AgentHandle,
pub help: Help,
pub alias_filter: AliasFilter,
pub spinner: Spinner,
pub notifications: Vec<Notification>,
pub controllers: Vec<Controller>,
Expand Down Expand Up @@ -110,7 +113,8 @@ impl App {
running: true,
session,
agent: handle,
help: Help::new(config),
help: Help::new(config.clone()),
alias_filter: AliasFilter::new(config.clone()),
spinner: Spinner::default(),
notifications: Vec::new(),
controllers,
Expand Down Expand Up @@ -613,14 +617,37 @@ impl App {
let rows: Vec<Row> = selected_controller
.new_devices
.iter()
.filter(|d| match &self.alias_filter.filter {
Some(pattern) => d.alias.contains(pattern),
None => true,
})
.map(|d| {
Row::new(vec![d.addr.to_string(), {
let device_name = match &self.alias_filter.filter {
Some(pattern) => {
let match_idxs: Vec<usize> =
d.alias.match_indices(pattern).map(|(i, _)| i).collect();

let highlighted = Style::default().bg(Color::LightBlue);

Line::from(vec![
Span::raw(&d.alias[0..match_idxs[0]]),
Span::styled(pattern, highlighted),
Span::raw(&d.alias[match_idxs[0] + pattern.len()..]),
])
}
None => Line::raw(d.alias.clone()),
};

let icon = Line::styled(
if let Some(icon) = &d.icon {
format!("{} {}", icon, &d.alias)
} else {
d.alias.to_owned()
}
}])
},
Style::default(),
);

Row::new(vec![device_name, icon])
})
.collect();
let rows_len = rows.len();
Expand Down
31 changes: 31 additions & 0 deletions src/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,28 @@ pub async fn handle_key_events(
.handle_event(&crossterm::event::Event::Key(key_event));
}
},

FocusedBlock::AliasFilterPopup => match key_event.code {
KeyCode::Backspace => {
app.alias_filter.delete_char();
}

KeyCode::Char(c) => {
app.alias_filter.insert_char(c);
}

KeyCode::Enter => {
app.focused_block = FocusedBlock::NewDevices;
}

KeyCode::Esc => {
app.alias_filter.filter = None;
app.focused_block = FocusedBlock::NewDevices;
}

_ => {}
},

_ => {
match key_event.code {
// Exit the app
Expand All @@ -72,6 +94,15 @@ pub async fn handle_key_events(
app.focused_block = FocusedBlock::Help;
}

KeyCode::Char('/') => {
if let Some(selected_controller_index) = app.controller_state.selected() {
let selected_controller = &app.controllers[selected_controller_index];
if !selected_controller.new_devices.is_empty() {
app.focused_block = FocusedBlock::AliasFilterPopup;
}
}
}

// Discard help popup
KeyCode::Esc => {
if app.focused_block == FocusedBlock::Help {
Expand Down
3 changes: 2 additions & 1 deletion src/help.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ impl Help {
Cell::from(config.new_device.pair.to_string()).bold(),
"Pair the device",
),
(Cell::from("/").bold(), "Filter device names"),
],
}
}
Expand Down Expand Up @@ -137,7 +138,7 @@ impl Help {
.direction(Direction::Vertical)
.constraints([
Constraint::Fill(1),
Constraint::Length(28),
Constraint::Length(29),
Constraint::Fill(1),
])
.flex(ratatui::layout::Flex::SpaceBetween)
Expand Down
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ pub mod spinner;

pub mod help;

pub mod alias_filter;

pub mod config;

pub mod rfkill;
Expand Down
1 change: 1 addition & 0 deletions src/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ pub fn render(app: &mut App, frame: &mut Frame) {

match app.focused_block {
FocusedBlock::Help => app.help.render(frame, app.color_mode),
FocusedBlock::AliasFilterPopup => app.alias_filter.render(frame),
FocusedBlock::SetDeviceAliasBox => app.render_set_alias(frame),
_ => {}
}
Expand Down