diff --git a/Cargo.lock b/Cargo.lock index 3d72c69..de2b59d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -69,7 +69,7 @@ checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] name = "angry_oxide" -version = "0.7.8" +version = "0.8.0" dependencies = [ "anyhow", "byteorder", diff --git a/Cargo.toml b/Cargo.toml index 8595afb..e9b4309 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ members = ["libs/libwifi", "libs/libwifi_macros", "libs/pcap-file"] [workspace.package] -version = "0.7.8" +version = "0.8.0" authors = ["Ryan Butler"] description = "80211 Attack Tool" license = "MIT" diff --git a/libs/libwifi/src/frame/components/mac_address.rs b/libs/libwifi/src/frame/components/mac_address.rs index 703a6bb..3bfe701 100644 --- a/libs/libwifi/src/frame/components/mac_address.rs +++ b/libs/libwifi/src/frame/components/mac_address.rs @@ -13,7 +13,7 @@ use rand::{thread_rng, Rng, RngCore}; /// // -> true /// ``` /// -#[derive(Clone, Debug, Eq, PartialEq, Copy)] +#[derive(Clone, Debug, Eq, PartialEq, Copy, Ord, PartialOrd)] pub struct MacAddress(pub [u8; 6]); impl Hash for MacAddress { diff --git a/src/auth.rs b/src/auth.rs index 69ab1b3..a0b0673 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -637,12 +637,14 @@ impl HandshakeSessionKey { #[derive(Debug, Clone)] pub struct HandshakeStorage { handshakes: HashMap>, + handshakes_sorted: Vec, } impl HandshakeStorage { pub fn new() -> Self { HandshakeStorage { handshakes: HashMap::new(), + handshakes_sorted: Vec::new(), } } @@ -666,6 +668,27 @@ impl HandshakeStorage { }) } + pub fn sort_handshakes(&mut self) { + // Make our handshakes list + let mut print_handshakes: Vec = Vec::new(); + + let binding = self.get_handshakes(); + for handshake_list in binding.values() { + for handshake in handshake_list { + print_handshakes.push(handshake.clone()); + } + } + + print_handshakes.sort_by(|a, b| { + b.last_msg + .clone() + .unwrap() + .timestamp + .cmp(&a.last_msg.clone().unwrap().timestamp) + }); + self.handshakes_sorted = print_handshakes; + } + pub fn add_or_update_handshake( &mut self, ap_mac: &MacAddress, @@ -738,22 +761,8 @@ impl HandshakeStorage { ]; // Make our handshakes list - let mut print_handshakes: Vec<&FourWayHandshake> = Vec::new(); - - let binding = self.get_handshakes(); - for handshake_list in binding.values() { - for handshake in handshake_list { - print_handshakes.push(handshake); - } - } - - print_handshakes.sort_by(|a, b| { - b.last_msg - .clone() - .unwrap() - .timestamp - .cmp(&a.last_msg.clone().unwrap().timestamp) - }); + self.sort_handshakes(); + let print_handshakes = &self.handshakes_sorted; let mut rows: Vec<(Vec, u16)> = Vec::new(); @@ -807,6 +816,10 @@ impl HandshakeStorage { } (headers, rows) } + + pub fn get_sorted(&self) -> &Vec { + &self.handshakes_sorted + } } fn add_handshake_header_row(hs_row: Vec) -> Vec { diff --git a/src/devices.rs b/src/devices.rs index 10c17fb..9193923 100644 --- a/src/devices.rs +++ b/src/devices.rs @@ -472,6 +472,7 @@ impl Station { #[derive(Clone, Debug, Default)] pub struct WiFiDeviceList { devices: HashMap, + devices_sorted: Vec, } // Common functions for any type of device @@ -479,6 +480,7 @@ impl WiFiDeviceList { pub fn new() -> Self { WiFiDeviceList { devices: HashMap::new(), + devices_sorted: Vec::new(), } } @@ -561,6 +563,75 @@ impl WiFiDeviceList { all_clients } + pub fn sort_devices(&mut self, sort: u8, sort_reverse: bool) { + let mut access_points: Vec = self + .get_devices() + .iter() + .map(|(_, access_point)| access_point.clone()) + .collect(); + + match sort { + 0 => access_points.sort_by(|a, b| { + // TGT + match ( + a.is_target(), + a.is_whitelisted(), + b.is_target(), + b.is_whitelisted(), + ) { + // Highest priority: is_target() = true, is_whitelist() = false + (true, false, _, _) => std::cmp::Ordering::Less, + (_, _, true, false) => std::cmp::Ordering::Greater, + + // Middle priority: is_target() = false, is_whitelist() = false + (false, false, false, true) => std::cmp::Ordering::Less, + (false, true, false, false) => std::cmp::Ordering::Greater, + + // Lowest priority: is_target() = false, is_whitelist() = true + // This case is covered implicitly by the previous matches + + // Fallback for equal cases + _ => std::cmp::Ordering::Equal, + } + }), + 1 => access_points.sort_by(|a, b| b.channel.cmp(&a.channel)), // CH + 2 => access_points.sort_by(|a, b| { + // RSSI + let a_val = a.last_signal_strength.value; + let b_val = b.last_signal_strength.value; + + match (a_val, b_val) { + // If both values are the same (and it doesn't matter if they are zero or not) + _ if a_val == b_val => std::cmp::Ordering::Equal, + + // Prioritize any non-zero value over zero + (0, _) => std::cmp::Ordering::Greater, // A is worse if it's zero + (_, 0) => std::cmp::Ordering::Less, // B is worse if it's zero + + // Otherwise, just do a normal comparison + _ => b_val.cmp(&a_val), + } + }), + 3 => access_points.sort_by(|a, b| b.last_recv.cmp(&a.last_recv)), // Last + 4 => access_points.sort_by(|a, b| b.client_list.size().cmp(&a.client_list.size())), // Clients + 5 => access_points.sort_by(|a, b| b.interactions.cmp(&a.interactions)), // Tx + 6 => access_points.sort_by(|a, b| b.has_hs.cmp(&a.has_hs)), // HS + 7 => access_points.sort_by(|a, b| b.has_pmkid.cmp(&a.has_pmkid)), // PM + _ => { + access_points.sort_by(|a, b| b.last_recv.cmp(&a.last_recv)); + } + } + + if sort_reverse { + access_points.reverse(); + } + self.devices_sorted = access_points; + } + + pub fn get_devices_sorted(&self) -> &Vec { + &self.devices_sorted + } + pub fn clear_all_interactions(&mut self) { for dev in self.devices.values_mut() { dev.interactions = 0; @@ -621,8 +692,6 @@ impl WiFiDeviceList { selected_row: Option, sort: u8, sort_reverse: bool, - copys: bool, - copyl: bool, ) -> (Vec, Vec<(Vec, u16)>) { // Header fields let headers = vec![ @@ -638,66 +707,8 @@ impl WiFiDeviceList { "PMKID".to_string(), ]; - let mut access_points: Vec<_> = self - .get_devices() - .iter() - .map(|(_, access_point)| access_point) - .collect(); - match sort { - 0 => access_points.sort_by(|a, b| { - // TGT - match ( - a.is_target(), - a.is_whitelisted(), - b.is_target(), - b.is_whitelisted(), - ) { - // Highest priority: is_target() = true, is_whitelist() = false - (true, false, _, _) => std::cmp::Ordering::Less, - (_, _, true, false) => std::cmp::Ordering::Greater, - - // Middle priority: is_target() = false, is_whitelist() = false - (false, false, false, true) => std::cmp::Ordering::Less, - (false, true, false, false) => std::cmp::Ordering::Greater, - - // Lowest priority: is_target() = false, is_whitelist() = true - // This case is covered implicitly by the previous matches - - // Fallback for equal cases - _ => std::cmp::Ordering::Equal, - } - }), - 1 => access_points.sort_by(|a, b| b.channel.cmp(&a.channel)), // CH - 2 => access_points.sort_by(|a, b| { - // RSSI - let a_val = a.last_signal_strength.value; - let b_val = b.last_signal_strength.value; - - match (a_val, b_val) { - // If both values are the same (and it doesn't matter if they are zero or not) - _ if a_val == b_val => std::cmp::Ordering::Equal, - - // Prioritize any non-zero value over zero - (0, _) => std::cmp::Ordering::Greater, // A is worse if it's zero - (_, 0) => std::cmp::Ordering::Less, // B is worse if it's zero - - // Otherwise, just do a normal comparison - _ => b_val.cmp(&a_val), - } - }), - 3 => access_points.sort_by(|a, b| b.last_recv.cmp(&a.last_recv)), // Last - 4 => access_points.sort_by(|a, b| b.client_list.size().cmp(&a.client_list.size())), // Clients - 5 => access_points.sort_by(|a, b| b.interactions.cmp(&a.interactions)), // Tx - 6 => access_points.sort_by(|a, b| b.has_hs.cmp(&a.has_hs)), // HS - 7 => access_points.sort_by(|a, b| b.has_pmkid.cmp(&a.has_pmkid)), // PM - _ => { - access_points.sort_by(|a, b| b.last_recv.cmp(&a.last_recv)); - } - } - - if sort_reverse { - access_points.reverse(); - } + self.sort_devices(sort, sort_reverse); + let access_points = &self.devices_sorted; let mut rows: Vec<(Vec, u16)> = Vec::new(); for (idx, ap) in access_points.iter().enumerate() { @@ -746,12 +757,7 @@ impl WiFiDeviceList { ap_row = merged; height += 1; } - if copys { - terminal_clipboard::set_string(ap.mac_address.to_string()).unwrap(); - } - if copyl { - terminal_clipboard::set_string(ap.to_json_str()).unwrap(); - } + } rows.push((ap_row, height)); } @@ -851,31 +857,11 @@ fn add_probe_rows( // Functions specific to a WiFiDeviceList holding Stations impl WiFiDeviceList { - pub fn get_table( - &mut self, - selected_row: Option, - sort: u8, - sort_reverse: bool, - copys: bool, - copyl: bool, - ) -> (Vec, Vec<(Vec, u16)>) { - // Header fields - //"MAC Address", "RSSI", "Last", Tx, "Probes" - let headers = vec![ - "MAC Address".to_string(), - "RSSI".to_string(), - "Last".to_string(), - "Tx".to_string(), - "Rogue M2".to_string(), - "Probes".to_string(), - ]; - - // Make our stations object - + pub fn sort_devices(&mut self, sort: u8, sort_reverse: bool) { let mut stations: Vec<_> = self .get_devices() .iter() - .map(|(_, access_point)| access_point) + .map(|(_, station)| station.clone()) .collect(); match sort { @@ -914,6 +900,33 @@ impl WiFiDeviceList { if sort_reverse { stations.reverse(); } + self.devices_sorted = stations; + } + + pub fn get_devices_sorted(&self) -> &Vec { + &self.devices_sorted + } + + pub fn get_table( + &mut self, + selected_row: Option, + sort: u8, + sort_reverse: bool + ) -> (Vec, Vec<(Vec, u16)>) { + // Header fields + //"MAC Address", "RSSI", "Last", Tx, "Probes" + let headers = vec![ + "MAC Address".to_string(), + "RSSI".to_string(), + "Last".to_string(), + "Tx".to_string(), + "Rogue M2".to_string(), + "Probes".to_string(), + ]; + + // Make our stations object + self.sort_devices(sort, sort_reverse); + let stations = &self.devices_sorted; let mut rows: Vec<(Vec, u16)> = Vec::new(); for (idx, station) in stations.iter().enumerate() { @@ -954,12 +967,6 @@ impl WiFiDeviceList { height += 1; } } - if copys { - terminal_clipboard::set_string(station.mac_address.to_string()).unwrap(); - } - if copyl { - terminal_clipboard::set_string(station.to_json_str()).unwrap(); - } } rows.push((cl_row, height)); } diff --git a/src/eventhandler.rs b/src/eventhandler.rs index 631b65f..4dbef32 100644 --- a/src/eventhandler.rs +++ b/src/eventhandler.rs @@ -77,7 +77,10 @@ impl EventHandler { KeyCode::Char('Y') => tx.send(EventType::Key(event)), KeyCode::Char('c') => tx.send(EventType::Key(event)), KeyCode::Char('C') => tx.send(EventType::Key(event)), - _ => Ok({}), + KeyCode::Char('t') => tx.send(EventType::Key(event)), + KeyCode::Char('T') => tx.send(EventType::Key(event)), + KeyCode::Char('k') => tx.send(EventType::Key(event)), + _ => Ok(()), }; } } else if let Event::Mouse(mouse) = event { diff --git a/src/main.rs b/src/main.rs index ec39a41..9a78d42 100644 --- a/src/main.rs +++ b/src/main.rs @@ -272,7 +272,7 @@ impl OxideRuntime { let interface_name = cli_args.interface.clone(); let targets = cli_args.target.clone(); let wh_list = cli_args.whitelist.clone(); - let mut notransmit = cli_args.notransmit.clone(); + let mut notransmit = cli_args.notransmit; // Setup initial lists / logs let access_points = WiFiDeviceList::new(); @@ -659,19 +659,25 @@ impl OxideRuntime { show_quit: false, copy_short: false, copy_long: false, + add_target: false, + set_autoexit: false, + show_keybinds: false, ui_snowstorm: use_snowstorm, ap_sort: 0, ap_state: TableState::new(), ap_table_data: access_points.clone(), ap_sort_reverse: false, - cl_sort: 0, - cl_state: TableState::new(), - cl_table_data: unassoc_clients.clone(), - cl_sort_reverse: false, + ap_selected_item: None, + sta_sort: 0, + sta_state: TableState::new(), + sta_table_data: unassoc_clients.clone(), + sta_sort_reverse: false, + sta_selected_item: None, hs_sort: 0, hs_state: TableState::new(), hs_table_data: handshake_storage.clone(), hs_sort_reverse: false, + hs_selected_item: None, messages_sort: 0, messages_state: TableState::new(), messages_table_data: log.get_all_messages(), @@ -951,10 +957,7 @@ fn process_frame(oxide: &mut OxideRuntime, packet: &[u8]) -> Result<(), String> bssid, signal_strength, ssid.clone(), - Some(( - band.clone(), - station_info.ds_parameter_set.unwrap_or(channel_u8), // TRY to use the broadcasted channel number - )), + station_info.ds_parameter_set.map(|ch| (band.clone(), ch)), Some(APFlags { apie_essid: station_info.ssid.as_ref().map(|_| true), gs_ccmp: station_info.rsn_information.as_ref().map(|rsn| { @@ -1159,7 +1162,7 @@ fn process_frame(oxide: &mut OxideRuntime, packet: &[u8]) -> Result<(), String> *bssid, signal_strength, ssid, - Some((band.clone(), channel_u8)), + station_info.ds_parameter_set.map(|ch| (band.clone(), ch)), Some(APFlags { apie_essid: station_info.ssid.as_ref().map(|_| true), gs_ccmp: station_info.rsn_information.as_ref().map(|rsn| { @@ -1288,7 +1291,7 @@ fn process_frame(oxide: &mut OxideRuntime, packet: &[u8]) -> Result<(), String> ap_addr, signal, None, - Some((band, channel_u8)), + None, None, oxide.target_data.rogue_client, ), @@ -1431,7 +1434,7 @@ fn process_frame(oxide: &mut OxideRuntime, packet: &[u8]) -> Result<(), String> AntennaSignal::from_bytes(&[0u8]).map_err(|err| err.to_string())? }, None, - Some((band, channel_u8)), + None, None, clients, oxide.target_data.rogue_client, @@ -1496,7 +1499,7 @@ fn process_frame(oxide: &mut OxideRuntime, packet: &[u8]) -> Result<(), String> ap_mac, AntennaSignal::from_bytes(&[0u8]).map_err(|err| err.to_string())?, None, - Some((band, channel_u8)), + None, None, oxide.target_data.rogue_client, ); @@ -1536,7 +1539,7 @@ fn process_frame(oxide: &mut OxideRuntime, packet: &[u8]) -> Result<(), String> AntennaSignal::from_bytes(&[0u8]).map_err(|err| err.to_string())?, ), None, - Some((band, channel_u8)), + None, Some(APFlags { apie_essid: station_info.ssid.as_ref().map(|_| true), gs_ccmp: station_info @@ -1625,7 +1628,7 @@ fn process_frame(oxide: &mut OxideRuntime, packet: &[u8]) -> Result<(), String> new_ap, AntennaSignal::from_bytes(&[0u8]).map_err(|err| err.to_string())?, ssid.clone(), - Some((band, channel_u8)), + None, None, WiFiDeviceList::::new(), oxide.target_data.rogue_client, @@ -1670,7 +1673,7 @@ fn process_frame(oxide: &mut OxideRuntime, packet: &[u8]) -> Result<(), String> AntennaSignal::from_bytes(&[0u8]).map_err(|err| err.to_string())?, ), None, - Some((band, channel_u8)), + None, None, clients, oxide.target_data.rogue_client, @@ -1732,7 +1735,7 @@ fn process_frame(oxide: &mut OxideRuntime, packet: &[u8]) -> Result<(), String> AntennaSignal::from_bytes(&[0u8]).map_err(|err| err.to_string())? }, None, - Some((band, channel_u8)), + None, None, clients, oxide.target_data.rogue_client, @@ -1801,7 +1804,7 @@ fn process_frame(oxide: &mut OxideRuntime, packet: &[u8]) -> Result<(), String> AntennaSignal::from_bytes(&[0u8]).map_err(|err| err.to_string())? }, None, - Some((band, channel_u8)), + None, None, clients, oxide.target_data.rogue_client, @@ -1861,7 +1864,7 @@ fn process_frame(oxide: &mut OxideRuntime, packet: &[u8]) -> Result<(), String> AntennaSignal::from_bytes(&[0u8]).map_err(|err| err.to_string())? }, None, - Some((band, channel_u8)), + None, None, clients, oxide.target_data.rogue_client, @@ -2529,6 +2532,17 @@ fn main() -> Result<(), Box> { KeyCode::Char('C') => { oxide.ui_state.copy_long = true; } + KeyCode::Char('t') => { + oxide.ui_state.add_target = true; + } + KeyCode::Char('T') => { + oxide.ui_state.add_target = true; + oxide.ui_state.set_autoexit = true; + } + KeyCode::Char('k') => { + oxide.ui_state.show_keybinds = + !oxide.ui_state.show_keybinds; + } _ => {} } } @@ -2549,6 +2563,43 @@ fn main() -> Result<(), Box> { } } + if oxide.ui_state.add_target { + match oxide.ui_state.current_menu { + MenuType::AccessPoints => { + if let Some(ref ap) = oxide.ui_state.ap_selected_item { + if let Some(accesspoint) = oxide.access_points.get_device(&ap.mac_address) { + oxide + .target_data + .targets + .add(Target::MAC(targets::TargetMAC { + addr: ap.mac_address, + })); + accesspoint.is_target = true; + if let Some(ssid) = &ap.ssid { + oxide + .target_data + .targets + .add(Target::SSID(targets::TargetSSID { + ssid: ssid.to_string(), + })); + } + if oxide.config.notx { + oxide.config.notx = false; + } + if oxide.ui_state.set_autoexit { + oxide.config.autoexit = true; + } + } + } + } + MenuType::Clients => {} + MenuType::Handshakes => {} + MenuType::Messages => {} + } + oxide.ui_state.add_target = false; + oxide.ui_state.set_autoexit = false; + } + // Headless UI status messages if last_status_time.elapsed() >= status_interval { last_status_time = Instant::now(); diff --git a/src/ui.rs b/src/ui.rs index 531bba9..259bfca 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -5,7 +5,7 @@ use rand::Rng; use std::{io::Result, time::Instant}; use crate::{ - auth::HandshakeStorage, + auth::{FourWayHandshake, HandshakeStorage}, devices::{AccessPoint, Station, WiFiDeviceList}, matrix::MatrixSnowstorm, snowstorm::Snowstorm, @@ -81,23 +81,29 @@ pub struct UiState { pub show_quit: bool, pub copy_short: bool, pub copy_long: bool, + pub add_target: bool, + pub set_autoexit: bool, + pub show_keybinds: bool, // AP Menu Options pub ap_sort: u8, pub ap_state: TableState, pub ap_table_data: WiFiDeviceList, + pub ap_selected_item: Option, pub ap_sort_reverse: bool, // Client Menu Options - pub cl_sort: u8, - pub cl_state: TableState, - pub cl_table_data: WiFiDeviceList, - pub cl_sort_reverse: bool, + pub sta_sort: u8, + pub sta_state: TableState, + pub sta_table_data: WiFiDeviceList, + pub sta_selected_item: Option, + pub sta_sort_reverse: bool, // Handshake Menu Options pub hs_sort: u8, pub hs_state: TableState, pub hs_table_data: HandshakeStorage, + pub hs_selected_item: Option, pub hs_sort_reverse: bool, // Messages @@ -147,9 +153,9 @@ impl UiState { fn cl_sort_next(&mut self) { if !self.show_quit { - self.cl_sort += 1; - if self.cl_sort == 5 { - self.cl_sort = 0; + self.sta_sort += 1; + if self.sta_sort == 5 { + self.sta_sort = 0; } } } @@ -182,7 +188,7 @@ impl UiState { if !self.show_quit { let sort = match self.current_menu { MenuType::AccessPoints => &mut self.ap_sort_reverse, - MenuType::Clients => &mut self.cl_sort_reverse, + MenuType::Clients => &mut self.sta_sort_reverse, MenuType::Handshakes => &mut self.hs_sort_reverse, MenuType::Messages => &mut self.messages_sort_reverse, }; @@ -194,7 +200,7 @@ impl UiState { if !self.show_quit { let state = match self.current_menu { MenuType::AccessPoints => &mut self.ap_state, - MenuType::Clients => &mut self.cl_state, + MenuType::Clients => &mut self.sta_state, MenuType::Handshakes => &mut self.hs_state, MenuType::Messages => &mut self.messages_state, }; @@ -216,7 +222,7 @@ impl UiState { if !self.show_quit { let state = match self.current_menu { MenuType::AccessPoints => &mut self.ap_state, - MenuType::Clients => &mut self.cl_state, + MenuType::Clients => &mut self.sta_state, MenuType::Handshakes => &mut self.hs_state, MenuType::Messages => &mut self.messages_state, }; @@ -239,7 +245,7 @@ impl UiState { if !self.show_quit { let state: &mut TableState = match self.current_menu { MenuType::AccessPoints => &mut self.ap_state, - MenuType::Clients => &mut self.cl_state, + MenuType::Clients => &mut self.sta_state, MenuType::Handshakes => &mut self.hs_state, MenuType::Messages => &mut self.messages_state, }; @@ -255,7 +261,7 @@ impl UiState { if !self.show_quit { let state: &mut TableState = match self.current_menu { MenuType::AccessPoints => &mut self.ap_state, - MenuType::Clients => &mut self.cl_state, + MenuType::Clients => &mut self.sta_state, MenuType::Handshakes => &mut self.hs_state, MenuType::Messages => &mut self.messages_state, }; @@ -337,32 +343,31 @@ pub fn print_ui( Paragraph::new(Line::from(vec![ Span::raw("| quit: ").style(Style::new()), Span::styled("[q]", Style::default().reversed()), - Span::raw(" | sort table: ").style(Style::new()), - Span::styled("[e]", Style::default().reversed()), - Span::raw(" | reverse: ").style(Style::new()), - Span::styled("[r]", Style::default().reversed()), Span::raw(" | change tab: ").style(Style::new()), Span::styled("[a]/[d]", Style::default().reversed()), Span::raw(" | pause: ").style(Style::new()), Span::styled("[space]", Style::default().reversed()), Span::raw(" | scroll: ").style(Style::new()), Span::styled("[w/W]/[s/S]", Style::default().reversed()), - Span::raw(" |").style(Style::new()), - Span::raw(" | copy: ").style(Style::new()), - Span::styled("[c/C]", Style::default().reversed()), + Span::raw(" | show keybinds: ").style(Style::new()), + Span::styled("[k]", Style::default().reversed()), Span::raw(" |").style(Style::new()), ])) .alignment(Alignment::Center), full_layout[2], ); + + if oxide.ui_state.show_keybinds { + create_keybind_popup(frame, frame.size()); + } })?; Ok(()) } fn create_quit_popup(frame: &mut Frame<'_>, area: Rect) { let popup_area = Rect { - x: area.width / 2 - 25, - y: area.height / 2 - 3, + x: (area.width / 2) - 25, + y: (area.height / 2) - 3, width: 50, height: 4, }; @@ -374,6 +379,89 @@ fn create_quit_popup(frame: &mut Frame<'_>, area: Rect) { frame.render_widget(popup, popup_area); } +fn create_keybind_popup(frame: &mut Frame<'_>, area: Rect) { + let window_area = Rect { + x: (area.width / 2) - 25, + y: (area.height / 2) - 9, + width: 50, + height: 16, + }; + + frame.render_widget(Clear, window_area); + let block = Block::new().borders(Borders::ALL); + let hotkeys: Vec> = vec![ + Line::from(vec![Span::styled("", Style::default().bold())]), + Line::from(vec![Span::styled("KEY BINDS", Style::default().bold())]), + Line::from(vec![Span::styled("", Style::default().bold())]), + Line::from(vec![ + Span::raw("Sort Table").style(Style::new()), + Span::raw(repeat_dot(33)).style(Style::new()), + Span::styled("[e]", Style::default().reversed()), + ]), + Line::from(vec![ + Span::raw("Reverse Sorting").style(Style::new()), + Span::raw(repeat_dot(28)).style(Style::new()), + Span::styled("[r]", Style::default().reversed()), + ]), + Line::from(vec![ + Span::raw("Change Tabs").style(Style::new()), + Span::raw(repeat_dot(28)).style(Style::new()), + Span::styled("[a]/[d]", Style::default().reversed()), + ]), + Line::from(vec![ + Span::raw("Scroll Table").style(Style::new()), + Span::raw(repeat_dot(27)).style(Style::new()), + Span::styled("[w]/[s]", Style::default().reversed()), + ]), + Line::from(vec![ + Span::raw("Scroll Table (by 10)").style(Style::new()), + Span::raw(repeat_dot(19)).style(Style::new()), + Span::styled("[W]/[S]", Style::default().reversed()), + ]), + Line::from(vec![ + Span::raw("Pause UI").style(Style::new()), + Span::raw(repeat_dot(31)).style(Style::new()), + Span::styled("[space]", Style::default().reversed()), + ]), + Line::from(vec![ + Span::raw("Add Selected as Target").style(Style::new()), + Span::raw(repeat_dot(21)).style(Style::new()), + Span::styled("[t]", Style::default().reversed()), + ]), + Line::from(vec![ + Span::raw("Add Selected as Target (with autoexit)").style(Style::new()), + Span::raw(repeat_dot(5)).style(Style::new()), + Span::styled("[T]", Style::default().reversed()), + ]), + Line::from(vec![ + Span::raw("Copy Selected (SHORT)").style(Style::new()), + Span::raw(repeat_dot(22)).style(Style::new()), + Span::styled("[c]", Style::default().reversed()), + ]), + Line::from(vec![ + Span::raw("Copy Selected (LONG JSON)").style(Style::new()), + Span::raw(repeat_dot(18)).style(Style::new()), + Span::styled("[C]", Style::default().reversed()), + ]), + Line::from(vec![Span::styled("", Style::default().bold())]), + ]; + + let hotkey_para = Paragraph::new(hotkeys) + .wrap(Wrap { trim: true }) + .block(block) + .alignment(Alignment::Center); + + frame.render_widget(hotkey_para, window_area); +} + +fn repeat_char(c: char, count: usize) -> String { + std::iter::repeat(c).take(count).collect() +} + +fn repeat_dot(count: usize) -> String { + std::iter::repeat('.').take(count).collect() +} + fn create_status_bar( frame: &mut Frame<'_>, top_area: Rect, @@ -503,12 +591,32 @@ fn create_ap_page(oxide: &mut OxideRuntime, frame: &mut Frame<'_>, area: Rect) { oxide.ui_state.ap_state.selected(), oxide.ui_state.ap_sort, oxide.ui_state.ap_sort_reverse, - oxide.ui_state.copy_short, - oxide.ui_state.copy_long, ); - oxide.ui_state.copy_short = false; - oxide.ui_state.copy_long = false; + let selected_object = if let Some(sel_idx) = oxide.ui_state.ap_state.selected() { + oxide + .ui_state + .ap_table_data + .get_devices_sorted() + .get(sel_idx) + } else { + None + }; + + oxide.ui_state.ap_selected_item = selected_object.cloned(); + + if oxide.ui_state.copy_short { + if let Some(ap) = selected_object { + terminal_clipboard::set_string(ap.mac_address.to_string()).unwrap(); + } + oxide.ui_state.copy_short = false; + } + if oxide.ui_state.copy_long { + if let Some(ap) = selected_object { + terminal_clipboard::set_string(ap.to_json_str()).unwrap(); + } + oxide.ui_state.copy_long = false; + } // Fill Rows let mut rows_vec: Vec = vec![]; @@ -583,19 +691,39 @@ fn create_ap_page(oxide: &mut OxideRuntime, frame: &mut Frame<'_>, area: Rect) { fn create_sta_page(oxide: &mut OxideRuntime, frame: &mut Frame<'_>, area: Rect) { // Update the table data (from the real source) - This allows us to "pause the data" if !oxide.ui_state.paused { - oxide.ui_state.cl_table_data = oxide.unassoc_clients.clone(); + oxide.ui_state.sta_table_data = oxide.unassoc_clients.clone(); } - let (mut headers, rows) = oxide.ui_state.cl_table_data.get_table( - oxide.ui_state.cl_state.selected(), - oxide.ui_state.cl_sort, - oxide.ui_state.cl_sort_reverse, - oxide.ui_state.copy_short, - oxide.ui_state.copy_long, + let (mut headers, rows) = oxide.ui_state.sta_table_data.get_table( + oxide.ui_state.sta_state.selected(), + oxide.ui_state.sta_sort, + oxide.ui_state.sta_sort_reverse, ); - oxide.ui_state.copy_short = false; - oxide.ui_state.copy_long = false; + let selected_object = if let Some(sel_idx) = oxide.ui_state.sta_state.selected() { + oxide + .ui_state + .sta_table_data + .get_devices_sorted() + .get(sel_idx) + } else { + None + }; + + oxide.ui_state.sta_selected_item = selected_object.cloned(); + + if oxide.ui_state.copy_short { + if let Some(station) = selected_object { + terminal_clipboard::set_string(station.mac_address.to_string()).unwrap(); + } + oxide.ui_state.copy_short = false; + } + if oxide.ui_state.copy_long { + if let Some(station) = selected_object { + terminal_clipboard::set_string(station.to_json_str()).unwrap(); + } + oxide.ui_state.copy_long = false; + } // Fill Rows let mut rows_vec: Vec = vec![]; @@ -607,12 +735,12 @@ fn create_sta_page(oxide: &mut OxideRuntime, frame: &mut Frame<'_>, area: Rect) } // Set headers for sort - let sort_icon = if oxide.ui_state.cl_sort_reverse { + let sort_icon = if oxide.ui_state.sta_sort_reverse { "▲" } else { "▼" }; - match oxide.ui_state.cl_sort { + match oxide.ui_state.sta_sort { 0 => headers[2] = format!("{} {}", headers[2], sort_icon), 1 => headers[1] = format!("{} {}", headers[1], sort_icon), 2 => headers[3] = format!("{} {}", headers[3], sort_icon), @@ -647,9 +775,9 @@ fn create_sta_page(oxide: &mut OxideRuntime, frame: &mut Frame<'_>, area: Rect) .end_symbol(Some("↓")); let mut scrollbar_state = ScrollbarState::new(oxide.get_current_menu_len()) - .position(oxide.ui_state.cl_state.selected().unwrap_or(0)); + .position(oxide.ui_state.sta_state.selected().unwrap_or(0)); - frame.render_stateful_widget(table, area, &mut oxide.ui_state.cl_state); + frame.render_stateful_widget(table, area, &mut oxide.ui_state.sta_state); frame.render_stateful_widget( scrollbar, area.inner(&Margin { @@ -672,8 +800,26 @@ fn create_hs_page(oxide: &mut OxideRuntime, frame: &mut Frame<'_>, area: Rect) { oxide.ui_state.copy_long, ); - oxide.ui_state.copy_short = false; - oxide.ui_state.copy_long = false; + let selected_object = if let Some(sel_idx) = oxide.ui_state.hs_state.selected() { + oxide.ui_state.hs_table_data.get_sorted().get(sel_idx) + } else { + None + }; + + oxide.ui_state.hs_selected_item = selected_object.cloned(); + + if oxide.ui_state.copy_short { + if let Some(hs) = selected_object { + terminal_clipboard::set_string(hs.json_summary()).unwrap(); + } + oxide.ui_state.copy_short = false; + } + if oxide.ui_state.copy_long { + if let Some(hs) = selected_object { + terminal_clipboard::set_string(hs.json_detail()).unwrap(); + } + oxide.ui_state.copy_long = false; + } // Fill Rows let mut rows_vec: Vec = vec![];