diff --git a/Readme.md b/Readme.md index beba688..6c4c54b 100644 --- a/Readme.md +++ b/Readme.md @@ -105,9 +105,16 @@ This will produce an executable file at `target/release/bluetui` that you can co ## Custom keybindings -Keybindings can be customized in the config file `$HOME/.config/bluetui/config.toml` +Keybindings can be customized in the default config file location `$HOME/.config/bluetui/config.toml` or from a custom path with `-c` ```toml +# Possible values: "Legacy", "Start", "End", "Center", "SpaceAround", "SpaceBetween" +layout = "SpaceAround" + +# Window width +# Possible values: "auto" or a positive integer +width = "auto" + toggle_scanning = "s" [adapter] diff --git a/src/app.rs b/src/app.rs index 4f71840..b989572 100644 --- a/src/app.rs +++ b/src/app.rs @@ -5,7 +5,7 @@ use bluer::{ use futures::FutureExt; use ratatui::{ Frame, - layout::{Alignment, Constraint, Direction, Layout, Margin}, + layout::{Alignment, Constraint, Direction, Layout, Margin, Rect}, style::{Color, Modifier, Style, Stylize}, text::{Line, Span}, widgets::{ @@ -17,7 +17,7 @@ use tui_input::Input; use crate::{ bluetooth::{Controller, request_confirmation}, - config::Config, + config::{Config, Width}, confirmation::PairingConfirmation, notification::Notification, spinner::Spinner, @@ -139,6 +139,24 @@ impl App { } } + pub fn area(&self, frame: &Frame) -> Rect { + match self.config.width { + Width::Size(v) => { + if v < frame.area().width { + let area = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Length(v), Constraint::Fill(1)]) + .split(frame.area()); + + area[0] + } else { + frame.area() + } + } + _ => frame.area(), + } + } + pub fn render_set_alias(&mut self, frame: &mut Frame) { let area = Layout::default() .direction(Direction::Vertical) @@ -147,7 +165,7 @@ impl App { Constraint::Length(6), Constraint::Fill(1), ]) - .split(frame.area()); + .split(self.area(frame)); let area = Layout::default() .direction(Direction::Horizontal) @@ -278,7 +296,7 @@ impl App { ] }) .margin(1) - .split(frame.area()); + .split(self.area(frame)); (chunks[0], chunks[1], chunks[2], chunks[3]) }; @@ -362,7 +380,7 @@ impl App { } }), ) - .flex(ratatui::layout::Flex::SpaceAround) + .flex(self.config.layout) .row_highlight_style(if self.focused_block == FocusedBlock::Adapter { Style::default().bg(Color::DarkGray).fg(Color::White) } else { @@ -545,7 +563,7 @@ impl App { } }), ) - .flex(ratatui::layout::Flex::SpaceAround) + .flex(self.config.layout) .row_highlight_style(if self.focused_block == FocusedBlock::PairedDevices { Style::default().bg(Color::DarkGray).fg(Color::White) } else { @@ -648,7 +666,7 @@ impl App { } }), ) - .flex(ratatui::layout::Flex::SpaceAround) + .flex(self.config.layout) .row_highlight_style(if self.focused_block == FocusedBlock::NewDevices { Style::default().bg(Color::DarkGray).fg(Color::White) } else { @@ -682,7 +700,7 @@ impl App { // Help let help = match self.focused_block { FocusedBlock::PairedDevices => { - if frame.area().width > 103 { + if self.area(frame).width > 103 { vec![Line::from(vec![ Span::from("k,").bold(), Span::from(" Up"), @@ -793,7 +811,7 @@ impl App { if self.pairing_confirmation.display.load(Ordering::Relaxed) { self.focused_block = FocusedBlock::PassKeyConfirmation; - self.pairing_confirmation.render(frame); + self.pairing_confirmation.render(frame, self.area(frame)); return; } diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..898e2aa --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,17 @@ +use std::path::PathBuf; + +use clap::{Command, arg, crate_description, crate_name, crate_version, value_parser}; + +pub fn cli() -> Command { + Command::new(crate_name!()) + .about(crate_description!()) + .version(crate_version!()) + .arg( + arg!(--config ) + .short('c') + .id("config") + .required(false) + .help("Config file path") + .value_parser(value_parser!(PathBuf)), + ) +} diff --git a/src/config.rs b/src/config.rs index 8927b50..3041a62 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,10 +1,23 @@ +use core::fmt; +use std::{path::PathBuf, process::exit}; + +use ratatui::layout::Flex; use toml; use dirs; -use serde::Deserialize; +use serde::{ + Deserialize, Deserializer, + de::{self, Unexpected, Visitor}, +}; #[derive(Deserialize, Debug)] pub struct Config { + #[serde(default = "default_layout", deserialize_with = "deserialize_layout")] + pub layout: Flex, + + #[serde(default = "Width::default")] + pub width: Width, + #[serde(default = "default_toggle_scanning")] pub toggle_scanning: char, @@ -15,6 +28,65 @@ pub struct Config { pub paired_device: PairedDevice, } +#[derive(Debug, Default)] +pub enum Width { + #[default] + Auto, + Size(u16), +} + +struct WidthVisitor; + +impl<'de> Visitor<'de> for WidthVisitor { + type Value = Width; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("the string \"auto\" or a positive integer (u16)") + } + + fn visit_str(self, value: &str) -> Result + where + E: de::Error, + { + match value { + "auto" => Ok(Width::Auto), + _ => value + .parse::() + .map(Width::Size) + .map_err(|_| de::Error::invalid_value(Unexpected::Str(value), &self)), + } + } + + fn visit_u64(self, value: u64) -> Result + where + E: de::Error, + { + match u16::try_from(value) { + Ok(v) => Ok(Width::Size(v)), + Err(_) => Err(de::Error::invalid_value(Unexpected::Unsigned(value), &self)), + } + } + + fn visit_i64(self, value: i64) -> Result + where + E: de::Error, + { + match u16::try_from(value) { + Ok(v) => Ok(Width::Size(v)), + Err(_) => Err(de::Error::invalid_value(Unexpected::Signed(value), &self)), + } + } +} + +impl<'de> Deserialize<'de> for Width { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_any(WidthVisitor) + } +} + #[derive(Deserialize, Debug)] pub struct Adapter { #[serde(default = "default_toggle_adapter_pairing")] @@ -59,6 +131,33 @@ impl Default for PairedDevice { } } +fn deserialize_layout<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let s = String::deserialize(deserializer)?; + + match s.as_str() { + "Legacy" => Ok(Flex::Legacy), + "Start" => Ok(Flex::Start), + "End" => Ok(Flex::End), + "Center" => Ok(Flex::Center), + "SpaceAround" => Ok(Flex::SpaceAround), + "SpaceBetween" => Ok(Flex::SpaceBetween), + _ => { + eprintln!("Wrong config: unknown layout variant {}", s); + eprintln!( + "The possible values are: Legacy, Start, End, Center, SpaceAround, SpaceBetween" + ); + std::process::exit(1); + } + } +} + +fn default_layout() -> Flex { + Flex::SpaceAround +} + fn default_set_new_name() -> char { 'e' } @@ -88,21 +187,23 @@ fn default_toggle_device_trust() -> char { } impl Config { - pub fn new() -> Self { - let conf_path = dirs::config_dir() - .unwrap() - .join("bluetui") - .join("config.toml"); + pub fn new(config_file_path: Option) -> Self { + let conf_path = config_file_path.unwrap_or( + dirs::config_dir() + .unwrap() + .join("bluetui") + .join("config.toml"), + ); let config = std::fs::read_to_string(conf_path).unwrap_or_default(); - let app_config: Config = toml::from_str(&config).unwrap(); + let app_config: Config = match toml::from_str(&config) { + Ok(c) => c, + Err(e) => { + eprintln!("{}", e); + exit(1); + } + }; app_config } } - -impl Default for Config { - fn default() -> Self { - Self::new() - } -} diff --git a/src/confirmation.rs b/src/confirmation.rs index 9436b95..09b9621 100644 --- a/src/confirmation.rs +++ b/src/confirmation.rs @@ -2,7 +2,7 @@ use std::sync::mpsc::channel; use std::sync::{Arc, atomic::AtomicBool}; use ratatui::Frame; -use ratatui::layout::{Alignment, Constraint, Direction, Layout}; +use ratatui::layout::{Alignment, Constraint, Direction, Layout, Rect}; use ratatui::style::{Color, Style}; use ratatui::text::{Span, Text}; use ratatui::widgets::{Block, BorderType, Borders, Clear}; @@ -40,7 +40,7 @@ impl PairingConfirmation { } } - pub fn render(&mut self, frame: &mut Frame) { + pub fn render(&mut self, frame: &mut Frame, area: Rect) { if self.message.is_none() { let msg = self.confirmation_message_receiver.recv().unwrap(); self.message = Some(msg); @@ -53,7 +53,7 @@ impl PairingConfirmation { Constraint::Length(5), Constraint::Fill(1), ]) - .split(frame.area()); + .split(area); let block = Layout::default() .direction(Direction::Horizontal) diff --git a/src/lib.rs b/src/lib.rs index 4f2fc37..40740e3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,21 +1,12 @@ pub mod app; - +pub mod bluetooth; +pub mod cli; +pub mod config; +pub mod confirmation; pub mod event; - -pub mod ui; - -pub mod tui; - pub mod handler; - -pub mod bluetooth; - pub mod notification; - -pub mod spinner; - -pub mod config; - pub mod rfkill; - -pub mod confirmation; +pub mod spinner; +pub mod tui; +pub mod ui; diff --git a/src/main.rs b/src/main.rs index dd913e0..bb3019a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,24 +1,34 @@ use bluetui::{ app::{App, AppResult}, + cli, config::Config, event::{Event, EventHandler}, handler::handle_key_events, rfkill, tui::Tui, }; -use clap::{Command, crate_version}; use ratatui::{Terminal, backend::CrosstermBackend}; -use std::{io, sync::Arc}; +use std::{io, path::PathBuf, process::exit, sync::Arc}; #[tokio::main] async fn main() -> AppResult<()> { - Command::new("bluetui") - .version(crate_version!()) - .get_matches(); + let args = cli::cli().get_matches(); + + let config_file_path = if let Some(config) = args.get_one::("config") { + if config.exists() { + Some(config.to_owned()) + } else { + eprintln!("Config file not found"); + exit(1); + } + } else { + None + }; rfkill::check()?; - let config = Arc::new(Config::new()); + let config = Arc::new(Config::new(config_file_path)); + let mut app = App::new(config.clone()).await?; let backend = CrosstermBackend::new(io::stdout()); let terminal = Terminal::new(backend)?; diff --git a/src/notification.rs b/src/notification.rs index e12637c..7cd9824 100644 --- a/src/notification.rs +++ b/src/notification.rs @@ -24,7 +24,7 @@ pub enum NotificationLevel { } impl Notification { - pub fn render(&self, index: usize, frame: &mut Frame) { + pub fn render(&self, index: usize, frame: &mut Frame, area: Rect) { let (color, title) = match self.level { NotificationLevel::Info => (Color::Green, "Info"), NotificationLevel::Warning => (Color::Yellow, "Warning"), @@ -51,12 +51,7 @@ impl Notification { .border_style(Style::default().fg(color)), ); - let area = notification_rect( - index as u16, - notification_height, - notification_width, - frame.area(), - ); + let area = notification_rect(index as u16, notification_height, notification_width, area); frame.render_widget(Clear, area); frame.render_widget(block, area); diff --git a/src/ui.rs b/src/ui.rs index 91b0758..d53ff47 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -6,6 +6,6 @@ pub fn render(app: &mut App, frame: &mut Frame) { app.render(frame); for (index, notification) in app.notifications.iter().enumerate() { - notification.render(index, frame); + notification.render(index, frame, app.area(frame)); } }