remove help popup and use help banner instead

This commit is contained in:
Badr
2025-10-20 23:34:45 +02:00
parent cdeb48d5e3
commit 16b3f5bcd8
8 changed files with 466 additions and 680 deletions

553
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -36,6 +36,7 @@ pacman -S bluetui
### 🐧 Gentoo
You can install `bluetui` from the [lamdness Gentoo Overlay](https://gpo.zugaina.org/net-wireless/bluetui):
```sh
sudo eselect repository enable lamdness
sudo emaint -r lamdness sync
@@ -50,7 +51,6 @@ If you are a user of [x-cmd](https://x-cmd.com), you can run:
x install bluetui
```
### ⚒️ Build from source
Run the following command:
@@ -93,7 +93,7 @@ This will produce an executable file at `target/release/bluetui` that you can co
`u`: Unpair the device.
`Space`: Connect/Disconnect the device.
`Space or Enter`: Connect/Disconnect the device.
`t`: Trust/Untrust the device.
@@ -101,7 +101,7 @@ This will produce an executable file at `target/release/bluetui` that you can co
### New devices
`p`: Pair the device.
`Space or Enter`: Pair the device.
## Custom keybindings
@@ -117,12 +117,8 @@ toggle_discovery = "d"
[paired_device]
unpair = "u"
toggle_connect = " "
toggle_trust = "t"
rename = "e"
[new_device]
pair = "p"
```
## ⚖️ License

View File

@@ -19,7 +19,6 @@ use crate::{
bluetooth::{Controller, request_confirmation},
config::Config,
confirmation::PairingConfirmation,
help::Help,
notification::Notification,
spinner::Spinner,
};
@@ -35,7 +34,6 @@ pub enum FocusedBlock {
Adapter,
PairedDevices,
NewDevices,
Help,
PassKeyConfirmation,
SetDeviceAliasBox,
}
@@ -51,7 +49,6 @@ pub struct App {
pub running: bool,
pub session: Arc<Session>,
pub agent: AgentHandle,
pub help: Help,
pub spinner: Spinner,
pub notifications: Vec<Notification>,
pub controllers: Vec<Controller>,
@@ -62,6 +59,7 @@ pub struct App {
pub pairing_confirmation: PairingConfirmation,
pub color_mode: ColorMode,
pub new_alias: Input,
pub config: Arc<Config>,
}
impl App {
@@ -110,7 +108,6 @@ impl App {
running: true,
session,
agent: handle,
help: Help::new(config),
spinner: Spinner::default(),
notifications: Vec::new(),
controllers,
@@ -121,6 +118,7 @@ impl App {
pairing_confirmation,
color_mode,
new_alias: Input::default(),
config,
})
}
@@ -257,7 +255,7 @@ impl App {
let paired_devices_block_height = selected_controller.paired_devices.len() as u16 + 4;
let (paired_devices_block, new_devices_block, controller_block) = {
let (paired_devices_block, new_devices_block, controller_block, help_block) = {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints(if render_new_devices {
@@ -265,17 +263,19 @@ impl App {
Constraint::Length(paired_devices_block_height),
Constraint::Fill(1),
Constraint::Length(adapter_block_height),
Constraint::Length(1),
]
} else {
[
Constraint::Fill(1),
Constraint::Length(0),
Constraint::Length(adapter_block_height),
Constraint::Length(1),
]
})
.margin(1)
.split(frame.area());
(chunks[0], chunks[1], chunks[2])
(chunks[0], chunks[1], chunks[2], chunks[3])
};
//Adapters
@@ -718,11 +718,85 @@ impl App {
}
}
// Help
let help = match self.focused_block {
FocusedBlock::PairedDevices => 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(" | "),
Span::from(self.config.paired_device.unpair.to_string()).bold(),
Span::from(" Unpair"),
Span::from(" | "),
Span::from("󱁐 or ↵ ").bold(),
Span::from(" Dis/Connect"),
Span::from(" | "),
Span::from(self.config.paired_device.toggle_trust.to_string()).bold(),
Span::from(" Un/Trust"),
Span::from(" | "),
Span::from(self.config.paired_device.rename.to_string()).bold(),
Span::from(" Rename"),
Span::from(" | "),
Span::from("").bold(),
Span::from(" Nav"),
]),
FocusedBlock::NewDevices => 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::Adapter => Line::from(vec![
Span::from("s").bold(),
Span::from(" Scan on/off"),
Span::from(" | "),
Span::from(self.config.adapter.toggle_pairing.to_string()).bold(),
Span::from(" Pairing on/off"),
Span::from(" | "),
Span::from(self.config.adapter.toggle_power.to_string()).bold(),
Span::from(" Power on/off"),
Span::from(" | "),
Span::from(self.config.adapter.toggle_discovery.to_string()).bold(),
Span::from(" Discory on/off"),
Span::from(" | "),
Span::from("").bold(),
Span::from(" Nav"),
]),
FocusedBlock::SetDeviceAliasBox => {
Line::from(vec![Span::from("󱊷 ").bold(), Span::from(" Discard")])
}
FocusedBlock::PassKeyConfirmation => {
Line::from(vec![Span::from("󱊷 ").bold(), Span::from(" Discard")])
}
};
frame.render_widget(help.centered().blue(), help_block);
// Pairing confirmation
if self.pairing_confirmation.display.load(Ordering::Relaxed) {
self.focused_block = FocusedBlock::PassKeyConfirmation;
self.pairing_confirmation.render(frame);
return;
}
// Set alias popup
if self.focused_block == FocusedBlock::SetDeviceAliasBox {
self.render_set_alias(frame)
}
}
}

View File

@@ -13,9 +13,6 @@ pub struct Config {
#[serde(default)]
pub paired_device: PairedDevice,
#[serde(default)]
pub new_device: NewDevice,
}
#[derive(Deserialize, Debug)]
@@ -45,9 +42,6 @@ pub struct PairedDevice {
#[serde(default = "default_unpair_device")]
pub unpair: char,
#[serde(default = "default_toggle_device_connection")]
pub toggle_connect: char,
#[serde(default = "default_toggle_device_trust")]
pub toggle_trust: char,
@@ -59,7 +53,6 @@ impl Default for PairedDevice {
fn default() -> Self {
Self {
unpair: 'u',
toggle_connect: ' ',
toggle_trust: 't',
rename: 'e',
}
@@ -70,18 +63,6 @@ fn default_set_new_name() -> char {
'e'
}
#[derive(Deserialize, Debug)]
pub struct NewDevice {
#[serde(default = "default_pair_new_device")]
pub pair: char,
}
impl Default for NewDevice {
fn default() -> Self {
Self { pair: 'p' }
}
}
fn default_toggle_scanning() -> char {
's'
}
@@ -102,18 +83,10 @@ fn default_unpair_device() -> char {
'u'
}
fn default_toggle_device_connection() -> char {
' '
}
fn default_toggle_device_trust() -> char {
't'
}
fn default_pair_new_device() -> char {
'p'
}
impl Config {
pub fn new() -> Self {
let conf_path = dirs::config_dir()

View File

@@ -12,6 +12,121 @@ use tokio::sync::mpsc::UnboundedSender;
use tui_input::backend::crossterm::EventHandler;
async fn toggle_connect(app: &mut App, sender: UnboundedSender<Event>) {
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_connected().await {
Ok(is_connected) => {
if is_connected {
match device.disconnect().await {
Ok(_) => {
let _ = Notification::send(
"Device disconnected".to_string(),
NotificationLevel::Info,
sender.clone(),
);
}
Err(e) => {
let _ = Notification::send(
e.to_string(),
NotificationLevel::Error,
sender.clone(),
);
}
}
} else {
match device.connect().await {
Ok(_) => {
let _ = Notification::send(
"Device connected".to_string(),
NotificationLevel::Info,
sender.clone(),
);
}
Err(e) => {
let _ = Notification::send(
e.to_string(),
NotificationLevel::Error,
sender.clone(),
);
}
}
}
}
Err(e) => {
let _ = Notification::send(
e.to_string(),
NotificationLevel::Error,
sender.clone(),
);
}
}
});
}
Err(e) => {
let _ =
Notification::send(e.to_string(), NotificationLevel::Error, sender.clone());
}
}
}
}
}
async fn pair(app: &mut App, sender: UnboundedSender<Event>) {
if let Some(selected_controller) = app.controller_state.selected() {
let controller = &app.controllers[selected_controller];
if let Some(index) = app.new_devices_state.selected() {
let addr = controller.new_devices[index].addr;
match controller.adapter.device(addr) {
Ok(device) => match device.alias().await {
Ok(device_name) => {
let _ = Notification::send(
format!("Start pairing with the device\n `{device_name}`",),
NotificationLevel::Info,
sender.clone(),
);
tokio::spawn(async move {
match device.pair().await {
Ok(_) => {
let _ = Notification::send(
"Device paired".to_string(),
NotificationLevel::Info,
sender.clone(),
);
}
Err(e) => {
let _ = Notification::send(
e.to_string(),
NotificationLevel::Error,
sender.clone(),
);
}
}
});
}
Err(e) => {
let _ = Notification::send(
e.to_string(),
NotificationLevel::Error,
sender.clone(),
);
}
},
Err(e) => {
let _ =
Notification::send(e.to_string(), NotificationLevel::Error, sender.clone());
}
}
}
}
}
pub async fn handle_key_events(
key_event: KeyEvent,
app: &mut App,
@@ -67,18 +182,6 @@ pub async fn handle_key_events(
app.quit();
}
// Show help
KeyCode::Char('?') => {
app.focused_block = FocusedBlock::Help;
}
// Discard help popup
KeyCode::Esc => {
if app.focused_block == FocusedBlock::Help {
app.focused_block = FocusedBlock::Adapter;
}
}
// Switch focus
KeyCode::Tab => match app.focused_block {
FocusedBlock::Adapter => {
@@ -221,9 +324,6 @@ pub async fn handle_key_events(
}
}
FocusedBlock::Help => {
app.help.scroll_down();
}
_ => {}
},
@@ -265,9 +365,6 @@ pub async fn handle_key_events(
}
}
}
FocusedBlock::Help => {
app.help.scroll_up();
}
_ => {}
},
@@ -356,78 +453,8 @@ pub async fn handle_key_events(
}
// Connect / Disconnect
KeyCode::Char(c) if c == config.paired_device.toggle_connect => {
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_connected().await {
Ok(is_connected) => {
if is_connected {
match device.disconnect().await
{
Ok(_) => {
let _ = Notification::send(
"Device disconnected"
.to_string(),
NotificationLevel::Info,
sender.clone(),
);
}
Err(e) => {
let _ = Notification::send(
e.to_string(),
NotificationLevel::Error,
sender.clone(),
);
}
}
} else {
match device.connect().await {
Ok(_) => {
let _ = Notification::send(
"Device connected"
.to_string(),
NotificationLevel::Info,
sender.clone(),
);
}
Err(e) => {
let _ = Notification::send(
e.to_string(),
NotificationLevel::Error,
sender.clone(),
);
}
}
}
}
Err(e) => {
let _ = Notification::send(
e.to_string(),
NotificationLevel::Error,
sender.clone(),
);
}
}
});
}
Err(e) => {
let _ = Notification::send(
e.to_string(),
NotificationLevel::Error,
sender.clone(),
);
}
}
}
}
}
KeyCode::Enter => toggle_connect(app, sender).await,
KeyCode::Char(' ') => toggle_connect(app, sender).await,
// Trust / Untrust
KeyCode::Char(c) if c == config.paired_device.toggle_trust => {
@@ -718,59 +745,10 @@ pub async fn handle_key_events(
FocusedBlock::NewDevices => {
// Pair new device
if KeyCode::Char(config.new_device.pair) == key_event.code
&& let Some(selected_controller) = app.controller_state.selected()
{
let controller = &app.controllers[selected_controller];
if let Some(index) = app.new_devices_state.selected() {
let addr = controller.new_devices[index].addr;
match controller.adapter.device(addr) {
Ok(device) => match device.alias().await {
Ok(device_name) => {
let _ = Notification::send(
format!(
"Start pairing with the device\n `{device_name}`",
),
NotificationLevel::Info,
sender.clone(),
);
tokio::spawn(async move {
match device.pair().await {
Ok(_) => {
let _ = Notification::send(
"Device paired".to_string(),
NotificationLevel::Info,
sender.clone(),
);
}
Err(e) => {
let _ = Notification::send(
e.to_string(),
NotificationLevel::Error,
sender.clone(),
);
}
}
});
}
Err(e) => {
let _ = Notification::send(
e.to_string(),
NotificationLevel::Error,
sender.clone(),
);
}
},
Err(e) => {
let _ = Notification::send(
e.to_string(),
NotificationLevel::Error,
sender.clone(),
);
}
}
}
match key_event.code {
KeyCode::Enter => pair(app, sender).await,
KeyCode::Char(' ') => pair(app, sender).await,
_ => {}
}
}

View File

@@ -1,194 +0,0 @@
use std::sync::Arc;
use ratatui::{
Frame,
layout::{Alignment, Constraint, Direction, Layout, Margin},
style::{Color, Style, Stylize},
widgets::{
Block, BorderType, Borders, Cell, Clear, Padding, Row, Scrollbar, ScrollbarOrientation,
ScrollbarState, Table, TableState,
},
};
use crate::{app::ColorMode, config::Config};
#[derive(Debug)]
pub struct Help {
block_height: usize,
state: TableState,
keys: Vec<(Cell<'static>, &'static str)>,
}
impl Help {
pub fn new(config: Arc<Config>) -> Self {
let mut state = TableState::new().with_offset(0);
state.select(Some(0));
Self {
block_height: 0,
state,
keys: vec![
(
Cell::from("## Global").style(Style::new().bold().fg(Color::Yellow)),
"",
),
(Cell::from("Esc").bold(), "Dismiss different pop-ups"),
(
Cell::from("Tab or h/l").bold(),
"Switch between different sections",
),
(Cell::from("j or Down").bold(), "Scroll down"),
(Cell::from("k or Up").bold(), "Scroll up"),
(
Cell::from(config.toggle_scanning.to_string()).bold(),
"Start/Stop scanning",
),
(Cell::from("?").bold(), "Show help"),
(Cell::from("ctrl+c or q").bold(), "Quit"),
(Cell::from(""), ""),
(
Cell::from("## Adapters").style(Style::new().bold().fg(Color::Yellow)),
"",
),
(
Cell::from(config.adapter.toggle_pairing.to_string()).bold(),
"Enable/Disable the pairing",
),
(
Cell::from(config.adapter.toggle_power.to_string()).bold(),
"Power on/off the adapter",
),
(
Cell::from(config.adapter.toggle_discovery.to_string()).bold(),
"Enable/Disable the discovery",
),
(Cell::from(""), ""),
(
Cell::from("## Paired devices").style(Style::new().bold().fg(Color::Yellow)),
"",
),
(
Cell::from(config.paired_device.unpair.to_string()).bold(),
"Unpair the device",
),
(
Cell::from({
if config.paired_device.toggle_connect == ' ' {
"Space".to_string()
} else {
config.paired_device.toggle_connect.to_string()
}
})
.bold(),
"Connect/Disconnect the device",
),
(
Cell::from(config.paired_device.toggle_trust.to_string()).bold(),
"Trust/Untrust the device",
),
(
Cell::from(config.paired_device.rename.to_string()).bold(),
"Rename the device",
),
(Cell::from(""), ""),
(
Cell::from("## New devices").style(Style::default().bold().fg(Color::Yellow)),
"",
),
(
Cell::from(config.new_device.pair.to_string()).bold(),
"Pair the device",
),
],
}
}
pub fn scroll_down(&mut self) {
let i = match self.state.selected() {
Some(i) => {
if i >= self.keys.len().saturating_sub(self.block_height - 6) {
i
} else {
i + 1
}
}
None => 1,
};
*self.state.offset_mut() = i;
self.state.select(Some(i));
}
pub fn scroll_up(&mut self) {
let i = match self.state.selected() {
Some(i) => i.saturating_sub(1),
None => 1,
};
*self.state.offset_mut() = i;
self.state.select(Some(i));
}
pub fn render(&mut self, frame: &mut Frame, color_mode: ColorMode) {
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Fill(1),
Constraint::Length(28),
Constraint::Fill(1),
])
.flex(ratatui::layout::Flex::SpaceBetween)
.split(frame.area());
let block = Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Fill(1),
Constraint::Length(70),
Constraint::Fill(1),
])
.flex(ratatui::layout::Flex::SpaceBetween)
.split(layout[1])[1];
self.block_height = block.height as usize;
let widths = [Constraint::Length(20), Constraint::Max(40)];
let rows: Vec<Row> = self
.keys
.iter()
.map(|key| {
Row::new(vec![key.0.to_owned(), key.1.into()]).style(match color_mode {
ColorMode::Dark => Style::default().fg(Color::White),
ColorMode::Light => Style::default().fg(Color::Black),
})
})
.collect();
let rows_len = self.keys.len().saturating_sub(self.block_height - 6);
let table = Table::new(rows, widths).block(
Block::default()
.padding(Padding::uniform(2))
.title(" Help ")
.title_style(Style::default().bold().fg(Color::Green))
.title_alignment(Alignment::Center)
.borders(Borders::ALL)
.style(Style::default())
.border_type(BorderType::Thick)
.border_style(Style::default().fg(Color::Green)),
);
frame.render_widget(Clear, block);
frame.render_stateful_widget(table, block, &mut self.state);
let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight)
.begin_symbol(Some(""))
.end_symbol(Some(""));
let mut scrollbar_state =
ScrollbarState::new(rows_len).position(self.state.selected().unwrap_or_default());
frame.render_stateful_widget(
scrollbar,
block.inner(Margin {
vertical: 1,
horizontal: 0,
}),
&mut scrollbar_state,
);
}
}

View File

@@ -14,8 +14,6 @@ pub mod notification;
pub mod spinner;
pub mod help;
pub mod config;
pub mod rfkill;

View File

@@ -1,18 +1,10 @@
use ratatui::Frame;
use crate::app::{App, FocusedBlock};
use crate::app::App;
pub fn render(app: &mut App, frame: &mut Frame) {
// App
app.render(frame);
match app.focused_block {
FocusedBlock::Help => app.help.render(frame, app.color_mode),
FocusedBlock::SetDeviceAliasBox => app.render_set_alias(frame),
_ => {}
}
// Notifications
for (index, notification) in app.notifications.iter().enumerate() {
notification.render(index, frame);
}