mirror of
https://github.com/pythops/bluetui.git
synced 2026-06-20 09:12:35 +08:00
support pairing pin code
This commit is contained in:
50
Cargo.lock
generated
50
Cargo.lock
generated
@@ -185,6 +185,7 @@ dependencies = [
|
||||
"serde",
|
||||
"tokio",
|
||||
"toml",
|
||||
"tui-big-text",
|
||||
"tui-input",
|
||||
]
|
||||
|
||||
@@ -509,6 +510,37 @@ dependencies = [
|
||||
"powerfmt",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_builder"
|
||||
version = "0.20.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947"
|
||||
dependencies = [
|
||||
"derive_builder_macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_builder_core"
|
||||
version = "0.20.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8"
|
||||
dependencies = [
|
||||
"darling",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.108",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_builder_macro"
|
||||
version = "0.20.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c"
|
||||
dependencies = [
|
||||
"derive_builder_core",
|
||||
"syn 2.0.108",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_more"
|
||||
version = "2.0.1"
|
||||
@@ -684,6 +716,12 @@ version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
|
||||
|
||||
[[package]]
|
||||
name = "font8x8"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "875488b8711a968268c7cf5d139578713097ca4635a76044e8fe8eedf831d07e"
|
||||
|
||||
[[package]]
|
||||
name = "futures"
|
||||
version = "0.3.31"
|
||||
@@ -1995,6 +2033,18 @@ version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2"
|
||||
|
||||
[[package]]
|
||||
name = "tui-big-text"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c7876e22ef305de349de2ef40197455a84980f1597277ce7fb2008989b19c572"
|
||||
dependencies = [
|
||||
"derive_builder",
|
||||
"font8x8",
|
||||
"itertools 0.14.0",
|
||||
"ratatui 0.29.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tui-input"
|
||||
version = "0.12.1"
|
||||
|
||||
@@ -23,6 +23,7 @@ toml = "0.9"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
clap = { version = "4", features = ["derive", "cargo"] }
|
||||
tui-input = "0.12"
|
||||
tui-big-text = "0.7"
|
||||
|
||||
[profile.release]
|
||||
strip = true
|
||||
|
||||
122
src/agent.rs
Normal file
122
src/agent.rs
Normal file
@@ -0,0 +1,122 @@
|
||||
use async_channel::{Receiver, Sender};
|
||||
use std::sync::{Arc, atomic::AtomicBool};
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
|
||||
use bluer::agent::{ReqError, ReqResult, RequestConfirmation, RequestPinCode};
|
||||
|
||||
use crate::{
|
||||
event::Event,
|
||||
requests::{confirmation::Confirmation, enter_pin_code::EnterPinCode},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AuthAgent {
|
||||
pub event_sender: UnboundedSender<Event>,
|
||||
pub tx_cancel: Sender<()>,
|
||||
pub rx_cancel: Receiver<()>,
|
||||
pub tx_pin_code: Sender<String>,
|
||||
pub rx_pin_code: Receiver<String>,
|
||||
pub tx_passkey: Sender<String>,
|
||||
pub rx_passkey: Receiver<String>,
|
||||
pub tx_request_confirmation: Sender<bool>,
|
||||
pub rx_request_confirmation: Receiver<bool>,
|
||||
pub request_passkey: Arc<AtomicBool>,
|
||||
pub request_pin_code: Arc<AtomicBool>,
|
||||
pub request_confirmation: Arc<AtomicBool>,
|
||||
pub request_authorization: Arc<AtomicBool>,
|
||||
pub request_display_pin: Arc<AtomicBool>,
|
||||
pub request_display_passkey: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl AuthAgent {
|
||||
pub fn new(sender: UnboundedSender<Event>) -> Self {
|
||||
let (tx_passkey, rx_passkey) = async_channel::unbounded();
|
||||
let (tx_pin_code, rx_pin_code) = async_channel::unbounded();
|
||||
let (tx_request_confirmation, rx_request_confirmation) = async_channel::unbounded();
|
||||
let (tx_cancel, rx_cancel) = async_channel::unbounded();
|
||||
|
||||
Self {
|
||||
event_sender: sender,
|
||||
tx_cancel,
|
||||
rx_cancel,
|
||||
tx_pin_code,
|
||||
rx_pin_code,
|
||||
tx_passkey,
|
||||
rx_passkey,
|
||||
tx_request_confirmation,
|
||||
rx_request_confirmation,
|
||||
request_passkey: Arc::new(AtomicBool::new(false)),
|
||||
request_pin_code: Arc::new(AtomicBool::new(false)),
|
||||
request_confirmation: Arc::new(AtomicBool::new(false)),
|
||||
request_authorization: Arc::new(AtomicBool::new(false)),
|
||||
request_display_pin: Arc::new(AtomicBool::new(false)),
|
||||
request_display_passkey: Arc::new(AtomicBool::new(false)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn request_confirmation(request: RequestConfirmation, agent: AuthAgent) -> ReqResult<()> {
|
||||
agent
|
||||
.request_confirmation
|
||||
.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
|
||||
agent
|
||||
.event_sender
|
||||
.send(Event::RequestConfirmation(Confirmation::new(
|
||||
request.adapter,
|
||||
request.device,
|
||||
request.passkey,
|
||||
)))
|
||||
.unwrap();
|
||||
|
||||
tokio::select! {
|
||||
r = agent.rx_request_confirmation.recv() => {
|
||||
match r {
|
||||
Ok(v) => {
|
||||
match v {
|
||||
true => Ok(()),
|
||||
false => Err(ReqError::Rejected)
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
Err(ReqError::Canceled)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
_ = agent.rx_cancel.recv() => {
|
||||
Err(ReqError::Canceled)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn request_pin_code(request: RequestPinCode, agent: AuthAgent) -> ReqResult<String> {
|
||||
agent
|
||||
.request_pin_code
|
||||
.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
|
||||
agent
|
||||
.event_sender
|
||||
.send(Event::RequestEnterPinCode(EnterPinCode::new(
|
||||
request.adapter,
|
||||
request.device,
|
||||
)))
|
||||
.unwrap();
|
||||
|
||||
tokio::select! {
|
||||
r = agent.rx_pin_code.recv() => {
|
||||
match r {
|
||||
Ok(v) => Ok(v),
|
||||
Err(_) => Err(ReqError::Canceled)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
_ = agent.rx_cancel.recv() => {
|
||||
Err(ReqError::Canceled)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
89
src/app.rs
89
src/app.rs
@@ -1,3 +1,7 @@
|
||||
use crate::{
|
||||
agent::{request_confirmation, request_pin_code},
|
||||
event::Event,
|
||||
};
|
||||
use bluer::{
|
||||
Session,
|
||||
agent::{Agent, AgentHandle},
|
||||
@@ -15,11 +19,14 @@ use ratatui::{
|
||||
};
|
||||
use tui_input::Input;
|
||||
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
|
||||
use crate::{
|
||||
bluetooth::{Controller, request_confirmation},
|
||||
agent::AuthAgent,
|
||||
bluetooth::Controller,
|
||||
config::{Config, Width},
|
||||
confirmation::PairingConfirmation,
|
||||
notification::Notification,
|
||||
requests::Requests,
|
||||
spinner::Spinner,
|
||||
};
|
||||
use std::{
|
||||
@@ -34,8 +41,9 @@ pub enum FocusedBlock {
|
||||
Adapter,
|
||||
PairedDevices,
|
||||
NewDevices,
|
||||
PassKeyConfirmation,
|
||||
SetDeviceAliasBox,
|
||||
RequestConfirmation,
|
||||
EnterPinCode,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -50,33 +58,27 @@ pub struct App {
|
||||
pub paired_devices_state: TableState,
|
||||
pub new_devices_state: TableState,
|
||||
pub focused_block: FocusedBlock,
|
||||
pub pairing_confirmation: PairingConfirmation,
|
||||
pub new_alias: Input,
|
||||
pub config: Arc<Config>,
|
||||
pub requests: Requests,
|
||||
pub auth_agent: AuthAgent,
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub async fn new(config: Arc<Config>) -> AppResult<Self> {
|
||||
pub async fn new(config: Arc<Config>, sender: UnboundedSender<Event>) -> AppResult<Self> {
|
||||
let session = Arc::new(bluer::Session::new().await?);
|
||||
|
||||
let pairing_confirmation = PairingConfirmation::new();
|
||||
|
||||
let user_confirmation_receiver = pairing_confirmation.user_confirmation_receiver.clone();
|
||||
|
||||
let confirmation_message_sender = pairing_confirmation.confirmation_message_sender.clone();
|
||||
|
||||
let confirmation_display = pairing_confirmation.display.clone();
|
||||
let auth_agent = AuthAgent::new(sender.clone());
|
||||
|
||||
let agent = Agent {
|
||||
request_default: false,
|
||||
request_confirmation: Some(Box::new(move |req| {
|
||||
request_confirmation(
|
||||
req,
|
||||
confirmation_display.clone(),
|
||||
user_confirmation_receiver.clone(),
|
||||
confirmation_message_sender.clone(),
|
||||
)
|
||||
.boxed()
|
||||
request_confirmation: Some(Box::new({
|
||||
let auth_agent = auth_agent.clone();
|
||||
move |request| request_confirmation(request, auth_agent.clone()).boxed()
|
||||
})),
|
||||
request_pin_code: Some(Box::new({
|
||||
let auth_agent = auth_agent.clone();
|
||||
move |request| request_pin_code(request, auth_agent.clone()).boxed()
|
||||
})),
|
||||
..Default::default()
|
||||
};
|
||||
@@ -102,9 +104,10 @@ impl App {
|
||||
paired_devices_state: TableState::default(),
|
||||
new_devices_state: TableState::default(),
|
||||
focused_block: FocusedBlock::PairedDevices,
|
||||
pairing_confirmation,
|
||||
new_alias: Input::default(),
|
||||
config,
|
||||
requests: Requests::default(),
|
||||
auth_agent,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -773,10 +776,25 @@ impl App {
|
||||
Span::from(" Discard"),
|
||||
])]
|
||||
}
|
||||
FocusedBlock::PassKeyConfirmation => {
|
||||
FocusedBlock::RequestConfirmation => {
|
||||
vec![Line::from(vec![
|
||||
Span::from(" ").bold(),
|
||||
Span::from(" Discard"),
|
||||
Span::from(" | "),
|
||||
Span::from("⇄").bold(),
|
||||
Span::from(" Nav"),
|
||||
])]
|
||||
}
|
||||
FocusedBlock::EnterPinCode => {
|
||||
vec![Line::from(vec![
|
||||
Span::from(" ").bold(),
|
||||
Span::from(" Discard"),
|
||||
Span::from(" | "),
|
||||
Span::from("⇄").bold(),
|
||||
Span::from(" Nav"),
|
||||
Span::from(" | "),
|
||||
Span::from("↵ ").bold(),
|
||||
Span::from(" Submit"),
|
||||
])]
|
||||
}
|
||||
};
|
||||
@@ -786,16 +804,25 @@ impl App {
|
||||
|
||||
// Pairing confirmation
|
||||
|
||||
if self.pairing_confirmation.display.load(Ordering::Relaxed) {
|
||||
self.focused_block = FocusedBlock::PassKeyConfirmation;
|
||||
self.pairing_confirmation.render(frame, self.area(frame));
|
||||
return;
|
||||
}
|
||||
|
||||
// Set alias popup
|
||||
if self.focused_block == FocusedBlock::SetDeviceAliasBox {
|
||||
self.render_set_alias(frame)
|
||||
}
|
||||
|
||||
// Request Confirmation
|
||||
if self.auth_agent.request_confirmation.load(Ordering::Relaxed)
|
||||
&& let Some(req) = &self.requests.confirmation
|
||||
{
|
||||
req.render(frame);
|
||||
}
|
||||
|
||||
// Request to enter pin code
|
||||
|
||||
if self.auth_agent.request_pin_code.load(Ordering::Relaxed)
|
||||
&& let Some(req) = &self.requests.enter_pin_code
|
||||
{
|
||||
req.render(frame);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -811,12 +838,6 @@ impl App {
|
||||
}
|
||||
|
||||
pub async fn refresh(&mut self) -> AppResult<()> {
|
||||
if !self.pairing_confirmation.display.load(Ordering::Relaxed)
|
||||
& self.pairing_confirmation.message.is_some()
|
||||
{
|
||||
self.pairing_confirmation.message = None;
|
||||
}
|
||||
|
||||
let refreshed_controllers = Controller::get_all(self.session.clone()).await?;
|
||||
|
||||
let names = {
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
use std::sync::{Arc, atomic::AtomicBool, mpsc::Sender};
|
||||
use std::sync::{Arc, atomic::AtomicBool};
|
||||
|
||||
use async_channel::Receiver;
|
||||
use bluer::{
|
||||
Adapter, Address, Session,
|
||||
agent::{ReqError, ReqResult, RequestConfirmation},
|
||||
};
|
||||
use bluer::{Adapter, Address, Session};
|
||||
|
||||
use bluer::Device as BTDevice;
|
||||
|
||||
use tokio::sync::oneshot;
|
||||
|
||||
use crate::app::AppResult;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -149,38 +143,3 @@ fn is_mac_addr(s: &str) -> bool {
|
||||
let s: String = s.chars().filter(|&c| c != '-').collect();
|
||||
s.len() == 12 && s.chars().all(|c| c.is_ascii_hexdigit())
|
||||
}
|
||||
|
||||
pub async fn request_confirmation(
|
||||
req: RequestConfirmation,
|
||||
display_confirmation_popup: Arc<AtomicBool>,
|
||||
rx: Receiver<bool>,
|
||||
sender: Sender<String>,
|
||||
) -> ReqResult<()> {
|
||||
display_confirmation_popup.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
|
||||
sender
|
||||
.send(format!(
|
||||
"Is passkey \"{:06}\" correct for device {} on {}?",
|
||||
req.passkey, &req.device, &req.adapter
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
// request cancel
|
||||
let (_done_tx, done_rx) = oneshot::channel::<()>();
|
||||
tokio::spawn(async move {
|
||||
if done_rx.await.is_err() {
|
||||
display_confirmation_popup.store(false, std::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
});
|
||||
match rx.recv().await {
|
||||
Ok(v) => {
|
||||
// false: reject the confirmation
|
||||
if !v {
|
||||
return Err(ReqError::Rejected);
|
||||
}
|
||||
}
|
||||
Err(_) => return Err(ReqError::Rejected),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,131 +0,0 @@
|
||||
use std::sync::mpsc::channel;
|
||||
use std::sync::{Arc, atomic::AtomicBool};
|
||||
|
||||
use ratatui::Frame;
|
||||
use ratatui::layout::{Alignment, Constraint, Direction, Layout, Rect};
|
||||
use ratatui::style::{Color, Style};
|
||||
use ratatui::text::{Span, Text};
|
||||
use ratatui::widgets::{Block, BorderType, Borders, Clear};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PairingConfirmation {
|
||||
pub confirmed: bool,
|
||||
pub display: Arc<AtomicBool>,
|
||||
pub message: Option<String>,
|
||||
pub user_confirmation_sender: async_channel::Sender<bool>,
|
||||
pub user_confirmation_receiver: async_channel::Receiver<bool>,
|
||||
pub confirmation_message_sender: std::sync::mpsc::Sender<String>,
|
||||
pub confirmation_message_receiver: std::sync::mpsc::Receiver<String>,
|
||||
}
|
||||
|
||||
impl Default for PairingConfirmation {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl PairingConfirmation {
|
||||
pub fn new() -> Self {
|
||||
let (user_confirmation_sender, user_confirmation_receiver) = async_channel::unbounded();
|
||||
|
||||
let (confirmation_message_sender, confirmation_message_receiver) = channel::<String>();
|
||||
Self {
|
||||
confirmed: true,
|
||||
display: Arc::new(AtomicBool::new(false)),
|
||||
message: None,
|
||||
user_confirmation_sender,
|
||||
user_confirmation_receiver,
|
||||
confirmation_message_sender,
|
||||
confirmation_message_receiver,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render(&mut self, frame: &mut Frame, area: Rect) {
|
||||
if self.message.is_none() {
|
||||
let msg = self.confirmation_message_receiver.recv().unwrap();
|
||||
self.message = Some(msg);
|
||||
}
|
||||
|
||||
let layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Fill(1),
|
||||
Constraint::Length(5),
|
||||
Constraint::Fill(1),
|
||||
])
|
||||
.split(area);
|
||||
|
||||
let block = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([
|
||||
Constraint::Fill(1),
|
||||
Constraint::Max(80),
|
||||
Constraint::Fill(1),
|
||||
])
|
||||
.split(layout[1])[1];
|
||||
|
||||
let (text_area, choices_area) = {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(1),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(block);
|
||||
|
||||
(chunks[1], chunks[3])
|
||||
};
|
||||
|
||||
let (yes_area, no_area) = {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Percentage(30),
|
||||
Constraint::Length(5),
|
||||
Constraint::Min(1),
|
||||
Constraint::Length(5),
|
||||
Constraint::Percentage(30),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(choices_area);
|
||||
|
||||
(chunks[1], chunks[3])
|
||||
};
|
||||
|
||||
let text = Text::from(self.message.clone().unwrap_or_default())
|
||||
.style(Style::default().fg(Color::White));
|
||||
|
||||
let (yes, no) = {
|
||||
if self.confirmed {
|
||||
let no = Span::from("[No]").style(Style::default());
|
||||
let yes = Span::from("[Yes]").style(Style::default().bg(Color::DarkGray));
|
||||
(yes, no)
|
||||
} else {
|
||||
let no = Span::from("[No]").style(Style::default().bg(Color::DarkGray));
|
||||
let yes = Span::from("[Yes]").style(Style::default());
|
||||
(yes, no)
|
||||
}
|
||||
};
|
||||
|
||||
frame.render_widget(Clear, block);
|
||||
|
||||
frame.render_widget(
|
||||
Block::new()
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Thick)
|
||||
.border_style(Style::default().fg(Color::Green)),
|
||||
block,
|
||||
);
|
||||
frame.render_widget(text.alignment(Alignment::Center), text_area);
|
||||
frame.render_widget(yes, yes_area);
|
||||
frame.render_widget(no, no_area);
|
||||
}
|
||||
}
|
||||
10
src/event.rs
10
src/event.rs
@@ -4,7 +4,11 @@ use crossterm::event::{Event as CrosstermEvent, KeyEvent, MouseEvent};
|
||||
use futures::{FutureExt, StreamExt};
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
use crate::{app::AppResult, notification::Notification};
|
||||
use crate::{
|
||||
app::AppResult,
|
||||
notification::Notification,
|
||||
requests::{confirmation::Confirmation, enter_pin_code::EnterPinCode},
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Event {
|
||||
@@ -14,6 +18,10 @@ pub enum Event {
|
||||
Resize(u16, u16),
|
||||
Notification(Notification),
|
||||
NewPairedDevice,
|
||||
RequestConfirmation(Confirmation),
|
||||
ConfirmationSubmitted,
|
||||
RequestEnterPinCode(EnterPinCode),
|
||||
PinCodeSumitted,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
|
||||
@@ -205,6 +205,39 @@ pub async fn handle_key_events(
|
||||
.handle_event(&crossterm::event::Event::Key(key_event));
|
||||
}
|
||||
},
|
||||
FocusedBlock::RequestConfirmation => match key_event.code {
|
||||
KeyCode::Tab => {
|
||||
if let Some(confirmation) = &mut app.requests.confirmation {
|
||||
confirmation.toggle_select();
|
||||
}
|
||||
}
|
||||
KeyCode::Esc => {
|
||||
if let Some(confirmation) = &mut app.requests.confirmation {
|
||||
confirmation.cancel(&app.auth_agent).await?;
|
||||
}
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
if let Some(confirmation) = &mut app.requests.confirmation {
|
||||
confirmation.submit(&app.auth_agent).await?;
|
||||
}
|
||||
}
|
||||
|
||||
_ => {}
|
||||
},
|
||||
FocusedBlock::EnterPinCode => {
|
||||
if let Some(req) = &mut app.requests.enter_pin_code {
|
||||
match key_event.code {
|
||||
KeyCode::Esc => {
|
||||
req.cancel(&app.auth_agent).await?;
|
||||
}
|
||||
|
||||
_ => {
|
||||
req.handle_key_events(key_event, &app.auth_agent).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_ => {
|
||||
match key_event.code {
|
||||
// Exit the app
|
||||
@@ -751,33 +784,6 @@ pub async fn handle_key_events(
|
||||
}
|
||||
}
|
||||
|
||||
FocusedBlock::PassKeyConfirmation => match key_event.code {
|
||||
KeyCode::Left | KeyCode::Char('h') => {
|
||||
if !app.pairing_confirmation.confirmed {
|
||||
app.pairing_confirmation.confirmed = true;
|
||||
}
|
||||
}
|
||||
KeyCode::Right | KeyCode::Char('l') => {
|
||||
if app.pairing_confirmation.confirmed {
|
||||
app.pairing_confirmation.confirmed = false;
|
||||
}
|
||||
}
|
||||
|
||||
KeyCode::Enter => {
|
||||
app.pairing_confirmation
|
||||
.user_confirmation_sender
|
||||
.send(app.pairing_confirmation.confirmed)
|
||||
.await?;
|
||||
app.pairing_confirmation
|
||||
.display
|
||||
.store(false, Ordering::Relaxed);
|
||||
app.focused_block = FocusedBlock::PairedDevices;
|
||||
app.pairing_confirmation.message = None;
|
||||
}
|
||||
|
||||
_ => {}
|
||||
},
|
||||
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
pub mod agent;
|
||||
pub mod app;
|
||||
pub mod bluetooth;
|
||||
pub mod cli;
|
||||
pub mod config;
|
||||
pub mod confirmation;
|
||||
pub mod event;
|
||||
pub mod handler;
|
||||
pub mod notification;
|
||||
pub mod requests;
|
||||
pub mod rfkill;
|
||||
pub mod spinner;
|
||||
pub mod tui;
|
||||
|
||||
26
src/main.rs
26
src/main.rs
@@ -29,13 +29,15 @@ async fn main() -> AppResult<()> {
|
||||
|
||||
let config = Arc::new(Config::new(config_file_path));
|
||||
|
||||
let mut app = App::new(config.clone()).await?;
|
||||
let backend = CrosstermBackend::new(io::stdout());
|
||||
let terminal = Terminal::new(backend)?;
|
||||
let events = EventHandler::new(2_000);
|
||||
let events = EventHandler::new(1_000);
|
||||
let mut tui = Tui::new(terminal, events);
|
||||
|
||||
tui.init()?;
|
||||
|
||||
let mut app = App::new(config.clone(), tui.events.sender.clone()).await?;
|
||||
|
||||
while app.running {
|
||||
tui.draw(&mut app)?;
|
||||
match tui.events.next().await? {
|
||||
@@ -55,6 +57,26 @@ async fn main() -> AppResult<()> {
|
||||
Event::NewPairedDevice => {
|
||||
app.focused_block = bluetui::app::FocusedBlock::PairedDevices;
|
||||
}
|
||||
Event::RequestConfirmation(request) => {
|
||||
app.requests.init_confirmation(request);
|
||||
app.focused_block = bluetui::app::FocusedBlock::RequestConfirmation;
|
||||
}
|
||||
|
||||
Event::ConfirmationSubmitted => {
|
||||
app.requests.confirmation = None;
|
||||
app.focused_block = bluetui::app::FocusedBlock::PairedDevices;
|
||||
}
|
||||
|
||||
Event::RequestEnterPinCode(request) => {
|
||||
app.requests.init_enter_pin_code(request);
|
||||
app.focused_block = bluetui::app::FocusedBlock::EnterPinCode;
|
||||
}
|
||||
|
||||
Event::PinCodeSumitted => {
|
||||
app.requests.enter_pin_code = None;
|
||||
app.focused_block = bluetui::app::FocusedBlock::PairedDevices;
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
28
src/requests.rs
Normal file
28
src/requests.rs
Normal file
@@ -0,0 +1,28 @@
|
||||
use crate::requests::{confirmation::Confirmation, enter_pin_code::EnterPinCode};
|
||||
|
||||
pub mod confirmation;
|
||||
pub mod enter_pin_code;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Requests {
|
||||
pub confirmation: Option<Confirmation>,
|
||||
pub enter_pin_code: Option<EnterPinCode>,
|
||||
}
|
||||
|
||||
impl Requests {
|
||||
pub fn init_confirmation(&mut self, req: Confirmation) {
|
||||
self.confirmation = Some(req);
|
||||
}
|
||||
pub fn init_enter_pin_code(&mut self, req: EnterPinCode) {
|
||||
self.enter_pin_code = Some(req);
|
||||
}
|
||||
}
|
||||
|
||||
fn pad_string(input: &str, length: usize) -> String {
|
||||
let current_length = input.chars().count();
|
||||
if current_length >= length {
|
||||
input.to_string()
|
||||
} else {
|
||||
format!("{:<width$}", input, width = length)
|
||||
}
|
||||
}
|
||||
128
src/requests/confirmation.rs
Normal file
128
src/requests/confirmation.rs
Normal file
@@ -0,0 +1,128 @@
|
||||
use ratatui::{
|
||||
Frame,
|
||||
layout::{Constraint, Direction, Layout},
|
||||
style::{Color, Style},
|
||||
text::{Line, Span, Text},
|
||||
widgets::{Block, BorderType, Borders, Clear},
|
||||
};
|
||||
|
||||
use bluer::Address;
|
||||
|
||||
use crate::{agent::AuthAgent, app::AppResult};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Confirmation {
|
||||
pub adapter: String,
|
||||
pub device: Address,
|
||||
pub passkey: u32,
|
||||
confirmed: bool,
|
||||
}
|
||||
|
||||
impl Confirmation {
|
||||
pub fn new(adapter: String, device: Address, passkey: u32) -> Self {
|
||||
Self {
|
||||
adapter,
|
||||
device,
|
||||
passkey,
|
||||
confirmed: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn submit(&mut self, agent: &AuthAgent) -> AppResult<()> {
|
||||
agent.tx_request_confirmation.send(self.confirmed).await?;
|
||||
agent
|
||||
.request_confirmation
|
||||
.store(false, std::sync::atomic::Ordering::Relaxed);
|
||||
agent
|
||||
.event_sender
|
||||
.send(crate::event::Event::ConfirmationSubmitted)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn cancel(&mut self, agent: &AuthAgent) -> AppResult<()> {
|
||||
agent.tx_cancel.send(()).await?;
|
||||
agent
|
||||
.request_confirmation
|
||||
.store(false, std::sync::atomic::Ordering::Relaxed);
|
||||
agent
|
||||
.event_sender
|
||||
.send(crate::event::Event::ConfirmationSubmitted)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn toggle_select(&mut self) {
|
||||
self.confirmed = !self.confirmed;
|
||||
}
|
||||
|
||||
pub fn render(&self, frame: &mut Frame) {
|
||||
let layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Fill(1),
|
||||
Constraint::Length(5),
|
||||
Constraint::Fill(1),
|
||||
])
|
||||
.split(frame.area());
|
||||
|
||||
let block = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([
|
||||
Constraint::Fill(1),
|
||||
Constraint::Max(80),
|
||||
Constraint::Fill(1),
|
||||
])
|
||||
.split(layout[1])[1];
|
||||
|
||||
let (message_area, choices_area) = {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(1),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(block);
|
||||
|
||||
(chunks[1], chunks[3])
|
||||
};
|
||||
|
||||
let message = Text::from(format!(
|
||||
"Is Passkey {:06} correct for the device {} ?",
|
||||
self.passkey, self.device,
|
||||
))
|
||||
.centered();
|
||||
|
||||
let choice = {
|
||||
if self.confirmed {
|
||||
Line::from(vec![
|
||||
Span::from("[No]").style(Style::default()),
|
||||
Span::from(" "),
|
||||
Span::from("[Yes]").style(Style::default().bg(Color::DarkGray)),
|
||||
])
|
||||
} else {
|
||||
Line::from(vec![
|
||||
Span::from("[No]").style(Style::default().bg(Color::DarkGray)),
|
||||
Span::from(" "),
|
||||
Span::from("[Yes]").style(Style::default()),
|
||||
])
|
||||
}
|
||||
};
|
||||
|
||||
frame.render_widget(Clear, block);
|
||||
|
||||
frame.render_widget(
|
||||
Block::new()
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Thick)
|
||||
.border_style(Style::default().fg(Color::Green)),
|
||||
block,
|
||||
);
|
||||
frame.render_widget(message, message_area);
|
||||
frame.render_widget(choice.centered(), choices_area);
|
||||
}
|
||||
}
|
||||
225
src/requests/enter_pin_code.rs
Normal file
225
src/requests/enter_pin_code.rs
Normal file
@@ -0,0 +1,225 @@
|
||||
use crossterm::event::{KeyCode, KeyEvent};
|
||||
|
||||
use ratatui::{
|
||||
Frame,
|
||||
layout::{Constraint, Direction, Layout},
|
||||
style::{Color, Style, Stylize},
|
||||
text::{Line, Span, Text},
|
||||
widgets::{Block, BorderType, Borders, Clear, List},
|
||||
};
|
||||
|
||||
use bluer::Address;
|
||||
use tui_input::{Input, backend::crossterm::EventHandler};
|
||||
|
||||
use crate::{agent::AuthAgent, app::AppResult, event::Event, requests::pad_string};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Default)]
|
||||
pub enum FocusedSection {
|
||||
#[default]
|
||||
Input,
|
||||
Submit,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EnterPinCode {
|
||||
pub adapter: String,
|
||||
pub device: Address,
|
||||
focused_section: FocusedSection,
|
||||
pin_code: UserInputField,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
struct UserInputField {
|
||||
field: Input,
|
||||
error: Option<String>,
|
||||
}
|
||||
|
||||
impl EnterPinCode {
|
||||
pub fn new(adapter: String, device: Address) -> Self {
|
||||
Self {
|
||||
adapter,
|
||||
device,
|
||||
focused_section: FocusedSection::default(),
|
||||
pin_code: UserInputField::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn submit(&mut self, agent: &AuthAgent) -> AppResult<()> {
|
||||
self.validate();
|
||||
|
||||
if self.pin_code.error.is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
agent
|
||||
.tx_pin_code
|
||||
.send(self.pin_code.field.value().to_string())
|
||||
.await?;
|
||||
agent
|
||||
.request_pin_code
|
||||
.store(false, std::sync::atomic::Ordering::Relaxed);
|
||||
|
||||
agent.event_sender.send(Event::PinCodeSumitted)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn cancel(&mut self, agent: &AuthAgent) -> AppResult<()> {
|
||||
agent.tx_cancel.send(()).await?;
|
||||
agent
|
||||
.request_pin_code
|
||||
.store(false, std::sync::atomic::Ordering::Relaxed);
|
||||
agent.event_sender.send(Event::PinCodeSumitted)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn validate(&mut self) {
|
||||
self.pin_code.error = None;
|
||||
if self.pin_code.field.value().is_empty() {
|
||||
self.pin_code.error = Some("Required field.".to_string());
|
||||
return;
|
||||
}
|
||||
|
||||
if self.pin_code.field.value().len() > 16 {
|
||||
self.pin_code.error =
|
||||
Some("Pin Code should be a string of 1-16 characters length".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn handle_key_events(
|
||||
&mut self,
|
||||
key_event: KeyEvent,
|
||||
agent: &AuthAgent,
|
||||
) -> AppResult<()> {
|
||||
match key_event.code {
|
||||
KeyCode::Tab | KeyCode::BackTab => {
|
||||
if self.focused_section == FocusedSection::Input {
|
||||
self.focused_section = FocusedSection::Submit;
|
||||
} else {
|
||||
self.focused_section = FocusedSection::Input;
|
||||
}
|
||||
}
|
||||
_ => match self.focused_section {
|
||||
FocusedSection::Submit => {
|
||||
if let KeyCode::Enter = key_event.code {
|
||||
self.submit(agent).await?;
|
||||
}
|
||||
}
|
||||
|
||||
_ => {
|
||||
self.pin_code
|
||||
.field
|
||||
.handle_event(&crossterm::event::Event::Key(key_event));
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn render(&self, frame: &mut Frame) {
|
||||
let layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Fill(1),
|
||||
Constraint::Length(8),
|
||||
Constraint::Fill(1),
|
||||
])
|
||||
.split(frame.area());
|
||||
|
||||
let block = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([
|
||||
Constraint::Fill(1),
|
||||
Constraint::Max(80),
|
||||
Constraint::Fill(1),
|
||||
])
|
||||
.split(layout[1])[1];
|
||||
|
||||
let (message_block, input_block, submit_block) = {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(1), // message
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(3), // input
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(1), // enter
|
||||
Constraint::Length(1),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(block);
|
||||
|
||||
(chunks[1], chunks[3], chunks[5])
|
||||
};
|
||||
|
||||
let input_block = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Fill(1),
|
||||
Constraint::Fill(1),
|
||||
Constraint::Fill(1),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.flex(ratatui::layout::Flex::Center)
|
||||
.split(input_block)[1];
|
||||
|
||||
let message = Text::from(format!(
|
||||
"Enter the PIN Code for the device {} on {}",
|
||||
self.device, self.adapter,
|
||||
))
|
||||
.centered();
|
||||
|
||||
let items = vec![
|
||||
Line::from(vec![
|
||||
{
|
||||
if self.focused_section == FocusedSection::Input {
|
||||
Span::from("Pin Code").green().bold()
|
||||
} else {
|
||||
Span::from("Pin Code")
|
||||
}
|
||||
},
|
||||
Span::from(" "),
|
||||
Span::from(pad_string(
|
||||
format!(" {}", self.pin_code.field.value()).as_str(),
|
||||
30,
|
||||
))
|
||||
.bg(Color::DarkGray),
|
||||
]),
|
||||
Line::from(vec![Span::from(pad_string(" ", 10)), {
|
||||
if let Some(error) = &self.pin_code.error {
|
||||
Span::from(error)
|
||||
} else {
|
||||
Span::from("")
|
||||
}
|
||||
}])
|
||||
.red(),
|
||||
];
|
||||
|
||||
let user_input = List::new(items);
|
||||
|
||||
let submit = if self.focused_section == FocusedSection::Submit {
|
||||
Text::from("Submit").centered().bold().green()
|
||||
} else {
|
||||
Text::from("Submit").centered()
|
||||
};
|
||||
|
||||
frame.render_widget(Clear, block);
|
||||
|
||||
frame.render_widget(
|
||||
Block::new()
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Thick)
|
||||
.border_style(Style::default().fg(Color::Green)),
|
||||
block,
|
||||
);
|
||||
|
||||
frame.render_widget(message, message_block);
|
||||
frame.render_widget(user_input, input_block);
|
||||
frame.render_widget(submit, submit_block);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user