use crate::{
config::{self, IconSet},
input::InputState,
wifi::{ConnectionEvent, WifiInfo, WifiListener},
};
use color_eyre::eyre::Result;
use ratatui::widgets::ListState;
use std::time::{Duration, Instant};
use tokio::sync::mpsc::{Receiver, UnboundedReceiver, UnboundedSender};
#[derive(Debug)]
pub struct NetworkState {
pub wifi_list: Vec<WifiInfo>,
pub filtered_wifi_list: Vec<WifiInfo>,
pub connected_ssid: Option<String>,
}
impl NetworkState {
pub fn new(wifi_list: Vec<WifiInfo>) -> Self {
Self {
filtered_wifi_list: wifi_list.clone(),
wifi_list,
connected_ssid: None,
}
}
}
#[derive(Debug)]
pub struct UiState {
pub l_state: ListState,
pub is_searching: bool,
pub show_password_popup: bool,
pub show_manual_add_popup: bool,
pub show_qr_popup: bool,
pub qr_code_lines: Vec<String>,
pub qr_result_rx: Option<Receiver<Result<Vec<String>>>>,
pub error_message: Option<String>,
pub loading_frame: usize,
pub show_key_logger: bool,
pub last_key_press: Option<(String, Instant)>,
pub icon_set: IconSet,
}
impl UiState {
pub fn new(show_key_logger: bool, use_ascii_icons: bool, has_networks: bool) -> Self {
Self {
l_state: ListState::default().with_selected(if has_networks { Some(0) } else { None }),
is_searching: false,
show_password_popup: false,
show_manual_add_popup: false,
show_qr_popup: false,
qr_code_lines: Vec::new(),
qr_result_rx: None,
error_message: None,
loading_frame: 0,
show_key_logger,
last_key_press: None,
icon_set: if use_ascii_icons {
IconSet::Ascii
} else {
IconSet::Nerd
},
}
}
}
#[derive(Debug)]
pub struct ConnectionState {
pub is_connecting: bool,
pub pending_password_ssid: Option<String>,
pub target_ssid: Option<String>,
pub is_disconnecting: bool,
pub disconnecting_ssid: Option<String>,
pub connection_start_time: Option<Instant>,
pub connection_result_rx: Option<Receiver<OperationResult>>,
pub next_operation_id: u64,
pub active_operation_id: Option<u64>,
pub active_connection_attempt_id: Option<u64>,
#[allow(dead_code)]
pub wifi_listener: Option<WifiListener>,
pub listener_init_rx: Option<Receiver<crate::error::WifiResult<WifiListener>>>,
pub connection_event_tx: Option<UnboundedSender<ConnectionEvent>>,
pub connection_event_rx: Option<UnboundedReceiver<ConnectionEvent>>,
}
pub type OperationResult = (u64, Result<()>);
impl ConnectionState {
pub fn new() -> Self {
let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
Self {
is_connecting: false,
pending_password_ssid: None,
target_ssid: None,
is_disconnecting: false,
disconnecting_ssid: None,
connection_start_time: None,
connection_result_rx: None,
next_operation_id: 0,
active_operation_id: None,
active_connection_attempt_id: None,
wifi_listener: None,
listener_init_rx: None,
connection_event_tx: Some(tx),
connection_event_rx: Some(rx),
}
}
pub fn begin_operation(&mut self) -> u64 {
self.next_operation_id = self.next_operation_id.wrapping_add(1).max(1);
let operation_id = self.next_operation_id;
self.active_operation_id = Some(operation_id);
operation_id
}
pub fn begin_connection_attempt(&mut self, ssid: String) -> u64 {
let operation_id = self.begin_operation();
self.is_connecting = true;
self.target_ssid = Some(ssid);
self.connection_start_time = Some(Instant::now());
self.active_connection_attempt_id = Some(operation_id);
operation_id
}
pub fn begin_disconnect_attempt(&mut self, ssid: String) -> u64 {
let operation_id = self.begin_operation();
self.is_disconnecting = true;
self.disconnecting_ssid = Some(ssid);
self.connection_start_time = Some(Instant::now());
operation_id
}
pub fn finish_disconnect_attempt(&mut self) {
self.is_disconnecting = false;
self.disconnecting_ssid = None;
}
pub fn finish_connection_attempt(&mut self) {
self.active_operation_id = None;
self.connection_result_rx = None;
self.is_connecting = false;
self.target_ssid = None;
self.connection_start_time = None;
self.active_connection_attempt_id = None;
}
pub fn cancel_operation(&mut self) {
self.active_operation_id = None;
self.finish_connection_attempt();
}
}
#[derive(Debug, Default)]
pub struct InputStates {
pub password_input: InputState,
pub search_input: InputState,
pub manual_ssid_input: InputState,
pub manual_password_input: InputState,
pub manual_security: String,
pub manual_hidden: bool,
pub manual_input_field: usize,
}
impl InputStates {
pub fn new() -> Self {
Self {
password_input: InputState::new(),
search_input: InputState::new(),
manual_ssid_input: InputState::new(),
manual_password_input: InputState::new(),
manual_security: "WPA2-Personal".to_string(),
manual_hidden: false,
manual_input_field: 0,
}
}
pub fn clear_manual(&mut self) {
self.manual_ssid_input.clear();
self.manual_password_input.clear();
self.manual_input_field = 0;
}
}
#[derive(Debug)]
pub struct RefreshState {
pub last_refresh: Instant,
pub last_interaction: Instant,
pub last_manual_refresh: Instant,
pub is_refreshing_networks: bool,
pub network_update_rx: Option<Receiver<Result<(Vec<WifiInfo>, Option<String>)>>>,
pub refresh_burst: u8,
pub is_initial_loading: bool,
}
impl RefreshState {
pub fn new() -> Self {
Self {
last_refresh: Instant::now() - Duration::from_secs(15), last_interaction: Instant::now(),
last_manual_refresh: Instant::now() - Duration::from_secs(15), is_refreshing_networks: false,
network_update_rx: None,
refresh_burst: config::STARTUP_REFRESH_BURST,
is_initial_loading: true,
}
}
}
#[derive(Debug, Default)]
pub struct MouseState {
pub hovered_row: Option<usize>,
pub last_list_click: Option<(usize, Instant)>,
pub scroll_offset: usize,
}
#[derive(Debug)]
pub struct AppState {
pub network: NetworkState,
pub ui: UiState,
pub connection: ConnectionState,
pub inputs: InputStates,
pub refresh: RefreshState,
pub mouse: MouseState,
}
impl AppState {
pub fn new(wifi_list: Vec<WifiInfo>, show_key_logger: bool, use_ascii_icons: bool) -> AppState {
let has_networks = !wifi_list.is_empty();
AppState {
network: NetworkState::new(wifi_list),
ui: UiState::new(show_key_logger, use_ascii_icons, has_networks),
connection: ConnectionState::new(),
inputs: InputStates::new(),
refresh: RefreshState::new(),
mouse: MouseState::default(),
}
}
pub fn next(&mut self) {
let i = match self.ui.l_state.selected() {
Some(i) => {
if i >= self.network.filtered_wifi_list.len().saturating_sub(1) {
i
} else {
i + 1
}
}
None => 0,
};
self.ui.l_state.select(Some(i));
}
pub fn previous(&mut self) {
let i = match self.ui.l_state.selected() {
Some(i) => {
if i == 0 {
0
} else {
i - 1
}
}
None => 0,
};
self.ui.l_state.select(Some(i));
}
pub fn go_to_top(&mut self) {
if !self.network.filtered_wifi_list.is_empty() {
self.ui.l_state.select(Some(0));
}
}
pub fn go_to_bottom(&mut self) {
if !self.network.filtered_wifi_list.is_empty() {
self.ui
.l_state
.select(Some(self.network.filtered_wifi_list.len() - 1));
}
}
pub fn update_filtered_list(&mut self) {
if self.inputs.search_input.value.is_empty() {
self.network.filtered_wifi_list = self.network.wifi_list.clone();
} else {
let search_lower = self.inputs.search_input.value.to_lowercase();
self.network.filtered_wifi_list = self
.network
.wifi_list
.iter()
.filter(|w| {
let ssid_lower = w.ssid.to_lowercase();
let mut search_chars = search_lower.chars();
let mut search_char = search_chars.next();
for c in ssid_lower.chars() {
if let Some(sc) = search_char {
if c == sc {
search_char = search_chars.next();
}
} else {
break;
}
}
search_char.is_none()
})
.cloned()
.collect();
}
if let Some(selected) = self.ui.l_state.selected() {
if selected >= self.network.filtered_wifi_list.len() {
self.ui.l_state.select(Some(0));
}
}
}
pub fn is_popup_open(&self) -> bool {
self.ui.show_manual_add_popup || self.ui.show_password_popup || self.ui.show_qr_popup
}
}