mod handlers;
use crate::{
app::AppState,
config,
error::WifiError,
ui::{LayoutAreas, render},
wifi::{
ConnectionEvent, get_connected_ssid, get_wifi_networks, is_backend_available,
start_wifi_listener,
},
};
use color_eyre::eyre::{Result, eyre};
use crossterm::{
cursor::SetCursorStyle,
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyModifiers},
};
use handlers::{
handle_main_view, handle_manual_add_popup, handle_mouse, handle_password_popup,
handle_qr_popup, handle_search_mode,
};
use ratatui::DefaultTerminal;
use std::time::{Duration, Instant};
use tokio::sync::mpsc;
struct CursorStyleGuard;
fn start_network_refresh(state: &mut AppState) {
if state.refresh.is_refreshing_networks || !is_backend_available() {
return;
}
state.refresh.is_refreshing_networks = true;
let (tx, rx) = mpsc::channel(1);
state.refresh.network_update_rx = Some(rx);
tokio::spawn(async move {
let result = tokio::task::spawn_blocking(|| {
let networks = get_wifi_networks()?;
let connected = get_connected_ssid()?;
Ok((networks, connected))
})
.await;
let result = match result {
Ok(inner) => inner,
Err(e) => Err(eyre!(e.to_string())),
};
let _ = tx.send(result).await;
});
}
impl Drop for CursorStyleGuard {
fn drop(&mut self) {
let _ = crossterm::execute!(std::io::stdout(), SetCursorStyle::DefaultUserShape);
let _ = crossterm::execute!(std::io::stdout(), DisableMouseCapture);
}
}
pub async fn run(mut terminal: DefaultTerminal, state: &mut AppState) -> Result<()> {
let _cursor_style_guard = CursorStyleGuard;
crossterm::execute!(std::io::stdout(), SetCursorStyle::BlinkingBlock)?;
crossterm::execute!(std::io::stdout(), EnableMouseCapture)?;
let mut listener_init_started = false;
let mut layout_areas = LayoutAreas::default();
loop {
terminal.draw(|frame| {
layout_areas = render(frame, state);
})?;
if !listener_init_started {
listener_init_started = true;
if is_backend_available()
&& let Some(connection_event_tx) = state.connection.connection_event_tx.take()
{
let (init_tx, init_rx) = mpsc::channel(1);
state.connection.listener_init_rx = Some(init_rx);
tokio::spawn(async move {
let result = tokio::task::spawn_blocking(move || {
start_wifi_listener(connection_event_tx)
})
.await;
let result = match result {
Ok(inner) => inner,
Err(e) => Err(WifiError::Internal(e.to_string())),
};
let _ = init_tx.send(result).await;
});
}
}
if let Some(rx) = &mut state.connection.listener_init_rx {
if let Ok(result) = rx.try_recv() {
state.connection.listener_init_rx = None;
match result {
Ok(listener) => {
state.connection.wifi_listener = Some(listener);
}
Err(e) => {
state.ui.error_message =
Some(format!("WiFi event listener unavailable: {}", e));
}
}
}
}
if let Some(rx) = &mut state.ui.qr_result_rx {
if let Ok(result) = rx.try_recv() {
state.ui.qr_result_rx = None;
match result {
Ok(qr_lines) => {
state.ui.qr_code_lines = qr_lines;
state.ui.show_qr_popup = true;
}
Err(error) => {
state.ui.error_message =
Some(format!("Could not share secured Wi-Fi network: {error}"));
}
}
}
}
if let Some(rx) = &mut state.connection.connection_result_rx {
if let Ok((operation_id, result)) = rx.try_recv() {
if state.connection.active_operation_id == Some(operation_id) {
state.connection.connection_result_rx = None;
state.connection.active_operation_id = None;
if let Err(e) = result {
let was_connecting = state.connection.is_connecting;
if was_connecting {
state.connection.finish_connection_attempt();
}
state.ui.error_message = Some(if was_connecting {
format!("Failed to connect: {e}")
} else {
format!("Wi-Fi operation failed: {e}")
});
} else {
state.refresh.refresh_burst = config::CONNECTION_REFRESH_BURST;
}
start_network_refresh(state);
}
}
}
if let Some(rx) = &mut state.refresh.network_update_rx {
if let Ok(result) = rx.try_recv() {
match result {
Ok((new_list, connected_ssid)) => {
let connection_changed = state.network.connected_ssid != connected_ssid;
let selected_network = state
.ui
.l_state
.selected()
.and_then(|i| state.network.filtered_wifi_list.get(i))
.map(|w| (w.ssid.clone(), w.bssid.clone()));
state.network.wifi_list = new_list;
state.network.connected_ssid = connected_ssid;
state.update_filtered_list();
if connection_changed && state.network.connected_ssid.is_some() {
state.ui.l_state.select(Some(0));
} else if let Some((ssid, bssid)) = selected_network {
let position = bssid.as_ref().and_then(|selected_bssid| {
state.network.filtered_wifi_list.iter().position(|w| {
w.ssid == ssid && w.bssid.as_ref() == Some(selected_bssid)
})
});
if let Some(pos) = position.or_else(|| {
state
.network
.filtered_wifi_list
.iter()
.position(|w| w.ssid == ssid)
}) {
state.ui.l_state.select(Some(pos));
} else {
state.ui.l_state.select(Some(0));
}
} else {
state.ui.l_state.select(Some(0));
}
}
Err(e) => {
state.ui.error_message = Some(format!("Failed to refresh networks: {e}"));
}
}
state.refresh.is_refreshing_networks = false;
state.refresh.is_initial_loading = false;
state.refresh.network_update_rx = None;
state.refresh.last_refresh = Instant::now();
}
}
let mut connection_events = Vec::new();
if let Some(rx) = &mut state.connection.connection_event_rx {
while let Ok(event) = rx.try_recv() {
connection_events.push(event);
}
}
for event in connection_events {
match event {
ConnectionEvent::Connected(ssid) => {
state.network.connected_ssid = Some(ssid.clone());
for w in &mut state.network.wifi_list {
w.is_connected = w.ssid == ssid;
}
state.update_filtered_list();
if state.connection.is_connecting {
state.connection.finish_connection_attempt();
}
state.refresh.refresh_burst = config::DISCONNECT_REFRESH_BURST;
}
ConnectionEvent::Disconnected => {
state.connection.finish_disconnect_attempt();
state.network.connected_ssid = None;
for w in &mut state.network.wifi_list {
w.is_connected = false;
}
state.update_filtered_list();
state.refresh.refresh_burst = config::DISCONNECT_REFRESH_BURST;
}
ConnectionEvent::Failed {
ssid, reason_str, ..
} => {
if state.connection.is_connecting
&& state
.connection
.target_ssid
.as_deref()
.is_some_and(|target| target == ssid)
{
state.connection.finish_connection_attempt();
state.ui.error_message = Some(format!("Connection failed: {}", reason_str));
}
}
}
}
if state.connection.is_connecting
|| state.connection.is_disconnecting
|| state.refresh.is_initial_loading
|| state.refresh.is_refreshing_networks
{
state.ui.loading_frame = state.ui.loading_frame.wrapping_add(1);
}
if state.connection.is_connecting {
if let Some(target) = &state.connection.target_ssid {
let is_target_connected = state
.network
.connected_ssid
.as_deref()
.is_some_and(|conn| conn == target)
|| state
.network
.filtered_wifi_list
.iter()
.any(|w| w.ssid == *target && w.is_connected);
if is_target_connected {
state.connection.finish_connection_attempt();
}
if let Some(start_time) = state.connection.connection_start_time {
if start_time.elapsed() > Duration::from_secs(config::CONNECTION_TIMEOUT_SECS) {
state.connection.finish_connection_attempt();
state.ui.error_message =
Some("Connection timed out (No response from OS)".to_string());
}
}
} else {
if state.connection.connection_result_rx.is_none() {
state.connection.finish_connection_attempt();
}
}
}
if state.connection.is_disconnecting {
if let Some(target) = &state.connection.disconnecting_ssid {
let is_still_connected = state
.network
.connected_ssid
.as_deref()
.is_some_and(|conn| conn == target)
|| state
.network
.filtered_wifi_list
.iter()
.any(|w| w.ssid == *target && w.is_connected);
if !is_still_connected {
state.connection.finish_disconnect_attempt();
}
if let Some(start_time) = state.connection.connection_start_time {
if start_time.elapsed() > Duration::from_secs(config::CONNECTION_TIMEOUT_SECS) {
state.connection.finish_disconnect_attempt();
}
}
} else if state.connection.connection_result_rx.is_none() {
state.connection.finish_disconnect_attempt();
}
}
let refresh_interval = if state.refresh.refresh_burst > 0 {
Duration::from_secs(config::BURST_REFRESH_INTERVAL_SECS)
} else if state.ui.is_searching || !state.inputs.search_input.value.is_empty() {
Duration::from_secs(config::SEARCHING_REFRESH_INTERVAL_SECS)
} else {
Duration::from_secs(config::AUTO_REFRESH_INTERVAL_SECS)
};
if is_backend_available()
&& !state.refresh.is_refreshing_networks
&& !state.ui.show_manual_add_popup
&& !state.ui.show_password_popup
&& !state.ui.show_qr_popup
&& state.refresh.last_refresh.elapsed() >= refresh_interval
&& state.refresh.last_interaction.elapsed()
>= Duration::from_secs(config::INTERACTION_COOLDOWN_SECS)
{
if state.refresh.refresh_burst > 0 {
state.refresh.refresh_burst -= 1;
}
start_network_refresh(state);
}
if event::poll(Duration::from_millis(config::EVENT_POLL_MS))? {
match event::read()? {
Event::Key(key) if key.kind == event::KeyEventKind::Press => {
state.refresh.last_interaction = Instant::now();
if state.ui.show_key_logger
&& !state.ui.show_password_popup
&& !state.ui.show_manual_add_popup
{
let mut key_str = String::new();
if key.modifiers.contains(KeyModifiers::CONTROL) {
key_str.push_str("Ctrl+");
}
if key.modifiers.contains(KeyModifiers::ALT) {
key_str.push_str("Alt+");
}
if key.modifiers.contains(KeyModifiers::SHIFT)
&& !matches!(key.code, event::KeyCode::Char(_))
{
key_str.push_str("Shift+");
}
let code_str = match key.code {
event::KeyCode::Char(c) => c.to_string(),
event::KeyCode::Enter => "Enter".to_string(),
event::KeyCode::Backspace => "Backspace".to_string(),
event::KeyCode::Left => "Left".to_string(),
event::KeyCode::Right => "Right".to_string(),
event::KeyCode::Up => "Up".to_string(),
event::KeyCode::Down => "Down".to_string(),
event::KeyCode::Tab => "Tab".to_string(),
event::KeyCode::Delete => "Delete".to_string(),
event::KeyCode::Home => "Home".to_string(),
event::KeyCode::End => "End".to_string(),
event::KeyCode::PageUp => "PageUp".to_string(),
event::KeyCode::PageDown => "PageDown".to_string(),
event::KeyCode::Esc => "Esc".to_string(),
event::KeyCode::F(n) => format!("F{}", n),
_ => format!("{:?}", key.code),
};
key_str.push_str(&code_str);
state.ui.last_key_press = Some((key_str, Instant::now()));
}
if state.ui.error_message.is_some() {
state.ui.error_message = None;
}
if key.code == event::KeyCode::Char('c')
&& key.modifiers.contains(KeyModifiers::CONTROL)
{
break;
}
let should_quit = if state.ui.show_qr_popup {
handle_qr_popup(key, state)
} else if state.ui.show_manual_add_popup {
handle_manual_add_popup(key, state)
} else if state.ui.show_password_popup {
handle_password_popup(key, state)
} else if state.ui.is_searching {
handle_search_mode(key, state)
} else {
handle_main_view(key, state)
};
if should_quit {
break;
}
}
Event::Mouse(mouse) => {
state.refresh.last_interaction = Instant::now();
handle_mouse(mouse, state, &layout_areas);
}
_ => {}
}
}
}
Ok(())
}