mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-01-27 10:18:54 +00:00
chore(gui-client): delete GTK+ and Iced prototypes (#7035)
We don't need these since Tauri v2 looks like it's about to succeed, and keeping packages outside of the workspace has been breaking dependabot PRs
This commit is contained in:
2
.github/actions/setup-rust/action.yml
vendored
2
.github/actions/setup-rust/action.yml
vendored
@@ -28,7 +28,7 @@ outputs:
|
||||
value: ${{
|
||||
(runner.os == 'Linux' && '--workspace') ||
|
||||
(runner.os == 'macOS' && '-p connlib-client-apple -p connlib-client-shared -p firezone-tunnel -p snownet') ||
|
||||
(runner.os == 'Windows' && '-p connlib-client-shared -p firezone-bin-shared -p firezone-gui-client -p firezone-gui-client-common -p firezone-headless-client -p firezone-iced-client -p firezone-logging -p firezone-telemetry -p firezone-tunnel -p gui-smoke-test -p snownet') }}
|
||||
(runner.os == 'Windows' && '-p connlib-client-shared -p firezone-bin-shared -p firezone-gui-client -p firezone-gui-client-common -p firezone-headless-client -p firezone-logging -p firezone-telemetry -p firezone-tunnel -p gui-smoke-test -p snownet') }}
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
|
||||
2
.github/codespellrc
vendored
2
.github/codespellrc
vendored
@@ -1,3 +1,3 @@
|
||||
[codespell]
|
||||
skip = ./**/*.svg,./elixir/deps,./**/*.min.js,./kotlin/android/app/build,./kotlin/android/build,./e2e/pnpm-lock.yaml,./website/.next,./website/pnpm-lock.yaml,./rust/connlib/tunnel/testcases,./rust/gtk-client/target,./rust/iced-client/target,./rust/target,Cargo.lock,./website/docs/reference/api/*.mdx,./**/erl_crash.dump,./cover,./vendor,*.json,seeds.exs,./**/node_modules,./deps,./priv/static,./priv/plts,./**/priv/static,./.git,./_build,*.cast,./**/proptest-regressions
|
||||
skip = ./**/*.svg,./elixir/deps,./**/*.min.js,./kotlin/android/app/build,./kotlin/android/build,./e2e/pnpm-lock.yaml,./website/.next,./website/pnpm-lock.yaml,./rust/connlib/tunnel/testcases,./rust/target,Cargo.lock,./website/docs/reference/api/*.mdx,./**/erl_crash.dump,./cover,./vendor,*.json,seeds.exs,./**/node_modules,./deps,./priv/static,./priv/plts,./**/priv/static,./.git,./_build,*.cast,./**/proptest-regressions
|
||||
ignore-words-list = optin,crate,keypair,keypairs,iif,statics,wee,anull,commitish,inout,fo,superceded
|
||||
|
||||
4
.github/dependabot.yml
vendored
4
.github/dependabot.yml
vendored
@@ -42,10 +42,6 @@ updates:
|
||||
- windows-core
|
||||
- windows-implement
|
||||
- windows-sys
|
||||
- package-ecosystem: cargo
|
||||
directory: rust/gtk-client/
|
||||
schedule:
|
||||
interval: weekly
|
||||
- package-ecosystem: gradle
|
||||
directory: kotlin/android/
|
||||
schedule:
|
||||
|
||||
54
.github/workflows/_gtk.yml
vendored
54
.github/workflows/_gtk.yml
vendored
@@ -1,54 +0,0 @@
|
||||
---
|
||||
name: GTK
|
||||
"on":
|
||||
workflow_call:
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./rust/gtk-client
|
||||
|
||||
permissions:
|
||||
contents: 'read'
|
||||
id-token: 'write' # Needed to publish artifacts to releases?
|
||||
|
||||
# Never tolerate warnings. Source of truth is `_rust.yml`
|
||||
env:
|
||||
RUSTFLAGS: "-Dwarnings"
|
||||
RUSTDOCFLAGS: "-D warnings"
|
||||
|
||||
jobs:
|
||||
build-gtk:
|
||||
name: build-gtk-${{ matrix.runs-on }}
|
||||
runs-on: ${{ matrix.runs-on }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- runs-on: ubuntu-20.04
|
||||
arch: x86_64
|
||||
- runs-on: ubuntu-22.04-arm
|
||||
arch: aarch64
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/setup-rust
|
||||
- name: fmt
|
||||
run: cargo fmt
|
||||
- name: Install deps
|
||||
run: sudo apt-get update && sudo apt-get install libgtk-3-dev libxdo-dev && cargo install cargo-deb@2.7.0
|
||||
- name: Clippy
|
||||
run: cargo clippy --all-targets
|
||||
- name: Test
|
||||
run: cargo test
|
||||
- name: Build
|
||||
run: ./build.sh
|
||||
- name: Ensure unmodified Git workspace
|
||||
run: git diff --exit-code
|
||||
- name: Upload package
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
# mark:next-gui-version
|
||||
name: firezone-client-gui-gtk-linux_1.3.10_${{ matrix.arch }}-pkg
|
||||
# This ignores the working dir or something
|
||||
path: rust/gtk-client/target/debian/firezone-client-gui.deb
|
||||
if-no-files-found: error
|
||||
# TODO: Upload debug symbols
|
||||
2
.github/workflows/_rust.yml
vendored
2
.github/workflows/_rust.yml
vendored
@@ -11,7 +11,7 @@ permissions:
|
||||
contents: 'read'
|
||||
id-token: 'write'
|
||||
|
||||
# Never tolerate warnings. Duplicated in `_gtk.yml` and `_tauri.yml`
|
||||
# Never tolerate warnings. Duplicated in `_tauri.yml`
|
||||
env:
|
||||
RUSTFLAGS: "-Dwarnings --cfg tokio_unstable"
|
||||
RUSTDOCFLAGS: "-D warnings"
|
||||
|
||||
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -19,8 +19,6 @@ concurrency:
|
||||
cancel-in-progress: ${{ github.event_name != 'workflow_call' }}
|
||||
|
||||
jobs:
|
||||
gtk:
|
||||
uses: ./.github/workflows/_gtk.yml
|
||||
kotlin:
|
||||
uses: ./.github/workflows/_kotlin.yml
|
||||
secrets: inherit
|
||||
|
||||
1961
rust/Cargo.lock
generated
1961
rust/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -12,7 +12,6 @@ members = [
|
||||
"gui-client/src-common",
|
||||
"gui-client/src-tauri",
|
||||
"headless-client",
|
||||
"iced-client",
|
||||
"ip-packet",
|
||||
"logging",
|
||||
"phoenix-channel",
|
||||
|
||||
5547
rust/gtk-client/Cargo.lock
generated
5547
rust/gtk-client/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,56 +0,0 @@
|
||||
[package]
|
||||
authors = ["Firezone, Inc."]
|
||||
default-run = "firezone-gui-client"
|
||||
description = "Firezone"
|
||||
name = "firezone-gui-client"
|
||||
# mark:next-gui-version
|
||||
version = "1.3.10"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.89"
|
||||
clap = { version = "4.5", features = ["derive", "env"] }
|
||||
firezone-bin-shared = { path = "../bin-shared" }
|
||||
firezone-gui-client-common = { path = "../gui-client/src-common" }
|
||||
firezone-headless-client = { path = "../headless-client" }
|
||||
firezone-logging = { path = "../logging" }
|
||||
firezone-telemetry = { path = "../telemetry" }
|
||||
gdk-pixbuf = "0.18.5"
|
||||
glib = "0.18.5"
|
||||
gtk = "0.18.1"
|
||||
notify-rust = "4.11.3"
|
||||
open = "5.3.0"
|
||||
rustls = { version = "0.23.10", default-features = false, features = ["ring"] }
|
||||
secrecy = "0.8"
|
||||
serde_json = "1.0.128"
|
||||
tokio = { version = "1.40.0", features = ["rt-multi-thread", "sync", "time"] }
|
||||
tracing = "0.1.40"
|
||||
tray-icon = "0.19.0"
|
||||
url = "2.5.2"
|
||||
|
||||
[patch.crates-io]
|
||||
boringtun = { git = "https://github.com/cloudflare/boringtun", branch = "master" }
|
||||
str0m = { git = "https://github.com/algesten/str0m", branch = "main" }
|
||||
ip_network = { git = "https://github.com/JakubOnderka/ip_network", branch = "master" } # Waiting for release.
|
||||
ip_network_table = { git = "https://github.com/edmonds/ip_network_table", branch = "some-useful-traits" } # For `Debug` and `Clone`
|
||||
tracing-stackdriver = { git = "https://github.com/thomaseizinger/tracing-stackdriver", branch = "deps/bump-otel-0.23" } # Waiting for release.
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
#debug = "full"
|
||||
lto = "thin" # Don't have enough RAM in my VM to do fat LTO
|
||||
split-debuginfo = "packed"
|
||||
strip = "none"
|
||||
|
||||
[workspace]
|
||||
|
||||
[package.metadata.deb]
|
||||
assets = [
|
||||
["../gui-client/src-tauri/deb_files/sysusers.conf", "usr/lib/sysusers.d/firezone-client-ipc.conf", "644"],
|
||||
["../gui-client/src-tauri/deb_files/firezone-client-ipc.service", "usr/lib/systemd/system/", "644"],
|
||||
["../gui-client/src-tauri/icons/128x128.png", "/usr/share/icons/hicolor/128x128/apps/firezone-client-gui.png", "644"],
|
||||
# TODO: Once Tauri is removed on Linux, we can move this under `/usr/lib` so it's out of $PATH. We don't want users accidentally running it. For now Tauri and GTK share the systemd service unit,
|
||||
["target/release/firezone-client-ipc", "usr/bin/", "755"],
|
||||
["target/release/firezone-gui-client", "usr/bin/firezone-client-gui", "755"],
|
||||
]
|
||||
maintainer-scripts = "../gui-client/src-tauri/deb_files"
|
||||
@@ -1,38 +0,0 @@
|
||||
# gtk-client
|
||||
|
||||
This crate houses a GTK+ 3 Client for Ubuntu 20.04, 22.04, and 24.04.
|
||||
|
||||
## Setup
|
||||
|
||||
1. [Install rustup](https://rustup.rs/)
|
||||
1. `sudo apt-get install libgtk-3-dev libxdo-dev`
|
||||
1. `cargo install cargo-deb@2.7.0`
|
||||
|
||||
## Debugging
|
||||
|
||||
```bash
|
||||
cargo build
|
||||
|
||||
# In one terminal
|
||||
sudo -u root -g firezone-client target/debug/firezone-client-ipc run-debug
|
||||
|
||||
# Concurrently, in a 2nd terminal
|
||||
target/debug/firezone-gui-client
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
`./build.sh`
|
||||
|
||||
This will install dev dependencies such as `libgtk-3-dev`, and the bundling tool `cargo-deb`.
|
||||
|
||||
## Installing
|
||||
|
||||
`sudo apt-get install target/debian/firezone-client-gui.deb`
|
||||
|
||||
## Platform support
|
||||
|
||||
- `aarch64` or `x86_64` CPU architecture
|
||||
- Ubuntu 20.04 through 24.04 inclusive
|
||||
|
||||
Other distributions may work but are not officially supported.
|
||||
@@ -1,10 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Delete old deb packages so the `mv` glob will work later on
|
||||
rm -f target/debian/*.deb
|
||||
|
||||
cargo deb
|
||||
mv target/debian/*.deb target/debian/firezone-client-gui.deb
|
||||
ls target/debian
|
||||
@@ -1,7 +0,0 @@
|
||||
fn main() -> anyhow::Result<()> {
|
||||
rustls::crypto::ring::default_provider()
|
||||
.install_default()
|
||||
.expect("Calling `install_default` only once per process should always succeed");
|
||||
|
||||
firezone_headless_client::run_only_ipc_service()
|
||||
}
|
||||
@@ -1,486 +0,0 @@
|
||||
use anyhow::{Context as _, Result};
|
||||
use clap::{Args, Parser};
|
||||
use firezone_gui_client_common::{
|
||||
self as common,
|
||||
compositor::{self, Image},
|
||||
controller::{Builder as ControllerBuilder, ControllerRequest, CtlrTx, GuiIntegration},
|
||||
deep_link,
|
||||
system_tray::{AppState, ConnlibState, Entry, Icon, IconBase},
|
||||
updates,
|
||||
};
|
||||
use firezone_headless_client::LogFilterReloader;
|
||||
use firezone_telemetry as telemetry;
|
||||
use gtk::prelude::*;
|
||||
use gtk::{Application, ApplicationWindow};
|
||||
use secrecy::{ExposeSecret as _, SecretString};
|
||||
use std::{cell::RefCell, rc::Rc, str::FromStr};
|
||||
use tokio::sync::mpsc;
|
||||
use tray_icon::{menu::MenuEvent, TrayIconBuilder};
|
||||
|
||||
// TODO: De-dupe icon compositing with the Tauri Client.
|
||||
|
||||
// Figma is the source of truth for the tray icon layers
|
||||
// <https://www.figma.com/design/THvQQ1QxKlsk47H9DZ2bhN/Core-Library?node-id=1250-772&t=nHBOzOnSY5Ol4asV-0>
|
||||
const LOGO_BASE: &[u8] = include_bytes!("../../gui-client/src-tauri/icons/tray/Logo.png");
|
||||
const LOGO_GREY_BASE: &[u8] = include_bytes!("../../gui-client/src-tauri/icons/tray/Logo grey.png");
|
||||
const BUSY_LAYER: &[u8] = include_bytes!("../../gui-client/src-tauri/icons/tray/Busy layer.png");
|
||||
const SIGNED_OUT_LAYER: &[u8] =
|
||||
include_bytes!("../../gui-client/src-tauri/icons/tray/Signed out layer.png");
|
||||
const UPDATE_READY_LAYER: &[u8] =
|
||||
include_bytes!("../../gui-client/src-tauri/icons/tray/Update ready layer.png");
|
||||
|
||||
const TOOLTIP: &str = "Firezone";
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(author, version, about, long_about = None)]
|
||||
struct Cli {
|
||||
#[command(subcommand)]
|
||||
command: Option<Cmd>,
|
||||
}
|
||||
|
||||
#[derive(clap::Subcommand)]
|
||||
enum Cmd {
|
||||
Debug,
|
||||
OpenDeepLink(DeepLink),
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
struct DeepLink {
|
||||
url: url::Url, // TODO: Should be `Secret`?
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let current_exe = std::env::current_exe()?;
|
||||
let cli = Cli::parse();
|
||||
|
||||
match cli.command {
|
||||
Some(Cmd::Debug) => return Ok(()), // I didn't want to use `if-let` here
|
||||
Some(Cmd::OpenDeepLink(deep_link)) => {
|
||||
let rt = tokio::runtime::Runtime::new()?;
|
||||
if let Err(error) = rt.block_on(deep_link::open(&deep_link.url)) {
|
||||
tracing::error!(?error, "Error in `OpenDeepLink`");
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
|
||||
// We're not a deep link handler, so start telemetry
|
||||
let telemetry = telemetry::Telemetry::default();
|
||||
// TODO: Fix missing stuff for telemetry
|
||||
telemetry.start(
|
||||
"wss://api.firez.one",
|
||||
firezone_bin_shared::git_version!("gtk-client-*"),
|
||||
telemetry::GUI_DSN,
|
||||
);
|
||||
|
||||
let common::logging::Handles {
|
||||
logger: _logger,
|
||||
reloader: log_filter_reloader,
|
||||
} = start_logging("info")?; // TODO: Load log filter from settings file
|
||||
|
||||
// The runtime must be multi-thread so that the main thread is free for GTK to consume
|
||||
// As long as Tokio has at least 1 worker thread (i.e. there is at least 1 CPU core in the system) this will work.
|
||||
let rt = tokio::runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.build()?;
|
||||
let _guard = rt.enter();
|
||||
|
||||
// This enforces single-instance
|
||||
let deep_link_server = rt.block_on(deep_link::Server::new())?;
|
||||
|
||||
let app = Application::builder()
|
||||
.application_id("dev.firezone.client")
|
||||
.build();
|
||||
|
||||
// Use a `RefCell` here because the ownership is weird for creating
|
||||
// the UI inside `connect_activate` and then using it inside the local future later.
|
||||
// Creating it lazily inside the future did not work.
|
||||
let ui_cell = Rc::new(RefCell::new(None));
|
||||
let ui_cell_2 = ui_cell.clone();
|
||||
// Must be `mpsc` to satisfy `connect_activate`'s signature
|
||||
// In practice it only gets used once.
|
||||
let (ui_ready_tx, ui_ready_rx) = mpsc::channel(1);
|
||||
app.connect_activate(move |app| match build_ui(app) {
|
||||
Ok(ui) => {
|
||||
*ui_cell_2.borrow_mut() = Some(ui);
|
||||
ui_ready_tx
|
||||
.try_send(())
|
||||
.expect("Should be able to signal that the UI is ready");
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::error!(?error, "`build_ui` failed");
|
||||
telemetry::capture_anyhow(&error);
|
||||
}
|
||||
});
|
||||
|
||||
gtk::init()?;
|
||||
let tray_icon = TrayIconBuilder::new()
|
||||
.with_tooltip(TOOLTIP)
|
||||
.with_icon(icon_to_native_icon(&Icon::default()))
|
||||
.build()?;
|
||||
|
||||
let (ctlr_tx, ctlr_rx) = mpsc::channel(100);
|
||||
|
||||
deep_link::register(current_exe)?;
|
||||
rt.spawn(accept_deep_links(deep_link_server, ctlr_tx.clone()));
|
||||
|
||||
{
|
||||
let ctlr_tx = ctlr_tx.clone();
|
||||
MenuEvent::set_event_handler(Some(move |event: MenuEvent| {
|
||||
let Ok(event) = serde_json::from_str::<common::system_tray::Event>(&event.id.0) else {
|
||||
tracing::error!("Couldn't parse system tray event");
|
||||
return;
|
||||
};
|
||||
if let Err(error) = ctlr_tx.blocking_send(ControllerRequest::SystemTrayMenu(event)) {
|
||||
tracing::error!(?error, "Couldn't send system tray event to Controller");
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
let (_updates_tx, updates_rx) = mpsc::channel(1);
|
||||
|
||||
let (main_tx, main_rx) = mpsc::channel(100);
|
||||
rt.spawn(run_controller(
|
||||
main_tx,
|
||||
ctlr_tx,
|
||||
ctlr_rx,
|
||||
log_filter_reloader,
|
||||
telemetry,
|
||||
updates_rx,
|
||||
));
|
||||
|
||||
glib::spawn_future_local(run_main_thread_loop(
|
||||
app.clone(),
|
||||
main_rx,
|
||||
tray_icon,
|
||||
ui_cell,
|
||||
ui_ready_rx,
|
||||
));
|
||||
|
||||
if app.run() != 0.into() {
|
||||
anyhow::bail!("GTK main loop returned non-zero exit code");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct Ui {
|
||||
about_win: ApplicationWindow,
|
||||
settings_win: ApplicationWindow,
|
||||
}
|
||||
|
||||
fn build_ui(app: >k::Application) -> Result<Ui> {
|
||||
let icon_pixbuf = gdk_pixbuf::Pixbuf::from_file(
|
||||
"/usr/share/icons/hicolor/128x128/apps/firezone-client-gui.png",
|
||||
)?;
|
||||
|
||||
let about_win = ApplicationWindow::builder()
|
||||
.application(app)
|
||||
.default_width(640)
|
||||
.default_height(480)
|
||||
.icon(&icon_pixbuf)
|
||||
.title("About Firezone")
|
||||
.build();
|
||||
about_win.connect_delete_event(move |win, _| {
|
||||
win.hide();
|
||||
glib::Propagation::Stop
|
||||
});
|
||||
|
||||
let settings_win = ApplicationWindow::builder()
|
||||
.application(app)
|
||||
.default_width(640)
|
||||
.default_height(480)
|
||||
.icon(&icon_pixbuf)
|
||||
.title("Settings")
|
||||
.build();
|
||||
settings_win.connect_delete_event(move |win, _| {
|
||||
win.hide();
|
||||
glib::Propagation::Stop
|
||||
});
|
||||
|
||||
Ok(Ui {
|
||||
about_win,
|
||||
settings_win,
|
||||
})
|
||||
}
|
||||
|
||||
// Worker task to accept deep links from a named pipe forever
|
||||
///
|
||||
/// * `server` An initial named pipe server to consume before making new servers. This lets us also use the named pipe to enforce single-instance
|
||||
async fn accept_deep_links(mut server: deep_link::Server, ctlr_tx: CtlrTx) -> Result<()> {
|
||||
loop {
|
||||
match server.accept().await {
|
||||
Ok(bytes) => {
|
||||
let url = SecretString::from_str(
|
||||
std::str::from_utf8(bytes.expose_secret())
|
||||
.context("Incoming deep link was not valid UTF-8")?,
|
||||
)
|
||||
.context("Impossible: can't wrap String into SecretString")?;
|
||||
// Ignore errors from this, it would only happen if the app is shutting down, otherwise we would wait
|
||||
ctlr_tx
|
||||
.send(ControllerRequest::SchemeRequest(url))
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
Err(error) => tracing::error!(?error, "error while accepting deep link"),
|
||||
}
|
||||
// We re-create the named pipe server every time we get a link, because of an oddity in the Windows API.
|
||||
server = deep_link::Server::new().await?;
|
||||
}
|
||||
}
|
||||
|
||||
/// Waits for the UI to be built and then starts the main thread loop.
|
||||
///
|
||||
/// This function typically never returns, GLib just stops polling it when the GTK app quits
|
||||
async fn run_main_thread_loop(
|
||||
app: gtk::Application,
|
||||
main_rx: mpsc::Receiver<MainThreadReq>,
|
||||
tray_icon: tray_icon::TrayIcon,
|
||||
ui_cell: Rc<RefCell<Option<Ui>>>,
|
||||
mut ui_ready_rx: mpsc::Receiver<()>,
|
||||
) -> Result<()> {
|
||||
ui_ready_rx.recv().await.unwrap();
|
||||
let ui = ui_cell
|
||||
.take()
|
||||
.expect("UI should have been built before we got `ui_ready` signal");
|
||||
let l = MainThreadLoop {
|
||||
app,
|
||||
last_icon_set: Default::default(),
|
||||
main_rx,
|
||||
tray_icon,
|
||||
ui,
|
||||
};
|
||||
l.run().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Handles messages from other tasks / thread to our GTK main thread, such as quitting the app and changing the tray menu.
|
||||
struct MainThreadLoop {
|
||||
app: gtk::Application,
|
||||
last_icon_set: Icon,
|
||||
main_rx: mpsc::Receiver<MainThreadReq>,
|
||||
tray_icon: tray_icon::TrayIcon,
|
||||
ui: Ui,
|
||||
}
|
||||
|
||||
impl MainThreadLoop {
|
||||
/// Handle messages that must be handled on the main thread where GTK is
|
||||
async fn run(mut self) {
|
||||
while let Some(req) = self.main_rx.recv().await {
|
||||
if let Err(error) = self.handle_req(req) {
|
||||
tracing::error!(?error, "`MainThreadLoop::handle_req` failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_req(&mut self, req: MainThreadReq) -> Result<()> {
|
||||
match req {
|
||||
MainThreadReq::Quit => self.app.quit(),
|
||||
MainThreadReq::SetTrayIcon(icon) => self.set_icon(icon)?,
|
||||
MainThreadReq::SetTrayMenu(app_state) => self.set_tray_menu(*app_state)?,
|
||||
MainThreadReq::ShowWindow(window) => match window {
|
||||
common::system_tray::Window::About => self.ui.about_win.show_all(),
|
||||
common::system_tray::Window::Settings => self.ui.settings_win.show_all(),
|
||||
},
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_tray_menu(&mut self, state: AppState) -> Result<()> {
|
||||
let base = match &state.connlib {
|
||||
ConnlibState::Loading
|
||||
| ConnlibState::Quitting
|
||||
| ConnlibState::RetryingConnection
|
||||
| ConnlibState::WaitingForBrowser
|
||||
| ConnlibState::WaitingForPortal
|
||||
| ConnlibState::WaitingForTunnel => IconBase::Busy,
|
||||
ConnlibState::SignedOut => IconBase::SignedOut,
|
||||
ConnlibState::SignedIn { .. } => IconBase::SignedIn,
|
||||
};
|
||||
let new_icon = Icon {
|
||||
base,
|
||||
update_ready: state.release.is_some(),
|
||||
};
|
||||
|
||||
let menu = build_menu("", &state.into_menu())?;
|
||||
self.tray_icon.set_menu(Some(Box::new(menu)));
|
||||
// TODO: Set menu tooltip here too
|
||||
self.set_icon(new_icon)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_icon(&mut self, icon: Icon) -> Result<()> {
|
||||
if icon == self.last_icon_set {
|
||||
return Ok(());
|
||||
}
|
||||
// TODO: Does `tray-icon` have the same problem as `tao`,
|
||||
// where it writes PNGs to `/run/user/$UID/` every time you set an icon?
|
||||
self.tray_icon.set_icon(Some(icon_to_native_icon(&icon)))?;
|
||||
self.last_icon_set = icon;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn icon_to_native_icon(that: &Icon) -> tray_icon::Icon {
|
||||
let layers = match that.base {
|
||||
IconBase::Busy => &[LOGO_GREY_BASE, BUSY_LAYER][..],
|
||||
IconBase::SignedIn => &[LOGO_BASE][..],
|
||||
IconBase::SignedOut => &[LOGO_GREY_BASE, SIGNED_OUT_LAYER][..],
|
||||
}
|
||||
.iter()
|
||||
.copied()
|
||||
.chain(that.update_ready.then_some(UPDATE_READY_LAYER));
|
||||
let composed =
|
||||
compositor::compose(layers).expect("PNG decoding should always succeed for baked-in PNGs");
|
||||
image_to_native_icon(composed)
|
||||
}
|
||||
|
||||
fn image_to_native_icon(val: Image) -> tray_icon::Icon {
|
||||
tray_icon::Icon::from_rgba(val.rgba, val.width, val.height)
|
||||
.expect("Converting a tray icon to RGBA should always work")
|
||||
}
|
||||
|
||||
fn build_menu(text: &str, that: &common::system_tray::Menu) -> Result<tray_icon::menu::Submenu> {
|
||||
let menu = tray_icon::menu::Submenu::new(text, true);
|
||||
for entry in &that.entries {
|
||||
match entry {
|
||||
Entry::Item(item) if item.checked.is_some() => {
|
||||
menu.append(&build_checked_item(item))?
|
||||
}
|
||||
Entry::Item(item) => menu.append(&build_item(item))?,
|
||||
Entry::Separator => menu.append(&tray_icon::menu::PredefinedMenuItem::separator())?,
|
||||
Entry::Submenu { title, inner } => menu.append(&build_menu(title, inner)?)?,
|
||||
};
|
||||
}
|
||||
Ok(menu)
|
||||
}
|
||||
|
||||
fn build_checked_item(that: &common::system_tray::Item) -> tray_icon::menu::CheckMenuItem {
|
||||
let id = serde_json::to_string(&that.event)
|
||||
.expect("`serde_json` should always be able to serialize tray menu events");
|
||||
|
||||
tray_icon::menu::CheckMenuItem::with_id(
|
||||
id,
|
||||
&that.title,
|
||||
that.event.is_some(),
|
||||
that.checked.unwrap_or_default(),
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
fn build_item(that: &common::system_tray::Item) -> tray_icon::menu::MenuItem {
|
||||
let id = serde_json::to_string(&that.event)
|
||||
.expect("`serde_json` should always be able to serialize tray menu events");
|
||||
|
||||
tray_icon::menu::MenuItem::with_id(id, &that.title, that.event.is_some(), None)
|
||||
}
|
||||
|
||||
/// Something that needs to be done on the GTK+ main thread.
|
||||
enum MainThreadReq {
|
||||
/// The controller exited, quit the GTK app and exit the loop
|
||||
Quit,
|
||||
SetTrayIcon(common::system_tray::Icon),
|
||||
SetTrayMenu(Box<common::system_tray::AppState>),
|
||||
ShowWindow(common::system_tray::Window),
|
||||
}
|
||||
|
||||
async fn run_controller(
|
||||
main_tx: mpsc::Sender<MainThreadReq>, // Runs stuff on the main thread
|
||||
ctlr_tx: CtlrTx,
|
||||
rx: mpsc::Receiver<ControllerRequest>,
|
||||
log_filter_reloader: LogFilterReloader,
|
||||
telemetry: telemetry::Telemetry,
|
||||
updates_rx: mpsc::Receiver<Option<updates::Notification>>,
|
||||
) -> Result<()> {
|
||||
let integration = GtkIntegration {
|
||||
main_tx: main_tx.clone(),
|
||||
};
|
||||
|
||||
let controller = ControllerBuilder {
|
||||
advanced_settings: Default::default(), // TODO
|
||||
ctlr_tx,
|
||||
integration,
|
||||
log_filter_reloader,
|
||||
rx,
|
||||
telemetry,
|
||||
updates_rx,
|
||||
}
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
let result = controller.main_loop().await;
|
||||
if let Err(error) = &result {
|
||||
tracing::error!(?error, "`Controller` failed");
|
||||
}
|
||||
main_tx.send(MainThreadReq::Quit).await?;
|
||||
Ok(result?)
|
||||
}
|
||||
|
||||
struct GtkIntegration {
|
||||
main_tx: mpsc::Sender<MainThreadReq>,
|
||||
}
|
||||
|
||||
impl GuiIntegration for GtkIntegration {
|
||||
fn set_welcome_window_visible(&self, _visible: bool) -> Result<()> {
|
||||
tracing::warn!("set_welcome_window_visible not implemented");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn open_url<P: AsRef<str>>(&self, url: P) -> Result<()> {
|
||||
open::that(std::ffi::OsStr::new(url.as_ref()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_tray_icon(&mut self, icon: common::system_tray::Icon) -> Result<()> {
|
||||
self.main_tx.try_send(MainThreadReq::SetTrayIcon(icon))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_tray_menu(&mut self, app_state: common::system_tray::AppState) -> Result<()> {
|
||||
self.main_tx
|
||||
.try_send(MainThreadReq::SetTrayMenu(Box::new(app_state)))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn show_notification(&self, title: &str, body: &str) -> Result<()> {
|
||||
notify_rust::Notification::new()
|
||||
.icon("/usr/share/icons/hicolor/128x128/apps/firezone-client-gui.png")
|
||||
.summary(title)
|
||||
.body(body)
|
||||
.show()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn show_update_notification(
|
||||
&self,
|
||||
_ctlr_tx: CtlrTx,
|
||||
_title: &str,
|
||||
_url: url::Url,
|
||||
) -> Result<()> {
|
||||
tracing::warn!("show_update_notification not implemented");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn show_window(&self, window: common::system_tray::Window) -> Result<()> {
|
||||
self.main_tx.try_send(MainThreadReq::ShowWindow(window))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Starts logging
|
||||
///
|
||||
/// Don't drop the log handle or logging will stop.
|
||||
fn start_logging(directives: &str) -> Result<common::logging::Handles> {
|
||||
let logging_handles = common::logging::setup(directives)?;
|
||||
tracing::info!(
|
||||
arch = std::env::consts::ARCH,
|
||||
os = std::env::consts::OS,
|
||||
?directives,
|
||||
git_version = firezone_bin_shared::git_version!("gui-client-*"),
|
||||
system_uptime_seconds = firezone_headless_client::uptime::get().map(|dur| dur.as_secs()),
|
||||
"`gui-client` started logging"
|
||||
);
|
||||
|
||||
Ok(logging_handles)
|
||||
}
|
||||
@@ -59,8 +59,6 @@ dirs = "5.0.1"
|
||||
ipconfig = "0.3.2"
|
||||
itertools = "0.13.0"
|
||||
known-folders = "1.2.0"
|
||||
thiserror = { version = "1.0", default-features = false }
|
||||
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
|
||||
windows-service = "0.7.0"
|
||||
winreg = "0.52.0"
|
||||
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
[package]
|
||||
name = "firezone-iced-client"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
# `tiny-skia` uses less RAM than the GPU backends. About 6.5 MiB on Ubuntu.
|
||||
iced = { version = "0.13", features = ["image", "tiny-skia", "multi-window"], default-features = false }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -1,347 +0,0 @@
|
||||
use iced::{
|
||||
event,
|
||||
widget::{button, column, container, image, row, text, text_input},
|
||||
window, Alignment, Background, Border, Color, Event, Length, Renderer, Subscription,
|
||||
};
|
||||
|
||||
type Element<'a> = iced::Element<'a, Message, FzTheme, Renderer>;
|
||||
|
||||
pub fn main() -> iced::Result {
|
||||
// The main icon should be at least 90x90, since Ubuntu's default
|
||||
// desktop wants 48 px, and that's nearly doubled if Ubuntu is
|
||||
// running in a HiDPI Parallels VM on a Mac.
|
||||
let icon = window::icon::from_file_data(
|
||||
include_bytes!("../../gui-client/src-tauri/icons/32x32.png"),
|
||||
None,
|
||||
)
|
||||
.expect("Baked-in icon PNG should always be decodable");
|
||||
|
||||
/*
|
||||
let mut settings = Settings::with_flags(Flags { icon: icon.clone() });
|
||||
settings.window.exit_on_close_request = false;
|
||||
settings.window.icon = Some(icon);
|
||||
settings.window.size = [640, 480].into();
|
||||
*/
|
||||
|
||||
let logo = image::Handle::from_bytes(&include_bytes!("../../gui-client/src/logo.png")[..]);
|
||||
|
||||
let settings = window::Settings {
|
||||
exit_on_close_request: false,
|
||||
icon: Some(icon),
|
||||
size: [640.0f32, 480.0].into(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let (about_window, about_task) = window::open(settings.clone());
|
||||
let (settings_window, settings_task) = window::open(settings.clone());
|
||||
let (welcome_window, welcome_task) = window::open(settings);
|
||||
|
||||
let tasks = iced::Task::batch([about_task, settings_task, welcome_task]);
|
||||
|
||||
let app_state = FirezoneApp {
|
||||
about_window,
|
||||
settings_window,
|
||||
welcome_window,
|
||||
|
||||
logo,
|
||||
|
||||
settings_tab: Default::default(),
|
||||
|
||||
auth_base_url: String::new(),
|
||||
api_url: String::new(),
|
||||
log_filter: String::new(),
|
||||
};
|
||||
|
||||
// What `iced` calls "daemon" here is a GUI app that doesn't
|
||||
// open any windows by default, has no "main" window, and continues
|
||||
// to run after all its windows are closed.
|
||||
let daemon = iced::daemon(FirezoneApp::title, FirezoneApp::update, FirezoneApp::view)
|
||||
.subscription(FirezoneApp::subscription);
|
||||
daemon.run_with(|| (app_state, tasks.map(|_| Message::WindowsAllOpen)))
|
||||
}
|
||||
|
||||
struct FirezoneApp {
|
||||
about_window: window::Id,
|
||||
settings_window: window::Id,
|
||||
welcome_window: window::Id,
|
||||
|
||||
logo: image::Handle,
|
||||
|
||||
settings_tab: SettingsTab,
|
||||
|
||||
auth_base_url: String,
|
||||
api_url: String,
|
||||
log_filter: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
enum SettingsTab {
|
||||
Advanced,
|
||||
DiagnosticLogs,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
enum SettingsField {
|
||||
AuthBaseUrl,
|
||||
ApiUrl,
|
||||
LogFilter,
|
||||
}
|
||||
|
||||
impl Default for SettingsTab {
|
||||
fn default() -> Self {
|
||||
Self::Advanced
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
enum Message {
|
||||
ChangeSettingsTab(SettingsTab),
|
||||
CloseRequested(window::Id),
|
||||
InputChanged((SettingsField, String)),
|
||||
SignIn,
|
||||
Quit,
|
||||
WindowsAllOpen,
|
||||
}
|
||||
|
||||
enum FzWindow {
|
||||
About,
|
||||
Settings,
|
||||
Welcome,
|
||||
}
|
||||
|
||||
impl FirezoneApp {
|
||||
fn subscription(&self) -> Subscription<Message> {
|
||||
event::listen_with(|event, _status, id| match event {
|
||||
Event::Keyboard(_) => None,
|
||||
Event::Mouse(_) => None,
|
||||
Event::Touch(_) => None,
|
||||
Event::Window(iced::window::Event::CloseRequested) => Some(Message::CloseRequested(id)),
|
||||
Event::Window(_) => None,
|
||||
})
|
||||
}
|
||||
|
||||
fn title(&self, id: window::Id) -> String {
|
||||
match self.fz_window(id) {
|
||||
FzWindow::About => "About Firezone",
|
||||
FzWindow::Settings => "Settings",
|
||||
FzWindow::Welcome => "Welcome to Firezone",
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
fn update(&mut self, message: Message) -> iced::Task<Message> {
|
||||
match message {
|
||||
Message::ChangeSettingsTab(new_tab) => self.settings_tab = new_tab,
|
||||
Message::InputChanged((field, s)) => match field {
|
||||
SettingsField::AuthBaseUrl => self.auth_base_url = s,
|
||||
SettingsField::ApiUrl => self.api_url = s,
|
||||
SettingsField::LogFilter => self.log_filter = s,
|
||||
},
|
||||
Message::CloseRequested(id) => {
|
||||
return window::change_mode::<Message>(id, window::Mode::Hidden)
|
||||
}
|
||||
Message::SignIn => {}
|
||||
// Closing all windows causes Iced to exit the app
|
||||
Message::Quit => {
|
||||
return iced::Task::batch([
|
||||
window::close(self.about_window),
|
||||
window::close(self.settings_window),
|
||||
window::close(self.welcome_window),
|
||||
])
|
||||
}
|
||||
Message::WindowsAllOpen => {}
|
||||
}
|
||||
iced::Task::none()
|
||||
}
|
||||
|
||||
fn view(&self, id: window::Id) -> Element {
|
||||
match self.fz_window(id) {
|
||||
FzWindow::About => self.view_about(),
|
||||
FzWindow::Settings => self.view_settings(),
|
||||
FzWindow::Welcome => self.view_welcome(),
|
||||
}
|
||||
}
|
||||
|
||||
fn view_about(&self) -> Element {
|
||||
let content = column![
|
||||
image::Image::new(self.logo.clone()).width(240).height(240),
|
||||
text("Version 42.9000"),
|
||||
button("Quit").on_press(Message::Quit).padding(16),
|
||||
]
|
||||
.align_x(Alignment::Center);
|
||||
container(content)
|
||||
.center_x(Length::Fill)
|
||||
.center_y(Length::Fill)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn view_settings(&self) -> Element {
|
||||
let tabs = row![
|
||||
button("Advanced").on_press(Message::ChangeSettingsTab(SettingsTab::Advanced)),
|
||||
button("DiagnosticLogs")
|
||||
.on_press(Message::ChangeSettingsTab(SettingsTab::DiagnosticLogs)),
|
||||
];
|
||||
let tabs = container(tabs).center_x(Length::Fill);
|
||||
|
||||
let content = match self.settings_tab {
|
||||
SettingsTab::Advanced => self.tab_advanced_settings(),
|
||||
SettingsTab::DiagnosticLogs => self.tab_diagnostic_logs(),
|
||||
};
|
||||
|
||||
let content = column!(tabs, content);
|
||||
|
||||
container(content).height(Length::Fill).into()
|
||||
}
|
||||
|
||||
fn tab_advanced_settings(&self) -> Element {
|
||||
let content = column![
|
||||
text("WARNING: These settings are intended for internal debug purposes only. Changing these is not supported and will disrupt access to your resources"),
|
||||
column![
|
||||
text_input("Auth Base URL", &self.auth_base_url).on_input(|s| Message::InputChanged((SettingsField::AuthBaseUrl, s))),
|
||||
text_input("API URL", &self.api_url).on_input(|s| Message::InputChanged((SettingsField::ApiUrl, s))),
|
||||
text_input("Log Filter", &self.log_filter).on_input(|s| Message::InputChanged((SettingsField::LogFilter, s))),
|
||||
]
|
||||
.padding(20)
|
||||
]
|
||||
.padding(20)
|
||||
.align_x(Alignment::Center);
|
||||
container(content)
|
||||
.center_x(Length::Fill)
|
||||
.center_y(Length::Fill)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn tab_diagnostic_logs(&self) -> Element {
|
||||
container(text("TODO"))
|
||||
.center_x(Length::Fill)
|
||||
.center_y(Length::Fill)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn view_welcome(&self) -> Element {
|
||||
let content = column![
|
||||
text("Welcome to Firezone.").size(32),
|
||||
text("Sign in below to get started."),
|
||||
image::Image::new(self.logo.clone()).width(200).height(200),
|
||||
button("Sign in").on_press(Message::SignIn).padding(16),
|
||||
]
|
||||
.padding(20)
|
||||
.align_x(Alignment::Center);
|
||||
|
||||
container(content)
|
||||
.center_x(Length::Fill)
|
||||
.center_y(Length::Fill)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn fz_window(&self, id: window::Id) -> FzWindow {
|
||||
if id == self.about_window {
|
||||
FzWindow::About
|
||||
} else if id == self.settings_window {
|
||||
FzWindow::Settings
|
||||
} else if id == self.welcome_window {
|
||||
FzWindow::Welcome
|
||||
} else {
|
||||
panic!("Impossible - Can't generate title for window we didn't make.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
struct FzTheme {}
|
||||
|
||||
impl iced::daemon::DefaultStyle for FzTheme {
|
||||
fn default_style(&self) -> iced::daemon::Appearance {
|
||||
iced::daemon::Appearance {
|
||||
background_color: Color::from_rgb8(0xf5, 0xf5, 0xf5),
|
||||
text_color: Color::from_rgb8(0x11, 0x18, 0x27),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl button::Catalog for FzTheme {
|
||||
type Class<'a> = ();
|
||||
|
||||
fn default<'a>() -> Self::Class<'a> {}
|
||||
|
||||
fn style(&self, _: &Self::Class<'_>, status: button::Status) -> button::Style {
|
||||
match status {
|
||||
button::Status::Active | button::Status::Pressed => button::Style {
|
||||
background: Some(Background::Color(Color::from_rgb8(94, 0, 214))),
|
||||
border: Border {
|
||||
color: Color::from_rgb8(0, 0, 0),
|
||||
width: 0.0,
|
||||
radius: 4.into(),
|
||||
},
|
||||
text_color: Color::from_rgb8(255, 255, 255),
|
||||
..Default::default()
|
||||
},
|
||||
button::Status::Hovered => button::Style {
|
||||
background: Some(Background::Color(Color::from_rgb8(94, 0, 214))),
|
||||
border: Border {
|
||||
color: Color::from_rgb8(0xf5, 0xf5, 0xf5),
|
||||
width: 2.0,
|
||||
radius: 4.into(),
|
||||
},
|
||||
text_color: Color::from_rgb8(255, 255, 255),
|
||||
..Default::default()
|
||||
},
|
||||
button::Status::Disabled => button::Style {
|
||||
background: Some(Background::Color(Color::from_rgb8(96, 96, 96))),
|
||||
border: Border {
|
||||
color: Color::from_rgb8(0, 0, 0),
|
||||
width: 0.0,
|
||||
radius: 4.into(),
|
||||
},
|
||||
text_color: Color::from_rgb8(0, 0, 0),
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl container::Catalog for FzTheme {
|
||||
type Class<'a> = ();
|
||||
|
||||
fn default<'a>() -> Self::Class<'a> {}
|
||||
|
||||
fn style(&self, _: &Self::Class<'_>) -> container::Style {
|
||||
Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
impl text::Catalog for FzTheme {
|
||||
type Class<'a> = ();
|
||||
|
||||
fn default<'a>() -> Self::Class<'a> {}
|
||||
|
||||
fn style(&self, _: &Self::Class<'_>) -> text::Style {
|
||||
Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
impl text_input::Catalog for FzTheme {
|
||||
type Class<'a> = ();
|
||||
|
||||
fn default<'a>() -> Self::Class<'a> {}
|
||||
|
||||
fn style(&self, _: &Self::Class<'_>, status: text_input::Status) -> text_input::Style {
|
||||
match status {
|
||||
text_input::Status::Active
|
||||
| text_input::Status::Disabled
|
||||
| text_input::Status::Focused
|
||||
| text_input::Status::Hovered => text_input::Style {
|
||||
background: Background::Color(Color::from_rgba8(0, 0, 0, 0.0)),
|
||||
border: Border {
|
||||
color: Color::from_rgb8(0, 0, 0),
|
||||
radius: 0.0.into(),
|
||||
width: 0.0,
|
||||
},
|
||||
icon: Color::from_rgb8(0, 0, 0),
|
||||
placeholder: Color::from_rgb8(180, 180, 180),
|
||||
value: Color::from_rgb8(0, 0, 0),
|
||||
selection: Color::from_rgb8(0, 0, 255),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user