diff --git a/Cargo.toml b/Cargo.toml
index 03923ab..18c2829 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -6,4 +6,5 @@ members = [
"plugin-run",
"plugin-desktop",
"plugin-audio",
+ "plugin-network",
]
diff --git a/Makefile b/Makefile
index 410e468..bb7601a 100644
--- a/Makefile
+++ b/Makefile
@@ -21,6 +21,7 @@ deploy:
cp -vf ./target/release/desktop ${DEST}/drun
cp -vf ./target/release/run ${DEST}/run
cp -vf ./target/release/audio ${DEST}/audio
+ cp -vf ./target/release/rmenu-network ${DEST}/network
cp -vf ./rmenu/public/config.yaml ${DEST}/config.yaml
build: build-rmenu build-plugins
@@ -32,3 +33,4 @@ build-plugins:
${CARGO} build -p run ${FLAGS}
${CARGO} build -p desktop ${FLAGS}
${CARGO} build -p audio ${FLAGS}
+ ${CARGO} build -p rmenu-network ${FLAGS}
diff --git a/plugin-network/Cargo.toml b/plugin-network/Cargo.toml
new file mode 100644
index 0000000..9c0fc04
--- /dev/null
+++ b/plugin-network/Cargo.toml
@@ -0,0 +1,23 @@
+[package]
+name = "rmenu-network"
+version = "0.0.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+anyhow = "1.0.72"
+async-std = "1.12.0"
+clap = { version = "4.3.21", features = ["derive"] }
+dioxus = "0.4.0"
+dioxus-desktop = "0.4.0"
+dioxus-free-icons = { version = "0.7.0", features = ["font-awesome-regular"] }
+env_logger = "0.10.0"
+futures-channel = "0.3.28"
+glib = { git = "https://github.com/gtk-rs/gtk-rs-core", version = "0.19.0" }
+keyboard-types = "0.6.2"
+log = "0.4.20"
+nm = { git = "https://github.com/balena-io-modules/libnm-rs.git", version = "0.4.0" }
+once_cell = "1.18.0"
+rmenu-plugin = { version = "0.0.0", path = "../rmenu-plugin" }
+serde_json = "1.0.104"
diff --git a/plugin-network/public/default.css b/plugin-network/public/default.css
new file mode 100644
index 0000000..abbf203
--- /dev/null
+++ b/plugin-network/public/default.css
@@ -0,0 +1,96 @@
+
+html, body {
+ overflow: hidden;
+ font-size: 14px;
+ background-color: #e8edee;
+}
+
+#header {
+ padding: 0.15rem;
+}
+
+#controls {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ margin-top: 0.5rem;
+}
+
+#secret {
+ width: -webkit-fill-available;
+ height: 2rem;
+
+ border: none;
+ outline: none;
+ padding: 0 0.5rem;
+ background-color: #ccd2df;
+}
+
+#icon {
+ float: right;
+ position: relative;
+ z-index: 2;
+ margin-left: -2.5rem;
+
+ display: flex;
+ align-items: center;
+ height: 2rem;
+ border: none;
+ background: none;
+}
+
+@keyframes movein {
+ from { margin-top: 10rem; }
+ to { margin-top: 1rem; }
+}
+
+@keyframes moveout {
+ from { margin-top: 1rem; }
+ to { bottom: 10rem; }
+}
+
+#message {
+ display: flex;
+ justify-content: center;
+ margin-top: 1rem;
+
+ font-size: 15px;
+ font-style: italic;
+ animation: movein 0.5s ease-in-out;
+}
+
+#message.error {
+ color: white;
+ background-color: #de5a5a;
+}
+
+#message.success {
+ color: white;
+ background-color: #53c351;
+}
+
+#blackout {
+ display: none;
+ width: 100%;
+ height: 100%;
+ position: absolute;
+ top: 0;
+ left: 0;
+ z-index: 100;
+
+ background-color: black;
+ filter: alpha(opacity=30);
+ opacity: 0.3;
+}
+
+#blackout > #spinner {
+ width: 100%;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+#blackout.active {
+ display: block;
+}
diff --git a/plugin-network/public/spinner.css b/plugin-network/public/spinner.css
new file mode 100644
index 0000000..c19270e
--- /dev/null
+++ b/plugin-network/public/spinner.css
@@ -0,0 +1,78 @@
+.lds-spinner {
+ color: official;
+ display: inline-block;
+ position: relative;
+ width: 80px;
+ height: 80px;
+}
+.lds-spinner div {
+ transform-origin: 40px 40px;
+ animation: lds-spinner 1.2s linear infinite;
+}
+.lds-spinner div:after {
+ content: " ";
+ display: block;
+ position: absolute;
+ top: 3px;
+ left: 37px;
+ width: 6px;
+ height: 18px;
+ border-radius: 20%;
+ background: #fff;
+}
+.lds-spinner div:nth-child(1) {
+ transform: rotate(0deg);
+ animation-delay: -1.1s;
+}
+.lds-spinner div:nth-child(2) {
+ transform: rotate(30deg);
+ animation-delay: -1s;
+}
+.lds-spinner div:nth-child(3) {
+ transform: rotate(60deg);
+ animation-delay: -0.9s;
+}
+.lds-spinner div:nth-child(4) {
+ transform: rotate(90deg);
+ animation-delay: -0.8s;
+}
+.lds-spinner div:nth-child(5) {
+ transform: rotate(120deg);
+ animation-delay: -0.7s;
+}
+.lds-spinner div:nth-child(6) {
+ transform: rotate(150deg);
+ animation-delay: -0.6s;
+}
+.lds-spinner div:nth-child(7) {
+ transform: rotate(180deg);
+ animation-delay: -0.5s;
+}
+.lds-spinner div:nth-child(8) {
+ transform: rotate(210deg);
+ animation-delay: -0.4s;
+}
+.lds-spinner div:nth-child(9) {
+ transform: rotate(240deg);
+ animation-delay: -0.3s;
+}
+.lds-spinner div:nth-child(10) {
+ transform: rotate(270deg);
+ animation-delay: -0.2s;
+}
+.lds-spinner div:nth-child(11) {
+ transform: rotate(300deg);
+ animation-delay: -0.1s;
+}
+.lds-spinner div:nth-child(12) {
+ transform: rotate(330deg);
+ animation-delay: 0s;
+}
+@keyframes lds-spinner {
+ 0% {
+ opacity: 1;
+ }
+ 100% {
+ opacity: 0;
+ }
+}
diff --git a/plugin-network/public/spinner.html b/plugin-network/public/spinner.html
new file mode 100644
index 0000000..92980e7
--- /dev/null
+++ b/plugin-network/public/spinner.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/plugin-network/src/gui.rs b/plugin-network/src/gui.rs
new file mode 100644
index 0000000..307a7c0
--- /dev/null
+++ b/plugin-network/src/gui.rs
@@ -0,0 +1,211 @@
+#![allow(non_snake_case)]
+
+///! NetworkManager Authenticator GUI
+use anyhow::{anyhow, Result};
+use dioxus::prelude::*;
+use dioxus_desktop::LogicalSize;
+use dioxus_free_icons::icons::fa_regular_icons::{FaEye, FaEyeSlash};
+use dioxus_free_icons::Icon;
+use keyboard_types::Code;
+
+use crate::network::Manager;
+
+static SPINNER_CSS: &'static str = include_str!("../public/spinner.css");
+static SPINNER_HTML: &'static str = include_str!("../public/spinner.html");
+static DEFAULT_CSS_CONTENT: &'static str = include_str!("../public/default.css");
+
+/// Run GUI Application
+pub fn run_app(ssid: &str, timeout: u32) {
+ let builder = dioxus_desktop::WindowBuilder::new()
+ .with_title("RMenu - Network Login")
+ .with_inner_size(LogicalSize {
+ width: 400,
+ height: 150,
+ })
+ .with_focused(true)
+ .with_decorations(false)
+ .with_always_on_top(true);
+ dioxus_desktop::launch_with_props(
+ App,
+ AppProp {
+ ssid: ssid.to_owned(),
+ timeout,
+ },
+ dioxus_desktop::Config::new().with_window(builder),
+ );
+}
+
+/// Message to send to GUI
+#[derive(Debug, Clone, PartialEq)]
+pub enum UserMessage {
+ Nothing,
+ Error(String),
+ Success(String),
+}
+
+impl UserMessage {
+ /// Retrieve CSS-Class to use on UserMessage
+ fn css_class(&self) -> &str {
+ match self {
+ Self::Error(_) => "error",
+ Self::Success(_) => "success",
+ Self::Nothing => "none",
+ }
+ }
+ /// Retrieve Message Value
+ fn message(&self) -> Option {
+ match self {
+ Self::Nothing => None,
+ Self::Error(msg) => Some(msg.to_owned()),
+ Self::Success(msg) => Some(msg.to_owned()),
+ }
+ }
+}
+
+#[derive(Debug, PartialEq, Props)]
+struct AppProp {
+ ssid: String,
+ timeout: u32,
+}
+
+//TODO: add auth timeout
+
+/// Set Cursor/Keyboard Focus onto Input
+#[inline]
+fn focus(cx: Scope) {
+ let eval = use_eval(cx);
+ let js = "document.getElementById(`secret`).focus()";
+ let _ = eval(&js);
+}
+
+/// Complete Network Connection/Authentication Attempt
+async fn connect_async(ssid: String, timeout: u32, secret: &str) -> Result<()> {
+ // retrieve access-point from manager
+ let manager = Manager::new().await?.with_timeout(timeout);
+ let access_point = manager
+ .access_points()
+ .into_iter()
+ .find(|a| a.ssid == ssid)
+ .ok_or_else(|| anyhow!("Unable to find Access Point: {ssid:?}"))?;
+ // attempt to establish connection w/ secret
+ log::debug!("Found AccessPoint: {ssid:?} | {:?}", access_point.security);
+ manager.connect(&access_point, Some(secret)).await
+}
+
+/// Simple Login GUI Application
+fn App(cx: Scope) -> Element {
+ let secret = use_state(cx, || String::new());
+ let message = use_state(cx, || UserMessage::Nothing);
+ let show_secret = use_state(cx, || false);
+ let loading = use_state(cx, || false);
+
+ // always ensure focus
+ focus(cx);
+
+ // build keyboard actions event handler
+ let keyboard_controls = move |e: KeyboardEvent| match e.code() {
+ Code::Escape => std::process::exit(0),
+ Code::Enter => {
+ let ssid = cx.props.ssid.to_owned();
+ let timeout = cx.props.timeout.to_owned();
+ let secret = secret.get().to_owned();
+ let message = message.to_owned();
+ let loading = loading.to_owned();
+ loading.set(true);
+ message.set(UserMessage::Nothing);
+
+ cx.spawn(async move {
+ log::info!("connecting to ssid: {ssid:?}");
+ let result = connect_async(ssid, timeout, &secret).await;
+ log::info!("connection result: {result:?}");
+ match result {
+ Ok(_) => {
+ // update message and unlock gui
+ message.set(UserMessage::Success("Connection Complete!".to_owned()));
+ loading.set(false);
+ // exit program after timeout
+ let wait = std::time::Duration::from_secs(1);
+ async_std::task::sleep(wait).await;
+ std::process::exit(0);
+ }
+ Err(err) => {
+ message.set(UserMessage::Error(format!("Connection Failed: {err:?}")));
+ loading.set(false);
+ }
+ }
+ });
+ }
+ _ => {}
+ };
+
+ // retrieve message details / set input-type / get loading css-class
+ let msg_css = message.css_class();
+ let msg_str = message.message();
+ let input_type = match show_secret.get() {
+ true => "text",
+ false => "password",
+ };
+ let blackout_css = match loading.get() {
+ true => "active",
+ false => "",
+ };
+
+ // complete final rendering
+ cx.render(rsx! {
+ style { DEFAULT_CSS_CONTENT }
+ style { SPINNER_CSS }
+ div {
+ onkeydown: keyboard_controls,
+ label {
+ id: "header",
+ "for": "secret",
+ "Wi-Fi Network {cx.props.ssid:?} Requires a Password"
+ }
+ div {
+ id: "controls",
+ input {
+ id: "secret",
+ value: "{secret}",
+ placeholder: "Password",
+ oninput: move |e| secret.set(e.value.clone()),
+ "type": "{input_type}"
+ }
+ button {
+ id: "icon",
+ onclick: |_| show_secret.modify(|v| !v),
+ match show_secret.get() {
+ true => cx.render(rsx! {
+ Icon {
+ fill: "black",
+ icon: FaEye,
+ }
+ }),
+ false => cx.render(rsx! {
+ Icon {
+ fill: "black",
+ icon: FaEyeSlash,
+ }
+ })
+ }
+ }
+ }
+ if let Some(msg) = msg_str {
+ cx.render(rsx! {
+ div {
+ id: "message",
+ class: "{msg_css}",
+ "{msg}"
+ }
+ })
+ }
+ div {
+ id: "blackout",
+ class: "{blackout_css}",
+ div {
+ id: "spinner",
+ dangerous_inner_html: "{SPINNER_HTML}"
+ }
+ }
+ }
+ })
+}
diff --git a/plugin-network/src/main.rs b/plugin-network/src/main.rs
new file mode 100644
index 0000000..8ec59ae
--- /dev/null
+++ b/plugin-network/src/main.rs
@@ -0,0 +1,105 @@
+use anyhow::{anyhow, Result};
+
+use clap::{Parser, Subcommand};
+use rmenu_plugin::Entry;
+
+mod gui;
+mod network;
+
+#[derive(Debug, Subcommand)]
+pub enum Commands {
+ ListAccessPoints,
+ Connect { ssid: String, timeout: Option },
+}
+
+#[derive(Parser)]
+#[command(author, version, about, long_about = None)]
+#[command(propagate_version = true)]
+pub struct Cli {
+ #[clap(subcommand)]
+ command: Option,
+}
+
+/// Get Signal Bars based on a signal-value
+fn get_bars(signal: u8) -> String {
+ if signal >= 80 {
+ return "▂▄▆█".to_owned();
+ }
+ if signal > 50 {
+ return "▂▄▆_".to_owned();
+ }
+ if signal > 30 {
+ return "▂▄__".to_owned();
+ }
+ "▂___".to_owned()
+}
+
+/// List AccessPoints using NetworkManager
+async fn list_aps() -> Result<()> {
+ let exe = std::env::current_exe()?.to_str().unwrap().to_string();
+ // spawn manager and complete scan (if nessesary)
+ let manager = network::Manager::new().await?;
+ if !manager.scanned_recently() {
+ log::info!("Scanning for Access-Points...");
+ manager.scan_wifi().await?;
+ }
+ // retrive access-points and print as entries
+ for ap in manager.access_points() {
+ let star = ap.is_active.then(|| " *").unwrap_or("");
+ let bars = get_bars(ap.signal);
+ let desc = format!("{bars} {}{star}", ap.ssid);
+ let exec = format!("{exe} connect {:?}", ap.ssid);
+ let entry = Entry::new(&desc, &exec, None);
+ println!("{}", serde_json::to_string(&entry).unwrap());
+ }
+ Ok(())
+}
+
+/// Attempt to Connect to the Specified SSID (If Connection Already Exists)
+async fn try_connect_ap(ssid: String, timeout: Option) -> Result {
+ // spawn manager and complete scan (if nessesary)
+ let mut manager = network::Manager::new().await?;
+ if let Some(timeout) = timeout {
+ manager = manager.with_timeout(timeout)
+ }
+ if !manager.scanned_recently() {
+ log::info!("Scanning for Access-Points...");
+ manager.scan_wifi().await?;
+ }
+ // attempt to find access-point
+ let access_point = manager
+ .access_points()
+ .into_iter()
+ .find(|ap| ap.ssid == ssid)
+ .ok_or_else(|| anyhow!("Unable to find Access-Point: {ssid:?}"))?;
+ // if connection already exists, try to connect
+ if access_point.connection.is_some() {
+ log::info!("Attempting Connection to {ssid:?} w/o Password");
+ match manager.connect(&access_point, None).await {
+ Err(err) => log::info!("Connection Failed: {err:?}"),
+ Ok(_) => {
+ log::info!("Connection Successful!");
+ return Ok(true);
+ }
+ };
+ }
+ Ok(false)
+}
+
+fn main() -> Result<()> {
+ env_logger::init();
+ let cli = Cli::parse();
+ let context = glib::MainContext::default();
+ let command = cli.command.unwrap_or(Commands::ListAccessPoints);
+ match command {
+ Commands::ListAccessPoints => context.block_on(list_aps())?,
+ Commands::Connect { ssid, timeout } => {
+ let connected = context.block_on(try_connect_ap(ssid.clone(), timeout))?;
+ if !connected {
+ log::info!("Spawning GUI to complete AP Login");
+ gui::run_app(&ssid, timeout.unwrap_or(30));
+ }
+ }
+ }
+ Ok(())
+}
diff --git a/plugin-network/src/network.rs b/plugin-network/src/network.rs
new file mode 100644
index 0000000..2b127c4
--- /dev/null
+++ b/plugin-network/src/network.rs
@@ -0,0 +1,309 @@
+use glib::translate::FromGlib;
+
+use async_std::task;
+use futures_channel::oneshot;
+use std::cell::RefCell;
+use std::collections::BTreeMap;
+use std::rc::Rc;
+use std::time::{Duration, SystemTime};
+
+use anyhow::{anyhow, Context, Result};
+use nm::*;
+
+static SCAN_INTERVAL_MS: u64 = 500;
+static SCAN_TOTAL_WAIT: u64 = 3;
+static SCAN_BETWEEN: i64 = 30;
+
+/// NetworkManager Simplified API
+#[derive(Debug)]
+pub struct Manager {
+ client: Client,
+ wifi: DeviceWifi,
+ timeout: Duration,
+}
+
+/// AccessPoint Information
+#[derive(Debug)]
+pub struct AccessPoint {
+ pub in_use: bool,
+ pub ssid: String,
+ pub rate: u32,
+ pub signal: u8,
+ pub security: String,
+ pub is_active: bool,
+ pub connection: Option,
+}
+
+// SETTING_WIRELESS_MODE
+// SETTING_IP4_CONFIG_METHOD_AUTO
+
+/// Generate a NEW Connection to Use AccessPoint
+fn new_conn(ap: &AccessPoint, password: Option<&str>) -> Result {
+ let connection = SimpleConnection::new();
+
+ // configure generate connection settings
+ let s_connection = SettingConnection::new();
+ s_connection.set_type(Some(&SETTING_WIRELESS_SETTING_NAME));
+ s_connection.set_id(Some(&ap.ssid));
+ s_connection.set_autoconnect(false);
+ // s_connection.set_interface_name(interface);
+ connection.add_setting(s_connection);
+
+ // configure wireless settings
+ let s_wireless = SettingWireless::new();
+ s_wireless.set_ssid(Some(&(ap.ssid.as_bytes().into())));
+ // s_wireless.set_band(Some("bg"));
+ // s_wireless.set_hidden(false);
+ // s_wireless.set_mode(Some(&SETTING_WIRELESS_MODE));
+ connection.add_setting(s_wireless);
+
+ // configure login settings
+ if let Some(password) = password {
+ //TODO: potentially determine key-mgmt based on ap-security
+ let s_wireless_security = SettingWirelessSecurity::new();
+ s_wireless_security.set_key_mgmt(Some("wpa-psk"));
+ s_wireless_security.set_psk(Some(password));
+ connection.add_setting(s_wireless_security);
+ }
+
+ // assume DHCP Assignment
+ let s_ip4 = SettingIP4Config::new();
+ s_ip4.set_method(Some(&SETTING_IP4_CONFIG_METHOD_AUTO));
+ connection.add_setting(s_ip4);
+
+ Ok(connection)
+}
+
+/// Exit Security Section to Specify New Password
+fn edit_conn(conn: &Connection, password: Option<&str>) -> Result<()> {
+ let s_wireless_security = conn
+ .setting_wireless_security()
+ .unwrap_or_else(|| SettingWirelessSecurity::new());
+ s_wireless_security.set_key_mgmt(Some("wpa-psk"));
+ s_wireless_security.set_psk(password);
+ conn.add_setting(s_wireless_security);
+ Ok(())
+}
+
+/// Wait for Connection to Fail or Complete
+async fn wait_conn(active: &ActiveConnection, timeout: Duration) -> Result<()> {
+ // spawn communication channels
+ let (sender, receiver) = oneshot::channel::>();
+ let sender = Rc::new(RefCell::new(Some(sender)));
+ // spawn state-callback
+ active.connect_state_changed(move |active_connection, state, _| {
+ let sender = sender.clone();
+ let active_connection = active_connection.clone();
+ glib::MainContext::ref_thread_default().spawn_local(async move {
+ let state = unsafe { ActiveConnectionState::from_glib(state as _) };
+ log::debug!("[Connect] Active connection state: {:?}", state);
+ // generate send function
+ let send = move |result| {
+ let sender = sender.borrow_mut().take();
+ if let Some(sender) = sender {
+ sender.send(result).expect("Sender Dropped");
+ }
+ };
+ // handle connection state-changes
+ match state {
+ ActiveConnectionState::Activated => {
+ log::debug!("[Connect] Successfully activated");
+ return send(Ok(()));
+ }
+ ActiveConnectionState::Deactivated => {
+ log::debug!("[Connect] Connection deactivated");
+ match active_connection.connection() {
+ Some(remote_connection) => {
+ let result = remote_connection
+ .delete_future()
+ .await
+ .context("Failed to delete connection");
+ if result.is_err() {
+ return send(result);
+ }
+ return send(Err(anyhow!("Connection Failed (Deactivated)")));
+ }
+ None => {
+ return send(Err(anyhow!(
+ "Failed to get remote connection from active connection"
+ )))
+ }
+ }
+ }
+ _ => {}
+ };
+ });
+ });
+ // wait until state notification is done
+ let result = async_std::future::timeout(timeout, receiver).await;
+ match result {
+ Ok(result) => match result {
+ Ok(res) => res,
+ Err(err) => Err(anyhow!("Connection Cancelled: {err:?}")),
+ },
+ Err(err) => Err(anyhow!("Timeout Reached: {err:?}")),
+ }
+}
+
+impl Manager {
+ /// Spawn new Wifi Manager Instance
+ pub async fn new() -> Result {
+ // get network-manager client
+ let client = Client::new_future()
+ .await
+ .context("Failed to create NM Client")?;
+ // get wifi device if any are available
+ let device = client
+ .devices()
+ .into_iter()
+ .filter(|d| d.device_type() == DeviceType::Wifi)
+ .next()
+ .ok_or_else(|| anyhow!("Cannot find a Wi-Fi device"))?;
+ // access inner wifi-device object
+ let wifi: DeviceWifi = device
+ .downcast()
+ .map_err(|_| anyhow!("Failed to Access Wi-Fi Device"))?;
+ log::debug!("NetworkManager Connection Established");
+ Ok(Self {
+ client,
+ wifi,
+ timeout: Duration::from_secs(30),
+ })
+ }
+
+ /// Update Manager Timeout and Return Self
+ pub fn with_timeout(mut self, secs: u32) -> Self {
+ self.timeout = Duration::from_secs(secs.into());
+ self
+ }
+
+ /// Check if NetworkManager already scanned recently
+ pub fn scanned_recently(&self) -> bool {
+ let last_ms = self.wifi.last_scan();
+ let now_ms = utils_get_timestamp_msec();
+ let elapsed = (now_ms - last_ms) / 1000;
+ last_ms > 0 && elapsed < SCAN_BETWEEN
+ }
+
+ /// Complete General Wifi-Scan
+ pub async fn scan_wifi(&self) -> Result<()> {
+ // request wifi-scan
+ self.wifi
+ .request_scan_future()
+ .await
+ .context("Failed to Request Wi-Fi Scan")?;
+ // wait until access-points are collected
+ let mut then = SystemTime::now();
+ let mut current = self.wifi.access_points().len();
+ loop {
+ // wait interval for more access-points
+ task::sleep(Duration::from_millis(SCAN_INTERVAL_MS)).await;
+ // check if time has elapsed
+ let now = SystemTime::now();
+ let elapsed = now.duration_since(then)?;
+ if elapsed.as_secs() > SCAN_TOTAL_WAIT {
+ break;
+ }
+ // check if more access-points were discovered
+ let found = self.wifi.access_points().len();
+ if found > current {
+ then = now;
+ current = found;
+ }
+ }
+ Ok(())
+ }
+
+ /// Retrieve Access-Point Information
+ pub fn access_points(&self) -> Vec {
+ let conns: Vec = self
+ .client
+ .connections()
+ .into_iter()
+ .map(|c| c.upcast())
+ .collect();
+ let mut access: BTreeMap = BTreeMap::new();
+ let active = self.wifi.active_access_point();
+ for a in self.wifi.access_points() {
+ // retrieve access-point information
+ let rate = a.max_bitrate() / 1000;
+ let signal = a.strength();
+ let ssid = a
+ .ssid()
+ .map(|b| b.escape_ascii().to_string())
+ .unwrap_or_else(|| "--".to_owned());
+ // determine if connection-map should be updated
+ let is_active = active
+ .as_ref()
+ .map(|b| a.bssid() == b.bssid())
+ .unwrap_or(false);
+ if !is_active {
+ if let Some(point) = access.get(&ssid) {
+ if point.rate > rate {
+ continue;
+ }
+ }
+ }
+ // build security-string
+ let mut security = vec![];
+ let wpa_flags = a.wpa_flags();
+ let wpa2_flags = a.rsn_flags();
+ if !wpa_flags.is_empty() {
+ security.push("WPA1");
+ }
+ if !wpa2_flags.is_empty() {
+ security.push("WPA2");
+ }
+ if wpa2_flags.intersects(_80211ApSecurityFlags::KEY_MGMT_802_1X) {
+ security.push("802.1X");
+ }
+ if security.is_empty() {
+ security.push("--");
+ }
+ // insert access-point
+ access.insert(
+ ssid.to_owned(),
+ AccessPoint {
+ in_use: is_active,
+ ssid,
+ rate,
+ signal,
+ is_active,
+ security: security.join(" ").to_owned(),
+ connection: a.filter_connections(&conns).get(0).cloned(),
+ },
+ );
+ }
+ // move map values into vector and sort by signal-strength
+ let mut points: Vec = access.into_values().collect();
+ points.sort_by_key(|a| a.signal);
+ points.reverse();
+ points
+ }
+
+ /// Attempt to Authenticate and Activate Access-Point Connection
+ pub async fn connect(&self, ap: &AccessPoint, password: Option<&str>) -> Result<()> {
+ let device = self.wifi.clone().upcast::();
+ match &ap.connection {
+ Some(conn) => {
+ edit_conn(conn, password)?;
+ let active_conn = self
+ .client
+ .activate_connection_future(Some(conn), Some(&device), None)
+ .await
+ .context("Failed to activate existing connection")?;
+ wait_conn(&active_conn, self.timeout).await?;
+ }
+ None => {
+ let conn = new_conn(ap, password)?;
+ let active_conn = self
+ .client
+ .add_and_activate_connection_future(Some(&conn), Some(&device), None)
+ .await
+ .context("Failed to add and activate connection")?;
+ wait_conn(&active_conn, self.timeout).await?;
+ }
+ }
+ Ok(())
+ }
+}
diff --git a/rmenu/public/99-rmenu-sway.conf b/rmenu/public/99-rmenu-sway.conf
index 3163368..af400ef 100644
--- a/rmenu/public/99-rmenu-sway.conf
+++ b/rmenu/public/99-rmenu-sway.conf
@@ -1,3 +1,4 @@
# Configure RMenu to Spawn Floating in the Center of the Screen
for_window [app_id="rmenu"] floating enable
+for_window [app_id="rmenu-network"] floating enable
diff --git a/rmenu/public/config.yaml b/rmenu/public/config.yaml
index 3fadeb6..b0cc52e 100644
--- a/rmenu/public/config.yaml
+++ b/rmenu/public/config.yaml
@@ -29,6 +29,10 @@ plugins:
exec: ["~/.config/rmenu/audio"]
cache: false
placeholder: "Select an Audio Sink"
+ network:
+ exec: ["~/.config/rmenu/network"]
+ cache: false
+ placeholder: "Connect to the Specified Wi-Fi"
# custom keybindings
keybinds: