mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-01-27 10:18:54 +00:00
chore(rust/gui-client): start a GTK 3 prototype (#6838)
Refs #6927 This PR creates a GTK+ event loop, a blank window, and the tray menu. It connects to the IPC service, you can sign in and everything, but the About window, Settings window, and Welcome window aren't implemented. We build a deb package in CI but it isn't pushed to the draft releases in CD yet.  Pros over Iced: - More mature - Easy integration with `tray-icon` - Small binaries (< 1 MB for this example) Cons: - GTK 3.x is abandoned as of March. GTK 4 isn't packaged for Ubuntu 20.04. - Widgets might be hard to use - Hard to set up on Windows, only using this for Linux for now --------- Signed-off-by: Reactor Scram <ReactorScram@users.noreply.github.com> Co-authored-by: Thomas Eizinger <thomas@eizinger.io>
This commit is contained in:
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/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/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
|
||||
ignore-words-list = optin,crate,keypair,keypairs,iif,statics,wee,anull,commitish,inout,fo,superceded
|
||||
|
||||
52
.github/workflows/_gtk.yml
vendored
Normal file
52
.github/workflows/_gtk.yml
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
---
|
||||
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: Upload package
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
# mark:next-gui-version
|
||||
name: firezone-client-gui-gtk-linux_1.3.8_${{ 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 `_tauri.yml`
|
||||
# Never tolerate warnings. Duplicated in `_gtk.yml` and `_tauri.yml`
|
||||
env:
|
||||
RUSTFLAGS: "-Dwarnings"
|
||||
RUSTDOCFLAGS: "-D warnings"
|
||||
|
||||
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -19,6 +19,8 @@ concurrency:
|
||||
cancel-in-progress: ${{ github.event_name != 'workflow_call' }}
|
||||
|
||||
jobs:
|
||||
gtk:
|
||||
uses: ./.github/workflows/_gtk.yml
|
||||
kotlin:
|
||||
uses: ./.github/workflows/_kotlin.yml
|
||||
secrets: inherit
|
||||
|
||||
5486
rust/gtk-client/Cargo.lock
generated
Normal file
5486
rust/gtk-client/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
55
rust/gtk-client/Cargo.toml
Normal file
55
rust/gtk-client/Cargo.toml
Normal file
@@ -0,0 +1,55 @@
|
||||
[package]
|
||||
authors = ["Firezone, Inc."]
|
||||
default-run = "firezone-gui-client"
|
||||
description = "Firezone"
|
||||
name = "firezone-gui-client"
|
||||
# mark:next-gui-version
|
||||
version = "1.3.8"
|
||||
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" }
|
||||
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"
|
||||
38
rust/gtk-client/README.md
Normal file
38
rust/gtk-client/README.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# 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.
|
||||
10
rust/gtk-client/build.sh
Executable file
10
rust/gtk-client/build.sh
Executable file
@@ -0,0 +1,10 @@
|
||||
#!/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
|
||||
7
rust/gtk-client/src/bin/firezone-client-ipc.rs
Normal file
7
rust/gtk-client/src/bin/firezone-client-ipc.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
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()
|
||||
}
|
||||
407
rust/gtk-client/src/main.rs
Normal file
407
rust/gtk-client/src/main.rs
Normal file
@@ -0,0 +1,407 @@
|
||||
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::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("debug")?; // TODO
|
||||
// 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();
|
||||
|
||||
app.connect_activate(|app| {
|
||||
// We create the main window.
|
||||
let win = ApplicationWindow::builder()
|
||||
.application(app)
|
||||
.default_width(640)
|
||||
.default_height(480)
|
||||
.title("Firezone GTK+ 3")
|
||||
.build();
|
||||
|
||||
// Don't forget to make all widgets visible.
|
||||
win.show_all();
|
||||
});
|
||||
|
||||
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,
|
||||
));
|
||||
|
||||
let l = MainThreadLoop {
|
||||
app: app.clone(),
|
||||
last_icon_set: Default::default(),
|
||||
main_rx,
|
||||
tray_icon,
|
||||
};
|
||||
glib::spawn_future_local(l.run());
|
||||
|
||||
if app.run() != 0.into() {
|
||||
anyhow::bail!("GTK main loop returned non-zero exit code");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// 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?;
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles messages from other tasks / thread to our GTK main thread, such as quitting the app and changing the tray menu.
|
||||
#[must_use]
|
||||
struct MainThreadLoop {
|
||||
app: gtk::Application,
|
||||
last_icon_set: Icon,
|
||||
main_rx: mpsc::Receiver<MainThreadReq>,
|
||||
tray_icon: tray_icon::TrayIcon,
|
||||
}
|
||||
|
||||
impl MainThreadLoop {
|
||||
/// Handle messages that must be handled on the main thread where GTK is
|
||||
///
|
||||
/// This function typically never returns, GLib just stops polling it when the GTK app quits
|
||||
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::SetTrayMenu(app_state) => self.set_tray_menu(*app_state)?,
|
||||
}
|
||||
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,
|
||||
SetTrayMenu(Box<common::system_tray::AppState>),
|
||||
}
|
||||
|
||||
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<()> {
|
||||
tracing::warn!("set_tray_icon not implemented");
|
||||
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<()> {
|
||||
tracing::warn!("show_window not implemented");
|
||||
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)
|
||||
}
|
||||
@@ -27,6 +27,7 @@ pub(crate) enum Error {
|
||||
pub(crate) fn run() -> Result<()> {
|
||||
let cli = Cli::parse();
|
||||
|
||||
// TODO: Remove, this is only needed for Portal connections and the GUI process doesn't connect to the Portal. Unless it's also needed for update checks.
|
||||
rustls::crypto::ring::default_provider()
|
||||
.install_default()
|
||||
.expect("Calling `install_default` only once per process should always succeed");
|
||||
|
||||
Reference in New Issue
Block a user