diff --git a/Cargo.lock b/Cargo.lock index a68b2ce..959f09f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -633,6 +633,16 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "fs2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "futures-channel" version = "0.3.32" @@ -2459,6 +2469,28 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows-link" version = "0.2.1" @@ -2567,9 +2599,10 @@ dependencies = [ [[package]] name = "wiretray" -version = "0.1.0" +version = "0.2.0" dependencies = [ "anyhow", + "fs2", "gio 0.20.12", "gtk4", "image", diff --git a/Cargo.toml b/Cargo.toml index e44e55e..894a924 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,10 @@ [package] name = "wiretray" -version = "0.1.0" +version = "0.2.0" edition = "2024" +description = "A Linux Wi-Fi hotspot manager with a system tray interface" +license = "MIT" +readme = "README.md" [[bin]] name = "wiretray-settings" @@ -23,3 +26,39 @@ uuid = { version = "1", features = ["v4"] } image = "0.25" gtk = { version = "0.11.3", package = "gtk4", features = ["v4_12"] } +fs2 = "0.4" + +[package.metadata.deb] +maintainer = "Augusto Domingos " +copyright = "2026, Augusto Domingos " +name = "wiretray" +section = "net" +priority = "optional" +depends = "$auto, network-manager, libglib2.0-bin" +maintainer-scripts = "packaging/debian" +extended-description = """ +Wiretray is a Linux Wi-Fi hotspot manager focused on quick and convenient hotspot control. + +It allows hotspots to be managed directly from the system tray, making hotspot management feel like any other background service on Linux. +""" + +assets = [ + ["target/release/wiretray", "usr/bin/", "755"], + ["target/release/wiretray-settings", "usr/bin/", "755"], + + ["data/applications/io.github.AghastyGD.Wiretray.desktop", + "usr/share/applications/", + "644"], + + ["assets/icons/app/io.github.AghastyGD.Wiretray.png", + "usr/share/icons/hicolor/256x256/apps/io.github.AghastyGD.Wiretray.png", + "644"], + + ["data/io.github.AghastyGD.Wiretray.gschema.xml", + "usr/share/glib-2.0/schemas/", + "644"], + + ["README.md", + "usr/share/doc/wiretray/README.md", + "644"], +] \ No newline at end of file diff --git a/README.md b/README.md index e541e2d..04053b4 100644 --- a/README.md +++ b/README.md @@ -4,15 +4,16 @@ # Wiretray -**A modern Wi-Fi hotspot manager for Linux.** +**A Wi-Fi hotspot manager for Linux.** ![CI](https://github.com/aghastygd/wiretray/actions/workflows/ci.yml/badge.svg) ![License](https://img.shields.io/github/license/aghastygd/wiretray) ![Issues](https://img.shields.io/github/issues/aghastygd/wiretray) -Wiretray is a Linux hotspot manager with system tray integration and a desktop configuration interface. -It integrates with NetworkManager to create and manage Wi-Fi hotspots. +Wiretray is a Linux Wi-Fi hotspot manager designed to make hotspot control available directly from the system tray. + +Built around NetworkManager, it provides hotspot management, configuration, and automatic startup through a desktop interface. ## Motivation @@ -22,18 +23,16 @@ I used it for a while and had a good experience with it, but one thing always bo I wanted a solution that could stay in the system tray and make hotspot management available with a couple of clicks. -wiretray began as an experiment, but quickly evolved into a tool I use daily for managing hotspots on Linux. +Wiretray began as an experiment, but quickly evolved into a tool I use daily for managing hotspots on Linux. The long-term goal is simple: make hotspot management feel like any other background service on Linux. -## Current Status - -The project is still under active development. +## Features -Implemented so far: +Available today: - [x] System tray integration - + - [x] Desktop configuration interface - [x] Wi-Fi device discovery @@ -41,33 +40,82 @@ Implemented so far: - [x] Hotspot creation and management - [x] Hotspot status monitoring - + - [x] Hotspot settings persistence - + +- [x] Autostart on login + - [x] Wi-Fi capability detection - Currently in progress: - -- Better error reporting - + +- Better error reporting +- Concurrent AP + Client support ## Requirements -- Linux - -- NetworkManager - -- A Wi-Fi adapter with Access Point (AP) support - -## Dependencies +- Linux +- NetworkManager +- A Wi-Fi adapter with Access Point (AP) support +- A desktop environment or panel with StatusNotifierItem/AppIndicator support + +## Installation + +### Debian / Ubuntu + +Download the latest `.deb` package from the [Releases](https://github.com/AghastyGD/wiretray/releases) page and install it with: + +``` +sudo apt install ./wiretray_*.deb +``` + +You can also install it by double-clicking the `.deb` package from your file manager. + +After installation, Wiretray can be opened from the application menu. + +The application launcher opens the settings window and starts the tray process automatically if it is not already running. + +## Autostart + +Wiretray can start automatically after login. + +Open the settings window and enable: + +``` +Start automatically on login +``` + +This creates an XDG autostart entry at: + +``` +~/.config/autostart/io.github.AghastyGD.Wiretray.desktop +``` + +When enabled, Wiretray starts the tray/background process automatically the next time you log in. + +## Installed Binaries + +The Debian package installs two binaries: + +``` +wiretray +wiretray-settings +``` + +`wiretray` starts the tray/background application. + +`wiretray-settings` opens the desktop configuration interface. + +Normally, users do not need to run these manually. The application launcher and autostart entry handle this automatically. + +## Building from Source Install the required development packages for your distribution. ### Debian / Ubuntu -```bash +``` sudo apt install \ libgtk-4-dev \ meson \ @@ -78,20 +126,19 @@ sudo apt install \ ### Fedora -```bash +``` sudo dnf install \ gtk4-devel \ meson \ desktop-file-utils \ gcc \ - glib-compile-resources \ - gtk4-update-icon-cache \ - update-desktop-database + glib2-devel \ + gtk4-update-icon-cache ``` ### Arch Linux -```bash +``` sudo pacman -S \ gtk4 \ meson \ @@ -99,32 +146,30 @@ sudo pacman -S \ gcc ``` -## Building - Build all binaries: -```bash +``` cargo build ``` Or build a specific binary: -```bash +``` cargo build --bin wiretray cargo build --bin wiretray-settings ``` -## Running +## Running from Source Start the tray application: -```bash +``` cargo run --bin wiretray ``` Launch the settings application directly: -```bash +``` cargo run --bin wiretray-settings ``` @@ -132,19 +177,19 @@ cargo run --bin wiretray-settings Format code: -```bash +``` cargo fmt ``` Run Clippy: -```bash +``` cargo clippy --all-targets --all-features -- -D warnings ``` Run tests: -```bash +``` cargo test ``` @@ -152,18 +197,11 @@ cargo test Planned work includes: -- QR code generation - -- Connected client monitoring - -- Hotspot notifications - -- Advanced hotspot configuration - -- Concurrent AP + Client support - -- Alternative hotspot backends - +- QR code generation +- Connected client monitoring +- Hotspot notifications +- Advanced hotspot configuration +- Alternative hotspot backends As with most side projects, priorities may change over time. @@ -173,4 +211,4 @@ Issues, suggestions, and pull requests are welcome. ## License -This project is licensed under the MIT License. +This project is licensed under the MIT License. \ No newline at end of file diff --git a/data/applications/io.github.AghastyGD.Wiretray.desktop b/data/applications/io.github.AghastyGD.Wiretray.desktop new file mode 100644 index 0000000..08d06d6 --- /dev/null +++ b/data/applications/io.github.AghastyGD.Wiretray.desktop @@ -0,0 +1,9 @@ +[Desktop Entry] +Type=Application +Name=Wiretray +Comment=Wi-Fi hotspot manager for Linux +Exec=wiretray-settings +Icon=io.github.AghastyGD.Wiretray +Terminal=false +Categories=Network;GTK; +StartupNotify=true diff --git a/packaging/debian/postinst b/packaging/debian/postinst new file mode 100755 index 0000000..59187c2 --- /dev/null +++ b/packaging/debian/postinst @@ -0,0 +1,16 @@ +#!/bin/sh +set -e + +if command -v glib-compile-schemas >/dev/null 2>&1; then + glib-compile-schemas /usr/share/glib-2.0/schemas || true +fi + +if command -v update-desktop-database >/dev/null 2>&1; then + update-desktop-database /usr/share/applications || true +fi + +if command -v gtk-update-icon-cache >/dev/null 2>&1; then + gtk-update-icon-cache -q -t -f /usr/share/icons/hicolor || true +fi + +exit 0 diff --git a/packaging/debian/postrm b/packaging/debian/postrm new file mode 100755 index 0000000..9e7570f --- /dev/null +++ b/packaging/debian/postrm @@ -0,0 +1,16 @@ +#!/bin/sh +set -e + +if command -v glib-compile-schemas >/dev/null 2>&1; then + glib-compile-schemas /usr/share/glib-2.0/schemas || true +fi + +if command -v update-desktop-database >/dev/null 2>&1; then + update-desktop-database /usr/share/applications || true +fif + +if command -v gtk-update-icon-cache >/dev/null 2>&1; then + gtk-update-icon-cache -q -t -f /usr/share/icons/hicolor || true +fi + +exit 0 diff --git a/scripts/run-settings-dev.sh b/scripts/run-settings-dev.sh new file mode 100755 index 0000000..9e7c5cc --- /dev/null +++ b/scripts/run-settings-dev.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -e + +glib-compile-schemas data + +cargo build --bin wiretray + +GSETTINGS_SCHEMA_DIR="$PWD/data" cargo run --bin wiretray-settings \ No newline at end of file diff --git a/src/application/mod.rs b/src/application/mod.rs index 0ab782a..91f4802 100644 --- a/src/application/mod.rs +++ b/src/application/mod.rs @@ -1 +1,2 @@ pub mod hotspot; +pub mod single_instance; diff --git a/src/application/single_instance.rs b/src/application/single_instance.rs new file mode 100644 index 0000000..a0d001f --- /dev/null +++ b/src/application/single_instance.rs @@ -0,0 +1,38 @@ +use std::fs::{File, OpenOptions}; +use std::path::PathBuf; + +use anyhow::{Context, Result}; +use fs2::FileExt; + +pub fn acquire_lock() -> Result { + let lock_path = lock_file_path().context("Failed to resolve Wiretray lock path")?; + + if let Some(parent) = lock_path.parent() { + std::fs::create_dir_all(parent) + .with_context(|| format!("Failed to create lock directory at {}", parent.display()))?; + } + + let file = OpenOptions::new() + .create(true) + .read(true) + .write(true) + .truncate(false) + .open(&lock_path) + .with_context(|| format!("Failed to open lock file at {}", lock_path.display()))?; + + tracing::info!("Using Wiretray lock file at {}", lock_path.display()); + + file.try_lock_exclusive() + .context("Wiretray is already running")?; + + Ok(file) +} + +fn lock_file_path() -> Result { + if let Some(runtime_dir) = std::env::var_os("XDG_RUNTIME_DIR") { + return Ok(PathBuf::from(runtime_dir).join("wiretray/wiretray.lock")); + } + + let temp_dir = std::env::temp_dir(); + Ok(temp_dir.join("wiretray/wiretray.lock")) +} diff --git a/src/bin/wiretray_settings.rs b/src/bin/wiretray_settings.rs index f6eb57b..63b195c 100644 --- a/src/bin/wiretray_settings.rs +++ b/src/bin/wiretray_settings.rs @@ -3,6 +3,7 @@ use gtk::Application; use gtk::prelude::*; use tokio::runtime::Runtime; +use wiretray::settings::tray_service::TrayService; use wiretray::ui::settings_window; const APP_ID: &str = "io.github.AghastyGD.Wiretray"; @@ -10,6 +11,10 @@ const APP_ID: &str = "io.github.AghastyGD.Wiretray"; fn main() -> Result<()> { setup_logging(); + if let Err(err) = TrayService::start() { + tracing::warn!("Failed to start Wiretray tray: {err:#}") + } + let runtime = Runtime::new()?; let app = Application::builder().application_id(APP_ID).build(); diff --git a/src/main.rs b/src/main.rs index 9449be1..70ca35b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,19 @@ use anyhow::Result; -use wiretray::tray; +use wiretray::{application::single_instance, tray}; #[tokio::main] async fn main() -> Result<()> { setup_logging(); + let _instance_lock = match single_instance::acquire_lock() { + Ok(lock) => lock, + Err(err) => { + tracing::info!("Wiretray is already running: {err:#}"); + return Ok(()); + } + }; + tracing::info!("Starting Wiretray..."); tray::run().await diff --git a/src/settings/autostart_service.rs b/src/settings/autostart_service.rs new file mode 100644 index 0000000..5fde380 --- /dev/null +++ b/src/settings/autostart_service.rs @@ -0,0 +1,87 @@ +use std::fs; +use std::path::PathBuf; + +use anyhow::{Context, Result, bail}; + +const AUTOSTART_FILE_NAME: &str = "io.github.AghastyGD.Wiretray.desktop"; + +pub struct AutostartService; + +impl AutostartService { + pub fn is_enabled() -> bool { + autostart_file_path().is_ok_and(|path| path.is_file()) + } + + pub fn enable() -> Result<()> { + let autostart_dir = autostart_dir().context("Failed to resolve autostart directory")?; + + fs::create_dir_all(&autostart_dir).with_context(|| { + format!( + "Failed to create autostart directory at {}", + autostart_dir.display() + ) + })?; + + let desktop_file = + autostart_file_path().context("Failed to resolve autostart desktop file path")?; + + fs::write(&desktop_file, desktop_entry()).with_context(|| { + format!( + "Failed to write autostart desktop file at {}", + desktop_file.display() + ) + })?; + + Ok(()) + } + + pub fn disable() -> Result<()> { + let desktop_file = + autostart_file_path().context("Failed to resolve autostart desktop file path")?; + + if desktop_file.is_file() { + fs::remove_file(&desktop_file).with_context(|| { + format!( + "Failed to remove autostart desktop file at {}", + desktop_file.display() + ) + })?; + } else if desktop_file.exists() { + bail!( + "Autostart path exists but is not a file: {}", + desktop_file.display() + ); + } + + Ok(()) + } +} + +fn autostart_file_path() -> Result { + Ok(autostart_dir()?.join(AUTOSTART_FILE_NAME)) +} + +fn autostart_dir() -> Result { + if let Some(config_home) = std::env::var_os("XDG_CONFIG_HOME") { + return Ok(PathBuf::from(config_home).join("autostart")); + } + + let home = std::env::var_os("HOME").context("HOME environment variable is not set")?; + + Ok(PathBuf::from(home).join(".config/autostart")) +} + +fn desktop_entry() -> &'static str { + r#"[Desktop Entry] +Type=Application +Name=Wiretray +Comment=Wi-Fi hotspot manager for Linux +Exec=/usr/bin/wiretray +Icon=io.github.AghastyGD.Wiretray +Terminal=false +StartupNotify=false +Hidden=false +X-GNOME-Autostart-enabled=true +X-GNOME-Autostart-Delay=5 +"# +} diff --git a/src/settings/mod.rs b/src/settings/mod.rs index b50a3fe..2eeca73 100644 --- a/src/settings/mod.rs +++ b/src/settings/mod.rs @@ -1,2 +1,4 @@ +pub mod autostart_service; pub mod hotspot_settings; pub mod service; +pub mod tray_service; diff --git a/src/settings/tray_service.rs b/src/settings/tray_service.rs new file mode 100644 index 0000000..ca544d3 --- /dev/null +++ b/src/settings/tray_service.rs @@ -0,0 +1,34 @@ +use std::path::PathBuf; +use std::process::{Command, Stdio}; + +use anyhow::{Context, Result}; + +pub struct TrayService; + +impl TrayService { + pub fn start() -> Result<()> { + let binary = resolve_wiretray_binary(); + tracing::info!("Starting Wiretray tray from {}", binary.display()); + + Command::new(&binary) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn() + .with_context(|| format!("Failed to start Wiretray from {}", binary.display()))?; + + Ok(()) + } +} + +fn resolve_wiretray_binary() -> PathBuf { + if let Ok(current_exe) = std::env::current_exe() { + let sibling = current_exe.with_file_name("wiretray"); + + if sibling.exists() { + return sibling; + } + } + + PathBuf::from("wiretray") +} diff --git a/src/ui/settings_window.rs b/src/ui/settings_window.rs index 9945d4f..c8cf6cb 100644 --- a/src/ui/settings_window.rs +++ b/src/ui/settings_window.rs @@ -1,15 +1,18 @@ use gtk::Align; use gtk::prelude::*; use gtk::{ - Application, ApplicationWindow, Box as GtkBox, Button, Entry, Grid, Label, Orientation, - PasswordEntry, Separator, Stack, StackSwitcher, + Application, ApplicationWindow, Box as GtkBox, Button, CheckButton, Entry, Grid, Label, + Orientation, PasswordEntry, Separator, Stack, StackSwitcher, }; use tokio::runtime::Handle; use crate::application::hotspot as hotspot_app; use crate::services::hotspot_service::HotspotService; -use crate::settings::{hotspot_settings::HotspotSettings, service::SettingsService}; +use crate::settings::{ + autostart_service::AutostartService, hotspot_settings::HotspotSettings, + service::SettingsService, +}; const WINDOW_NAME: &str = "wiretray-settings"; @@ -143,6 +146,27 @@ pub fn present(app: &Application, tokio_handle: Handle) { .sensitive(active) .build(); + let autostart_check = CheckButton::builder() + .label("Start automatically on login") + .active(AutostartService::is_enabled()) + .margin_start(24) + .margin_end(24) + .build(); + + autostart_check.connect_toggled(|check| { + let result = if check.is_active() { + AutostartService::enable() + } else { + AutostartService::disable() + }; + + if let Err(err) = result { + tracing::error!("Failed to update autostart setting: {err:#}"); + + check.set_active(!check.is_active()); + } + }); + let save_btn = Button::builder() .label("Save") .css_classes(["suggested-action"]) @@ -271,6 +295,7 @@ pub fn present(app: &Application, tokio_handle: Handle) { hotspot_page.append(&status_label); hotspot_page.append(&grid); + hotspot_page.append(&autostart_check); hotspot_page.append(&separator); hotspot_page.append(&hint); hotspot_page.append(&error_label);