Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
36 changes: 27 additions & 9 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand All @@ -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,
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -278,7 +296,7 @@ impl App {
]
})
.margin(1)
.split(frame.area());
.split(self.area(frame));
(chunks[0], chunks[1], chunks[2], chunks[3])
};

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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"),
Expand Down Expand 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;
}

Expand Down
17 changes: 17 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
@@ -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 <config>)
.short('c')
.id("config")
.required(false)
.help("Config file path")
.value_parser(value_parser!(PathBuf)),
)
}
127 changes: 114 additions & 13 deletions src/config.rs
Original file line number Diff line number Diff line change
@@ -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,

Expand All @@ -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<E>(self, value: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
match value {
"auto" => Ok(Width::Auto),
_ => value
.parse::<u16>()
.map(Width::Size)
.map_err(|_| de::Error::invalid_value(Unexpected::Str(value), &self)),
}
}

fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
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<E>(self, value: i64) -> Result<Self::Value, E>
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<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_any(WidthVisitor)
}
}

#[derive(Deserialize, Debug)]
pub struct Adapter {
#[serde(default = "default_toggle_adapter_pairing")]
Expand Down Expand Up @@ -59,6 +131,33 @@ impl Default for PairedDevice {
}
}

fn deserialize_layout<'de, D>(deserializer: D) -> Result<Flex, D::Error>
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'
}
Expand Down Expand Up @@ -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<PathBuf>) -> 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()
}
}
6 changes: 3 additions & 3 deletions src/confirmation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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);
Expand All @@ -53,7 +53,7 @@ impl PairingConfirmation {
Constraint::Length(5),
Constraint::Fill(1),
])
.split(frame.area());
.split(area);

let block = Layout::default()
.direction(Direction::Horizontal)
Expand Down
23 changes: 7 additions & 16 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -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;
22 changes: 16 additions & 6 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -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::<PathBuf>("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)?;
Expand Down
Loading