support pairing pin code

This commit is contained in:
Badr
2025-11-10 02:23:05 +01:00
parent f25e2ef4dc
commit b6f05d2e8b
13 changed files with 679 additions and 239 deletions

50
Cargo.lock generated
View File

@@ -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"

View File

@@ -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
View 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)
}
}
}

View File

@@ -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 = {

View File

@@ -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(())
}

View File

@@ -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);
}
}

View File

@@ -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)]

View File

@@ -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;
}
_ => {}
},
_ => {}
}
}

View File

@@ -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;

View File

@@ -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
View 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)
}
}

View 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);
}
}

View 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);
}
}