diff --git a/CHANGELOG.md b/CHANGELOG.md index 4708d1b..0667db8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,13 +6,26 @@ All notable changes to this project will be documented in this file. ### Fixed -- Fixed the rerendering issue of the cursor by removing it from the render method and placing it to the draw method. +### Fixed + +- Fixed the re-rendering issue of the cursor by removing it from the render method and placing it to the draw method. +- Fixed the issue of pop-up for pressing 'd' key in the unsaved connection from the main list. + +### Changed + +- Added a flag to indicate whether the app is scanning the networks or not. +- Changed the type of list that was send to the scan_networks function from main App `struct` to be `RwLock` wrapped type rather than Arc wrapped type. + +### Changed + +- Changed the type of list that was send to the scan_networks function from main App `struct` to be `RwLock` wrapped type rather than Arc wrapped type. +- Changed the render method to render the scanning status with small animation in accordance with the scanning flag rather then the `.try_lock()` method that was used earlier. ## [1.1.0] - 2025-01-01 ### Added -- Listner for disconnecting the current connection +- Listener for disconnecting the current connection - Helper function to disconnect from the current connection - Utility function that will actually disconnect from the current connection @@ -24,7 +37,7 @@ All notable changes to this project will be documented in this file. - Fixed the issue in the saved-connection list where when we try to delete a connection, it was doing the deletion from the index of the main list instead of the saved connection list. - Fixed the refresh issue of the list after the deletion of a saved connection. -- Fixed the outo of bound issue when the list gets refreshed after deletion. +- Fixed the out of bound issue when the list gets refreshed after deletion. ## [1.0.0] - 2025-12-30 @@ -33,8 +46,8 @@ All notable changes to this project will be documented in this file. - documentation and comments - help menu - list for saved connections, -- delete connection confirmation popup, -- listeners for all the popups/menus +- delete connection confirmation pop-up, +- listeners for all the pop-ups/menus ### Changed diff --git a/src/apps/core.rs b/src/apps/core.rs index 215c014..8c50df6 100644 --- a/src/apps/core.rs +++ b/src/apps/core.rs @@ -17,14 +17,16 @@ use crossterm::execute; use ratatui::Frame; use ratatui::layout::Position; use std::io; -use std::sync::{Arc, Mutex}; +use std::sync::Arc; mod delete_handler; mod help_handlers; +use std::sync::RwLock; +use std::sync::atomic::AtomicBool; #[derive(Debug)] pub struct App { wifi_credentials: WifiInputState, - wifi_list: Arc>>, + wifi_list: Arc>>, selected: usize, app_state: AppState, saved_connection: SavedConnections, @@ -45,15 +47,23 @@ impl Default for App { /// assert!(app.wifi_list.lock().unwrap().is_empty()); /// ``` fn default() -> Self { - let wifi_list = Arc::new(Mutex::new(Vec::new())); - scan_networks(wifi_list.clone()); + let wifi_list = Arc::new(RwLock::new(Vec::new())); + + // Setting up scanning flag + let is_scanning = Arc::new(AtomicBool::new(true)); + scan_networks(wifi_list.clone(), is_scanning.clone()); Self { wifi_credentials: WifiInputState::default(), wifi_list, selected: 0, app_state: AppState::default(), saved_connection: SavedConnections::default(), - flags: Flags::default(), + flags: { + Flags { + is_scanning, + ..Default::default() + } + }, } } } @@ -108,7 +118,7 @@ impl App { } fn prepare_to_connect(&mut self) { - match self.wifi_list.try_lock() { + match self.wifi_list.write() { Ok(wifi_list) => { // if the selected network is already in use, do nothing if wifi_list[self.selected].in_use { @@ -120,7 +130,7 @@ impl App { self.wifi_credentials.flags.show_status_popup = true; // refresh the network list after connection attempt - scan_networks(self.wifi_list.clone()); + scan_networks(self.wifi_list.clone(), self.flags.is_scanning.clone()); } // if the selected network is hidden network option // the show status popup will be handled by the password input listener @@ -142,7 +152,7 @@ impl App { self.wifi_credentials.status = status; self.wifi_credentials.flags.show_status_popup = true; // refresh the network list after connection attempt - scan_networks(self.wifi_list.clone()); + scan_networks(self.wifi_list.clone(), self.flags.is_scanning.clone()); } // else show the password popup else { @@ -158,7 +168,7 @@ impl App { } fn update_selected_network(&mut self, direction: isize) { - if let Ok(wifi_list) = self.wifi_list.try_lock() { + if let Ok(wifi_list) = self.wifi_list.read() { let len = wifi_list.len(); if len > 0 { self.selected = @@ -171,7 +181,7 @@ impl App { fn disconnect(&mut self) { self.wifi_credentials.status = disconnect_connected_network(self.wifi_list.clone()); self.wifi_credentials.flags.show_status_popup = true; - scan_networks(self.wifi_list.clone()); + scan_networks(self.wifi_list.clone(), self.flags.is_scanning.clone()); } fn reset_selection(&mut self) { diff --git a/src/apps/core/delete_handler.rs b/src/apps/core/delete_handler.rs index dc85d22..4b207c5 100644 --- a/src/apps/core/delete_handler.rs +++ b/src/apps/core/delete_handler.rs @@ -120,9 +120,13 @@ impl App { self.saved_connection.fetch_saved_connections(); } else { // this one will delete the connection from the wifi list - delete_connection(self.wifi_list.lock().unwrap()[self.selected].ssid.clone()); + delete_connection( + self.wifi_list.read().expect("WifiNetworks lock poisoned")[self.selected] + .ssid + .clone(), + ); self.reset_selection(); - scan_networks(self.wifi_list.clone()); + scan_networks(self.wifi_list.clone(), self.flags.is_scanning.clone()); } self.flags.show_delete_confirmation = false; } diff --git a/src/apps/core/event_handlers.rs b/src/apps/core/event_handlers.rs index 99f2450..7a9f8cd 100644 --- a/src/apps/core/event_handlers.rs +++ b/src/apps/core/event_handlers.rs @@ -65,7 +65,7 @@ impl App { kind: Press, .. }) => { - scan_networks(self.wifi_list.clone()); + scan_networks(self.wifi_list.clone(), self.flags.is_scanning.clone()); } Event::Key(KeyEvent { code: KeyCode::Enter, @@ -115,7 +115,14 @@ impl App { kind: Press, .. }) => { - self.flags.show_delete_confirmation = true; + if self + .wifi_list + .read() + .expect("Wifi list lock poisoned while deleting")[self.selected] + .is_saved + { + self.flags.show_delete_confirmation = true; + } } Event::Key(KeyEvent { code: KeyCode::Char('s'), diff --git a/src/apps/core/widget.rs b/src/apps/core/widget.rs index 02a7465..483db88 100644 --- a/src/apps/core/widget.rs +++ b/src/apps/core/widget.rs @@ -8,6 +8,8 @@ use ratatui::{ text::Line, widgets::{Block, Paragraph, Row, Table, TableState, Widget}, }; +use std::sync::atomic::Ordering; +use std::time::{self, SystemTime}; const INFO_TEXT: [&str; 2] = [ "[Esc] quit | (Ctrl+R) scan for networks | (h) help ", @@ -63,31 +65,56 @@ impl Widget for &App { ); let mut rows = Vec::new(); - match self.wifi_list.try_lock() { - Ok(wifi_list) => { - for (i, network) in wifi_list.iter().enumerate() { - let ssid = if network.in_use { - format!("* {}", network.ssid) - } else { - network.ssid.clone() - }; - let mut row = Row::new(vec![ - ssid, - network.security.clone(), - network.is_saved.to_string(), - ]); - if i == self.selected { - row = row.style( - ratatui::style::Style::default() - .fg(ratatui::style::Color::Black) - .bg(ratatui::style::Color::White), - ); - } - rows.push(row); + + if self.flags.is_scanning.load(Ordering::SeqCst) { + let spinner = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; + + // Use some time-based value for the index + // this will select a different spinner character every 100ms + // and push it to teh rows vector + // as this function is called every frame during the scanning process + // this will create the illusion of a spinner animation + let index = SystemTime::now() + .duration_since(time::UNIX_EPOCH) + .expect("Time went backwards") + .as_millis() + / 100; + + let spinner_char = spinner[index as usize % spinner.len()]; + + rows.push(Row::new(vec![ + format!("{} Scanning...", spinner_char), + "".into(), + "".into(), + ])); + } else { + // This will not panic untill the thread holding the write lock panics so we can just unwrap here + let wifi_list = self.wifi_list.read().expect("WifiNetworks lock poisoned"); + + // populate the rows of the table that displays the wifi networks and highlights the selected one + // This will never be empty because we always add the "Connect to Hidden network" entry + // untill I implement a better way to handle the addition of hidden networks + + // TODO: implement the case if there are not networks found with a message to the user + for (i, network) in wifi_list.iter().enumerate() { + let ssid = if network.in_use { + format!("* {}", network.ssid) + } else { + network.ssid.clone() + }; + let mut row = Row::new(vec![ + ssid, + network.security.clone(), + network.is_saved.to_string(), + ]); + if i == self.selected { + row = row.style( + ratatui::style::Style::default() + .fg(ratatui::style::Color::Black) + .bg(ratatui::style::Color::White), + ); } - } - Err(_) => { - rows.push(Row::new(vec!["Scanning...", ""])); + rows.push(row); } } diff --git a/src/apps/handlers/flags.rs b/src/apps/handlers/flags.rs index 08ae65d..08eaa53 100644 --- a/src/apps/handlers/flags.rs +++ b/src/apps/handlers/flags.rs @@ -1,10 +1,14 @@ +use std::sync::Arc; +use std::sync::atomic::AtomicBool; + #[derive(Debug, Default)] pub struct Flags { pub is_hidden: bool, + pub is_scanning: Arc, + pub show_delete_confirmation: bool, + pub show_help: bool, pub show_password_popup: bool, + pub show_saved: bool, pub show_ssid_popup: bool, pub show_status_popup: bool, - pub show_delete_confirmation: bool, - pub show_saved: bool, - pub show_help: bool, } diff --git a/src/main.rs b/src/main.rs index 7bebf3d..8e792f0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,6 +18,6 @@ struct AppState { fn main() -> Result<()> { color_eyre::install()?; - tui().unwrap(); + tui().expect("Failed to run TUI application"); Ok(()) } diff --git a/src/utils/disconnect_connection.rs b/src/utils/disconnect_connection.rs index 586d479..d32126a 100644 --- a/src/utils/disconnect_connection.rs +++ b/src/utils/disconnect_connection.rs @@ -1,11 +1,11 @@ use crate::{WifiNetwork, apps::handlers::status::Status}; use std::{ process::{Command, ExitStatus}, - sync::{Arc, Mutex}, + sync::{Arc, RwLock}, }; -pub fn disconnect_connected_network(wifi_list: Arc>>) -> Status { - let list = wifi_list.lock().expect("WifiNetworks lock poisoned"); +pub fn disconnect_connected_network(wifi_list: Arc>>) -> Status { + let list = wifi_list.read().expect("WifiNetworks lock poisoned"); for network in list.iter() { if network.in_use { diff --git a/src/utils/scan.rs b/src/utils/scan.rs index 0c8961b..d053471 100644 --- a/src/utils/scan.rs +++ b/src/utils/scan.rs @@ -1,13 +1,14 @@ use crate::WifiNetwork; use crate::utils::saved_connection::saved_connections; use std::process::Command; -use std::sync::{Arc, Mutex}; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, RwLock}; use std::thread; -pub fn scan_networks(wifi_list: Arc>>) { +pub fn scan_networks(wifi_list: Arc>>, is_scanning: Arc) { + is_scanning.store(true, Ordering::SeqCst); // // nmcli -t -f IN-USE,SSID,SECURITY device wifi list thread::spawn(move || { - let mut wifi_list_lock = wifi_list.lock().unwrap(); let output = Command::new("nmcli") .args(["-t", "-f", "IN-USE,SSID,SECURITY", "device", "wifi", "list"]) .output() @@ -55,6 +56,9 @@ pub fn scan_networks(wifi_list: Arc>>) { ssid: "Connect to Hidden network".to_string(), security: "?".to_string(), }); + + let mut wifi_list_lock = wifi_list.write().expect("WifiNetworks lock poisoned"); *wifi_list_lock = networks; + is_scanning.store(false, Ordering::SeqCst); }); } diff --git a/src/utils/tui.rs b/src/utils/tui.rs index f459247..f589a0d 100644 --- a/src/utils/tui.rs +++ b/src/utils/tui.rs @@ -2,6 +2,6 @@ use crate::apps::core::App; pub fn tui() -> Result<(), Box> { let mut terminal = ratatui::init(); let app_result = App::default().run(&mut terminal); - ratatui::try_restore().unwrap(); + ratatui::try_restore().expect("Failed to restore terminal"); app_result }