Skip to content

Commit d330577

Browse files
authored
customize layout (#84)
1 parent 01e3de8 commit d330577

File tree

9 files changed

+195
-56
lines changed

9 files changed

+195
-56
lines changed

Readme.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,9 +105,16 @@ This will produce an executable file at `target/release/bluetui` that you can co
105105

106106
## Custom keybindings
107107

108-
Keybindings can be customized in the config file `$HOME/.config/bluetui/config.toml`
108+
Keybindings can be customized in the default config file location `$HOME/.config/bluetui/config.toml` or from a custom path with `-c`
109109

110110
```toml
111+
# Possible values: "Legacy", "Start", "End", "Center", "SpaceAround", "SpaceBetween"
112+
layout = "SpaceAround"
113+
114+
# Window width
115+
# Possible values: "auto" or a positive integer
116+
width = "auto"
117+
111118
toggle_scanning = "s"
112119

113120
[adapter]

src/app.rs

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use bluer::{
55
use futures::FutureExt;
66
use ratatui::{
77
Frame,
8-
layout::{Alignment, Constraint, Direction, Layout, Margin},
8+
layout::{Alignment, Constraint, Direction, Layout, Margin, Rect},
99
style::{Color, Modifier, Style, Stylize},
1010
text::{Line, Span},
1111
widgets::{
@@ -17,7 +17,7 @@ use tui_input::Input;
1717

1818
use crate::{
1919
bluetooth::{Controller, request_confirmation},
20-
config::Config,
20+
config::{Config, Width},
2121
confirmation::PairingConfirmation,
2222
notification::Notification,
2323
spinner::Spinner,
@@ -139,6 +139,24 @@ impl App {
139139
}
140140
}
141141

142+
pub fn area(&self, frame: &Frame) -> Rect {
143+
match self.config.width {
144+
Width::Size(v) => {
145+
if v < frame.area().width {
146+
let area = Layout::default()
147+
.direction(Direction::Horizontal)
148+
.constraints([Constraint::Length(v), Constraint::Fill(1)])
149+
.split(frame.area());
150+
151+
area[0]
152+
} else {
153+
frame.area()
154+
}
155+
}
156+
_ => frame.area(),
157+
}
158+
}
159+
142160
pub fn render_set_alias(&mut self, frame: &mut Frame) {
143161
let area = Layout::default()
144162
.direction(Direction::Vertical)
@@ -147,7 +165,7 @@ impl App {
147165
Constraint::Length(6),
148166
Constraint::Fill(1),
149167
])
150-
.split(frame.area());
168+
.split(self.area(frame));
151169

152170
let area = Layout::default()
153171
.direction(Direction::Horizontal)
@@ -278,7 +296,7 @@ impl App {
278296
]
279297
})
280298
.margin(1)
281-
.split(frame.area());
299+
.split(self.area(frame));
282300
(chunks[0], chunks[1], chunks[2], chunks[3])
283301
};
284302

@@ -362,7 +380,7 @@ impl App {
362380
}
363381
}),
364382
)
365-
.flex(ratatui::layout::Flex::SpaceAround)
383+
.flex(self.config.layout)
366384
.row_highlight_style(if self.focused_block == FocusedBlock::Adapter {
367385
Style::default().bg(Color::DarkGray).fg(Color::White)
368386
} else {
@@ -545,7 +563,7 @@ impl App {
545563
}
546564
}),
547565
)
548-
.flex(ratatui::layout::Flex::SpaceAround)
566+
.flex(self.config.layout)
549567
.row_highlight_style(if self.focused_block == FocusedBlock::PairedDevices {
550568
Style::default().bg(Color::DarkGray).fg(Color::White)
551569
} else {
@@ -648,7 +666,7 @@ impl App {
648666
}
649667
}),
650668
)
651-
.flex(ratatui::layout::Flex::SpaceAround)
669+
.flex(self.config.layout)
652670
.row_highlight_style(if self.focused_block == FocusedBlock::NewDevices {
653671
Style::default().bg(Color::DarkGray).fg(Color::White)
654672
} else {
@@ -682,7 +700,7 @@ impl App {
682700
// Help
683701
let help = match self.focused_block {
684702
FocusedBlock::PairedDevices => {
685-
if frame.area().width > 103 {
703+
if self.area(frame).width > 103 {
686704
vec![Line::from(vec![
687705
Span::from("k,").bold(),
688706
Span::from(" Up"),
@@ -793,7 +811,7 @@ impl App {
793811

794812
if self.pairing_confirmation.display.load(Ordering::Relaxed) {
795813
self.focused_block = FocusedBlock::PassKeyConfirmation;
796-
self.pairing_confirmation.render(frame);
814+
self.pairing_confirmation.render(frame, self.area(frame));
797815
return;
798816
}
799817

src/cli.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
use std::path::PathBuf;
2+
3+
use clap::{Command, arg, crate_description, crate_name, crate_version, value_parser};
4+
5+
pub fn cli() -> Command {
6+
Command::new(crate_name!())
7+
.about(crate_description!())
8+
.version(crate_version!())
9+
.arg(
10+
arg!(--config <config>)
11+
.short('c')
12+
.id("config")
13+
.required(false)
14+
.help("Config file path")
15+
.value_parser(value_parser!(PathBuf)),
16+
)
17+
}

src/config.rs

Lines changed: 114 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,23 @@
1+
use core::fmt;
2+
use std::{path::PathBuf, process::exit};
3+
4+
use ratatui::layout::Flex;
15
use toml;
26

37
use dirs;
4-
use serde::Deserialize;
8+
use serde::{
9+
Deserialize, Deserializer,
10+
de::{self, Unexpected, Visitor},
11+
};
512

613
#[derive(Deserialize, Debug)]
714
pub struct Config {
15+
#[serde(default = "default_layout", deserialize_with = "deserialize_layout")]
16+
pub layout: Flex,
17+
18+
#[serde(default = "Width::default")]
19+
pub width: Width,
20+
821
#[serde(default = "default_toggle_scanning")]
922
pub toggle_scanning: char,
1023

@@ -15,6 +28,65 @@ pub struct Config {
1528
pub paired_device: PairedDevice,
1629
}
1730

31+
#[derive(Debug, Default)]
32+
pub enum Width {
33+
#[default]
34+
Auto,
35+
Size(u16),
36+
}
37+
38+
struct WidthVisitor;
39+
40+
impl<'de> Visitor<'de> for WidthVisitor {
41+
type Value = Width;
42+
43+
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
44+
formatter.write_str("the string \"auto\" or a positive integer (u16)")
45+
}
46+
47+
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
48+
where
49+
E: de::Error,
50+
{
51+
match value {
52+
"auto" => Ok(Width::Auto),
53+
_ => value
54+
.parse::<u16>()
55+
.map(Width::Size)
56+
.map_err(|_| de::Error::invalid_value(Unexpected::Str(value), &self)),
57+
}
58+
}
59+
60+
fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
61+
where
62+
E: de::Error,
63+
{
64+
match u16::try_from(value) {
65+
Ok(v) => Ok(Width::Size(v)),
66+
Err(_) => Err(de::Error::invalid_value(Unexpected::Unsigned(value), &self)),
67+
}
68+
}
69+
70+
fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
71+
where
72+
E: de::Error,
73+
{
74+
match u16::try_from(value) {
75+
Ok(v) => Ok(Width::Size(v)),
76+
Err(_) => Err(de::Error::invalid_value(Unexpected::Signed(value), &self)),
77+
}
78+
}
79+
}
80+
81+
impl<'de> Deserialize<'de> for Width {
82+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
83+
where
84+
D: Deserializer<'de>,
85+
{
86+
deserializer.deserialize_any(WidthVisitor)
87+
}
88+
}
89+
1890
#[derive(Deserialize, Debug)]
1991
pub struct Adapter {
2092
#[serde(default = "default_toggle_adapter_pairing")]
@@ -59,6 +131,33 @@ impl Default for PairedDevice {
59131
}
60132
}
61133

134+
fn deserialize_layout<'de, D>(deserializer: D) -> Result<Flex, D::Error>
135+
where
136+
D: Deserializer<'de>,
137+
{
138+
let s = String::deserialize(deserializer)?;
139+
140+
match s.as_str() {
141+
"Legacy" => Ok(Flex::Legacy),
142+
"Start" => Ok(Flex::Start),
143+
"End" => Ok(Flex::End),
144+
"Center" => Ok(Flex::Center),
145+
"SpaceAround" => Ok(Flex::SpaceAround),
146+
"SpaceBetween" => Ok(Flex::SpaceBetween),
147+
_ => {
148+
eprintln!("Wrong config: unknown layout variant {}", s);
149+
eprintln!(
150+
"The possible values are: Legacy, Start, End, Center, SpaceAround, SpaceBetween"
151+
);
152+
std::process::exit(1);
153+
}
154+
}
155+
}
156+
157+
fn default_layout() -> Flex {
158+
Flex::SpaceAround
159+
}
160+
62161
fn default_set_new_name() -> char {
63162
'e'
64163
}
@@ -88,21 +187,23 @@ fn default_toggle_device_trust() -> char {
88187
}
89188

90189
impl Config {
91-
pub fn new() -> Self {
92-
let conf_path = dirs::config_dir()
93-
.unwrap()
94-
.join("bluetui")
95-
.join("config.toml");
190+
pub fn new(config_file_path: Option<PathBuf>) -> Self {
191+
let conf_path = config_file_path.unwrap_or(
192+
dirs::config_dir()
193+
.unwrap()
194+
.join("bluetui")
195+
.join("config.toml"),
196+
);
96197

97198
let config = std::fs::read_to_string(conf_path).unwrap_or_default();
98-
let app_config: Config = toml::from_str(&config).unwrap();
199+
let app_config: Config = match toml::from_str(&config) {
200+
Ok(c) => c,
201+
Err(e) => {
202+
eprintln!("{}", e);
203+
exit(1);
204+
}
205+
};
99206

100207
app_config
101208
}
102209
}
103-
104-
impl Default for Config {
105-
fn default() -> Self {
106-
Self::new()
107-
}
108-
}

src/confirmation.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use std::sync::mpsc::channel;
22
use std::sync::{Arc, atomic::AtomicBool};
33

44
use ratatui::Frame;
5-
use ratatui::layout::{Alignment, Constraint, Direction, Layout};
5+
use ratatui::layout::{Alignment, Constraint, Direction, Layout, Rect};
66
use ratatui::style::{Color, Style};
77
use ratatui::text::{Span, Text};
88
use ratatui::widgets::{Block, BorderType, Borders, Clear};
@@ -40,7 +40,7 @@ impl PairingConfirmation {
4040
}
4141
}
4242

43-
pub fn render(&mut self, frame: &mut Frame) {
43+
pub fn render(&mut self, frame: &mut Frame, area: Rect) {
4444
if self.message.is_none() {
4545
let msg = self.confirmation_message_receiver.recv().unwrap();
4646
self.message = Some(msg);
@@ -53,7 +53,7 @@ impl PairingConfirmation {
5353
Constraint::Length(5),
5454
Constraint::Fill(1),
5555
])
56-
.split(frame.area());
56+
.split(area);
5757

5858
let block = Layout::default()
5959
.direction(Direction::Horizontal)

src/lib.rs

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,12 @@
11
pub mod app;
2-
2+
pub mod bluetooth;
3+
pub mod cli;
4+
pub mod config;
5+
pub mod confirmation;
36
pub mod event;
4-
5-
pub mod ui;
6-
7-
pub mod tui;
8-
97
pub mod handler;
10-
11-
pub mod bluetooth;
12-
138
pub mod notification;
14-
15-
pub mod spinner;
16-
17-
pub mod config;
18-
199
pub mod rfkill;
20-
21-
pub mod confirmation;
10+
pub mod spinner;
11+
pub mod tui;
12+
pub mod ui;

src/main.rs

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,34 @@
11
use bluetui::{
22
app::{App, AppResult},
3+
cli,
34
config::Config,
45
event::{Event, EventHandler},
56
handler::handle_key_events,
67
rfkill,
78
tui::Tui,
89
};
9-
use clap::{Command, crate_version};
1010
use ratatui::{Terminal, backend::CrosstermBackend};
11-
use std::{io, sync::Arc};
11+
use std::{io, path::PathBuf, process::exit, sync::Arc};
1212

1313
#[tokio::main]
1414
async fn main() -> AppResult<()> {
15-
Command::new("bluetui")
16-
.version(crate_version!())
17-
.get_matches();
15+
let args = cli::cli().get_matches();
16+
17+
let config_file_path = if let Some(config) = args.get_one::<PathBuf>("config") {
18+
if config.exists() {
19+
Some(config.to_owned())
20+
} else {
21+
eprintln!("Config file not found");
22+
exit(1);
23+
}
24+
} else {
25+
None
26+
};
1827

1928
rfkill::check()?;
2029

21-
let config = Arc::new(Config::new());
30+
let config = Arc::new(Config::new(config_file_path));
31+
2232
let mut app = App::new(config.clone()).await?;
2333
let backend = CrosstermBackend::new(io::stdout());
2434
let terminal = Terminal::new(backend)?;

0 commit comments

Comments
 (0)