pub mod bluetooth;
pub mod wifi;
use std::{cell::RefCell, time};
use anyhow::Result;
use ratatui::{
DefaultTerminal,
buffer::Buffer,
crossterm::event::{self, Event, KeyCode, KeyEvent},
layout::{Constraint, Flex, Layout, Rect},
style::{Color, Style, Stylize},
text::{Line, Span},
widgets::{
Block, BorderType, Cell, Clear, Paragraph, Row, StatefulWidget, Table, TableState, Tabs,
Widget,
},
};
use crate::network::{
bluetooth::{Address, BluetoothManager, PairingRequest},
wifi::WifiManager,
};
#[repr(usize)]
#[derive(Clone, Copy, Debug)]
pub enum NetworkManagerState {
WIFI = 0,
BLUETOOTH = 1,
}
impl NetworkManagerState {
pub fn cycle(self) -> Self {
match self {
NetworkManagerState::WIFI => NetworkManagerState::BLUETOOTH,
NetworkManagerState::BLUETOOTH => NetworkManagerState::WIFI,
}
}
}
#[derive(Clone, Copy, Debug)]
pub enum WifiPane {
Networks,
Devices,
}
impl WifiPane {
pub fn cycle(self) -> Self {
match self {
WifiPane::Networks => WifiPane::Devices,
WifiPane::Devices => WifiPane::Networks,
}
}
}
pub struct NetworkManager {
bt: BluetoothManager,
w: WifiManager,
}
impl NetworkManager {
pub fn new() -> Self {
Self {
bt: BluetoothManager::new(),
w: WifiManager::new(),
}
}
}
pub struct NetworkManagerTui {
state: NetworkManagerState,
wifi_pane: WifiPane,
selected_wifi_device: usize,
selected_bluetooth_address: Option<Address>,
wifi_networks_pane_state: RefCell<TableState>,
wifi_devices_pane_state: RefCell<TableState>,
bluetooth_pane_state: RefCell<TableState>,
manager: NetworkManager,
exit: bool,
}
impl NetworkManagerTui {
pub fn new(state: NetworkManagerState) -> Self {
Self {
state,
exit: false,
wifi_pane: WifiPane::Networks,
selected_wifi_device: 0,
selected_bluetooth_address: None,
wifi_networks_pane_state: RefCell::new(TableState::default().with_selected(0)),
wifi_devices_pane_state: RefCell::new(TableState::default().with_selected(0)),
bluetooth_pane_state: RefCell::new(TableState::default().with_selected(0)),
manager: NetworkManager::new(),
}
}
pub async fn run(&mut self, terminal: &mut DefaultTerminal) -> Result<()> {
self.manager.bt.init();
self.manager.w.find_devices();
self.manager.w.scan_all_devices();
while self.exit == false {
self.manager.w.run().await?;
self.manager.bt.run().await?;
self.handle_events()?;
terminal.draw(|frame| {
frame.render_widget(&*self, frame.area());
})?;
}
Ok(())
}
pub fn handle_events(&mut self) -> Result<()> {
if event::poll(time::Duration::from_millis(10))? {
let event = event::read()?;
return match event {
Event::Key(key_event) => self.handle_key_event(&key_event),
_ => Ok(()),
};
}
Ok(())
}
fn selected_bluetooth_index(&self) -> Option<usize> {
let devices = self.manager.bt.sorted_devices();
if devices.is_empty() {
return None;
}
Some(
self.selected_bluetooth_address
.and_then(|address| devices.iter().position(|d| d.address == address))
.unwrap_or(0),
)
}
pub fn handle_key_event(&mut self, key_event: &KeyEvent) -> Result<()> {
if !key_event.is_press() {
return Ok(());
}
if self.manager.bt.pairing_request.is_some() {
match key_event.code {
KeyCode::Char(' ') => self.manager.bt.accept_pairing(),
KeyCode::Char('q') => self.exit = true,
_ => {}
}
return Ok(());
}
match key_event.code {
KeyCode::Left | KeyCode::Right => {
self.state = self.state.cycle();
}
KeyCode::Char('q') => {
self.exit = true;
}
_ => {}
}
match self.state {
NetworkManagerState::WIFI => match key_event.code {
KeyCode::Tab => {
self.wifi_pane = self.wifi_pane.cycle();
}
KeyCode::Up => match self.wifi_pane {
WifiPane::Networks => self.wifi_networks_pane_state.get_mut().select_previous(),
WifiPane::Devices => self.wifi_devices_pane_state.get_mut().select_previous(),
},
KeyCode::Down => match self.wifi_pane {
WifiPane::Networks => self.wifi_networks_pane_state.get_mut().select_next(),
WifiPane::Devices => self.wifi_devices_pane_state.get_mut().select_next(),
},
KeyCode::Char(' ') => match self.wifi_pane {
WifiPane::Devices => {
if let Some(index) = self.wifi_devices_pane_state.get_mut().selected() {
if index < self.manager.w.wifi_devices.len() {
self.selected_wifi_device = index;
*self.wifi_networks_pane_state.get_mut() =
TableState::default().with_selected(0);
}
}
}
WifiPane::Networks => {
let Some(index) = self.wifi_networks_pane_state.get_mut().selected() else {
return Ok(());
};
let Some(device) = self
.manager
.w
.wifi_devices
.get_mut(self.selected_wifi_device)
else {
return Ok(());
};
let Some(network) = device.wifi_networks.get(index) else {
return Ok(());
};
let in_use = network.in_use;
let target = if network.ssid.is_empty() {
network.bssid.clone()
} else {
network.ssid.clone()
};
if in_use {
device.disconnect();
} else {
device.connect(target, None);
}
}
},
KeyCode::Char('s') => {
self.manager.w.scan_all_devices();
}
_ => {}
},
NetworkManagerState::BLUETOOTH => match key_event.code {
KeyCode::Up => {
let index = self
.selected_bluetooth_index()
.unwrap_or(0)
.saturating_sub(1);
self.selected_bluetooth_address = self
.manager
.bt
.sorted_devices()
.get(index)
.map(|d| d.address);
}
KeyCode::Down => {
let devices = self.manager.bt.sorted_devices();
let index = (self.selected_bluetooth_index().unwrap_or(0) + 1)
.min(devices.len().saturating_sub(1));
self.selected_bluetooth_address = devices.get(index).map(|d| d.address);
}
KeyCode::Char(' ') => {
let Some(index) = self.selected_bluetooth_index() else {
return Ok(());
};
let Some(device) = self
.manager
.bt
.sorted_devices()
.get(index)
.map(|d| (*d).clone())
else {
return Ok(());
};
match (device.connected == Some(true), device.paired == Some(true)) {
(true, _) => self.manager.bt.disconnect(device.address),
(_, true) => self.manager.bt.connect(device.address),
_ => self.manager.bt.pair(device.address),
}
}
KeyCode::Char('u') => {
let Some(index) = self.selected_bluetooth_index() else {
return Ok(());
};
let Some(address) = self
.manager
.bt
.sorted_devices()
.get(index)
.map(|d| d.address)
else {
return Ok(());
};
self.manager.bt.unpair(address);
}
KeyCode::Char('s') => {
self.manager.bt.scan();
}
_ => {}
},
}
Ok(())
}
fn render_pairing_popup(&self, request: &PairingRequest, area: Rect, buf: &mut Buffer) {
let name = self
.manager
.bt
.devices
.get(&request.address)
.and_then(|device| device.name.clone())
.unwrap_or_else(|| "Unknown".to_string());
let [popup_area] = Layout::vertical([Constraint::Length(8.min(area.height))])
.flex(Flex::Center)
.areas(area);
let [popup_area] = Layout::horizontal([Constraint::Length(50.min(area.width))])
.flex(Flex::Center)
.areas(popup_area);
let popup_block = Block::bordered()
.title(" Pairing request ".black().on_blue().into_centered_line())
.border_style(Style::new().blue())
.border_type(BorderType::Double);
let popup = Paragraph::new(vec![
Line::from(""),
Line::from(name.bold().green()).centered(),
Line::from(request.address.to_string().yellow().bold()).centered(),
Line::from(""),
Line::from(vec![
"Press ".white(),
"<space>".blue().bold(),
" to accept the pairing request.".white(),
])
.centered(),
Line::from(""),
])
.block(popup_block);
Clear.render(popup_area, buf);
popup.render(popup_area, buf);
}
}
fn yes_no(value: Option<bool>) -> Span<'static> {
match value {
Some(true) => "yes".green(),
Some(false) => "no".red(),
None => "idk".blue(),
}
}
impl Widget for &NetworkManagerTui {
fn render(self, area: Rect, buf: &mut Buffer)
where
Self: Sized,
{
let state = self.state.clone();
let color = match state {
NetworkManagerState::WIFI => Color::Magenta,
NetworkManagerState::BLUETOOTH => Color::Blue,
};
let keybinds = Line::from(match state {
NetworkManagerState::WIFI => vec![
" Switch Pane".white(),
" <tab>".fg(color).bold(),
" (Dis)connect/Select".white(),
" <space>".fg(color).bold(),
" Forget".white(),
" <f>".fg(color).bold(),
" Scan".white(),
" <s>".fg(color).bold(),
" Quit".white(),
" <q> ".fg(color).bold(),
],
NetworkManagerState::BLUETOOTH => vec![
" Pair/Connect/Disconnect".white(),
" <space>".fg(color).bold(),
" Unpair".white(),
" <u>".fg(color).bold(),
" Scan".white(),
" <s>".fg(color).bold(),
" Quit".white(),
" <q> ".fg(color).bold(),
],
});
let mut outer_block = Block::bordered()
.border_type(BorderType::Double)
.style(Style::new().fg(color))
.title_bottom(keybinds.centered());
let bottom_right = match self.state {
NetworkManagerState::BLUETOOTH => {
if self.manager.bt.init_task.is_some() {
Some("Initializing".to_string())
} else if let (Some(label), Some(address)) =
(self.manager.bt.action_label, self.manager.bt.action_address)
{
let name = self
.manager
.bt
.devices
.get(&address)
.and_then(|device| device.name.clone())
.unwrap_or_else(|| address.to_string());
Some(format!("{label}: {name}"))
} else if self.manager.bt.scan_task.is_some() {
Some("Scanning".to_string())
} else {
None
}
}
NetworkManagerState::WIFI => {
if self.manager.w.find_devices_task.is_some() {
Some("Finding Devices".to_string())
} else if let Some(device) = self
.manager
.w
.wifi_devices
.iter()
.find(|d| d.connect_task.is_some())
{
Some(match &device.connect_ssid {
Some(ssid) => format!("Connecting: {ssid}"),
None => format!("Disconnecting: {}", device.name),
})
} else {
self.manager
.w
.scanning_device
.as_ref()
.map(|name| format!("Scanning: {name}"))
}
}
};
if let Some(text) = bottom_right {
outer_block = outer_block.title_bottom(Line::from(text).right_aligned());
}
let outer_block_area = outer_block.clone().inner(area);
let [tabs_area, content_area] =
Layout::vertical([Constraint::Length(1), Constraint::Fill(1)]).areas(outer_block_area);
let tab_titles = vec![" WiFi ", " Bluetooth "];
let tab_width = tab_titles.iter().map(|title| title.len()).sum::<usize>()
+ tab_titles.len() * 2
+ tab_titles.len()
- 1;
let tabs = Tabs::new(tab_titles.clone())
.select(self.state as usize)
.padding(" ", " ");
outer_block.render(area, buf);
tabs.render(
tabs_area.centered_horizontally(Constraint::Length(tab_width as u16)),
buf,
);
if let NetworkManagerState::WIFI = self.state {
let [networks_area, devices_area] =
Layout::vertical([Constraint::Fill(3), Constraint::Fill(1)]).areas(content_area);
let (networks_border, devices_border) = match self.wifi_pane {
WifiPane::Networks => (Style::new().fg(color), Style::new().fg(Color::DarkGray)),
WifiPane::Devices => (Style::new().fg(Color::DarkGray), Style::new().fg(color)),
};
let networks = self
.manager
.w
.wifi_devices
.get(self.selected_wifi_device)
.map(|device| device.wifi_networks.as_slice())
.unwrap_or(&[]);
let network_rows = networks.iter().map(|network| {
Row::new([
if network.in_use {
"yes".green()
} else {
"no".red()
},
network.bssid.clone().yellow(),
if network.ssid.clone() == "" {
"<Hidden>".to_string()
} else {
network.ssid.clone()
}
.white(),
network.mode.clone().to_string().blue(),
match network.band.as_str() {
"5 GHz" => network.band.clone().green(),
"2.4 GHz" => network.band.clone().yellow(),
_ => network.band.clone().red(),
},
network.channel.to_string().blue(),
match network
.rate
.split_whitespace()
.next()
.and_then(|r| r.parse::<usize>().ok())
.unwrap_or(0)
{
1001.. => network.rate.clone().green(),
501.. => network.rate.clone().yellow(),
_ => network.rate.clone().red(),
},
match network.signal {
76.. => network.signal.to_string().green(),
51.. => network.signal.to_string().yellow(),
_ => network.signal.to_string().red(),
},
network.security.clone().blue(),
])
.dim()
});
let networks_table = Table::new(
network_rows,
[
Constraint::Length(10),
Constraint::Length(18),
Constraint::Fill(1),
Constraint::Length(8),
Constraint::Length(8),
Constraint::Length(8),
Constraint::Length(12),
Constraint::Length(7),
Constraint::Length(14),
],
)
.header(
Row::new([
"Connected",
"BSSID",
"SSID",
"Mode",
"Band",
"Channel",
"Rate",
"Signal",
"Security",
])
.style(Style::new().bold().white()),
)
.row_highlight_style(Style::new().not_dim().bold())
.block(
Block::bordered()
.title(" Networks ")
.border_style(networks_border),
);
StatefulWidget::render(
networks_table,
networks_area,
buf,
&mut *self.wifi_networks_pane_state.borrow_mut(),
);
let device_rows = self
.manager
.w
.wifi_devices
.iter()
.enumerate()
.map(|(i, device)| {
Row::new([
if i == self.selected_wifi_device {
Cell::from("yes").green()
} else {
Cell::from("no").red()
},
Cell::from(device.name.clone()),
Cell::from(device.device_type.clone()),
Cell::from(device.status.clone()),
])
.dim()
});
let devices_table = Table::new(
device_rows,
[
Constraint::Length(10),
Constraint::Fill(1),
Constraint::Fill(1),
Constraint::Fill(1),
],
)
.header(
Row::new(["Selected", "Name", "Type", "Status"]).style(Style::new().bold().white()),
)
.row_highlight_style(Style::new().not_dim().bold())
.block(
Block::bordered()
.title(" Devices ")
.border_style(devices_border),
);
StatefulWidget::render(
devices_table,
devices_area,
buf,
&mut *self.wifi_devices_pane_state.borrow_mut(),
);
}
if let NetworkManagerState::BLUETOOTH = self.state {
let devices = self.manager.bt.sorted_devices();
let device_rows = devices.iter().map(|device| {
let connected = match self.manager.bt.action_label {
Some(label) if self.manager.bt.action_address == Some(device.address) => {
label.to_lowercase().yellow()
}
_ => yes_no(device.connected),
};
Row::new([
device
.name
.clone()
.unwrap_or_else(|| "Unknown".to_string())
.blue(),
device.address.to_string().yellow(),
yes_no(device.paired),
connected,
])
.dim()
});
let devices_table = Table::new(
device_rows,
[
Constraint::Fill(1),
Constraint::Length(18),
Constraint::Length(8),
Constraint::Length(14),
],
)
.header(
Row::new(["Name", "Address", "Paired", "Connected"])
.style(Style::new().bold().white()),
)
.row_highlight_style(Style::new().not_dim().bold())
.block(
Block::bordered()
.title(" Devices ")
.border_style(Style::new().fg(color)),
);
let mut bluetooth_pane_state = self.bluetooth_pane_state.borrow_mut();
bluetooth_pane_state.select(self.selected_bluetooth_index());
StatefulWidget::render(devices_table, content_area, buf, &mut *bluetooth_pane_state);
}
if let Some(request) = &self.manager.bt.pairing_request {
self.render_pairing_popup(request, area, buf);
}
}
}