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:
Reactor Scram
2024-10-15 10:29:11 -05:00
committed by GitHub
parent dbe618c080
commit 786fbc6689
17 changed files with 35 additions and 8497 deletions

View File

@@ -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
View File

@@ -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

View File

@@ -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:

View File

@@ -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

View File

@@ -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"

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -12,7 +12,6 @@ members = [
"gui-client/src-common",
"gui-client/src-tauri",
"headless-client",
"iced-client",
"ip-packet",
"logging",
"phoenix-channel",

File diff suppressed because it is too large Load Diff

View File

@@ -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"

View File

@@ -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.

View File

@@ -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

View File

@@ -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()
}

View File

@@ -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: &gtk::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)
}

View File

@@ -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"

View File

@@ -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

View File

@@ -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),
},
}
}
}