use std::{io, time::Duration};
use ratatui::{
crossterm::event::{self, Event, KeyCode},
layout::{Constraint, HorizontalAlignment, Layout, Rect},
prelude::*,
widgets::*,
};
use tokio::sync::mpsc;
use crate::{
cache::{add_wifi_list_cache, delete_wifi_list_cache, read_wifi_list_cache},
nmcli::{connect, connection_free, connection_up, disconnect, get_wifi_list},
};
#[derive(Debug, Clone, PartialEq)]
enum AppMode {
Menu,
Connect,
FailToConnect,
Password,
ToDisconnect,
Disconnect,
FailToDisconnect,
}
#[allow(dead_code)]
#[derive(Debug, Clone, PartialEq)]
pub enum Message {
Thick,
WifiConnected(String),
WifiDisconnected(String),
WifiConnectedFailed(String),
WifiFailed(String),
WifiListUpdated(Vec<String>),
WifiDisconnectFail(String),
}
#[derive(Debug, Clone, PartialEq)]
pub enum AppStatus {
Running,
Exit,
}
#[allow(dead_code)]
pub struct App {
status: AppStatus,
mode: AppMode,
state: ListState,
tab: usize,
wifi: Vec<String>,
security: Vec<String>,
wifi_connected: Vec<String>,
used_wifi: Option<String>,
selected_wifi: Option<String>,
password: String,
connect_frame: u16,
error_msg: Option<String>,
tx: mpsc::Sender<Message>,
rx: mpsc::Receiver<Message>,
}
impl App {
pub fn new(
wifi: Vec<String>,
security: Vec<String>,
tx: mpsc::Sender<Message>,
rx: mpsc::Receiver<Message>,
) -> Self {
let mut state = ListState::default();
if !wifi.is_empty() {
state.select_first();
}
let wifi_connected = read_wifi_list_cache();
Self {
status: AppStatus::Running,
mode: AppMode::Menu,
state,
tab: 0,
used_wifi: None,
wifi,
security,
wifi_connected,
selected_wifi: None,
password: String::new(),
connect_frame: 0,
error_msg: None,
tx,
rx,
}
}
pub fn set_used(&mut self, used_wifi: String) {
self.used_wifi = Some(used_wifi)
}
pub async fn run(&mut self) -> io::Result<()> {
let tx = self.tx.clone();
let tx_update = tx.clone();
let tx_thick = tx.clone();
tokio::spawn(async move {
let mut previous = Vec::new();
loop {
let current = get_wifi_list().unwrap().get_list().to_vec();
if current != previous {
tx_update
.send(Message::WifiListUpdated(current.clone()))
.await
.unwrap();
previous = current;
}
tokio::time::sleep(Duration::from_secs(5)).await;
}
});
tokio::spawn(async move {
loop {
tx_thick.send(Message::Thick).await.unwrap();
tokio::time::sleep(Duration::from_millis(500)).await;
}
});
ratatui::run(|terminal| {
while self.status == AppStatus::Running {
while let Ok(msg) = self.rx.try_recv() {
self.handle_message(msg);
}
terminal.draw(|frame| {
self.draw(frame);
})?;
self.handle_events()?;
}
Ok(())
})
}
fn draw(&mut self, frame: &mut Frame) {
let area = frame.area();
let [top_area, body_area] = Layout::vertical([Constraint::Length(6), Constraint::Min(0)])
.vertical_margin(1)
.horizontal_margin(10)
.areas(area);
frame.render_widget(
Block::new().style(Style::new().bg(Color::Rgb(32, 39, 45))),
area,
);
self.render_title(frame, top_area);
self.render_wifi_list(frame, body_area);
if self.mode == AppMode::Password {
self.render_password_popup(frame, area);
} else if self.mode == AppMode::Connect {
self.render_connect_popup(frame, area);
} else if self.mode == AppMode::FailToConnect {
self.render_fail_connect_popup(frame, area);
} else if self.mode == AppMode::ToDisconnect {
self.render_disconnect(frame, area);
} else if self.mode == AppMode::Disconnect {
self.render_disconnect_wait(frame, area);
} else if self.mode == AppMode::FailToDisconnect {
self.render_fail_disconnect_popup(frame, area);
}
}
fn render_title(&self, frame: &mut Frame, area: Rect) {
let title = concat!(
" _ _ _ _ \n",
"| \\ | | ___| |___ _____ _ __| | __\n",
"| \\| |/ _ \\ __\\ \\ /\\ / / _ \\| '__| |/ /\n",
"| |\\ | __/ |_ \\ V V / (_) | | | < \n",
"|_| \\_|\\___|\\__| \\_/\\_/ \\___/|_| |_|\\_\\\\\n",
);
let paragraph = Paragraph::new(title)
.centered()
.block(Block::bordered().borders(Borders::BOTTOM));
frame.render_widget(paragraph, area);
}
fn render_wifi_list(&mut self, frame: &mut Frame, area: Rect) {
let items: Vec<ListItem> = self
.wifi
.iter()
.enumerate()
.map(|(index, wifi)| {
if let Some(used_wifi) = &self.used_wifi
&& wifi == used_wifi
{
return ListItem::new(format!("{wifi} - Connected"))
.style(Color::Rgb(200, 158, 20));
} else if self.security[index].is_empty() {
return ListItem::new(format!("{wifi} - Open")).style(Color::Rgb(100, 158, 20));
}
ListItem::new(wifi.clone())
})
.collect();
let list = List::new(items)
.block(Block::bordered().title("Wifi"))
.highlight_style(Style::new().fg(Color::Rgb(12, 12, 12)).bg(Color::DarkGray))
.highlight_symbol(" ");
frame.render_stateful_widget(list, area, &mut self.state);
}
fn render_password_popup(&self, frame: &mut Frame, area: Rect) {
frame.render_widget(Clear, area);
let popup = Rect {
x: (area.width.saturating_sub(50)) / 2,
y: (area.height.saturating_sub(3)) / 2,
width: 50,
height: 3,
};
let masked_password = "*".repeat(self.password.chars().count());
let wifi_name = self.selected_wifi.as_deref().unwrap_or("Unknown");
let content = Line::from(vec![Span::raw("Password: "), Span::raw(masked_password)]);
let paragraph = Paragraph::new(content).centered().block(
Block::bordered()
.title(format!(" {wifi_name} "))
.title_alignment(HorizontalAlignment::Center),
);
frame.render_widget(paragraph, popup);
}
fn render_connect_popup(&self, frame: &mut Frame, area: Rect) {
let popup = Rect {
x: (area.width.saturating_sub(50)) / 2,
y: (area.height.saturating_sub(3)) / 2,
width: 50,
height: 3,
};
frame.render_widget(Clear, popup);
let content = Line::from(vec![
Span::raw("Connecting"),
Span::from(".".repeat((self.connect_frame % 5) as usize)),
]);
let paragraph = Paragraph::new(content).centered().block(Block::bordered());
frame.render_widget(paragraph, popup);
}
fn render_disconnect_wait(&self, frame: &mut Frame, area: Rect) {
let popup = Rect {
x: (area.width.saturating_sub(50)) / 2,
y: (area.height.saturating_sub(3)) / 2,
width: 50,
height: 3,
};
frame.render_widget(Clear, popup);
let content = Line::from(vec![
Span::raw("Disconnecting"),
Span::from(".".repeat((self.connect_frame % 5) as usize)),
]);
let paragraph = Paragraph::new(content).centered().block(Block::bordered());
frame.render_widget(paragraph, popup);
}
fn render_fail_connect_popup(&self, frame: &mut Frame, area: Rect) {
let popup = Rect {
x: (area.width.saturating_sub(50)) / 2,
y: (area.height.saturating_sub(3)) / 2,
width: 50,
height: 3,
};
frame.render_widget(Clear, popup);
let msg = if let Some(e) = &self.error_msg {
if e.contains("password for") {
"Wrong password"
} else {
"Connection activation failed"
}
} else {
""
};
let content = Line::from(vec![Span::from(msg)]);
let paragraph = Paragraph::new(content)
.centered()
.block(Block::bordered().title_bottom(Line::from("Press q or Esc").centered()));
frame.render_widget(paragraph, popup);
}
fn render_fail_disconnect_popup(&self, frame: &mut Frame, area: Rect) {
let popup = Rect {
x: (area.width.saturating_sub(50)) / 2,
y: (area.height.saturating_sub(5)) / 2,
width: 50,
height: 5,
};
frame.render_widget(Clear, popup);
let msg = if let Some(e) = &self.error_msg {
e.as_str()
} else {
"Something went wrong"
};
let content = Line::from(vec![Span::from(msg)]);
let paragraph = Paragraph::new(content)
.centered()
.block(Block::bordered().title_bottom(Line::from("Press q or Esc").centered()));
frame.render_widget(paragraph, popup);
}
fn render_disconnect(&mut self, frame: &mut Frame, area: Rect) {
let popup = Rect {
x: (area.width.saturating_sub(50)) / 2,
y: (area.height.saturating_sub(3)) / 2,
width: 50,
height: 3,
};
let tabs = Tabs::new(vec![" Yes ", " No "])
.select(self.tab)
.block(
Block::bordered()
.title("Disconnect")
.title_alignment(Alignment::Center)
.padding(Padding {
left: 15,
right: 15,
top: 0,
bottom: 0,
}),
)
.highlight_style(
Style::default()
.fg(Color::Black)
.bg(Color::Rgb(255, 255, 255)),
)
.divider("");
frame.render_widget(&tabs, popup);
}
fn handle_events(&mut self) -> io::Result<()> {
if event::poll(Duration::from_millis(50))?
&& let Event::Key(key) = event::read()?
{
match self.mode {
AppMode::Menu => {
self.handle_menu_key(key.code);
}
AppMode::Password => {
self.handle_password_key(key.code);
}
AppMode::Connect | AppMode::FailToConnect => {
self.handle_connect_key(key.code);
}
AppMode::ToDisconnect => {
self.handle_disconnect_key(key.code);
}
AppMode::FailToDisconnect => {
self.handle_fail_disconnect_key(key.code);
}
AppMode::Disconnect => {}
}
}
Ok(())
}
fn handle_fail_disconnect_key(&mut self, key: KeyCode) {
match key {
KeyCode::Esc | KeyCode::Char('q') => {
self.mode = AppMode::Menu;
}
_ => {}
}
}
fn handle_disconnect_key(&mut self, key: KeyCode) {
match key {
KeyCode::Esc | KeyCode::Char('q') => self.mode = AppMode::Menu,
KeyCode::Tab => self.tab_next(),
KeyCode::Enter => {
if self.tab.is_multiple_of(2) {
self.wait_disconnect();
} else {
self.mode = AppMode::Menu;
}
}
_ => {}
}
}
fn handle_menu_key(&mut self, key: KeyCode) {
match key {
KeyCode::Esc | KeyCode::Char('q') => {
self.status = AppStatus::Exit;
}
KeyCode::Up => {
self.select_previous();
}
KeyCode::Down => {
self.select_next();
}
KeyCode::Enter => {
if self.is_free() {
self.enter();
self.connect_free();
} else if !self.is_connected() {
self.open_password_popup();
} else {
self.mode = AppMode::ToDisconnect;
}
}
_ => {}
}
}
fn handle_password_key(&mut self, key: KeyCode) {
match key {
KeyCode::Esc => {
self.close_password_popup();
}
KeyCode::Backspace => {
self.password.pop();
}
KeyCode::Enter => {
self.submit_password();
}
KeyCode::Char(ch) => {
self.password.push(ch);
}
_ => {}
}
}
fn handle_connect_key(&mut self, key: KeyCode) {
match key {
KeyCode::Esc | KeyCode::Char('q') => {
self.mode = AppMode::Menu;
}
_ => {}
}
}
fn tab_next(&mut self) {
self.tab = (self.tab + 1) % 2;
}
fn select_next(&mut self) {
if self.wifi.is_empty() {
return;
}
let next = match self.state.selected() {
Some(i) => (i + 1) % self.wifi.len(),
None => 0,
};
self.state.select(Some(next));
}
fn select_previous(&mut self) {
if self.wifi.is_empty() {
return;
}
let prev = match self.state.selected() {
Some(0) => self.wifi.len() - 1,
Some(i) => i - 1,
None => 0,
};
self.state.select(Some(prev));
}
fn is_connected(&mut self) -> bool {
let Some(_) = self.state.selected() else {
return false;
};
if self.used_wifi.is_none() {
return false;
}
self.wifi
.get(self.state.selected().unwrap())
.cloned()
.unwrap()
== self.used_wifi.clone().unwrap()
}
fn is_free(&mut self) -> bool {
let Some(_) = self.state.selected() else {
return false;
};
self.security[self.state.selected().unwrap()].is_empty()
}
fn connect_free(&mut self) {
if let Some(wifi) = self.selected_wifi.clone() {
self.mode = AppMode::Connect;
let tx = self.tx.clone();
tokio::spawn(async move {
match connection_free(&wifi) {
Ok(_) => {
tx.send(Message::WifiConnected(wifi)).await.unwrap();
}
Err(e) => {
tx.send(Message::WifiFailed(e)).await.unwrap();
}
}
});
}
}
fn enter(&mut self) {
let Some(selected) = self.state.selected() else {
return;
};
self.selected_wifi = Some(self.wifi[selected].clone());
}
fn open_password_popup(&mut self) {
let Some(selected) = self.state.selected() else {
return;
};
self.selected_wifi = Some(self.wifi[selected].clone());
if self
.wifi_connected
.contains(&self.selected_wifi.clone().unwrap())
{
self.submit_password();
return;
}
self.password.clear();
self.mode = AppMode::Password;
}
fn close_password_popup(&mut self) {
self.password.clear();
self.mode = AppMode::Menu;
}
fn submit_password(&mut self) {
if let Some(wifi) = self.selected_wifi.clone() {
self.mode = AppMode::Connect;
let password = self.password.clone();
let tx = self.tx.clone();
let wifi_connected = self.wifi_connected.clone();
tokio::spawn(async move {
let wifi = wifi;
if wifi_connected.contains(&wifi) {
match connection_up(&wifi) {
Ok(_) => tx.send(Message::WifiConnected(wifi)).await.unwrap(),
Err(_) => tx.send(Message::WifiConnectedFailed(wifi)).await.unwrap(),
}
} else {
let result = connect(&wifi, &password);
match result {
Ok(_) => {
tx.send(Message::WifiConnected(wifi)).await.unwrap();
}
Err(e) => tx.send(Message::WifiFailed(e)).await.unwrap(),
}
}
});
}
}
fn wait_disconnect(&mut self) {
let selected = self.state.selected().unwrap();
self.selected_wifi = Some(self.wifi[selected].clone());
self.used_wifi = None;
self.mode = AppMode::Disconnect;
if let Some(wifi) = self.selected_wifi.clone() {
let tx = self.tx.clone();
tokio::spawn(async move {
let result = disconnect(&wifi);
match result {
Ok(_) => {
tx.send(Message::WifiDisconnected(wifi)).await.unwrap();
}
Err(e) => tx.send(Message::WifiDisconnectFail(e)).await.unwrap(),
}
});
}
}
fn handle_message(&mut self, msg: Message) {
match msg {
Message::WifiListUpdated(list) => {
self.wifi = list;
if self.wifi.is_empty() {
self.state.select(None);
} else if self.state.selected().is_none() {
self.state.select_first();
}
}
Message::Thick => {
self.connect_frame = self.connect_frame.wrapping_add(1);
}
Message::WifiFailed(e) => {
self.mode = AppMode::FailToConnect;
self.error_msg = Some(e);
}
Message::WifiConnectedFailed(wifi) => {
self.mode = AppMode::FailToConnect;
delete_wifi_list_cache(&wifi).unwrap();
}
Message::WifiConnected(used) => {
add_wifi_list_cache(&used).unwrap();
self.used_wifi = Some(used);
self.mode = AppMode::Menu;
}
Message::WifiDisconnectFail(e) => {
self.error_msg = Some(e);
}
Message::WifiDisconnected(wifi) => {
self.wifi_connected.push(wifi);
self.mode = AppMode::Menu;
}
}
}
}