Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
# Pissible values: "auto" or a positive integer
width = "auto"

toggle_scanning = "s"

[adapter]
Expand Down
50 changes: 42 additions & 8 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 @@ -245,6 +263,22 @@ impl App {
}

pub fn render(&mut self, frame: &mut Frame) {
let area = 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(),
};

if let Some(selected_controller_index) = self.controller_state.selected() {
let selected_controller = &self.controllers[selected_controller_index];
// Layout
Expand Down Expand Up @@ -278,7 +312,7 @@ impl App {
]
})
.margin(1)
.split(frame.area());
.split(area);
(chunks[0], chunks[1], chunks[2], chunks[3])
};

Expand Down Expand Up @@ -362,7 +396,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 +579,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 +682,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 @@ -793,7 +827,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, area);
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;
Loading