1 Commits
Author SHA1 Message Date
Emil 50a2f49e7c Release 0.1.1: hide client in system tray 2026-09-10 19:16:13 +03:00
9 changed files with 137 additions and 21 deletions
+5 -2
View File
@@ -8,10 +8,10 @@
## Установка готового релиза
Скачайте `MiniVLESS_0.1.0_amd64.deb` из Releases и выполните из каталога загрузки:
Скачайте `MiniVLESS_0.1.1_amd64.deb` из Releases и выполните из каталога загрузки:
```bash
sudo apt install ./MiniVLESS_0.1.0_amd64.deb libcap2-bin
sudo apt install ./MiniVLESS_0.1.1_amd64.deb libcap2-bin
sudo setcap cap_net_admin,cap_net_raw+ep /usr/lib/MiniVLESS/binaries/sing-box
```
@@ -25,6 +25,9 @@ sudo setcap cap_net_admin,cap_net_raw+ep /usr/lib/MiniVLESS/binaries/sing-box
- **Tunneling выключен:** VPN для всей системы, независимо от списка программ.
3. `Refresh` обновляет запущенные приложения. `Add application` позволяет выбрать установленную программу или указать абсолютный путь к её настоящему ELF-бинарнику.
4. Нажмите `Connect`. `Disconnect` останавливает core и освобождает TUN.
5. Кнопка **−** в заголовке скрывает окно в системный трей, сохраняя подключение. В меню значка доступны **Show MiniVLESS**, **Disconnect** и **Quit**. Крестик и Quit полностью завершают приложение и останавливают VPN.
Если системный трей недоступен (например, GNOME без расширения AppIndicator), кнопка − сворачивает окно в панель задач, откуда его можно вернуть. Для значка трея требуется поддержка StatusNotifier/AppIndicator в рабочем окружении.
Пути сохраняются независимо от PID. Несколько процессов одного исполняемого файла объединяются. Отмеченные/добавленные приложения остаются в списке после перезапуска. Переключатель меняет **выборочную маршрутизацию**, а не отключает TUN: TUN используется в обоих режимах.
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "minivless",
"version": "0.1.0",
"version": "0.1.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "minivless",
"version": "0.1.0",
"version": "0.1.1",
"dependencies": {
"@tauri-apps/api": "^2.11.0",
"react": "^19.2.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "minivless",
"version": "0.1.0",
"version": "0.1.1",
"private": true,
"type": "module",
"scripts": {
+2 -1
View File
@@ -1804,9 +1804,10 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "minivless"
version = "0.1.0"
version = "0.1.1"
dependencies = [
"base64 0.22.1",
"dbus",
"libc",
"serde",
"serde_json",
+4 -3
View File
@@ -1,6 +1,6 @@
[package]
name = "minivless"
version = "0.1.0"
version = "0.1.1"
description = "A small personal VLESS client for Linux"
edition = "2021"
license = "MIT"
@@ -8,13 +8,14 @@ rust-version = "1.85"
[features]
default = ["desktop"]
desktop = ["dep:tauri", "dep:tauri-build"]
desktop = ["dep:tauri", "dep:tauri-build", "dep:dbus"]
[build-dependencies]
tauri-build = { version = "2", optional = true , features = [] }
[dependencies]
tauri = { version = "2", optional = true , features = [] }
tauri = { version = "2", optional = true , features = ["tray-icon"] }
dbus = { version = "0.9", optional = true }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
url = "2"
+81 -2
View File
@@ -10,7 +10,83 @@ use std::{path::PathBuf, sync::Arc};
mod desktop {
use super::*;
use std::sync::atomic::{AtomicBool, Ordering};
use tauri::{Manager, State};
use tauri::{
menu::{Menu, MenuItem},
tray::TrayIconBuilder,
Manager, State,
};
struct TrayReady(bool);
fn tray_host_available() -> bool {
use std::time::Duration;
let Ok(connection) = dbus::blocking::Connection::new_session() else {
return false;
};
let proxy = connection.with_proxy(
"org.freedesktop.DBus",
"/org/freedesktop/DBus",
Duration::from_millis(300),
);
let reply: Result<(bool,), _> = proxy.method_call(
"org.freedesktop.DBus",
"NameHasOwner",
("org.kde.StatusNotifierWatcher",),
);
matches!(reply, Ok((true,)))
}
#[tauri::command]
async fn hide_window(
app: tauri::AppHandle,
window: tauri::WebviewWindow,
) -> Result<(), String> {
let tray_ready = app.state::<TrayReady>().0;
let has_host = tray_ready
&& tauri::async_runtime::spawn_blocking(tray_host_available)
.await
.unwrap_or(false);
// A tray icon can be created even when GNOME has no indicator extension.
// Keep the window recoverable from the taskbar in that case.
if has_host {
window.hide()
} else {
window.minimize()
}
.map_err(|_| "Cannot hide the window".into())
}
fn create_tray(app: &tauri::App) -> tauri::Result<()> {
let show = MenuItem::with_id(app, "show", "Show MiniVLESS", true, None::<&str>)?;
let disconnect = MenuItem::with_id(app, "disconnect", "Disconnect", true, None::<&str>)?;
let quit = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?;
let menu = Menu::with_items(app, &[&show, &disconnect, &quit])?;
let mut tray = TrayIconBuilder::with_id("minivless-tray")
.menu(&menu)
.tooltip("MiniVLESS");
if let Some(icon) = app.default_window_icon() {
tray = tray.icon(icon.clone());
}
tray.on_menu_event(|app, event| match event.id.as_ref() {
"show" => {
if let Some(window) = app.get_webview_window("main") {
let _ = window.unminimize();
let _ = window.show();
let _ = window.set_focus();
}
}
"disconnect" => app.state::<Arc<AppState>>().disconnect(),
// Reuse the existing CloseRequested handler, which awaits core shutdown.
"quit" => {
if let Some(window) = app.get_webview_window("main") {
let _ = window.close();
}
}
_ => {}
})
.build(app)?;
Ok(())
}
#[tauri::command]
async fn list_applications() -> Result<Vec<ApplicationInfo>, String> {
tauri::async_runtime::spawn_blocking(processes::list_running)
@@ -79,7 +155,8 @@ mod desktop {
get_status,
get_logs,
load_preferences,
save_preferences
save_preferences,
hide_window
])
.setup(move |app| {
let resources = app.path().resource_dir().ok();
@@ -89,6 +166,8 @@ mod desktop {
std::env::current_exe()?,
);
app.manage(state.clone());
// Lack of tray support must never prevent the VPN client from starting.
app.manage(TrayReady(create_tray(app).is_ok()));
let signals = Arc::new(AtomicBool::new(false));
for sig in [libc::SIGINT, libc::SIGTERM, libc::SIGHUP] {
signal_hook::flag::register(sig, signals.clone())?;
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "MiniVLESS",
"version": "0.1.0",
"version": "0.1.1",
"identifier": "dev.minivless.desktop",
"build": {
"beforeDevCommand": "npm run dev",
+36 -9
View File
@@ -17,7 +17,11 @@ const initialStatus: Status = {
const message = (e: unknown) =>
typeof e === "string" ? e : "Something went wrong. Please try again.";
const basename = (p: string) => p.split("/").pop() || p;
function Icon({ kind }: { kind: "close" | "eye" | "refresh" | "plus" }) {
function Icon({
kind,
}: {
kind: "close" | "eye" | "refresh" | "plus" | "hide";
}) {
return (
<svg
width="18"
@@ -30,7 +34,9 @@ function Icon({ kind }: { kind: "close" | "eye" | "refresh" | "plus" }) {
strokeLinejoin="round"
aria-hidden="true"
>
{kind === "close" ? (
{kind === "hide" ? (
<path d="M5 12h14" />
) : kind === "close" ? (
<path d="m6 6 12 12M18 6 6 18" />
) : kind === "plus" ? (
<path d="M12 5v14M5 12h14" />
@@ -199,6 +205,16 @@ export default function App() {
);
}
};
const hide = async () => {
if (!desktop) return;
setShown(false);
try {
await saveQueue.current;
await invoke("hide_window");
} catch (e) {
setError(message(e));
}
};
const add = (app: ApplicationInfo) => {
setApps((old) => [
...old.filter((a) => a.executable !== app.executable),
@@ -233,13 +249,24 @@ export default function App() {
}}
>
<span>MiniVLESS</span>
<button
className="icon-button close"
aria-label="Close MiniVLESS"
onClick={() => void close()}
>
<Icon kind="close" />
</button>
<div className="window-actions">
<button
className="icon-button"
aria-label="Hide MiniVLESS"
title="Hide window — VPN stays connected"
onClick={() => void hide()}
disabled={!desktop}
>
<Icon kind="hide" />
</button>
<button
className="icon-button close"
aria-label="Close MiniVLESS"
onClick={() => void close()}
>
<Icon kind="close" />
</button>
</div>
</header>
<main>
{!desktop && (
+5
View File
@@ -58,6 +58,11 @@ button svg {
font-size: 15px;
font-weight: 500;
}
.window-actions {
display: flex;
align-items: center;
gap: 2px;
}
.icon-button {
display: inline-flex;
align-items: center;