Implement Gtk Network Status (#165)

Implemented
- Switch reacts to burrow socket and network changes 
- meson as build system
- Basic diagnostics to ensure burrow is installed properly
- Flatpak / Meson Building
This commit is contained in:
David Zhong 2024-01-25 22:10:24 -08:00 committed by GitHub
parent baa81eb939
commit 6990f90c2e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 1571 additions and 665 deletions

View file

@ -0,0 +1,136 @@
use super::*;
use anyhow::Context;
use std::time::Duration;
const RECONNECT_POLL_TIME: Duration = Duration::from_secs(5);
pub struct App {
daemon_client: Arc<Mutex<Option<DaemonClient>>>,
_settings_screen: Controller<settings_screen::SettingsScreen>,
switch_screen: AsyncController<switch_screen::SwitchScreen>,
}
#[derive(Debug)]
pub enum AppMsg {
None,
PostInit,
}
impl App {
pub fn run() {
let app = RelmApp::new(config::ID);
Self::setup_gresources().unwrap();
Self::setup_i18n().unwrap();
app.run_async::<App>(());
}
fn setup_i18n() -> Result<()> {
gettextrs::setlocale(gettextrs::LocaleCategory::LcAll, "");
gettextrs::bindtextdomain(config::GETTEXT_PACKAGE, config::LOCALEDIR)?;
gettextrs::bind_textdomain_codeset(config::GETTEXT_PACKAGE, "UTF-8")?;
gettextrs::textdomain(config::GETTEXT_PACKAGE)?;
Ok(())
}
fn setup_gresources() -> Result<()> {
gtk::gio::resources_register_include!("compiled.gresource")
.context("Failed to register and include compiled gresource.")
}
}
#[relm4::component(pub, async)]
impl AsyncComponent for App {
type Init = ();
type Input = AppMsg;
type Output = ();
type CommandOutput = ();
view! {
adw::Window {
set_title: Some("Burrow"),
set_default_size: (640, 480),
}
}
async fn init(
_: Self::Init,
root: Self::Root,
sender: AsyncComponentSender<Self>,
) -> AsyncComponentParts<Self> {
let daemon_client = Arc::new(Mutex::new(DaemonClient::new().await.ok()));
let switch_screen = switch_screen::SwitchScreen::builder()
.launch(switch_screen::SwitchScreenInit {
daemon_client: Arc::clone(&daemon_client),
})
.forward(sender.input_sender(), |_| AppMsg::None);
let settings_screen = settings_screen::SettingsScreen::builder()
.launch(settings_screen::SettingsScreenInit {
daemon_client: Arc::clone(&daemon_client),
})
.forward(sender.input_sender(), |_| AppMsg::None);
let widgets = view_output!();
let view_stack = adw::ViewStack::new();
view_stack.add_titled(switch_screen.widget(), None, "Switch");
view_stack.add_titled(settings_screen.widget(), None, "Settings");
let view_switcher_bar = adw::ViewSwitcherBar::builder().stack(&view_stack).build();
view_switcher_bar.set_reveal(true);
let toolbar = adw::ToolbarView::new();
toolbar.add_top_bar(
&adw::HeaderBar::builder()
.title_widget(&gtk::Label::new(Some("Burrow")))
.build(),
);
toolbar.add_bottom_bar(&view_switcher_bar);
toolbar.set_content(Some(&view_stack));
root.set_content(Some(&toolbar));
sender.input(AppMsg::PostInit);
let model = App {
daemon_client,
switch_screen,
_settings_screen: settings_screen,
};
AsyncComponentParts { model, widgets }
}
async fn update(
&mut self,
_msg: Self::Input,
_sender: AsyncComponentSender<Self>,
_root: &Self::Root,
) {
loop {
tokio::time::sleep(RECONNECT_POLL_TIME).await;
{
let mut daemon_client = self.daemon_client.lock().await;
let mut disconnected_daemon_client = false;
if let Some(daemon_client) = daemon_client.as_mut() {
if let Err(_e) = daemon_client.send_command(DaemonCommand::ServerInfo).await {
disconnected_daemon_client = true;
self.switch_screen
.emit(switch_screen::SwitchScreenMsg::DaemonDisconnect);
}
}
if disconnected_daemon_client || daemon_client.is_none() {
*daemon_client = DaemonClient::new().await.ok();
if daemon_client.is_some() {
self.switch_screen
.emit(switch_screen::SwitchScreenMsg::DaemonReconnect);
}
}
}
}
}
}

View file

@ -0,0 +1,20 @@
use super::*;
use adw::prelude::*;
use burrow::{DaemonClient, DaemonCommand, DaemonResponseData};
use gtk::Align;
use relm4::{
component::{
AsyncComponent, AsyncComponentController, AsyncComponentParts, AsyncComponentSender,
AsyncController,
},
prelude::*,
};
use std::sync::Arc;
use tokio::sync::Mutex;
mod app;
mod settings;
mod settings_screen;
mod switch_screen;
pub use app::*;

View file

@ -0,0 +1,126 @@
use super::*;
use diag::{StatusTernary, SystemSetup};
#[derive(Debug)]
pub struct DiagGroup {
daemon_client: Arc<Mutex<Option<DaemonClient>>>,
init_system: SystemSetup,
service_installed: StatusTernary,
socket_installed: StatusTernary,
socket_enabled: StatusTernary,
daemon_running: bool,
}
pub struct DiagGroupInit {
pub daemon_client: Arc<Mutex<Option<DaemonClient>>>,
}
impl DiagGroup {
async fn new(daemon_client: Arc<Mutex<Option<DaemonClient>>>) -> Result<Self> {
let setup = SystemSetup::new();
let daemon_running = daemon_client.lock().await.is_some();
Ok(Self {
service_installed: setup.is_service_installed()?,
socket_installed: setup.is_socket_installed()?,
socket_enabled: setup.is_socket_enabled()?,
daemon_running,
init_system: setup,
daemon_client,
})
}
}
#[derive(Debug)]
pub enum DiagGroupMsg {
Refresh,
}
#[relm4::component(pub, async)]
impl AsyncComponent for DiagGroup {
type Init = DiagGroupInit;
type Input = DiagGroupMsg;
type Output = ();
type CommandOutput = ();
view! {
#[name(group)]
adw::PreferencesGroup {
set_title: "Diagnose",
set_description: Some("Diagnose Burrow"),
adw::ActionRow {
#[watch]
set_title: &format!("Init System: {}", model.init_system)
},
adw::ActionRow {
#[watch]
set_title: &format!(
"Service installed: {}",
status_ternary_to_str(model.service_installed)
)
},
adw::ActionRow {
#[watch]
set_title: &format!(
"Socket installed: {}",
status_ternary_to_str(model.socket_installed)
)
},
adw::ActionRow {
#[watch]
set_title: &format!(
"Socket enabled: {}",
status_ternary_to_str(model.socket_enabled)
)
},
adw::ActionRow {
#[watch]
set_title: &format!(
"Daemon running: {}",
if model.daemon_running { "Yes" } else { "No" }
)
},
gtk::Button {
set_label: "Refresh",
connect_clicked => DiagGroupMsg::Refresh
}
}
}
async fn init(
init: Self::Init,
root: Self::Root,
sender: AsyncComponentSender<Self>,
) -> AsyncComponentParts<Self> {
// Should be impossible to panic here
let model = DiagGroup::new(init.daemon_client).await.unwrap();
let widgets = view_output!();
AsyncComponentParts { model, widgets }
}
async fn update(
&mut self,
msg: Self::Input,
_sender: AsyncComponentSender<Self>,
_root: &Self::Root,
) {
match msg {
DiagGroupMsg::Refresh => {
// Should be impossible to panic here
*self = Self::new(Arc::clone(&self.daemon_client)).await.unwrap();
}
}
}
}
fn status_ternary_to_str(status: StatusTernary) -> &'static str {
match status {
StatusTernary::True => "Yes",
StatusTernary::False => "No",
StatusTernary::NA => "N/A",
}
}

View file

@ -0,0 +1,5 @@
use super::*;
mod diag_group;
pub use diag_group::{DiagGroup, DiagGroupInit};

View file

@ -0,0 +1,44 @@
use super::*;
pub struct SettingsScreen {
_diag_group: AsyncController<settings::DiagGroup>,
}
pub struct SettingsScreenInit {
pub daemon_client: Arc<Mutex<Option<DaemonClient>>>,
}
#[relm4::component(pub)]
impl SimpleComponent for SettingsScreen {
type Init = SettingsScreenInit;
type Input = ();
type Output = ();
view! {
#[name(preferences)]
adw::PreferencesPage {}
}
fn init(
init: Self::Init,
root: Self::Root,
sender: ComponentSender<Self>,
) -> ComponentParts<Self> {
let diag_group = settings::DiagGroup::builder()
.launch(settings::DiagGroupInit {
daemon_client: Arc::clone(&init.daemon_client),
})
.forward(sender.input_sender(), |_| ());
let widgets = view_output!();
widgets.preferences.add(diag_group.widget());
let model = SettingsScreen {
_diag_group: diag_group,
};
ComponentParts { model, widgets }
}
fn update(&mut self, _: Self::Input, _sender: ComponentSender<Self>) {}
}

View file

@ -0,0 +1,158 @@
use super::*;
pub struct SwitchScreen {
daemon_client: Arc<Mutex<Option<DaemonClient>>>,
switch: gtk::Switch,
switch_screen: gtk::Box,
disconnected_banner: adw::Banner,
}
pub struct SwitchScreenInit {
pub daemon_client: Arc<Mutex<Option<DaemonClient>>>,
}
#[derive(Debug, PartialEq, Eq)]
pub enum SwitchScreenMsg {
DaemonReconnect,
DaemonDisconnect,
Start,
Stop,
}
#[relm4::component(pub, async)]
impl AsyncComponent for SwitchScreen {
type Init = SwitchScreenInit;
type Input = SwitchScreenMsg;
type Output = ();
type CommandOutput = ();
view! {
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_valign: Align::BaselineFill,
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_spacing: 5,
set_margin_all: 5,
set_valign: Align::Start,
#[name(setup_banner)]
adw::Banner {
set_title: "Burrow is not running!",
},
},
#[name(switch_screen)]
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_spacing: 10,
set_margin_all: 5,
set_valign: Align::Center,
set_vexpand: true,
gtk::Label {
set_label: "Burrow Switch",
},
#[name(switch)]
gtk::Switch {
set_halign: Align::Center,
set_hexpand: false,
set_vexpand: false,
connect_active_notify => move |switch|
sender.input(if switch.is_active() { SwitchScreenMsg::Start } else { SwitchScreenMsg::Stop })
},
}
}
}
async fn init(
init: Self::Init,
root: Self::Root,
sender: AsyncComponentSender<Self>,
) -> AsyncComponentParts<Self> {
let mut initial_switch_status = false;
let mut initial_daemon_server_down = false;
if let Some(daemon_client) = init.daemon_client.lock().await.as_mut() {
if let Ok(res) = daemon_client
.send_command(DaemonCommand::ServerInfo)
.await
.as_ref()
{
initial_switch_status = match res.result.as_ref() {
Ok(DaemonResponseData::None) => false,
Ok(DaemonResponseData::ServerInfo(_)) => true,
_ => false,
};
} else {
initial_daemon_server_down = true;
}
} else {
initial_daemon_server_down = true;
}
let widgets = view_output!();
widgets.switch.set_active(initial_switch_status);
if initial_daemon_server_down {
*init.daemon_client.lock().await = None;
widgets.switch.set_active(false);
widgets.switch_screen.set_sensitive(false);
widgets.setup_banner.set_revealed(true);
}
let model = SwitchScreen {
daemon_client: init.daemon_client,
switch: widgets.switch.clone(),
switch_screen: widgets.switch_screen.clone(),
disconnected_banner: widgets.setup_banner.clone(),
};
AsyncComponentParts { model, widgets }
}
async fn update(
&mut self,
msg: Self::Input,
_: AsyncComponentSender<Self>,
_root: &Self::Root,
) {
let mut disconnected_daemon_client = false;
if let Some(daemon_client) = self.daemon_client.lock().await.as_mut() {
match msg {
Self::Input::Start => {
if let Err(_e) = daemon_client
.send_command(DaemonCommand::Start(Default::default()))
.await
{
disconnected_daemon_client = true;
}
}
Self::Input::Stop => {
if let Err(_e) = daemon_client.send_command(DaemonCommand::Stop).await {
disconnected_daemon_client = true;
}
}
_ => {}
}
} else {
disconnected_daemon_client = true;
}
if msg == Self::Input::DaemonReconnect {
self.disconnected_banner.set_revealed(false);
self.switch_screen.set_sensitive(true);
}
if disconnected_daemon_client || msg == Self::Input::DaemonDisconnect {
*self.daemon_client.lock().await = None;
self.switch.set_active(false);
self.switch_screen.set_sensitive(false);
self.disconnected_banner.set_revealed(true);
}
}
}