use anyhow::{Context, Result};
pub mod auth;
pub mod hidden_network;
pub mod known_network;
pub mod network;
pub mod share;
use std::sync::Arc;
use futures::future::join_all;
use iwdrs::{
error::{IWDError, station::ScanError},
hidden_network::HiddenNetwork,
session::Session,
station::{State, diagnostics::ActiveStationDiagnostics},
};
use ratatui::{
Frame,
layout::{Constraint, Direction, Flex, Layout},
style::{Color, Style, Stylize},
text::{Line, Span},
widgets::{Block, BorderType, Borders, Padding, Paragraph, Row, Table, TableState},
};
use tokio::sync::mpsc::UnboundedSender;
use crate::{
app::FocusedBlock,
config::Config,
device::Device,
event::Event,
mode::station::{known_network::KnownNetwork, share::Share},
notification::{Notification, NotificationLevel},
};
use network::Network;
#[derive(Clone)]
pub struct Station {
pub session: Arc<Session>,
pub state: State,
pub is_scanning: bool,
pub connected_network: Option<Network>,
pub new_networks: Vec<(Network, i16)>,
pub new_hidden_networks: Vec<HiddenNetwork>,
pub known_networks: Vec<(Network, i16)>,
pub unavailable_known_networks: Vec<KnownNetwork>,
pub known_networks_state: TableState,
pub new_networks_state: TableState,
pub diagnostic: Option<ActiveStationDiagnostics>,
pub show_unavailable_known_networks: bool,
pub show_hidden_networks: bool,
pub share: Option<Share>,
pub connct_hidden_network: Option<hidden_network::ConnectHiddenNetwork>,
}
impl Station {
pub async fn new(session: Arc<Session>) -> Result<Self> {
let iwd_station = session
.stations()
.await?
.pop()
.context("no station found")?;
let iwd_station_diagnostic = session.stations_diagnostics().await?.pop();
let state = iwd_station.state().await?;
let connected_network = {
if let Some(n) = iwd_station.connected_network().await? {
let network = Network::new(n.clone()).await?;
Some(network)
} else {
None
}
};
let is_scanning = iwd_station.is_scanning().await?;
let discovered_networks = iwd_station.discovered_networks().await?;
let networks = {
let collected_futures = discovered_networks
.iter()
.map(|(n, signal)| async {
match Network::new(n.clone()).await {
Ok(network) => Ok((network, signal.to_owned())),
Err(e) => Err(e),
}
})
.collect::<Vec<_>>();
let results = join_all(collected_futures).await;
results
.into_iter()
.filter_map(Result::ok)
.collect::<Vec<(Network, i16)>>()
};
let new_networks: Vec<(Network, i16)> = networks
.clone()
.into_iter()
.filter(|(net, _signal)| net.known_network.is_none())
.collect();
let new_hidden_networks = iwd_station
.get_hidden_networks()
.await
.unwrap_or(Vec::new());
let known_networks: Vec<(Network, i16)> = networks
.into_iter()
.filter(|(net, _signal)| net.known_network.is_some())
.collect();
let available_networks_names: Vec<String> =
known_networks.iter().map(|(n, _)| n.name.clone()).collect();
let unavailable_known_networks =
if let Ok(iwd_known_networks) = session.known_networks().await {
let mut unavailable_known_networks = Vec::new();
for iwd_network in iwd_known_networks {
if let Ok(known_network) = KnownNetwork::new(iwd_network).await
&& !available_networks_names.contains(&known_network.name)
{
unavailable_known_networks.push(known_network);
}
}
unavailable_known_networks
} else {
Vec::new()
};
let mut new_networks_state = TableState::default();
if new_networks.is_empty() {
new_networks_state.select(None);
} else {
new_networks_state.select(Some(0));
}
let mut known_networks_state = TableState::default();
if known_networks.is_empty() {
known_networks_state.select(None);
} else {
known_networks_state.select(Some(0));
}
let diagnostic = if let Some(diagnostic) = iwd_station_diagnostic {
diagnostic.get().await.ok()
} else {
None
};
Ok(Self {
session,
state,
is_scanning,
connected_network,
new_networks,
new_hidden_networks,
known_networks,
unavailable_known_networks,
known_networks_state,
new_networks_state,
diagnostic,
show_unavailable_known_networks: false,
show_hidden_networks: false,
share: None,
connct_hidden_network: None,
})
}
pub async fn connect_hidden_network(&self, ssid: String) -> Result<()> {
let iwd_station = self
.session
.stations()
.await?
.pop()
.context("No station found")?;
iwd_station.connect_hidden_network(ssid).await?;
Ok(())
}
pub async fn refresh(&mut self) -> Result<()> {
let iwd_station = self
.session
.stations()
.await?
.pop()
.context("No station found")?;
self.state = iwd_station.state().await?;
self.is_scanning = iwd_station.is_scanning().await?;
let connected_network = {
if let Some(n) = iwd_station.connected_network().await? {
let network = Network::new(n.clone()).await?;
Some(network)
} else {
None
}
};
let discovered_networks = iwd_station.discovered_networks().await?;
let networks = {
let collected_futures = discovered_networks
.iter()
.map(|(n, signal)| async {
match Network::new(n.clone()).await {
Ok(network) => Ok((network, signal.to_owned())),
Err(e) => Err(e),
}
})
.collect::<Vec<_>>();
let results = join_all(collected_futures).await;
results
.into_iter()
.filter_map(Result::ok)
.collect::<Vec<(Network, i16)>>()
};
let new_networks: Vec<(Network, i16)> = networks
.clone()
.into_iter()
.filter(|(net, _signal)| net.known_network.is_none())
.collect();
let known_networks: Vec<(Network, i16)> = networks
.into_iter()
.filter(|(net, _signal)| net.known_network.is_some())
.collect();
if self.new_networks.len() == new_networks.len() {
self.new_networks.iter_mut().for_each(|(net, signal)| {
let n = new_networks
.iter()
.find(|(refreshed_net, _signal)| refreshed_net.name == net.name);
if let Some((_, refreshed_signal)) = n {
*signal = *refreshed_signal;
}
});
} else {
let mut new_networks_state = TableState::default();
if new_networks.is_empty() {
new_networks_state.select(None);
} else {
new_networks_state.select(Some(0));
}
self.new_networks_state = new_networks_state;
self.new_networks = new_networks;
}
if self.known_networks.len() == known_networks.len() {
self.known_networks.iter_mut().for_each(|(net, signal)| {
let n = known_networks
.iter()
.find(|(refreshed_net, _signal)| refreshed_net.name == net.name);
if let Some((refreshed_net, refreshed_signal)) = n {
net.known_network.as_mut().unwrap().is_autoconnect =
refreshed_net.known_network.as_ref().unwrap().is_autoconnect;
*signal = *refreshed_signal;
}
});
} else {
let mut known_networks_state = TableState::default();
if known_networks.is_empty() {
known_networks_state.select(None);
} else {
known_networks_state.select(Some(0));
}
self.known_networks_state = known_networks_state;
self.known_networks = known_networks;
}
self.new_hidden_networks = iwd_station
.get_hidden_networks()
.await
.unwrap_or(Vec::new());
let available_networks_names: Vec<String> = self
.known_networks
.iter()
.map(|(n, _)| n.name.clone())
.collect();
let unavailable_known_networks =
if let Ok(iwd_known_networks) = self.session.known_networks().await {
let mut unavailable_known_networks = Vec::new();
for iwd_network in iwd_known_networks {
if let Ok(known_network) = KnownNetwork::new(iwd_network).await
&& !available_networks_names.contains(&known_network.name)
{
unavailable_known_networks.push(known_network);
}
}
unavailable_known_networks
} else {
Vec::new()
};
self.unavailable_known_networks = unavailable_known_networks;
self.connected_network = connected_network;
let iwd_station_diagnostic = self.session.stations_diagnostics().await.unwrap().pop();
self.diagnostic = if let Some(diagnostic) = iwd_station_diagnostic {
diagnostic.get().await.ok()
} else {
None
};
Ok(())
}
pub async fn scan(&self, sender: UnboundedSender<Event>) -> Result<()> {
let iwd_station = self.session.stations().await.unwrap().pop().unwrap();
match iwd_station.scan().await {
Ok(()) => Notification::send(
"Start Scanning".to_string(),
NotificationLevel::Info,
&sender,
)?,
Err(e) => match e {
IWDError::OperationError(e) => match e {
ScanError::Busy => {
Notification::send(e.to_string(), NotificationLevel::Info, &sender.clone())?
}
_ => Notification::send(
e.to_string(),
NotificationLevel::Error,
&sender.clone(),
)?,
},
_ => Notification::send(e.to_string(), NotificationLevel::Error, &sender.clone())?,
},
}
Ok(())
}
pub async fn disconnect(&self, sender: UnboundedSender<Event>) -> Result<()> {
let iwd_station = self.session.stations().await.unwrap().pop().unwrap();
match iwd_station.disconnect().await {
Ok(()) => Notification::send(
format!(
"Disconnected from {}",
self.connected_network.as_ref().unwrap().name
),
NotificationLevel::Info,
&sender,
)?,
Err(e) => Notification::send(e.to_string(), NotificationLevel::Error, &sender.clone())?,
}
Ok(())
}
pub fn render(
&mut self,
frame: &mut Frame,
focused_block: FocusedBlock,
device: &Device,
config: Arc<Config>,
) {
let (known_networks_block, new_networks_block, device_block, help_block) = {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Min(5),
Constraint::Min(5),
Constraint::Length(5),
Constraint::Length(2),
])
.margin(1)
.split(frame.area());
(chunks[0], chunks[1], chunks[2], chunks[3])
};
let row = Row::new(vec![
Line::from(device.name.clone()).centered(),
Line::from("station").centered(),
{
if device.is_powered {
Line::from("On").centered()
} else {
Line::from("Off").centered()
}
},
Line::from(self.state.to_string()).centered(),
Line::from(if self.is_scanning { "Yes" } else { "No" }).centered(),
Line::from({
if let Some(diagnostic) = &self.diagnostic {
format!("{:.2} GHz", diagnostic.frequency_mhz as f32 / 1000.)
} else {
"-".to_string()
}
})
.centered(),
Line::from({
if let Some(diagnostic) = &self.diagnostic {
diagnostic.security.to_string()
} else {
"-".to_string()
}
})
.centered(),
]);
let widths = [
Constraint::Length(10),
Constraint::Length(8),
Constraint::Length(10),
Constraint::Length(12),
Constraint::Length(10),
Constraint::Length(10),
Constraint::Length(15),
];
let device_table = Table::new(vec![row], widths)
.header({
if focused_block == FocusedBlock::Device {
Row::new(vec![
Line::from("Name").yellow().centered(),
Line::from("Mode").yellow().centered(),
Line::from("Powered").yellow().centered(),
Line::from("State").yellow().centered(),
Line::from("Scanning").yellow().centered(),
Line::from("Frequency").yellow().centered(),
Line::from("Security").yellow().centered(),
])
.style(Style::new().bold())
.bottom_margin(1)
} else {
Row::new(vec![
Line::from("Name").centered(),
Line::from("Mode").centered(),
Line::from("Powered").centered(),
Line::from("State").centered(),
Line::from("Scanning").centered(),
Line::from("Frequency").centered(),
Line::from("Security").centered(),
])
.bottom_margin(1)
}
})
.block(
Block::default()
.title(" Device ")
.title_style({
if focused_block == FocusedBlock::Device {
Style::default().bold()
} else {
Style::default()
}
})
.borders(Borders::ALL)
.border_style({
if focused_block == FocusedBlock::Device {
Style::default().fg(Color::Green)
} else {
Style::default()
}
})
.border_type({
if focused_block == FocusedBlock::Device {
BorderType::Thick
} else {
BorderType::default()
}
})
.padding(Padding::horizontal(1)),
)
.column_spacing(1)
.flex(Flex::SpaceAround)
.row_highlight_style(if focused_block == FocusedBlock::Device {
Style::default().bg(Color::DarkGray).fg(Color::White)
} else {
Style::default()
});
let mut device_state = TableState::default().with_selected(0);
frame.render_stateful_widget(device_table, device_block, &mut device_state);
let mut rows: Vec<Row> = self
.known_networks
.iter()
.map(|(net, signal)| {
let net = net.known_network.as_ref().unwrap();
let signal = format!("{}%", {
if *signal / 100 >= -50 {
100
} else {
2 * (100 + signal / 100)
}
});
if let Some(connected_net) = &self.connected_network {
if connected_net.name == net.name {
let row = vec![
Line::from(" ").centered(),
Line::from(net.name.clone()).centered(),
Line::from(net.network_type.to_string()).centered(),
Line::from(if net.is_hidden { "Yes" } else { "No" }).centered(),
Line::from(if net.is_autoconnect { "Yes" } else { "No" }).centered(),
Line::from(signal).centered(),
];
Row::new(row)
} else {
let row = vec![
Line::from(""),
Line::from(net.name.clone()).centered(),
Line::from(net.network_type.to_string()).centered(),
Line::from(if net.is_hidden { "Yes" } else { "No" }).centered(),
Line::from(if net.is_autoconnect { "Yes" } else { "No" }).centered(),
Line::from(signal).centered(),
];
Row::new(row)
}
} else {
let row = vec![
Line::from("").centered(),
Line::from(net.name.clone()).centered(),
Line::from(net.network_type.to_string()).centered(),
Line::from(if net.is_hidden { "Yes" } else { "No" }).centered(),
Line::from(if net.is_autoconnect { "Yes" } else { "No" }).centered(),
Line::from(signal).centered(),
];
Row::new(row)
}
})
.collect();
if self.show_unavailable_known_networks {
self.unavailable_known_networks.iter().for_each(|net| {
let row = Row::new(vec![
Line::from(""),
Line::from(net.name.clone()).centered(),
Line::from(net.network_type.to_string()).centered(),
Line::from(""),
Line::from(""),
Line::from(""),
])
.fg(Color::DarkGray);
rows.push(row);
});
}
let widths = [
Constraint::Length(2),
Constraint::Length(25),
Constraint::Length(8),
Constraint::Length(6),
Constraint::Length(12),
Constraint::Length(6),
];
let known_networks_table = Table::new(rows, widths)
.header({
if focused_block == FocusedBlock::KnownNetworks {
Row::new(vec![
Line::from(""),
Line::from("Name").yellow().centered(),
Line::from("Security").yellow().centered(),
Line::from("Hidden").yellow().centered(),
Line::from("Auto Connect").yellow().centered(),
Line::from("Signal").yellow().centered(),
])
.style(Style::new().bold())
.bottom_margin(1)
} else {
Row::new(vec![
Line::from(""),
Line::from("Name").centered(),
Line::from("Security").centered(),
Line::from("Hidden").centered(),
Line::from("Auto Connect").centered(),
Line::from("Signal").centered(),
])
.bottom_margin(1)
}
})
.block(
Block::default()
.title(" Known Networks ")
.title_style({
if focused_block == FocusedBlock::KnownNetworks {
Style::default().bold()
} else {
Style::default()
}
})
.borders(Borders::ALL)
.border_style({
if focused_block == FocusedBlock::KnownNetworks {
Style::default().fg(Color::Green)
} else {
Style::default()
}
})
.border_type({
if focused_block == FocusedBlock::KnownNetworks {
BorderType::Thick
} else {
BorderType::default()
}
})
.padding(Padding::horizontal(1)),
)
.column_spacing(1)
.flex(Flex::SpaceAround)
.row_highlight_style(if focused_block == FocusedBlock::KnownNetworks {
Style::default().bg(Color::DarkGray).fg(Color::White)
} else {
Style::default()
});
frame.render_stateful_widget(
known_networks_table,
known_networks_block,
&mut self.known_networks_state,
);
let mut rows: Vec<Row> = self
.new_networks
.iter()
.map(|(net, signal)| {
Row::new(vec![
Line::from(net.name.clone()).centered(),
Line::from(net.network_type.to_string().clone()).centered(),
Line::from({
let signal = {
if *signal / 100 >= -50 {
100
} else {
2 * (100 + signal / 100)
}
};
match signal {
n if n >= 75 => format!("{signal:3}% "),
n if (50..75).contains(&n) => format!("{signal:3}% "),
n if (25..50).contains(&n) => format!("{signal:3}% "),
_ => format!("{signal:3}% "),
}
})
.centered(),
])
})
.collect();
if self.show_hidden_networks {
self.new_hidden_networks.iter().for_each(|net| {
rows.push(
Row::new(vec![
Line::from(net.address.clone()).centered(),
Line::from(net.network_type.to_string().clone()).centered(),
Line::from({
let signal = {
if net.signal_strength / 100 >= -50 {
100
} else {
2 * (100 + net.signal_strength / 100)
}
};
match signal {
n if n >= 75 => format!("{signal:3}% "),
n if (50..75).contains(&n) => format!("{signal:3}% "),
n if (25..50).contains(&n) => format!("{signal:3}% "),
_ => format!("{signal:3}% "),
}
})
.centered(),
])
.dark_gray(),
)
})
};
let widths = [
Constraint::Length(25),
Constraint::Length(15),
Constraint::Length(8),
];
let new_networks_table = Table::new(rows, widths)
.header({
if focused_block == FocusedBlock::NewNetworks {
Row::new(vec![
Line::from("Name").yellow().centered(),
Line::from("Security").yellow().centered(),
Line::from("Signal").yellow().centered(),
])
.style(Style::new().bold())
.bottom_margin(1)
} else {
Row::new(vec![
Line::from("Name").centered(),
Line::from("Security").centered(),
Line::from("Signal").centered(),
])
.bottom_margin(1)
}
})
.block(
Block::default()
.title(" New Networks ")
.title_style({
if focused_block == FocusedBlock::NewNetworks {
Style::default().bold()
} else {
Style::default()
}
})
.borders(Borders::ALL)
.border_style({
if focused_block == FocusedBlock::NewNetworks {
Style::default().fg(Color::Green)
} else {
Style::default()
}
})
.border_type({
if focused_block == FocusedBlock::NewNetworks {
BorderType::Thick
} else {
BorderType::default()
}
})
.padding(Padding::horizontal(1)),
)
.column_spacing(1)
.flex(Flex::SpaceAround)
.row_highlight_style(if focused_block == FocusedBlock::NewNetworks {
Style::default().bg(Color::DarkGray).fg(Color::White)
} else {
Style::default()
});
frame.render_stateful_widget(
new_networks_table,
new_networks_block,
&mut self.new_networks_state,
);
let help_message = match focused_block {
FocusedBlock::Device => vec![Line::from(vec![
Span::from(config.station.start_scanning.to_string()).bold(),
Span::from(" Scan"),
Span::from(" | "),
Span::from(config.device.infos.to_string()).bold(),
Span::from(" Infos"),
Span::from(" | "),
Span::from(config.device.toggle_power.to_string()).bold(),
Span::from(" Toggle Power"),
Span::from(" | "),
Span::from("ctrl+r").bold(),
Span::from(" Switch Mode"),
Span::from(" | "),
Span::from("⇄").bold(),
Span::from(" Nav"),
])],
FocusedBlock::KnownNetworks => {
if frame.area().width <= 130 {
vec![
Line::from(vec![
Span::from(" or ↵ ").bold(),
Span::from(" Dis/connect"),
Span::from(" | "),
Span::from(config.station.known_network.show_all.to_string()).bold(),
Span::from(" Show All"),
Span::from(" | "),
Span::from(config.station.known_network.remove.to_string()).bold(),
Span::from(" Remove"),
Span::from(" | "),
Span::from(config.station.known_network.share.to_string()).bold(),
Span::from(" Share"),
Span::from(" | "),
Span::from(config.station.start_scanning.to_string()).bold(),
Span::from(" Scan"),
]),
Line::from(vec![
Span::from("k,").bold(),
Span::from(" Up"),
Span::from(" | "),
Span::from("j,").bold(),
Span::from(" Down"),
Span::from(" | "),
Span::from("⇄").bold(),
Span::from(" Nav"),
Span::from(" | "),
Span::from("ctrl+r").bold(),
Span::from(" Switch Mode"),
Span::from(" | "),
Span::from(config.station.known_network.toggle_autoconnect.to_string())
.bold(),
Span::from(" Autoconnect"),
]),
]
} else {
vec![Line::from(vec![
Span::from("k,").bold(),
Span::from(" Up"),
Span::from(" | "),
Span::from("j,").bold(),
Span::from(" Down"),
Span::from(" | "),
Span::from(" or ↵ ").bold(),
Span::from(" Dis/connect"),
Span::from(" | "),
Span::from(config.station.known_network.show_all.to_string()).bold(),
Span::from(" Show All"),
Span::from(" | "),
Span::from(config.station.known_network.remove.to_string()).bold(),
Span::from(" Remove"),
Span::from(" | "),
Span::from(config.station.known_network.toggle_autoconnect.to_string())
.bold(),
Span::from(" Autoconnect"),
Span::from(" | "),
Span::from(config.station.start_scanning.to_string()).bold(),
Span::from(" Scan"),
Span::from(" | "),
Span::from(config.station.known_network.share.to_string()).bold(),
Span::from(" Share"),
Span::from(" | "),
Span::from("ctrl+r").bold(),
Span::from(" Switch Mode"),
Span::from(" | "),
Span::from("⇄").bold(),
Span::from(" Nav"),
])]
}
}
FocusedBlock::NewNetworks => {
if frame.area().width < 108 {
vec![
Line::from(vec![
Span::from(" or ↵ ").bold(),
Span::from(" Connect"),
Span::from(" | "),
Span::from(config.station.start_scanning.to_string()).bold(),
Span::from(" Scan"),
Span::from(" | "),
Span::from(config.station.new_network.connect_hidden.to_string())
.bold(),
Span::from(" Connect Hidden"),
]),
Line::from(vec![
Span::from("k,").bold(),
Span::from(" Up"),
Span::from(" | "),
Span::from("j,").bold(),
Span::from(" Down"),
Span::from(" | "),
Span::from("ctrl+r").bold(),
Span::from(" Switch Mode"),
Span::from(" | "),
Span::from("⇄").bold(),
Span::from(" Nav"),
]),
]
} else {
vec![Line::from(vec![
Span::from("k,").bold(),
Span::from(" Up"),
Span::from(" | "),
Span::from("j,").bold(),
Span::from(" Down"),
Span::from(" | "),
Span::from(" or ↵ ").bold(),
Span::from(" Connect"),
Span::from(" | "),
Span::from(config.station.new_network.connect_hidden.to_string()).bold(),
Span::from(" Connect Hidden"),
Span::from(" | "),
Span::from(config.station.new_network.show_all.to_string()).bold(),
Span::from(" Show All"),
Span::from(" | "),
Span::from(config.station.start_scanning.to_string()).bold(),
Span::from(" Scan"),
Span::from(" | "),
Span::from("ctrl+r").bold(),
Span::from(" Switch Mode"),
Span::from(" | "),
Span::from("⇄").bold(),
Span::from(" Nav"),
])]
}
}
FocusedBlock::AdapterInfos => {
vec![Line::from(vec![
Span::from(" ").bold(),
Span::from(" Discard"),
])]
}
FocusedBlock::PskAuthKey => vec![Line::from(vec![
Span::from(" ↵ ").bold(),
Span::from(" Apply"),
Span::from(" | "),
Span::from("⇄").bold(),
Span::from(" Hide/Show password"),
Span::from(" | "),
Span::from(" ").bold(),
Span::from(" Discard"),
])],
FocusedBlock::WpaEntrepriseAuth => vec![Line::from(vec![
Span::from(" ↵ ").bold(),
Span::from(" Apply"),
Span::from(" | "),
Span::from("h,l,←,→").bold(),
Span::from(" Switch EAP/Method"),
Span::from(" | "),
Span::from(" ").bold(),
Span::from(" Discard"),
Span::from(" | "),
Span::from("⇄").bold(),
Span::from(" Nav"),
])],
FocusedBlock::ConnectHiddenNetwork => {
vec![Line::from(vec![
Span::from(" ↵ ").bold(),
Span::from(" Connect"),
Span::from(" | "),
Span::from(" ").bold(),
Span::from(" Discard"),
])]
}
_ => vec![Line::from(vec![
Span::from(" ").bold(),
Span::from(" Discard"),
])],
};
let help_message = Paragraph::new(help_message).centered().blue();
frame.render_widget(help_message, help_block);
if let Some(share) = &self.share {
share.render(frame);
}
}
}