From 6b182d6d80d9d3f2c98f5400fae708a11276df6f Mon Sep 17 00:00:00 2001 From: Badr Date: Mon, 25 Nov 2024 09:05:44 +0100 Subject: [PATCH] Refactor: reorgonize the layout (#27) * regroup unused events * bump dep * add confirmation module * show scrollbar when necessary --- Cargo.lock | 30 +++- Cargo.toml | 6 +- src/app.rs | 401 ++++++++++++++++---------------------------- src/confirmation.rs | 131 +++++++++++++++ src/lib.rs | 2 + src/main.rs | 3 +- 6 files changed, 300 insertions(+), 273 deletions(-) create mode 100644 src/confirmation.rs diff --git a/Cargo.lock b/Cargo.lock index fadc476..4101f7d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" @@ -654,6 +654,12 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "indoc" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" + [[package]] name = "instability" version = "0.3.2" @@ -948,23 +954,23 @@ dependencies = [ [[package]] name = "ratatui" -version = "0.28.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ba6a365afbe5615999275bea2446b970b10a41102500e27ce7678d50d978303" +checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" dependencies = [ "bitflags", "cassowary", "compact_str", "crossterm 0.28.1", + "indoc", "instability", "itertools 0.13.0", "lru", "paste", "strum", - "strum_macros", "unicode-segmentation", "unicode-truncate", - "unicode-width", + "unicode-width 0.2.0", ] [[package]] @@ -1285,12 +1291,12 @@ dependencies = [ [[package]] name = "tui-input" -version = "0.10.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68699e8bb4ca025ab41fcc602d3d53a5714a56b0cf2d6e93c98aaf4231e3d937" +checksum = "ffde6d8fcffe86b617018ca9b2171d673b41def44ebf802de203d2f2c598d3de" dependencies = [ "ratatui", - "unicode-width", + "unicode-width 0.2.0", ] [[package]] @@ -1313,7 +1319,7 @@ checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" dependencies = [ "itertools 0.13.0", "unicode-segmentation", - "unicode-width", + "unicode-width 0.1.13", ] [[package]] @@ -1322,6 +1328,12 @@ version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" +[[package]] +name = "unicode-width" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" + [[package]] name = "utf8parse" version = "0.2.2" diff --git a/Cargo.toml b/Cargo.toml index 25ca0c6..52035e1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,14 +14,16 @@ async-channel = "2" bluer = { version = "0.17", features = ["full"] } crossterm = { version = "0.28", features = ["event-stream"] } futures = "0.3" -ratatui = "0.28" +ratatui = "0.29" tokio = { version = "1", features = ["full"] } dirs = "5" toml = { version = "0.8" } serde = { version = "1", features = ["derive"] } clap = { version = "4", features = ["derive", "cargo"] } terminal-light = "1" -tui-input = "0.10" +tui-input = "0.11" [profile.release] strip = true +codegen-units = 1 +lto = "fat" diff --git a/src/app.rs b/src/app.rs index ceec0ba..11d1760 100644 --- a/src/app.rs +++ b/src/app.rs @@ -4,31 +4,28 @@ use bluer::{ }; use futures::FutureExt; use ratatui::{ - layout::{Alignment, Constraint, Direction, Layout, Margin, Rect}, + layout::{Alignment, Constraint, Direction, Layout, Margin}, style::{Color, Modifier, Style, Stylize}, - text::{Line, Span, Text}, + text::{Line, Span}, widgets::{ Block, BorderType, Borders, Cell, Clear, Padding, Paragraph, Row, Scrollbar, ScrollbarOrientation, ScrollbarState, Table, TableState, }, Frame, }; -use std::sync::mpsc::channel; use tui_input::Input; use crate::{ bluetooth::{request_confirmation, Controller}, config::Config, + confirmation::PairingConfirmation, help::Help, notification::Notification, spinner::Spinner, }; use std::{ error, - sync::{ - atomic::{AtomicBool, Ordering}, - Arc, - }, + sync::{atomic::Ordering, Arc}, }; pub type AppResult = std::result::Result>; @@ -67,60 +64,66 @@ pub struct App { pub new_alias: Input, } -#[derive(Debug)] -pub struct PairingConfirmation { - pub confirmed: bool, - pub display: Arc, - pub message: Option, - pub user_confirmation_sender: async_channel::Sender, - pub user_confirmation_receiver: async_channel::Receiver, - pub confirmation_message_sender: std::sync::mpsc::Sender, - pub confirmation_message_receiver: std::sync::mpsc::Receiver, -} +impl App { + pub async fn new(config: Arc) -> AppResult { + let color_mode = match terminal_light::luma() { + Ok(luma) if luma > 0.6 => ColorMode::Light, + Ok(_) => ColorMode::Dark, + Err(_) => ColorMode::Dark, + }; + + 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 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() + })), + ..Default::default() + }; -impl Default for PairingConfirmation { - fn default() -> Self { - let (user_confirmation_sender, user_confirmation_receiver) = async_channel::unbounded(); - - let (confirmation_message_sender, confirmation_message_receiver) = channel::(); - Self { - confirmed: true, - display: Arc::new(AtomicBool::new(false)), - message: None, - user_confirmation_sender, - user_confirmation_receiver, - confirmation_message_sender, - confirmation_message_receiver, + let handle = session.register_agent(agent).await?; + let controllers: Vec = Controller::get_all(session.clone()).await?; + + let mut controller_state = TableState::default(); + if controllers.is_empty() { + controller_state.select(None); + } else { + controller_state.select(Some(0)); } + + Ok(Self { + running: true, + session, + agent: handle, + help: Help::new(config), + spinner: Spinner::default(), + notifications: Vec::new(), + controllers, + controller_state, + paired_devices_state: TableState::default(), + new_devices_state: TableState::default(), + focused_block: FocusedBlock::PairedDevices, + pairing_confirmation, + color_mode, + new_alias: Input::default(), + }) } -} -pub fn popup(r: Rect) -> Rect { - let popup_layout = Layout::default() - .direction(Direction::Vertical) - .constraints( - [ - Constraint::Percentage(45), - Constraint::Length(5), - Constraint::Percentage(45), - ] - .as_ref(), - ) - .split(r); - - Layout::default() - .direction(Direction::Horizontal) - .constraints( - [ - Constraint::Length((r.width - 80) / 2), - Constraint::Min(80), - Constraint::Length((r.width - 80) / 2), - ] - .as_ref(), - ) - .split(popup_layout[1])[1] -} -impl App { pub fn reset_devices_state(&mut self) { if let Some(selected_controller) = self.controller_state.selected() { let controller = &self.controllers[selected_controller]; @@ -141,26 +144,20 @@ impl App { pub fn render_set_alias(&mut self, frame: &mut Frame) { let area = Layout::default() .direction(Direction::Vertical) - .constraints( - [ - Constraint::Percentage(45), - Constraint::Length(6), - Constraint::Percentage(45), - ] - .as_ref(), - ) + .constraints([ + Constraint::Fill(1), + Constraint::Length(6), + Constraint::Fill(1), + ]) .split(frame.area()); let area = Layout::default() .direction(Direction::Horizontal) - .constraints( - [ - Constraint::Length((frame.area().width - 80) / 2), - Constraint::Min(80), - Constraint::Length((frame.area().width - 80) / 2), - ] - .as_ref(), - ) + .constraints([ + Constraint::Fill(1), + Constraint::Min(80), + Constraint::Fill(1), + ]) .split(area[1]); let area = area[1]; @@ -255,20 +252,25 @@ impl App { // Layout let render_new_devices = !selected_controller.new_devices.is_empty() | selected_controller.is_scanning.load(Ordering::Relaxed); - let (controller_block, paired_devices_block, new_devices_block) = { + + let adapter_block_height = self.controllers.len() as u16 + 6; + + let paired_devices_block_height = selected_controller.paired_devices.len() as u16 + 4; + + let (paired_devices_block, new_devices_block, controller_block) = { let chunks = Layout::default() .direction(Direction::Vertical) .constraints(if render_new_devices { - &[ - Constraint::Percentage(33), - Constraint::Percentage(33), - Constraint::Percentage(33), + [ + Constraint::Length(paired_devices_block_height), + Constraint::Fill(1), + Constraint::Length(adapter_block_height), ] } else { - &[ - Constraint::Percentage(50), - Constraint::Percentage(50), + [ Constraint::Fill(1), + Constraint::Length(0), + Constraint::Length(adapter_block_height), ] }) .margin(1) @@ -322,6 +324,8 @@ impl App { Constraint::Length(14), ]; + let rows_len = rows.len(); + let controller_table = Table::new(rows, widths) .header({ if self.focused_block == FocusedBlock::Adapter { @@ -393,7 +397,7 @@ impl App { ColorMode::Dark => Style::default().fg(Color::White), ColorMode::Light => Style::default().fg(Color::Black), }) - .highlight_style(if self.focused_block == FocusedBlock::Adapter { + .row_highlight_style(if self.focused_block == FocusedBlock::Adapter { Style::default().bg(Color::DarkGray) } else { Style::default() @@ -401,19 +405,21 @@ impl App { frame.render_widget(controller_table, controller_block); - let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight) - .begin_symbol(Some("↑")) - .end_symbol(Some("↓")); - let mut scrollbar_state = - ScrollbarState::new(self.controllers.len()).position(selected_controller_index); - frame.render_stateful_widget( - scrollbar, - controller_block.inner(Margin { - vertical: 1, - horizontal: 0, - }), - &mut scrollbar_state, - ); + if rows_len > controller_block.height.saturating_sub(4) as usize { + let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight) + .begin_symbol(Some("↑")) + .end_symbol(Some("↓")); + let mut scrollbar_state = + ScrollbarState::new(self.controllers.len()).position(selected_controller_index); + frame.render_stateful_widget( + scrollbar, + controller_block.inner(Margin { + vertical: 1, + horizontal: 0, + }), + &mut scrollbar_state, + ); + } //Paired devices let rows: Vec = selected_controller @@ -473,6 +479,13 @@ impl App { .collect(); let rows_len = rows.len(); + if rows_len > 0 + && self.focused_block == FocusedBlock::PairedDevices + && self.paired_devices_state.selected().is_none() + { + self.paired_devices_state.select(Some(0)); + } + let show_battery_column = selected_controller .paired_devices .iter() @@ -579,7 +592,7 @@ impl App { ColorMode::Dark => Style::default().fg(Color::White), ColorMode::Light => Style::default().fg(Color::Black), }) - .highlight_style(if self.focused_block == FocusedBlock::PairedDevices { + .row_highlight_style(if self.focused_block == FocusedBlock::PairedDevices { Style::default().bg(Color::DarkGray) } else { Style::default() @@ -591,19 +604,21 @@ impl App { &mut self.paired_devices_state.clone(), ); - let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight) - .begin_symbol(Some("↑")) - .end_symbol(Some("↓")); - let mut scrollbar_state = ScrollbarState::new(rows_len) - .position(self.paired_devices_state.selected().unwrap_or_default()); - frame.render_stateful_widget( - scrollbar, - paired_devices_block.inner(Margin { - vertical: 1, - horizontal: 0, - }), - &mut scrollbar_state, - ); + if rows_len > paired_devices_block.height.saturating_sub(4) as usize { + let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight) + .begin_symbol(Some("↑")) + .end_symbol(Some("↓")); + let mut scrollbar_state = ScrollbarState::new(rows_len) + .position(self.paired_devices_state.selected().unwrap_or_default()); + frame.render_stateful_widget( + scrollbar, + paired_devices_block.inner(Margin { + vertical: 1, + horizontal: 0, + }), + &mut scrollbar_state, + ); + } //New devices @@ -685,7 +700,7 @@ impl App { ColorMode::Dark => Style::default().fg(Color::White), ColorMode::Light => Style::default().fg(Color::Black), }) - .highlight_style(if self.focused_block == FocusedBlock::NewDevices { + .row_highlight_style(if self.focused_block == FocusedBlock::NewDevices { Style::default().bg(Color::DarkGray) } else { Style::default() @@ -698,165 +713,31 @@ impl App { frame.render_stateful_widget(new_devices_table, new_devices_block, &mut state); - let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight) - .begin_symbol(Some("↑")) - .end_symbol(Some("↓")); - let mut scrollbar_state = - ScrollbarState::new(rows_len).position(state.selected().unwrap_or_default()); - frame.render_stateful_widget( - scrollbar, - new_devices_block.inner(Margin { - vertical: 1, - horizontal: 0, - }), - &mut scrollbar_state, - ); + if rows_len > new_devices_block.height.saturating_sub(4) as usize { + let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight) + .begin_symbol(Some("↑")) + .end_symbol(Some("↓")); + let mut scrollbar_state = ScrollbarState::new(rows_len) + .position(state.selected().unwrap_or_default()); + frame.render_stateful_widget( + scrollbar, + new_devices_block.inner(Margin { + vertical: 1, + horizontal: 0, + }), + &mut scrollbar_state, + ); + } } // Pairing confirmation if self.pairing_confirmation.display.load(Ordering::Relaxed) { self.focused_block = FocusedBlock::PassKeyConfirmation; - if self.pairing_confirmation.message.is_none() { - let msg = self - .pairing_confirmation - .confirmation_message_receiver - .recv() - .unwrap(); - self.pairing_confirmation.message = Some(msg); - } - - let popup_area = popup(frame.area()); - - 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(popup_area); - - (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.pairing_confirmation - .message - .clone() - .unwrap_or_default(), - ) - .style(Style::default().fg(Color::White)); - let (yes, no) = { - if self.pairing_confirmation.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, popup_area); - - frame.render_widget( - Block::new() - .borders(Borders::ALL) - .border_type(BorderType::Thick) - .border_style(Style::default().fg(Color::Green)), - popup_area, - ); - frame.render_widget(text.alignment(Alignment::Center), text_area); - frame.render_widget(yes, yes_area); - frame.render_widget(no, no_area); + self.pairing_confirmation.render(frame); } } } - pub async fn new(config: Arc) -> AppResult { - let color_mode = match terminal_light::luma() { - Ok(luma) if luma > 0.6 => ColorMode::Light, - Ok(_) => ColorMode::Dark, - Err(_) => ColorMode::Dark, - }; - - let session = Arc::new(bluer::Session::new().await?); - - // Pairing confirmation - let pairing_confirmation = PairingConfirmation::default(); - - 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 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() - })), - ..Default::default() - }; - - let handle = session.register_agent(agent).await?; - let controllers: Vec = Controller::get_all(session.clone()).await?; - - let mut controller_state = TableState::default(); - if controllers.is_empty() { - controller_state.select(None); - } else { - controller_state.select(Some(0)); - } - - Ok(Self { - running: true, - session, - agent: handle, - help: Help::new(config), - spinner: Spinner::default(), - notifications: Vec::new(), - controllers, - controller_state, - paired_devices_state: TableState::default(), - new_devices_state: TableState::default(), - focused_block: FocusedBlock::Adapter, - pairing_confirmation, - color_mode, - new_alias: Input::default(), - }) - } pub async fn tick(&mut self) -> AppResult<()> { self.notifications.retain(|n| n.ttl > 0); diff --git a/src/confirmation.rs b/src/confirmation.rs new file mode 100644 index 0000000..835685b --- /dev/null +++ b/src/confirmation.rs @@ -0,0 +1,131 @@ +use std::sync::mpsc::channel; +use std::sync::{atomic::AtomicBool, Arc}; + +use ratatui::layout::{Alignment, Constraint, Direction, Layout}; +use ratatui::style::{Color, Style}; +use ratatui::text::{Span, Text}; +use ratatui::widgets::{Block, BorderType, Borders, Clear}; +use ratatui::Frame; + +#[derive(Debug)] +pub struct PairingConfirmation { + pub confirmed: bool, + pub display: Arc, + pub message: Option, + pub user_confirmation_sender: async_channel::Sender, + pub user_confirmation_receiver: async_channel::Receiver, + pub confirmation_message_sender: std::sync::mpsc::Sender, + pub confirmation_message_receiver: std::sync::mpsc::Receiver, +} + +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::(); + 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) { + 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(frame.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); + } +} diff --git a/src/lib.rs b/src/lib.rs index 5a28756..11f4851 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,3 +19,5 @@ pub mod help; pub mod config; pub mod rfkill; + +pub mod confirmation; diff --git a/src/main.rs b/src/main.rs index 8b53ab5..793c818 100644 --- a/src/main.rs +++ b/src/main.rs @@ -39,11 +39,10 @@ async fn main() -> AppResult<()> { ) .await? } - Event::Mouse(_) => {} - Event::Resize(_, _) => {} Event::Notification(notification) => { app.notifications.push(notification); } + _ => {} } }