From 2a5522b10edce41f2584567919330a0379fe1a1c Mon Sep 17 00:00:00 2001 From: oto <13265059+otomist@users.noreply.github.com> Date: Fri, 27 Feb 2026 14:22:25 -0500 Subject: [PATCH 1/5] mouse handler and collect + store section areas for click locations --- src/app.rs | 25 +++++++- src/handler.rs | 106 ++++++++++++++++++++++++++++++++ src/help.rs | 163 ++++++++++++++++++++++++++++++++++++++++++------- src/main.rs | 15 ++++- 4 files changed, 282 insertions(+), 27 deletions(-) diff --git a/src/app.rs b/src/app.rs index 63cd931..4afcfaf 100644 --- a/src/app.rs +++ b/src/app.rs @@ -38,6 +38,14 @@ pub type AppResult = anyhow::Result; const STAR_SYMBOL: &str = "★"; +#[derive(Debug, Clone)] +pub struct HelpSection { + pub label: String, + pub x_start: u16, + pub x_end: u16, + pub y: u16, +} + #[derive(Debug, Clone, Copy, PartialEq)] pub enum FocusedBlock { Adapter, @@ -68,6 +76,11 @@ pub struct App { pub config: Arc, pub requests: Requests, pub auth_agent: AuthAgent, + pub adapter_block_bounds: Option, + pub paired_devices_block_bounds: Option, + pub new_devices_block_bounds: Option, + pub help_block_bounds: Option, + pub help_sections: Vec, } impl App { @@ -130,6 +143,11 @@ impl App { config, requests: Requests::default(), auth_agent, + adapter_block_bounds: None, + paired_devices_block_bounds: None, + new_devices_block_bounds: None, + help_block_bounds: None, + help_sections: Vec::new(), }) } @@ -308,6 +326,10 @@ impl App { (chunks[0], chunks[1], chunks[2], chunks[3]) }; + self.adapter_block_bounds = Some(controller_block); + self.paired_devices_block_bounds = Some(paired_devices_block); + self.new_devices_block_bounds = render_new_devices.then_some(new_devices_block); + //Adapters let rows: Vec = self .controllers @@ -697,7 +719,8 @@ impl App { let area = self.area(frame); // Help - Help::render( + self.help_block_bounds = Some(help_block); + self.help_sections = Help::render( frame, area, self.focused_block, diff --git a/src/handler.rs b/src/handler.rs index 46e9bcf..d29916b 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -844,3 +844,109 @@ pub async fn handle_key_events( Ok(()) } + +fn handle_mouse_down(mouse_event: crossterm::event::MouseEvent, app: &mut App) -> AppResult<()> { + let col = mouse_event.column; + let row = mouse_event.row; + + if let Some(bounds) = app.adapter_block_bounds { + if col >= bounds.x + && col < bounds.x + bounds.width + && row >= bounds.y + && row < bounds.y + bounds.height + { + app.focused_block = FocusedBlock::Adapter; + + // Todo: use actual header height here instead of hardcoding + let header_offset = 3; + if row >= bounds.y + header_offset && !app.controllers.is_empty() { + let clicked_row = (row - bounds.y - header_offset) as usize; + if clicked_row < app.controllers.len() { + app.controller_state.select(Some(clicked_row)); + app.reset_devices_state(); + } + } + return Ok(()); + } + } + + if let Some(bounds) = app.paired_devices_block_bounds { + if col >= bounds.x + && col < bounds.x + bounds.width + && row >= bounds.y + && row < bounds.y + bounds.height + { + app.focused_block = FocusedBlock::PairedDevices; + + if let Some(selected_controller) = app.controller_state.selected() { + let controller = &app.controllers[selected_controller]; + let header_offset = 3; + if row >= bounds.y + header_offset && !controller.paired_devices.is_empty() + { + let clicked_row = (row - bounds.y - header_offset) as usize; + if clicked_row < controller.paired_devices.len() { + app.paired_devices_state.select(Some(clicked_row)); + } + } + } + return Ok(()); + } + } + + if let Some(bounds) = app.new_devices_block_bounds { + if col >= bounds.x + && col < bounds.x + bounds.width + && row >= bounds.y + && row < bounds.y + bounds.height + { + app.focused_block = FocusedBlock::NewDevices; + + if let Some(selected_controller) = app.controller_state.selected() { + let controller = &app.controllers[selected_controller]; + let header_offset = 3; + if row >= bounds.y + header_offset && !controller.new_devices.is_empty() { + let clicked_row = (row - bounds.y - header_offset) as usize; + if clicked_row < controller.new_devices.len() { + app.new_devices_state.select(Some(clicked_row)); + } + } + } + return Ok(()); + } + } + + if let Some(_help_bounds) = app.help_block_bounds { + for section in &app.help_sections { + if row == section.y && col >= section.x_start && col < section.x_end { + eprintln!("Help section clicked: '{}'", section.label); + } + } + } + return Ok(()); +} + +pub async fn handle_mouse_events( + mouse_event: crossterm::event::MouseEvent, + app: &mut App, + _sender: UnboundedSender, + _config: Arc, +) -> AppResult<()> { + use crossterm::event::{MouseButton, MouseEventKind}; + + match mouse_event.kind { + MouseEventKind::Down(MouseButton::Left) => { + let _ = handle_mouse_down(mouse_event, app); + } + MouseEventKind::ScrollDown => { + // I'll add this later once we have a basic sestup in place for clicking + // and defining sections for click interactions. + } + MouseEventKind::ScrollUp => { + // I'll add this later once we have a basic sestup in place for clicking + // and defining sections for click interactions. + } + _ => {} + } + + Ok(()) +} diff --git a/src/help.rs b/src/help.rs index 12d00d6..e9c636b 100644 --- a/src/help.rs +++ b/src/help.rs @@ -8,7 +8,54 @@ use ratatui::{ widgets::Paragraph, }; -use crate::{app::FocusedBlock, config::Config}; +use crate::{ + app::{FocusedBlock, HelpSection}, + config::Config, +}; + +struct HelpItem<'a> { + spans: Vec>, + x_start: u16, + x_end: u16, +} + +impl<'a> HelpItem<'a> { + fn new(spans: Vec>) -> Self { + let width: u16 = spans.iter().map(|s| s.content.len() as u16).sum(); + Self { + spans, + x_start: 0, + x_end: width, + } + } + + fn width(&self) -> u16 { + self.x_end - self.x_start + } + + fn set_position(&mut self, x_start: u16) { + let width = self.width(); + self.x_start = x_start; + self.x_end = x_start + width; + } + + fn get_spans(&self) -> Vec> { + self.spans.clone() + } + + fn label(&self) -> String { + self.spans.iter().map(|s| s.content.as_ref()).collect() + } + + fn to_section(&self, y: u16) -> HelpSection { + HelpSection { + label: self.label().trim().to_string(), + x_start: self.x_start, + x_end: self.x_end, + y, + } + } +} pub struct Help; @@ -19,38 +66,99 @@ impl Help { focused_block: FocusedBlock, rendering_block: Rect, config: Arc, - ) { + ) -> Vec { + let mut section_indexes: Vec<(HelpItem, u16)> = Vec::new(); // (item, line_index) + let help = match focused_block { FocusedBlock::PairedDevices => { if area.width > 120 { - vec![Line::from(vec![ - Span::from("k,").bold(), - Span::from(" Up"), - Span::from(" | "), - Span::from("j,").bold(), - Span::from(" Down"), - Span::from(" | "), - Span::from("s").bold(), - Span::from(" Scan on/off"), - Span::from(" | "), + let mut up_item = + HelpItem::new(vec![Span::from("k,").bold(), Span::from(" Up")]); + let mut down_item = + HelpItem::new(vec![Span::from("j,").bold(), Span::from(" Down")]); + let mut scan_item = + HelpItem::new(vec![Span::from("s").bold(), Span::from(" Scan on/off")]); + let mut unpair_item = HelpItem::new(vec![ Span::from(config.paired_device.unpair.to_string()).bold(), - Span::from(" Unpair"), - Span::from(" | "), - Span::from("󱁐 or ↵ ").bold(), - Span::from(" Dis/Connect"), - Span::from(" | "), + Span::from(" Unpair----"), + ]); + let mut connect_item = + HelpItem::new(vec![Span::from("󱁐 or ↵ ").bold(), Span::from(" Dis/Connect")]); + let mut trust_item = HelpItem::new(vec![ Span::from(config.paired_device.toggle_trust.to_string()).bold(), Span::from(" Un/Trust"), - Span::from(" | "), + ]); + let mut favorite_item = HelpItem::new(vec![ Span::from(config.paired_device.toggle_favorite.to_string()).bold(), Span::from(" Un/Favorite"), - Span::from(" | "), + ]); + let mut rename_item = HelpItem::new(vec![ Span::from(config.paired_device.rename.to_string()).bold(), Span::from(" Rename"), - Span::from(" | "), - Span::from("⇄").bold(), - Span::from(" Nav"), - ])] + ]); + let mut nav_item = + HelpItem::new(vec![Span::from("⇄").bold(), Span::from(" Nav")]); + + let separator = Span::from(" | "); + + let mut all_spans: Vec = Vec::new(); + all_spans.extend(up_item.get_spans()); + all_spans.push(separator.clone()); + all_spans.extend(down_item.get_spans()); + all_spans.push(separator.clone()); + all_spans.extend(scan_item.get_spans()); + all_spans.push(separator.clone()); + all_spans.extend(unpair_item.get_spans()); + all_spans.push(separator.clone()); + all_spans.extend(connect_item.get_spans()); + all_spans.push(separator.clone()); + all_spans.extend(trust_item.get_spans()); + all_spans.push(separator.clone()); + all_spans.extend(favorite_item.get_spans()); + all_spans.push(separator.clone()); + all_spans.extend(rename_item.get_spans()); + all_spans.push(separator.clone()); + all_spans.extend(nav_item.get_spans()); + + // Calculate start_x for centered line + let total_width: u16 = all_spans.iter().map(|s| s.content.len() as u16).sum(); + let start_x = + rendering_block.x + (rendering_block.width.saturating_sub(total_width)) / 2; + + // Set positions on each item + let sep_width = separator.content.len() as u16; + let mut current_x = start_x; + + up_item.set_position(current_x); + current_x += up_item.width() + sep_width; + down_item.set_position(current_x); + current_x += down_item.width() + sep_width; + scan_item.set_position(current_x); + current_x += scan_item.width() + sep_width; + unpair_item.set_position(current_x); + current_x += unpair_item.width() + sep_width; + connect_item.set_position(current_x); + current_x += connect_item.width() + sep_width; + trust_item.set_position(current_x); + current_x += trust_item.width() + sep_width; + favorite_item.set_position(current_x); + current_x += favorite_item.width() + sep_width; + rename_item.set_position(current_x); + current_x += rename_item.width() + sep_width; + nav_item.set_position(current_x); + + // Add all items to helpItem_lines with line index 0 since it's a single line + section_indexes.push((up_item, 0)); + section_indexes.push((down_item, 0)); + section_indexes.push((scan_item, 0)); + section_indexes.push((unpair_item, 0)); + section_indexes.push((connect_item, 0)); + section_indexes.push((trust_item, 0)); + section_indexes.push((favorite_item, 0)); + section_indexes.push((rename_item, 0)); + section_indexes.push((nav_item, 0)); + + vec![Line::from(all_spans)] } else { vec![ Line::from(vec![ @@ -187,7 +295,16 @@ impl Help { ])] } }; + + let mut sections = Vec::new(); + for (item, line_idx) in section_indexes { + let y = rendering_block.y + line_idx; + sections.push(item.to_section(y)); + } + let help = Paragraph::new(help).centered().blue(); frame.render_widget(help, rendering_block); + + sections } } diff --git a/src/main.rs b/src/main.rs index e73e279..e234635 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,7 +3,7 @@ use bluetui::{ cli, config::Config, event::{Event, EventHandler}, - handler::handle_key_events, + handler::{handle_key_events, handle_mouse_events}, rfkill, tui::Tui, }; @@ -49,7 +49,16 @@ async fn main() -> AppResult<()> { config.clone(), ) .await? - } + }, + Event::Mouse(mouse_event) => { + handle_mouse_events( + mouse_event, + &mut app, + tui.events.sender.clone(), + config.clone(), + ) + .await? + }, Event::Notification(notification) => { app.notifications.push(notification); } @@ -135,7 +144,7 @@ async fn main() -> AppResult<()> { app.focused_block = bluetui::app::FocusedBlock::PairedDevices; } - Event::Mouse(_) | Event::Resize(_, _) => {} + Event::Resize(_, _) => {} } } From 05cbbcd913d9f7b79755578ce8e6dcd001d427d3 Mon Sep 17 00:00:00 2001 From: oto <13265059+otomist@users.noreply.github.com> Date: Tue, 17 Mar 2026 12:12:58 -0400 Subject: [PATCH 2/5] enums actions for mouse clicks --- src/app.rs | 22 ++ src/handler.rs | 789 ++++++++++++++++++++++++++++--------------------- src/help.rs | 365 ++++++++++++++++++----- 3 files changed, 754 insertions(+), 422 deletions(-) diff --git a/src/app.rs b/src/app.rs index 4afcfaf..5ee2935 100644 --- a/src/app.rs +++ b/src/app.rs @@ -38,12 +38,34 @@ pub type AppResult = anyhow::Result; const STAR_SYMBOL: &str = "★"; +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum HelpAction { + // Navigation + ScrollUp, + ScrollDown, + // PairedDevices + ToggleConnect, + Unpair, + ToggleTrust, + ToggleFavorite, + Rename, + // Shared + ToggleScan, + // NewDevices + Pair, + // Adapter + TogglePairing, + TogglePower, + ToggleDiscovery, +} + #[derive(Debug, Clone)] pub struct HelpSection { pub label: String, pub x_start: u16, pub x_end: u16, pub y: u16, + pub action: Option, } #[derive(Debug, Clone, Copy, PartialEq)] diff --git a/src/handler.rs b/src/handler.rs index d29916b..584af41 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -1,8 +1,7 @@ use std::sync::Arc; use std::sync::atomic::Ordering; -use crate::app::FocusedBlock; -use crate::app::{App, AppResult}; +use crate::app::{App, AppResult, FocusedBlock, HelpAction}; use crate::config::Config; use crate::event::Event; use crate::notification::{Notification, NotificationLevel}; @@ -12,6 +11,58 @@ use tokio::sync::mpsc::UnboundedSender; use tui_input::backend::crossterm::EventHandler; +async fn toggle_scan(app: &mut App, sender: UnboundedSender) -> AppResult<()> { + if let Some(selected_controller) = app.controller_state.selected() { + let controller = &app.controllers[selected_controller]; + + if controller.is_scanning.load(Ordering::Relaxed) { + controller + .is_scanning + .store(false, std::sync::atomic::Ordering::Relaxed); + + Notification::send( + "Scanning stopped".into(), + NotificationLevel::Info, + sender, + )?; + + app.spinner.active = false; + } else { + controller + .is_scanning + .store(true, std::sync::atomic::Ordering::Relaxed); + app.spinner.active = true; + let adapter = controller.adapter.clone(); + let is_scanning = controller.is_scanning.clone(); + tokio::spawn(async move { + let _ = Notification::send( + "Scanning started".into(), + NotificationLevel::Info, + sender.clone(), + ); + + match adapter.discover_devices().await { + Ok(mut discover) => { + while let Some(_evt) = discover.next().await { + if !is_scanning.load(Ordering::Relaxed) { + break; + } + } + } + Err(e) => { + let _ = Notification::send( + e.into(), + NotificationLevel::Error, + sender.clone(), + ); + } + } + }); + } + } + Ok(()) +} + async fn toggle_connect(app: &mut App, sender: UnboundedSender) { if let Some(selected_controller) = app.controller_state.selected() { let controller = &app.controllers[selected_controller]; @@ -157,6 +208,277 @@ async fn pair(app: &mut App, sender: UnboundedSender) { } } +async fn unpair(app: &mut App, sender: UnboundedSender) -> AppResult<()> { + if let Some(selected_controller) = app.controller_state.selected() { + let controller = &app.controllers[selected_controller]; + if let Some(index) = app.paired_devices_state.selected() { + let addr = controller.paired_devices[index].addr; + match controller.adapter.remove_device(addr).await { + Ok(_) => { + let _ = Notification::send( + "Device unpaired".into(), + NotificationLevel::Info, + sender.clone(), + ); + } + Err(e) => { + let _ = Notification::send( + e.into(), + NotificationLevel::Error, + sender.clone(), + ); + } + } + } + } + Ok(()) +} + +async fn toggle_trust(app: &mut App, sender: UnboundedSender) -> AppResult<()> { + if let Some(selected_controller) = app.controller_state.selected() { + let controller = &app.controllers[selected_controller]; + if let Some(index) = app.paired_devices_state.selected() { + let addr = controller.paired_devices[index].addr; + match controller.adapter.device(addr) { + Ok(device) => { + tokio::spawn(async move { + match device.is_trusted().await { + Ok(is_trusted) => { + if is_trusted { + match device.set_trusted(false).await { + Ok(_) => { + let _ = Notification::send( + "Device untrusted".into(), + NotificationLevel::Info, + sender.clone(), + ); + } + Err(e) => { + let _ = Notification::send( + e.into(), + NotificationLevel::Error, + sender.clone(), + ); + } + } + } else { + match device.set_trusted(true).await { + Ok(_) => { + let _ = Notification::send( + "Device trusted".into(), + NotificationLevel::Info, + sender.clone(), + ); + } + Err(e) => { + let _ = Notification::send( + e.into(), + NotificationLevel::Error, + sender.clone(), + ); + } + } + } + } + Err(e) => { + let _ = Notification::send( + e.into(), + NotificationLevel::Error, + sender.clone(), + ); + } + } + }); + } + Err(e) => { + let _ = Notification::send( + e.into(), + NotificationLevel::Error, + sender.clone(), + ); + } + } + } + } + Ok(()) +} + +async fn toggle_favorite(app: &mut App, sender: UnboundedSender) -> AppResult<()> { + if let Some(selected_controller) = app.controller_state.selected() { + let controller = &app.controllers[selected_controller]; + if let Some(index) = app.paired_devices_state.selected() { + let address = controller.paired_devices[index].addr; + let _ = sender.send(Event::ToggleFavorite(address)); + } + } + Ok(()) +} + +async fn toggle_pairing(app: &mut App, sender: UnboundedSender) -> AppResult<()> { + if let Some(selected_controller) = app.controller_state.selected() { + let adapter = app.controllers[selected_controller].adapter.clone(); + tokio::spawn(async move { + match adapter.is_pairable().await { + Ok(is_pairable) => { + if is_pairable { + match adapter.set_pairable(false).await { + Ok(_) => { + let _ = Notification::send( + "Adapter unpairable".into(), + NotificationLevel::Info, + sender.clone(), + ); + } + Err(e) => { + let _ = Notification::send( + e.into(), + NotificationLevel::Error, + sender.clone(), + ); + } + } + } else { + match adapter.set_pairable(true).await { + Ok(_) => { + let _ = Notification::send( + "Adapter pairable".into(), + NotificationLevel::Info, + sender.clone(), + ); + } + Err(e) => { + let _ = Notification::send( + e.into(), + NotificationLevel::Error, + sender.clone(), + ); + } + } + } + } + Err(e) => { + let _ = Notification::send( + e.into(), + NotificationLevel::Error, + sender.clone(), + ); + } + } + }); + } + Ok(()) +} + +async fn toggle_power(app: &mut App, sender: UnboundedSender) -> AppResult<()> { + if let Some(selected_controller) = app.controller_state.selected() { + let adapter = app.controllers[selected_controller].adapter.clone(); + tokio::spawn(async move { + match adapter.is_powered().await { + Ok(is_powered) => { + if is_powered { + match adapter.set_powered(false).await { + Ok(_) => { + let _ = Notification::send( + "Adapter powered off".into(), + NotificationLevel::Info, + sender.clone(), + ); + } + Err(e) => { + let _ = Notification::send( + e.into(), + NotificationLevel::Error, + sender.clone(), + ); + } + } + } else { + match adapter.set_powered(true).await { + Ok(_) => { + let _ = Notification::send( + "Adapter powered on".into(), + NotificationLevel::Info, + sender.clone(), + ); + } + Err(e) => { + let _ = Notification::send( + e.into(), + NotificationLevel::Error, + sender.clone(), + ); + } + } + } + } + Err(e) => { + let _ = Notification::send( + e.into(), + NotificationLevel::Error, + sender.clone(), + ); + } + } + }); + } + Ok(()) +} + +async fn toggle_discovery(app: &mut App, sender: UnboundedSender) -> AppResult<()> { + if let Some(selected_controller) = app.controller_state.selected() { + let adapter = app.controllers[selected_controller].adapter.clone(); + tokio::spawn(async move { + match adapter.is_discoverable().await { + Ok(is_discoverable) => { + if is_discoverable { + match adapter.set_discoverable(false).await { + Ok(_) => { + let _ = Notification::send( + "Adapter undiscoverable".into(), + NotificationLevel::Info, + sender.clone(), + ); + } + Err(e) => { + let _ = Notification::send( + e.into(), + NotificationLevel::Error, + sender.clone(), + ); + } + } + } else { + match adapter.set_discoverable(true).await { + Ok(_) => { + let _ = Notification::send( + "Adapter discoverable".into(), + NotificationLevel::Info, + sender.clone(), + ); + } + Err(e) => { + let _ = Notification::send( + e.into(), + NotificationLevel::Error, + sender.clone(), + ); + } + } + } + } + Err(e) => { + let _ = Notification::send( + e.into(), + NotificationLevel::Error, + sender.clone(), + ); + } + } + }); + } + Ok(()) +} + pub async fn handle_key_events( key_event: KeyEvent, app: &mut App, @@ -443,54 +765,7 @@ pub async fn handle_key_events( // Start/Stop Scan KeyCode::Char(c) if c == config.toggle_scanning => { - if let Some(selected_controller) = app.controller_state.selected() { - let controller = &app.controllers[selected_controller]; - - if controller.is_scanning.load(Ordering::Relaxed) { - controller - .is_scanning - .store(false, std::sync::atomic::Ordering::Relaxed); - - Notification::send( - "Scanning stopped".into(), - NotificationLevel::Info, - sender, - )?; - - app.spinner.active = false; - } else { - controller - .is_scanning - .store(true, std::sync::atomic::Ordering::Relaxed); - app.spinner.active = true; - let adapter = controller.adapter.clone(); - let is_scanning = controller.is_scanning.clone(); - tokio::spawn(async move { - let _ = Notification::send( - "Scanning started".into(), - NotificationLevel::Info, - sender.clone(), - ); - - match adapter.discover_devices().await { - Ok(mut discover) => { - while let Some(_evt) = discover.next().await { - if !is_scanning.load(Ordering::Relaxed) { - break; - } - } - } - Err(e) => { - let _ = Notification::send( - e.into(), - NotificationLevel::Error, - sender.clone(), - ); - } - } - }); - } - } + toggle_scan(app, sender).await?; } _ => { @@ -499,30 +774,7 @@ pub async fn handle_key_events( match key_event.code { // Unpair KeyCode::Char(c) if c == config.paired_device.unpair => { - if let Some(selected_controller) = - app.controller_state.selected() - { - let controller = &app.controllers[selected_controller]; - if let Some(index) = app.paired_devices_state.selected() { - let addr = controller.paired_devices[index].addr; - match controller.adapter.remove_device(addr).await { - Ok(_) => { - let _ = Notification::send( - "Device unpaired".into(), - NotificationLevel::Info, - sender.clone(), - ); - } - Err(e) => { - let _ = Notification::send( - e.into(), - NotificationLevel::Error, - sender.clone(), - ); - } - } - } - } + unpair(app, sender.clone()).await?; } // Connect / Disconnect @@ -531,95 +783,12 @@ pub async fn handle_key_events( // Trust / Untrust KeyCode::Char(c) if c == config.paired_device.toggle_trust => { - if let Some(selected_controller) = - app.controller_state.selected() - { - let controller = &app.controllers[selected_controller]; - if let Some(index) = app.paired_devices_state.selected() { - let addr = controller.paired_devices[index].addr; - match controller.adapter.device(addr) { - Ok(device) => { - tokio::spawn(async move { - match device.is_trusted().await { - Ok(is_trusted) => { - if is_trusted { - match device - .set_trusted(false) - .await - { - Ok(_) => { - let _ = Notification::send( - "Device untrusted" - .into(), - NotificationLevel::Info, - sender.clone(), - ); - } - Err(e) => { - let _ = Notification::send( - e.into(), - NotificationLevel::Error, - sender.clone(), - ); - } - } - } else { - match device - .set_trusted(true) - .await - { - Ok(_) => { - let _ = Notification::send( - "Device trusted" - .into(), - NotificationLevel::Info, - sender.clone(), - ); - } - - Err(e) => { - let _ = Notification::send( - e.into(), - NotificationLevel::Error, - sender.clone(), - ); - } - } - } - } - Err(e) => { - let _ = Notification::send( - e.into(), - NotificationLevel::Error, - sender.clone(), - ); - } - } - }); - } - Err(e) => { - let _ = Notification::send( - e.into(), - NotificationLevel::Error, - sender.clone(), - ); - } - } - } - } + toggle_trust(app, sender.clone()).await?; } // Favorite / Unfavorite KeyCode::Char(c) if c == config.paired_device.toggle_favorite => { - if let Some(selected_controller) = - app.controller_state.selected() - { - let controller = &app.controllers[selected_controller]; - if let Some(index) = app.paired_devices_state.selected() { - let address = controller.paired_devices[index].addr; - let _ = sender.send(Event::ToggleFavorite(address)); - } - } + toggle_favorite(app, sender.clone()).await?; } KeyCode::Char(c) if c == config.paired_device.rename => { @@ -634,192 +803,17 @@ pub async fn handle_key_events( match key_event.code { // toggle pairing KeyCode::Char(c) if c == config.adapter.toggle_pairing => { - if let Some(selected_controller) = - app.controller_state.selected() - { - let adapter = &app.controllers[selected_controller].adapter; - tokio::spawn({ - let adapter = adapter.clone(); - async move { - match adapter.is_pairable().await { - Ok(is_pairable) => { - if is_pairable { - match adapter.set_pairable(false).await - { - Ok(_) => { - let _ = Notification::send( - "Adapter unpairable".into(), - NotificationLevel::Info, - sender.clone(), - ); - } - Err(e) => { - let _ = Notification::send( - e.into(), - NotificationLevel::Error, - sender.clone(), - ); - } - } - } else { - match adapter.set_pairable(true).await { - Ok(_) => { - let _ = Notification::send( - "Adapter pairable".into(), - NotificationLevel::Info, - sender.clone(), - ); - } - Err(e) => { - let _ = Notification::send( - e.into(), - NotificationLevel::Error, - sender.clone(), - ); - } - } - } - } - Err(e) => { - let _ = Notification::send( - e.into(), - NotificationLevel::Error, - sender.clone(), - ); - } - } - } - }); - } + toggle_pairing(app, sender.clone()).await?; } // toggle power KeyCode::Char(c) if c == config.adapter.toggle_power => { - if let Some(selected_controller) = - app.controller_state.selected() - { - let adapter = &app.controllers[selected_controller].adapter; - tokio::spawn({ - let adapter = adapter.clone(); - async move { - match adapter.is_powered().await { - Ok(is_powered) => { - if is_powered { - match adapter.set_powered(false).await { - Ok(_) => { - let _ = Notification::send( - "Adapter powered off" - .into(), - NotificationLevel::Info, - sender.clone(), - ); - } - Err(e) => { - let _ = Notification::send( - e.into(), - NotificationLevel::Error, - sender.clone(), - ); - } - } - } else { - match adapter.set_powered(true).await { - Ok(_) => { - let _ = Notification::send( - "Adapter powered on".into(), - NotificationLevel::Info, - sender.clone(), - ); - } - Err(e) => { - let _ = Notification::send( - e.into(), - NotificationLevel::Error, - sender.clone(), - ); - } - } - } - } - Err(e) => { - let _ = Notification::send( - e.into(), - NotificationLevel::Error, - sender.clone(), - ); - } - } - } - }); - } + toggle_power(app, sender.clone()).await?; } // toggle discovery KeyCode::Char(c) if c == config.adapter.toggle_discovery => { - if let Some(selected_controller) = - app.controller_state.selected() - { - let adapter = &app.controllers[selected_controller].adapter; - tokio::spawn({ - let adapter = adapter.clone(); - async move { - match adapter.is_discoverable().await { - Ok(is_discoverable) => { - if is_discoverable { - match adapter - .set_discoverable(false) - .await - { - Ok(_) => { - let _ = Notification::send( - "Adapter undiscoverable" - .into(), - NotificationLevel::Info, - sender.clone(), - ); - } - Err(e) => { - let _ = Notification::send( - e.into(), - NotificationLevel::Error, - sender.clone(), - ); - } - } - } else { - match adapter - .set_discoverable(true) - .await - { - Ok(_) => { - let _ = Notification::send( - "Adapter discoverable" - .into(), - NotificationLevel::Info, - sender.clone(), - ); - } - Err(e) => { - let _ = Notification::send( - e.into(), - NotificationLevel::Error, - sender.clone(), - ); - } - } - } - } - Err(e) => { - let _ = Notification::send( - e.into(), - NotificationLevel::Error, - sender.clone(), - ); - } - } - } - }); - } + toggle_discovery(app, sender.clone()).await?; } _ => {} @@ -845,7 +839,7 @@ pub async fn handle_key_events( Ok(()) } -fn handle_mouse_down(mouse_event: crossterm::event::MouseEvent, app: &mut App) -> AppResult<()> { +async fn handle_mouse_down(mouse_event: crossterm::event::MouseEvent, app: &mut App, sender: UnboundedSender) -> AppResult<()> { let col = mouse_event.column; let row = mouse_event.row; @@ -916,9 +910,118 @@ fn handle_mouse_down(mouse_event: crossterm::event::MouseEvent, app: &mut App) - } if let Some(_help_bounds) = app.help_block_bounds { - for section in &app.help_sections { - if row == section.y && col >= section.x_start && col < section.x_end { - eprintln!("Help section clicked: '{}'", section.label); + let found_item = app + .help_sections + .iter() + .find(|s| row == s.y && col >= s.x_start && col < s.x_end); + + if found_item.is_some() { + eprintln!("Clicked on help section: {:?}", found_item); + } + let clicked_action = found_item.and_then(|s| s.action); + + if let Some(action) = clicked_action { + match action { + HelpAction::ScrollUp => { + match app.focused_block { + FocusedBlock::Adapter => { + if !app.controllers.is_empty() { + let i = app.controller_state.selected() + .map(|i| if i > 0 { i - 1 } else { app.controllers.len() - 1 }) + .unwrap_or(0); + app.controller_state.select(Some(i)); + } + } + FocusedBlock::PairedDevices => { + if let Some(sel) = app.controller_state.selected() { + let controller = &app.controllers[sel]; + if !controller.paired_devices.is_empty() { + let i = app.paired_devices_state.selected() + .map(|i| if i > 0 { i - 1 } else { controller.paired_devices.len() - 1 }) + .unwrap_or(0); + app.paired_devices_state.select(Some(i)); + } + } + } + FocusedBlock::NewDevices => { + if let Some(sel) = app.controller_state.selected() { + let controller = &app.controllers[sel]; + if !controller.new_devices.is_empty() { + let i = app.new_devices_state.selected() + .map(|i| if i > 0 { i - 1 } else { controller.new_devices.len() - 1 }) + .unwrap_or(0); + app.new_devices_state.select(Some(i)); + } + } + } + _ => {} + } + } + HelpAction::ScrollDown => { + match app.focused_block { + FocusedBlock::Adapter => { + if !app.controllers.is_empty() { + let i = app.controller_state.selected() + .map(|i| if i < app.controllers.len() - 1 { i + 1 } else { 0 }) + .unwrap_or(0); + app.controller_state.select(Some(i)); + } + } + FocusedBlock::PairedDevices => { + if let Some(sel) = app.controller_state.selected() { + let controller = &app.controllers[sel]; + if !controller.paired_devices.is_empty() { + let i = app.paired_devices_state.selected() + .map(|i| if i < controller.paired_devices.len() - 1 { i + 1 } else { 0 }) + .unwrap_or(0); + app.paired_devices_state.select(Some(i)); + } + } + } + FocusedBlock::NewDevices => { + if let Some(sel) = app.controller_state.selected() { + let controller = &app.controllers[sel]; + if !controller.new_devices.is_empty() { + let i = app.new_devices_state.selected() + .map(|i| if i < controller.new_devices.len() - 1 { i + 1 } else { 0 }) + .unwrap_or(0); + app.new_devices_state.select(Some(i)); + } + } + } + _ => {} + } + } + HelpAction::ToggleConnect => { + toggle_connect(app, sender).await; + } + HelpAction::Unpair => { + unpair(app, sender).await?; + } + HelpAction::ToggleTrust => { + toggle_trust(app, sender).await?; + } + HelpAction::ToggleFavorite => { + toggle_favorite(app, sender).await?; + } + HelpAction::Rename => { + app.focused_block = FocusedBlock::SetDeviceAliasBox; + } + HelpAction::ToggleScan => { + toggle_scan(app, sender).await?; + } + HelpAction::Pair => { + pair(app, sender).await; + } + HelpAction::TogglePairing => { + toggle_pairing(app, sender).await?; + } + HelpAction::TogglePower => { + toggle_power(app, sender).await?; + } + HelpAction::ToggleDiscovery => { + toggle_discovery(app, sender).await?; + } } } } @@ -928,14 +1031,14 @@ fn handle_mouse_down(mouse_event: crossterm::event::MouseEvent, app: &mut App) - pub async fn handle_mouse_events( mouse_event: crossterm::event::MouseEvent, app: &mut App, - _sender: UnboundedSender, + sender: UnboundedSender, _config: Arc, ) -> AppResult<()> { use crossterm::event::{MouseButton, MouseEventKind}; match mouse_event.kind { MouseEventKind::Down(MouseButton::Left) => { - let _ = handle_mouse_down(mouse_event, app); + let _ = handle_mouse_down(mouse_event, app, sender).await; } MouseEventKind::ScrollDown => { // I'll add this later once we have a basic sestup in place for clicking diff --git a/src/help.rs b/src/help.rs index e9c636b..d14eacd 100644 --- a/src/help.rs +++ b/src/help.rs @@ -9,7 +9,7 @@ use ratatui::{ }; use crate::{ - app::{FocusedBlock, HelpSection}, + app::{FocusedBlock, HelpAction, HelpSection}, config::Config, }; @@ -17,15 +17,17 @@ struct HelpItem<'a> { spans: Vec>, x_start: u16, x_end: u16, + action: Option, } impl<'a> HelpItem<'a> { - fn new(spans: Vec>) -> Self { + fn new(spans: Vec>, action: Option) -> Self { let width: u16 = spans.iter().map(|s| s.content.len() as u16).sum(); Self { spans, x_start: 0, x_end: width, + action, } } @@ -53,6 +55,7 @@ impl<'a> HelpItem<'a> { x_start: self.x_start, x_end: self.x_end, y, + action: self.action, } } } @@ -73,31 +76,31 @@ impl Help { FocusedBlock::PairedDevices => { if area.width > 120 { let mut up_item = - HelpItem::new(vec![Span::from("k,").bold(), Span::from(" Up")]); + HelpItem::new(vec![Span::from("k,").bold(), Span::from(" Up")], Some(HelpAction::ScrollUp)); let mut down_item = - HelpItem::new(vec![Span::from("j,").bold(), Span::from(" Down")]); + HelpItem::new(vec![Span::from("j,").bold(), Span::from(" Down")], Some(HelpAction::ScrollDown)); let mut scan_item = - HelpItem::new(vec![Span::from("s").bold(), Span::from(" Scan on/off")]); + HelpItem::new(vec![Span::from("s").bold(), Span::from(" Scan on/off")], Some(HelpAction::ToggleScan)); let mut unpair_item = HelpItem::new(vec![ Span::from(config.paired_device.unpair.to_string()).bold(), Span::from(" Unpair----"), - ]); + ], Some(HelpAction::Unpair)); let mut connect_item = - HelpItem::new(vec![Span::from("󱁐 or ↵ ").bold(), Span::from(" Dis/Connect")]); + HelpItem::new(vec![Span::from("󱁐 or ↵ ").bold(), Span::from(" Dis/Connect")], Some(HelpAction::ToggleConnect)); let mut trust_item = HelpItem::new(vec![ Span::from(config.paired_device.toggle_trust.to_string()).bold(), Span::from(" Un/Trust"), - ]); + ], Some(HelpAction::ToggleTrust)); let mut favorite_item = HelpItem::new(vec![ Span::from(config.paired_device.toggle_favorite.to_string()).bold(), Span::from(" Un/Favorite"), - ]); + ], Some(HelpAction::ToggleFavorite)); let mut rename_item = HelpItem::new(vec![ Span::from(config.paired_device.rename.to_string()).bold(), Span::from(" Rename"), - ]); + ], Some(HelpAction::Rename)); let mut nav_item = - HelpItem::new(vec![Span::from("⇄").bold(), Span::from(" Nav")]); + HelpItem::new(vec![Span::from("⇄").bold(), Span::from(" Nav")], None); let separator = Span::from(" | "); @@ -160,93 +163,297 @@ impl Help { vec![Line::from(all_spans)] } else { - vec![ - Line::from(vec![ - Span::from("󱁐 or ↵ ").bold(), - Span::from(" Dis/Connect"), - Span::from(" | "), - Span::from("s").bold(), - Span::from(" Scan on/off"), - Span::from(" | "), + let mut connect_item = HelpItem::new( + vec![Span::from("󱁐 or ↵ ").bold(), Span::from(" Dis/Connect")], + Some(HelpAction::ToggleConnect), + ); + let mut scan_item = HelpItem::new( + vec![Span::from("s").bold(), Span::from(" Scan on/off")], + Some(HelpAction::ToggleScan), + ); + let mut unpair_item = HelpItem::new( + vec![ Span::from(config.paired_device.unpair.to_string()).bold(), Span::from(" Unpair"), - Span::from(" | "), + ], + Some(HelpAction::Unpair), + ); + let mut favorite_item = HelpItem::new( + vec![ Span::from(config.paired_device.toggle_favorite.to_string()).bold(), Span::from(" Un/Favorite"), - ]), - Line::from(vec![ + ], + Some(HelpAction::ToggleFavorite), + ); + + let mut trust_item = HelpItem::new( + vec![ Span::from(config.paired_device.toggle_trust.to_string()).bold(), Span::from(" Un/Trust"), - Span::from(" | "), + ], + Some(HelpAction::ToggleTrust), + ); + let mut rename_item = HelpItem::new( + vec![ Span::from(config.paired_device.rename.to_string()).bold(), Span::from(" Rename"), - Span::from(" | "), - Span::from("k,").bold(), - Span::from(" Up"), - Span::from(" | "), - Span::from("j,").bold(), - Span::from(" Down"), - Span::from(" | "), - Span::from("⇄").bold(), - Span::from(" Nav"), - ]), - ] + ], + Some(HelpAction::Rename), + ); + let mut up_item = + HelpItem::new(vec![Span::from("k,").bold(), Span::from(" Up")], Some(HelpAction::ScrollUp)); + let mut down_item = + HelpItem::new(vec![Span::from("j,").bold(), Span::from(" Down")], Some(HelpAction::ScrollDown)); + let mut nav_item = + HelpItem::new(vec![Span::from("⇄").bold(), Span::from(" Nav")], None); + + let separator = Span::from(" | "); + let sep_width = separator.content.len() as u16; + + let mut line1: Vec = Vec::new(); + line1.extend(connect_item.get_spans()); + line1.push(separator.clone()); + line1.extend(scan_item.get_spans()); + line1.push(separator.clone()); + line1.extend(unpair_item.get_spans()); + line1.push(separator.clone()); + line1.extend(favorite_item.get_spans()); + + let total_width1: u16 = line1.iter().map(|s| s.content.len() as u16).sum(); + let start_x1 = + rendering_block.x + (rendering_block.width.saturating_sub(total_width1)) / 2; + let mut current_x1 = start_x1; + + connect_item.set_position(current_x1); + current_x1 += connect_item.width() + sep_width; + scan_item.set_position(current_x1); + current_x1 += scan_item.width() + sep_width; + unpair_item.set_position(current_x1); + current_x1 += unpair_item.width() + sep_width; + favorite_item.set_position(current_x1); + + section_indexes.push((connect_item, 0)); + section_indexes.push((scan_item, 0)); + section_indexes.push((unpair_item, 0)); + section_indexes.push((favorite_item, 0)); + + let mut line2: Vec = Vec::new(); + line2.extend(trust_item.get_spans()); + line2.push(separator.clone()); + line2.extend(rename_item.get_spans()); + line2.push(separator.clone()); + line2.extend(up_item.get_spans()); + line2.push(separator.clone()); + line2.extend(down_item.get_spans()); + line2.push(separator.clone()); + line2.extend(nav_item.get_spans()); + + let total_width2: u16 = line2.iter().map(|s| s.content.len() as u16).sum(); + let start_x2 = + rendering_block.x + (rendering_block.width.saturating_sub(total_width2)) / 2; + let mut current_x2 = start_x2; + + trust_item.set_position(current_x2); + current_x2 += trust_item.width() + sep_width; + rename_item.set_position(current_x2); + current_x2 += rename_item.width() + sep_width; + up_item.set_position(current_x2); + current_x2 += up_item.width() + sep_width; + down_item.set_position(current_x2); + current_x2 += down_item.width() + sep_width; + nav_item.set_position(current_x2); + + section_indexes.push((trust_item, 1)); + section_indexes.push((rename_item, 1)); + section_indexes.push((up_item, 1)); + section_indexes.push((down_item, 1)); + section_indexes.push((nav_item, 1)); + + vec![Line::from(line1), Line::from(line2)] } } - FocusedBlock::NewDevices => vec![Line::from(vec![ - Span::from("k,").bold(), - Span::from(" Up"), - Span::from(" | "), - Span::from("j,").bold(), - Span::from(" Down"), - Span::from(" | "), - Span::from("󱁐 or ↵ ").bold(), - Span::from(" Pair"), - Span::from(" | "), - Span::from("s").bold(), - Span::from(" Scan on/off"), - Span::from(" | "), - Span::from("⇄").bold(), - Span::from(" Nav"), - ])], + FocusedBlock::NewDevices => { + let mut up_item = + HelpItem::new(vec![Span::from("k,").bold(), Span::from(" Up")], Some(HelpAction::ScrollUp)); + let mut down_item = + HelpItem::new(vec![Span::from("j,").bold(), Span::from(" Down")], Some(HelpAction::ScrollDown)); + let mut pair_item = + HelpItem::new(vec![Span::from("󱁐 or ↵ ").bold(), Span::from(" Pair")], Some(HelpAction::Pair)); + let mut scan_item = + HelpItem::new(vec![Span::from("s").bold(), Span::from(" Scan on/off")], Some(HelpAction::ToggleScan)); + let mut nav_item = + HelpItem::new(vec![Span::from("⇄").bold(), Span::from(" Nav")], None); + + let separator = Span::from(" | "); + let sep_width = separator.content.len() as u16; + + let mut all_spans: Vec = Vec::new(); + all_spans.extend(up_item.get_spans()); + all_spans.push(separator.clone()); + all_spans.extend(down_item.get_spans()); + all_spans.push(separator.clone()); + all_spans.extend(pair_item.get_spans()); + all_spans.push(separator.clone()); + all_spans.extend(scan_item.get_spans()); + all_spans.push(separator.clone()); + all_spans.extend(nav_item.get_spans()); + + let total_width: u16 = all_spans.iter().map(|s| s.content.len() as u16).sum(); + let start_x = + rendering_block.x + (rendering_block.width.saturating_sub(total_width)) / 2; + let mut current_x = start_x; + + up_item.set_position(current_x); + current_x += up_item.width() + sep_width; + down_item.set_position(current_x); + current_x += down_item.width() + sep_width; + pair_item.set_position(current_x); + current_x += pair_item.width() + sep_width; + scan_item.set_position(current_x); + current_x += scan_item.width() + sep_width; + nav_item.set_position(current_x); + + section_indexes.push((up_item, 0)); + section_indexes.push((down_item, 0)); + section_indexes.push((pair_item, 0)); + section_indexes.push((scan_item, 0)); + section_indexes.push((nav_item, 0)); + + vec![Line::from(all_spans)] + } FocusedBlock::Adapter => { if area.width > 80 { - vec![Line::from(vec![ - Span::from("s").bold(), - Span::from(" Scan on/off"), - Span::from(" | "), - Span::from(config.adapter.toggle_pairing.to_string()).bold(), - Span::from(" Pairing on/off"), - Span::from(" | "), - Span::from(config.adapter.toggle_power.to_string()).bold(), - Span::from(" Power on/off"), - Span::from(" | "), - Span::from(config.adapter.toggle_discovery.to_string()).bold(), - Span::from(" Discovery on/off"), - Span::from(" | "), - Span::from("⇄").bold(), - Span::from(" Nav"), - ])] + let mut scan_item = + HelpItem::new(vec![Span::from("s").bold(), Span::from(" Scan on/off")], Some(HelpAction::ToggleScan)); + let mut pairing_item = HelpItem::new( + vec![ + Span::from(config.adapter.toggle_pairing.to_string()).bold(), + Span::from(" Pairing on/off"), + ], + Some(HelpAction::TogglePairing), + ); + let mut power_item = HelpItem::new( + vec![ + Span::from(config.adapter.toggle_power.to_string()).bold(), + Span::from(" Power on/off"), + ], + Some(HelpAction::TogglePower), + ); + let mut discovery_item = HelpItem::new( + vec![ + Span::from(config.adapter.toggle_discovery.to_string()).bold(), + Span::from(" Discovery on/off"), + ], + Some(HelpAction::ToggleDiscovery), + ); + let mut nav_item = + HelpItem::new(vec![Span::from("⇄").bold(), Span::from(" Nav")], None); + + let separator = Span::from(" | "); + let sep_width = separator.content.len() as u16; + + let mut all_spans: Vec = Vec::new(); + all_spans.extend(scan_item.get_spans()); + all_spans.push(separator.clone()); + all_spans.extend(pairing_item.get_spans()); + all_spans.push(separator.clone()); + all_spans.extend(power_item.get_spans()); + all_spans.push(separator.clone()); + all_spans.extend(discovery_item.get_spans()); + all_spans.push(separator.clone()); + all_spans.extend(nav_item.get_spans()); + + let total_width: u16 = all_spans.iter().map(|s| s.content.len() as u16).sum(); + let start_x = + rendering_block.x + (rendering_block.width.saturating_sub(total_width)) / 2; + let mut current_x = start_x; + + scan_item.set_position(current_x); + current_x += scan_item.width() + sep_width; + pairing_item.set_position(current_x); + current_x += pairing_item.width() + sep_width; + power_item.set_position(current_x); + current_x += power_item.width() + sep_width; + discovery_item.set_position(current_x); + current_x += discovery_item.width() + sep_width; + nav_item.set_position(current_x); + + section_indexes.push((scan_item, 0)); + section_indexes.push((pairing_item, 0)); + section_indexes.push((power_item, 0)); + section_indexes.push((discovery_item, 0)); + section_indexes.push((nav_item, 0)); + + vec![Line::from(all_spans)] } else { - vec![ - Line::from(vec![ - Span::from("s").bold(), - Span::from(" Scan on/off"), - Span::from(" | "), + let mut scan_item = + HelpItem::new(vec![Span::from("s").bold(), Span::from(" Scan on/off")], Some(HelpAction::ToggleScan)); + let mut pairing_item = HelpItem::new( + vec![ Span::from(config.adapter.toggle_pairing.to_string()).bold(), Span::from(" Pairing on/off"), - ]), - Line::from(vec![ + ], + Some(HelpAction::TogglePairing), + ); + + let mut power_item = HelpItem::new( + vec![ Span::from(config.adapter.toggle_power.to_string()).bold(), Span::from(" Power on/off"), - Span::from(" | "), + ], + Some(HelpAction::TogglePower), + ); + let mut discovery_item = HelpItem::new( + vec![ Span::from(config.adapter.toggle_discovery.to_string()).bold(), Span::from(" Discovery on/off"), - Span::from(" | "), - Span::from("⇄").bold(), - Span::from(" Nav"), - ]), - ] + ], + Some(HelpAction::ToggleDiscovery), + ); + let mut nav_item = + HelpItem::new(vec![Span::from("⇄").bold(), Span::from(" Nav")], None); + + let separator = Span::from(" | "); + let sep_width = separator.content.len() as u16; + + let mut line1: Vec = Vec::new(); + line1.extend(scan_item.get_spans()); + line1.push(separator.clone()); + line1.extend(pairing_item.get_spans()); + + let total_width1: u16 = line1.iter().map(|s| s.content.len() as u16).sum(); + let start_x1 = + rendering_block.x + (rendering_block.width.saturating_sub(total_width1)) / 2; + let mut current_x1 = start_x1; + scan_item.set_position(current_x1); + current_x1 += scan_item.width() + sep_width; + pairing_item.set_position(current_x1); + + section_indexes.push((scan_item, 0)); + section_indexes.push((pairing_item, 0)); + + let mut line2: Vec = Vec::new(); + line2.extend(power_item.get_spans()); + line2.push(separator.clone()); + line2.extend(discovery_item.get_spans()); + line2.push(separator.clone()); + line2.extend(nav_item.get_spans()); + + let total_width2: u16 = line2.iter().map(|s| s.content.len() as u16).sum(); + let start_x2 = + rendering_block.x + (rendering_block.width.saturating_sub(total_width2)) / 2; + let mut current_x2 = start_x2; + power_item.set_position(current_x2); + current_x2 += power_item.width() + sep_width; + discovery_item.set_position(current_x2); + current_x2 += discovery_item.width() + sep_width; + nav_item.set_position(current_x2); + + section_indexes.push((power_item, 1)); + section_indexes.push((discovery_item, 1)); + section_indexes.push((nav_item, 1)); + + vec![Line::from(line1), Line::from(line2)] } } FocusedBlock::SetDeviceAliasBox => { From 0642c83fab87d0765e42328b7d6f2d353b58993d Mon Sep 17 00:00:00 2001 From: oto <13265059+otomist@users.noreply.github.com> Date: Tue, 17 Mar 2026 12:35:43 -0400 Subject: [PATCH 3/5] cleanup --- src/app.rs | 6 ------ src/handler.rs | 31 +++++++++++++++---------------- src/help.rs | 5 ----- 3 files changed, 15 insertions(+), 27 deletions(-) diff --git a/src/app.rs b/src/app.rs index 5ee2935..6a9187c 100644 --- a/src/app.rs +++ b/src/app.rs @@ -40,20 +40,15 @@ const STAR_SYMBOL: &str = "★"; #[derive(Debug, Clone, Copy, PartialEq)] pub enum HelpAction { - // Navigation ScrollUp, ScrollDown, - // PairedDevices ToggleConnect, Unpair, ToggleTrust, ToggleFavorite, Rename, - // Shared ToggleScan, - // NewDevices Pair, - // Adapter TogglePairing, TogglePower, ToggleDiscovery, @@ -61,7 +56,6 @@ pub enum HelpAction { #[derive(Debug, Clone)] pub struct HelpSection { - pub label: String, pub x_start: u16, pub x_end: u16, pub y: u16, diff --git a/src/handler.rs b/src/handler.rs index 584af41..ad85c2d 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -839,16 +839,19 @@ pub async fn handle_key_events( Ok(()) } +fn is_within_bounds(col: u16, row: u16, bounds: ratatui::layout::Rect) -> bool { + let right = bounds.x.saturating_add(bounds.width); + let bottom = bounds.y.saturating_add(bounds.height); + + col >= bounds.x && col < right && row >= bounds.y && row < bottom +} + async fn handle_mouse_down(mouse_event: crossterm::event::MouseEvent, app: &mut App, sender: UnboundedSender) -> AppResult<()> { let col = mouse_event.column; let row = mouse_event.row; if let Some(bounds) = app.adapter_block_bounds { - if col >= bounds.x - && col < bounds.x + bounds.width - && row >= bounds.y - && row < bounds.y + bounds.height - { + if is_within_bounds(col, row, bounds) { app.focused_block = FocusedBlock::Adapter; // Todo: use actual header height here instead of hardcoding @@ -865,11 +868,7 @@ async fn handle_mouse_down(mouse_event: crossterm::event::MouseEvent, app: &mut } if let Some(bounds) = app.paired_devices_block_bounds { - if col >= bounds.x - && col < bounds.x + bounds.width - && row >= bounds.y - && row < bounds.y + bounds.height - { + if is_within_bounds(col, row, bounds) { app.focused_block = FocusedBlock::PairedDevices; if let Some(selected_controller) = app.controller_state.selected() { @@ -888,11 +887,7 @@ async fn handle_mouse_down(mouse_event: crossterm::event::MouseEvent, app: &mut } if let Some(bounds) = app.new_devices_block_bounds { - if col >= bounds.x - && col < bounds.x + bounds.width - && row >= bounds.y - && row < bounds.y + bounds.height - { + if is_within_bounds(col, row, bounds) { app.focused_block = FocusedBlock::NewDevices; if let Some(selected_controller) = app.controller_state.selected() { @@ -909,7 +904,11 @@ async fn handle_mouse_down(mouse_event: crossterm::event::MouseEvent, app: &mut } } - if let Some(_help_bounds) = app.help_block_bounds { + if let Some(help_bounds) = app.help_block_bounds { + if !is_within_bounds(col, row, help_bounds) { + return Ok(()); + } + let found_item = app .help_sections .iter() diff --git a/src/help.rs b/src/help.rs index d14eacd..6dc8fb5 100644 --- a/src/help.rs +++ b/src/help.rs @@ -45,13 +45,8 @@ impl<'a> HelpItem<'a> { self.spans.clone() } - fn label(&self) -> String { - self.spans.iter().map(|s| s.content.as_ref()).collect() - } - fn to_section(&self, y: u16) -> HelpSection { HelpSection { - label: self.label().trim().to_string(), x_start: self.x_start, x_end: self.x_end, y, From e4e05e5ef95455496af3b3dad5491aa20db92a8e Mon Sep 17 00:00:00 2001 From: oto <13265059+otomist@users.noreply.github.com> Date: Tue, 17 Mar 2026 12:53:49 -0400 Subject: [PATCH 4/5] add helper function for creating rows to cleanup lots of duplicate addition lines --- src/help.rs | 133 ++++++++++++++++++++++++---------------------------- 1 file changed, 62 insertions(+), 71 deletions(-) diff --git a/src/help.rs b/src/help.rs index 6dc8fb5..db8133b 100644 --- a/src/help.rs +++ b/src/help.rs @@ -55,6 +55,21 @@ impl<'a> HelpItem<'a> { } } +fn place_items_in_row(items: &mut [&mut HelpItem<'_>], start_x: u16, sep_width: u16) { + let mut current_x = start_x; + let last_idx = items.len().saturating_sub(1); + + for (idx, item) in items.iter_mut().enumerate() { + item.set_position(current_x); + + if idx < last_idx { + current_x = current_x + .saturating_add(item.width()) + .saturating_add(sep_width); + } + } +} + pub struct Help; impl Help { @@ -125,25 +140,18 @@ impl Help { // Set positions on each item let sep_width = separator.content.len() as u16; - let mut current_x = start_x; - - up_item.set_position(current_x); - current_x += up_item.width() + sep_width; - down_item.set_position(current_x); - current_x += down_item.width() + sep_width; - scan_item.set_position(current_x); - current_x += scan_item.width() + sep_width; - unpair_item.set_position(current_x); - current_x += unpair_item.width() + sep_width; - connect_item.set_position(current_x); - current_x += connect_item.width() + sep_width; - trust_item.set_position(current_x); - current_x += trust_item.width() + sep_width; - favorite_item.set_position(current_x); - current_x += favorite_item.width() + sep_width; - rename_item.set_position(current_x); - current_x += rename_item.width() + sep_width; - nav_item.set_position(current_x); + let mut items = [ + &mut up_item, + &mut down_item, + &mut scan_item, + &mut unpair_item, + &mut connect_item, + &mut trust_item, + &mut favorite_item, + &mut rename_item, + &mut nav_item, + ]; + place_items_in_row(&mut items, start_x, sep_width); // Add all items to helpItem_lines with line index 0 since it's a single line section_indexes.push((up_item, 0)); @@ -217,15 +225,13 @@ impl Help { let total_width1: u16 = line1.iter().map(|s| s.content.len() as u16).sum(); let start_x1 = rendering_block.x + (rendering_block.width.saturating_sub(total_width1)) / 2; - let mut current_x1 = start_x1; - - connect_item.set_position(current_x1); - current_x1 += connect_item.width() + sep_width; - scan_item.set_position(current_x1); - current_x1 += scan_item.width() + sep_width; - unpair_item.set_position(current_x1); - current_x1 += unpair_item.width() + sep_width; - favorite_item.set_position(current_x1); + let mut line1_items = [ + &mut connect_item, + &mut scan_item, + &mut unpair_item, + &mut favorite_item, + ]; + place_items_in_row(&mut line1_items, start_x1, sep_width); section_indexes.push((connect_item, 0)); section_indexes.push((scan_item, 0)); @@ -246,17 +252,14 @@ impl Help { let total_width2: u16 = line2.iter().map(|s| s.content.len() as u16).sum(); let start_x2 = rendering_block.x + (rendering_block.width.saturating_sub(total_width2)) / 2; - let mut current_x2 = start_x2; - - trust_item.set_position(current_x2); - current_x2 += trust_item.width() + sep_width; - rename_item.set_position(current_x2); - current_x2 += rename_item.width() + sep_width; - up_item.set_position(current_x2); - current_x2 += up_item.width() + sep_width; - down_item.set_position(current_x2); - current_x2 += down_item.width() + sep_width; - nav_item.set_position(current_x2); + let mut line2_items = [ + &mut trust_item, + &mut rename_item, + &mut up_item, + &mut down_item, + &mut nav_item, + ]; + place_items_in_row(&mut line2_items, start_x2, sep_width); section_indexes.push((trust_item, 1)); section_indexes.push((rename_item, 1)); @@ -296,17 +299,14 @@ impl Help { let total_width: u16 = all_spans.iter().map(|s| s.content.len() as u16).sum(); let start_x = rendering_block.x + (rendering_block.width.saturating_sub(total_width)) / 2; - let mut current_x = start_x; - - up_item.set_position(current_x); - current_x += up_item.width() + sep_width; - down_item.set_position(current_x); - current_x += down_item.width() + sep_width; - pair_item.set_position(current_x); - current_x += pair_item.width() + sep_width; - scan_item.set_position(current_x); - current_x += scan_item.width() + sep_width; - nav_item.set_position(current_x); + let mut items = [ + &mut up_item, + &mut down_item, + &mut pair_item, + &mut scan_item, + &mut nav_item, + ]; + place_items_in_row(&mut items, start_x, sep_width); section_indexes.push((up_item, 0)); section_indexes.push((down_item, 0)); @@ -361,17 +361,14 @@ impl Help { let total_width: u16 = all_spans.iter().map(|s| s.content.len() as u16).sum(); let start_x = rendering_block.x + (rendering_block.width.saturating_sub(total_width)) / 2; - let mut current_x = start_x; - - scan_item.set_position(current_x); - current_x += scan_item.width() + sep_width; - pairing_item.set_position(current_x); - current_x += pairing_item.width() + sep_width; - power_item.set_position(current_x); - current_x += power_item.width() + sep_width; - discovery_item.set_position(current_x); - current_x += discovery_item.width() + sep_width; - nav_item.set_position(current_x); + let mut items = [ + &mut scan_item, + &mut pairing_item, + &mut power_item, + &mut discovery_item, + &mut nav_item, + ]; + place_items_in_row(&mut items, start_x, sep_width); section_indexes.push((scan_item, 0)); section_indexes.push((pairing_item, 0)); @@ -419,10 +416,8 @@ impl Help { let total_width1: u16 = line1.iter().map(|s| s.content.len() as u16).sum(); let start_x1 = rendering_block.x + (rendering_block.width.saturating_sub(total_width1)) / 2; - let mut current_x1 = start_x1; - scan_item.set_position(current_x1); - current_x1 += scan_item.width() + sep_width; - pairing_item.set_position(current_x1); + let mut line1_items = [&mut scan_item, &mut pairing_item]; + place_items_in_row(&mut line1_items, start_x1, sep_width); section_indexes.push((scan_item, 0)); section_indexes.push((pairing_item, 0)); @@ -437,12 +432,8 @@ impl Help { let total_width2: u16 = line2.iter().map(|s| s.content.len() as u16).sum(); let start_x2 = rendering_block.x + (rendering_block.width.saturating_sub(total_width2)) / 2; - let mut current_x2 = start_x2; - power_item.set_position(current_x2); - current_x2 += power_item.width() + sep_width; - discovery_item.set_position(current_x2); - current_x2 += discovery_item.width() + sep_width; - nav_item.set_position(current_x2); + let mut line2_items = [&mut power_item, &mut discovery_item, &mut nav_item]; + place_items_in_row(&mut line2_items, start_x2, sep_width); section_indexes.push((power_item, 1)); section_indexes.push((discovery_item, 1)); From 8af0d03f9796ecbe432b26a350c7d28e2da979e2 Mon Sep 17 00:00:00 2001 From: oto <13265059+otomist@users.noreply.github.com> Date: Tue, 17 Mar 2026 13:09:38 -0400 Subject: [PATCH 5/5] rename and add helper function --- src/app.rs | 4 +- src/help.rs | 124 ++++++++++++++++++++++++++-------------------------- 2 files changed, 65 insertions(+), 63 deletions(-) diff --git a/src/app.rs b/src/app.rs index 6a9187c..39ef672 100644 --- a/src/app.rs +++ b/src/app.rs @@ -55,7 +55,7 @@ pub enum HelpAction { } #[derive(Debug, Clone)] -pub struct HelpSection { +pub struct ClickableHelpItem { pub x_start: u16, pub x_end: u16, pub y: u16, @@ -96,7 +96,7 @@ pub struct App { pub paired_devices_block_bounds: Option, pub new_devices_block_bounds: Option, pub help_block_bounds: Option, - pub help_sections: Vec, + pub help_sections: Vec, } impl App { diff --git a/src/help.rs b/src/help.rs index db8133b..1c5b1b3 100644 --- a/src/help.rs +++ b/src/help.rs @@ -9,7 +9,7 @@ use ratatui::{ }; use crate::{ - app::{FocusedBlock, HelpAction, HelpSection}, + app::{FocusedBlock, HelpAction, ClickableHelpItem}, config::Config, }; @@ -45,8 +45,8 @@ impl<'a> HelpItem<'a> { self.spans.clone() } - fn to_section(&self, y: u16) -> HelpSection { - HelpSection { + fn to_clickable_item(&self, y: u16) -> ClickableHelpItem { + ClickableHelpItem { x_start: self.x_start, x_end: self.x_end, y, @@ -55,12 +55,19 @@ impl<'a> HelpItem<'a> { } } -fn place_items_in_row(items: &mut [&mut HelpItem<'_>], start_x: u16, sep_width: u16) { +fn place_items_in_row( + items: &mut [&mut HelpItem<'_>], + start_x: u16, + sep_width: u16, + y: u16, + clickable_items: &mut Vec, +) { let mut current_x = start_x; let last_idx = items.len().saturating_sub(1); for (idx, item) in items.iter_mut().enumerate() { item.set_position(current_x); + clickable_items.push(item.to_clickable_item(y)); if idx < last_idx { current_x = current_x @@ -79,8 +86,8 @@ impl Help { focused_block: FocusedBlock, rendering_block: Rect, config: Arc, - ) -> Vec { - let mut section_indexes: Vec<(HelpItem, u16)> = Vec::new(); // (item, line_index) + ) -> Vec { + let mut clickable_items: Vec = Vec::new(); let help = match focused_block { FocusedBlock::PairedDevices => { @@ -151,18 +158,13 @@ impl Help { &mut rename_item, &mut nav_item, ]; - place_items_in_row(&mut items, start_x, sep_width); - - // Add all items to helpItem_lines with line index 0 since it's a single line - section_indexes.push((up_item, 0)); - section_indexes.push((down_item, 0)); - section_indexes.push((scan_item, 0)); - section_indexes.push((unpair_item, 0)); - section_indexes.push((connect_item, 0)); - section_indexes.push((trust_item, 0)); - section_indexes.push((favorite_item, 0)); - section_indexes.push((rename_item, 0)); - section_indexes.push((nav_item, 0)); + place_items_in_row( + &mut items, + start_x, + sep_width, + rendering_block.y, + &mut clickable_items, + ); vec![Line::from(all_spans)] } else { @@ -231,12 +233,13 @@ impl Help { &mut unpair_item, &mut favorite_item, ]; - place_items_in_row(&mut line1_items, start_x1, sep_width); - - section_indexes.push((connect_item, 0)); - section_indexes.push((scan_item, 0)); - section_indexes.push((unpair_item, 0)); - section_indexes.push((favorite_item, 0)); + place_items_in_row( + &mut line1_items, + start_x1, + sep_width, + rendering_block.y, + &mut clickable_items, + ); let mut line2: Vec = Vec::new(); line2.extend(trust_item.get_spans()); @@ -259,13 +262,13 @@ impl Help { &mut down_item, &mut nav_item, ]; - place_items_in_row(&mut line2_items, start_x2, sep_width); - - section_indexes.push((trust_item, 1)); - section_indexes.push((rename_item, 1)); - section_indexes.push((up_item, 1)); - section_indexes.push((down_item, 1)); - section_indexes.push((nav_item, 1)); + place_items_in_row( + &mut line2_items, + start_x2, + sep_width, + rendering_block.y + 1, + &mut clickable_items, + ); vec![Line::from(line1), Line::from(line2)] } @@ -306,13 +309,13 @@ impl Help { &mut scan_item, &mut nav_item, ]; - place_items_in_row(&mut items, start_x, sep_width); - - section_indexes.push((up_item, 0)); - section_indexes.push((down_item, 0)); - section_indexes.push((pair_item, 0)); - section_indexes.push((scan_item, 0)); - section_indexes.push((nav_item, 0)); + place_items_in_row( + &mut items, + start_x, + sep_width, + rendering_block.y, + &mut clickable_items, + ); vec![Line::from(all_spans)] } @@ -368,13 +371,13 @@ impl Help { &mut discovery_item, &mut nav_item, ]; - place_items_in_row(&mut items, start_x, sep_width); - - section_indexes.push((scan_item, 0)); - section_indexes.push((pairing_item, 0)); - section_indexes.push((power_item, 0)); - section_indexes.push((discovery_item, 0)); - section_indexes.push((nav_item, 0)); + place_items_in_row( + &mut items, + start_x, + sep_width, + rendering_block.y, + &mut clickable_items, + ); vec![Line::from(all_spans)] } else { @@ -417,10 +420,13 @@ impl Help { let start_x1 = rendering_block.x + (rendering_block.width.saturating_sub(total_width1)) / 2; let mut line1_items = [&mut scan_item, &mut pairing_item]; - place_items_in_row(&mut line1_items, start_x1, sep_width); - - section_indexes.push((scan_item, 0)); - section_indexes.push((pairing_item, 0)); + place_items_in_row( + &mut line1_items, + start_x1, + sep_width, + rendering_block.y, + &mut clickable_items, + ); let mut line2: Vec = Vec::new(); line2.extend(power_item.get_spans()); @@ -433,11 +439,13 @@ impl Help { let start_x2 = rendering_block.x + (rendering_block.width.saturating_sub(total_width2)) / 2; let mut line2_items = [&mut power_item, &mut discovery_item, &mut nav_item]; - place_items_in_row(&mut line2_items, start_x2, sep_width); - - section_indexes.push((power_item, 1)); - section_indexes.push((discovery_item, 1)); - section_indexes.push((nav_item, 1)); + place_items_in_row( + &mut line2_items, + start_x2, + sep_width, + rendering_block.y + 1, + &mut clickable_items, + ); vec![Line::from(line1), Line::from(line2)] } @@ -489,15 +497,9 @@ impl Help { } }; - let mut sections = Vec::new(); - for (item, line_idx) in section_indexes { - let y = rendering_block.y + line_idx; - sections.push(item.to_section(y)); - } - let help = Paragraph::new(help).centered().blue(); frame.render_widget(help, rendering_block); - sections + clickable_items } }