Compare commits

...
Sign in to create a new pull request.

6 commits

Author SHA1 Message Date
dav
e12312d26f Update Flatpak Github Workflow 2024-01-24 18:10:01 -08:00
dav
6cde50daf3 Overhaul Meson / Flatpak Building 2024-01-24 18:07:56 -08:00
David Zhong
1b98054024
Merge branch 'main' into gtk-devel 2024-01-24 15:51:53 -08:00
dav
f7f59fd24d GTK App reactive switch and better UI 2024-01-06 10:55:50 -08:00
dav
29eedb7e9a Intial GTK, swtich to Relm, basic Flatpak Build 2023-11-18 12:30:45 -08:00
reesericci
7eec6e73c4 Initialized burrow-gtk project 2023-11-11 11:59:35 -08:00
31 changed files with 1571 additions and 665 deletions

View file

@ -15,5 +15,5 @@ jobs:
- uses: flatpak/flatpak-github-actions/flatpak-builder@v6 - uses: flatpak/flatpak-github-actions/flatpak-builder@v6
with: with:
bundle: Burrow.flatpak bundle: Burrow.flatpak
manifest-path: burrow-gtk/com.hackclub.burrow.devel.json manifest-path: burrow-gtk/build-aux/com.hackclub.burrow.devel.json
cache-key: flatpak-builder-${{ github.sha }} cache-key: flatpak-builder-${{ github.sha }}

View file

@ -0,0 +1,2 @@
[target.'cfg(unix)']
runner = "sh -c"

1259
burrow-gtk/Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -6,7 +6,12 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
relm4 = { version = "0.6.2", features = ["libadwaita"] } anyhow = "1.0"
relm4-components = "0.6.2" relm4 = { features = ["libadwaita", "gnome_45"], git = "https://github.com/Relm4/Relm4" }
relm4-icons = { version = "0.6.0", features = ["plus"] }
burrow = { version = "*", path = "../burrow/" } burrow = { version = "*", path = "../burrow/" }
tokio = { version = "1.35.0", features = ["time", "sync"] }
gettext-rs = { version = "0.7.0", features = ["gettext-system"] }
[build-dependencies]
anyhow = "1.0"
glib-build-tools = "0.18.0"

View file

@ -40,15 +40,12 @@
"name" : "burrow-gtk", "name" : "burrow-gtk",
"builddir" : true, "builddir" : true,
"subdir" : "burrow-gtk", "subdir" : "burrow-gtk",
"buildsystem" : "simple", "buildsystem" : "meson",
"build-commands": [ "config-opts": ["--buildtype=debug"],
"cargo build",
"install -Dm755 -t /app/bin target/debug/burrow-gtk"
],
"sources" : [ "sources" : [
{ {
"type": "dir", "type": "dir",
"path": "../" "path": "../../"
} }
] ]
} }

View file

@ -40,15 +40,12 @@
"name" : "burrow-gtk", "name" : "burrow-gtk",
"builddir" : true, "builddir" : true,
"subdir" : "burrow-gtk", "subdir" : "burrow-gtk",
"buildsystem" : "simple", "buildsystem" : "meson",
"build-commands": [ "config-opts": ["--buildtype=release"],
"cargo build --release",
"install -Dm755 -t /app/bin target/release/burrow-gtk"
],
"sources" : [ "sources" : [
{ {
"type": "dir", "type": "dir",
"path": "../" "path": "../../"
} }
] ]
} }

16
burrow-gtk/build.rs Normal file
View file

@ -0,0 +1,16 @@
use anyhow::Result;
fn main() -> Result<()> {
compile_gresources()?;
Ok(())
}
fn compile_gresources() -> Result<()> {
glib_build_tools::compile_resources(
&["data"],
"data/resources.gresource.xml",
"compiled.gresource",
);
Ok(())
}

View file

@ -1,7 +1,7 @@
[Desktop Entry] [Desktop Entry]
Name=Burrow Name=@APP_NAME_CAPITALIZED@
Exec=burrow-gtk Exec=@APP_NAME@
Icon=com.hackclub.burrow Icon=@APP_ID@
Terminal=false Terminal=false
Type=Application Type=Application
Categories=GTK;Network Categories=GTK;Network

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<schemalist gettext-domain="@APP_NAME@">
<schema id="@APP_ID" path="@APP_IDPATH@">
</schema>
</schemalist>

View file

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<component type="desktop">
<id>@APP_ID@</id>
<metadata_license>CC0</metadata_license>
<project_license>GPL-3.0-or-later</project_license>
<name translatable="no">@APP_NAME_CAPITALIZED@</name>
<launchable type="desktop-id">@APP_ID@.desktop</launchable>
<description>
<p>No description</p>
</description>
<summary>
<p>No Summary</p>
</summary>
</component>

View file

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<component type="desktop">
<id>com.hackclub.burrow.desktop</id>
<project_license>GPL-3.0-or-later</project_license>
<description>
<p>No description</p>
</description>
</component>

View file

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<schemalist gettext-domain="burrow-gtk">
<schema id="com.hackclub.burrow" path="/com/hackclub/burrow/">
</schema>
</schemalist>

View file

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Before After
Before After

View file

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><g color="#000" fill="#2e3436"><path d="M7.188 2.281c-.094.056-.192.125-.29.19L5.566 3.803a1.684 1.684 0 11-2.17 2.17L2.332 7.037c.506-.069 1.017-.136 1.2.026.242.214.139 1.031.155 1.656.213.088.427.171.657.219.04.008.085-.007.125 0 .337-.525.683-1.288 1-1.344.322-.057.905.562 1.406.937a3.67 3.67 0 00.656-.468c-.195-.595-.594-1.369-.437-1.657.158-.29 1.019-.37 1.625-.531.028-.183.062-.371.062-.562 0-.075-.027-.146-.031-.22-.587-.217-1.435-.385-1.562-.687-.128-.302.34-1.021.593-1.593a3.722 3.722 0 00-.593-.532zm3.875 3.25c-.165.475-.305 1.086-.47 1.563-.43.047-.84.14-1.218.312-.38-.322-.787-.773-1.156-1.093a5.562 5.562 0 00-.688.468c.177.46.453 1.001.625 1.469-.298.309-.531.67-.719 1.063-.494 0-1.102-.084-1.593-.094a5.68 5.68 0 00-.219.812c.435.24 1.006.468 1.438.72-.006.093-.032.185-.032.28 0 .333.049.66.125.97-.382.304-.898.63-1.28.937.015.044.04.083.058.127l.613.613c.417-.1.868-.223 1.266-.303.248.343.532.626.875.875-.027.135-.068.283-.104.428.174-.063.34-.155.482-.297l1.432-1.432a1.994 1.994 0 01.533-3.918c.919 0 1.684.623 1.918 1.467l1.338-1.338c.06-.06.11-.124.156-.191-.035-.062-.06-.13-.1-.188.096-.152.205-.31.315-.47.017-.348-.1-.7-.37-.971l-.177-.176c-.28.192-.561.387-.83.555-.345-.233-.746-.383-1.156-.5-.077-.507-.107-1.132-.187-1.625a5.44 5.44 0 00-.875-.063zm-9.247.608c-.087.068-.173.138-.254.205l.014.035z" style="marker:none" overflow="visible"/><path d="M8.707.293a1 1 0 00-1.415 0l-6.999 7a1 1 0 000 1.413l7 7.001a1 1 0 001.415 0l7-7a1 1 0 000-1.413zm-.708 2.121l5.587 5.587L8 13.586 2.414 7.999z" style="line-height:normal;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000;text-transform:none;text-orientation:mixed;shape-padding:0;isolation:auto;mix-blend-mode:normal;marker:none" font-weight="400" font-family="sans-serif" overflow="visible"/></g></svg> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><g color="#000" fill="#2e3436"><path d="M7.188 2.281c-.094.056-.192.125-.29.19L5.566 3.803a1.684 1.684 0 11-2.17 2.17L2.332 7.037c.506-.069 1.017-.136 1.2.026.242.214.139 1.031.155 1.656.213.088.427.171.657.219.04.008.085-.007.125 0 .337-.525.683-1.288 1-1.344.322-.057.905.562 1.406.937a3.67 3.67 0 00.656-.468c-.195-.595-.594-1.369-.437-1.657.158-.29 1.019-.37 1.625-.531.028-.183.062-.371.062-.562 0-.075-.027-.146-.031-.22-.587-.217-1.435-.385-1.562-.687-.128-.302.34-1.021.593-1.593a3.722 3.722 0 00-.593-.532zm3.875 3.25c-.165.475-.305 1.086-.47 1.563-.43.047-.84.14-1.218.312-.38-.322-.787-.773-1.156-1.093a5.562 5.562 0 00-.688.468c.177.46.453 1.001.625 1.469-.298.309-.531.67-.719 1.063-.494 0-1.102-.084-1.593-.094a5.68 5.68 0 00-.219.812c.435.24 1.006.468 1.438.72-.006.093-.032.185-.032.28 0 .333.049.66.125.97-.382.304-.898.63-1.28.937.015.044.04.083.058.127l.613.613c.417-.1.868-.223 1.266-.303.248.343.532.626.875.875-.027.135-.068.283-.104.428.174-.063.34-.155.482-.297l1.432-1.432a1.994 1.994 0 01.533-3.918c.919 0 1.684.623 1.918 1.467l1.338-1.338c.06-.06.11-.124.156-.191-.035-.062-.06-.13-.1-.188.096-.152.205-.31.315-.47.017-.348-.1-.7-.37-.971l-.177-.176c-.28.192-.561.387-.83.555-.345-.233-.746-.383-1.156-.5-.077-.507-.107-1.132-.187-1.625a5.44 5.44 0 00-.875-.063zm-9.247.608c-.087.068-.173.138-.254.205l.014.035z" style="marker:none" overflow="visible"/><path d="M8.707.293a1 1 0 00-1.415 0l-6.999 7a1 1 0 000 1.413l7 7.001a1 1 0 001.415 0l7-7a1 1 0 000-1.413zm-.708 2.121l5.587 5.587L8 13.586 2.414 7.999z" style="line-height:normal;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000;text-transform:none;text-orientation:mixed;shape-padding:0;isolation:auto;mix-blend-mode:normal;marker:none" font-weight="400" font-family="sans-serif" overflow="visible"/></g></svg>

Before

Width:  |  Height:  |  Size: 2 KiB

After

Width:  |  Height:  |  Size: 2 KiB

Before After
Before After

View file

@ -1,13 +0,0 @@
application_id = 'com.hackclub.burrow'
scalable_dir = join_paths('hicolor', 'scalable', 'apps')
install_data(
join_paths(scalable_dir, ('@0@.svg').format(application_id)),
install_dir: join_paths(get_option('datadir'), 'icons', scalable_dir)
)
symbolic_dir = join_paths('hicolor', 'symbolic', 'apps')
install_data(
join_paths(symbolic_dir, ('@0@-symbolic.svg').format(application_id)),
install_dir: join_paths(get_option('datadir'), 'icons', symbolic_dir)
)

View file

@ -1,39 +1,90 @@
# app.desktop.in.in
desktop_conf = configuration_data()
desktop_conf.set('APP_ID', app_id)
desktop_conf.set('APP_NAME', app_name)
desktop_conf.set('APP_NAME_CAPITALIZED', app_name_capitalized)
desktop_file_in = configure_file(
input: 'app.desktop.in.in',
output: '@BASENAME@',
configuration: desktop_conf,
)
desktop_file = i18n.merge_file( desktop_file = i18n.merge_file(
input: 'com.hackclub.burrow.desktop.in', input: desktop_file_in,
output: 'com.hackclub.burrow.desktop', output: app_id + '.desktop',
type: 'desktop', type: 'desktop',
po_dir: '../po', po_dir: '../po',
install: true, install: true,
install_dir: join_paths(get_option('datadir'), 'applications') install_dir: datadir / 'applications',
) )
desktop_utils = find_program('desktop-file-validate', required: false) if desktop_file_validate.found()
if desktop_utils.found() test(
test('Validate desktop file', desktop_utils, args: [desktop_file]) 'validate-desktop',
desktop_file_validate,
args: [desktop_file],
)
endif endif
appstream_file = i18n.merge_file( # app.gschema.xml.in
input: 'com.hackclub.burrow.appdata.xml.in', gschema_conf = configuration_data()
output: 'com.hackclub.burrow.appdata.xml', gschema_conf.set('APP_ID', app_id)
po_dir: '../po', gschema_conf.set('APP_NAME', app_name)
install: true, gschema_conf.set('APP_IDPATH', app_idpath)
install_dir: join_paths(get_option('datadir'), 'appdata') gschema_file = configure_file(
input: 'app.gschema.xml.in',
output: app_id + '.gschema.xml',
configuration: gschema_conf,
install: true,
install_dir: datadir / 'glib-2.0' / 'schemas',
)
if glib_compile_schemas.found()
test(
'validate-gschema',
glib_compile_schemas,
args: [
'--dry-run',
datadir / 'glib-2.0' / 'schemas',
],
)
endif
# app.metainfo.xml.in
appdata_conf = configuration_data()
appdata_conf.set('APP_ID', app_id)
appdata_conf.set('APP_NAME', app_name)
appdata_conf.set('APP_NAME_CAPITALIZED', app_name_capitalized)
appdata_file_in = configure_file(
input: 'app.metainfo.xml.in',
output: '@BASENAME@',
configuration: appdata_conf,
)
appdata_file = i18n.merge_file(
input: appdata_file_in,
output: app_id + '.metainfo.xml',
po_dir: '../po',
install: true,
install_dir: datadir / 'metainfo',
) )
appstream_util = find_program('appstream-util', required: false)
if appstream_util.found() if appstream_util.found()
test('Validate appstream file', appstream_util, args: ['validate', appstream_file]) test(
'validate-appdata',
appstream_util,
args: ['validate', '--nonet', appdata_file],
)
endif endif
install_data('com.hackclub.burrow.gschema.xml', install_data(
install_dir: join_paths(get_option('datadir'), 'glib-2.0/schemas') 'icons/hicolor/scalable/apps/' + app_name + '.svg',
install_dir: datadir / 'icons' / 'hicolor' / 'scalable' / 'apps',
rename: app_id + '.svg',
) )
compile_schemas = find_program('glib-compile-schemas', required: false) install_data(
if compile_schemas.found() 'icons/hicolor/symbolic/apps/' + app_name + '-symbolic.svg',
test('Validate schema file', install_dir: datadir / 'icons' / 'hicolor' / 'symbolic' / 'apps',
compile_schemas, rename: app_id + '-symbolic.svg',
args: ['--strict', '--dry-run', meson.current_source_dir()]) )
endif
subdir('icons')

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<gresources>
<gresource prefix="/com/hackclub/burrow">
</gresource>
</gresources>

56
burrow-gtk/meson.build Normal file
View file

@ -0,0 +1,56 @@
project(
'burrow-gtk',
['rust'],
version: '0.0.1',
meson_version: '>= 1.0',
)
# Find Cargo
cargo_bin = find_program('cargo')
cargo_env = ['CARGO_HOME=' + meson.project_build_root()]
cargo_opt = ['--manifest-path', meson.project_source_root() / 'Cargo.toml']
cargo_opt += ['--target-dir', meson.project_build_root() / 'target']
# Config
prefix = get_option('prefix')
datadir = prefix / get_option('datadir')
localedir = prefix / get_option('localedir')
app_name = 'burrow-gtk'
app_name_capitalized = 'Burrow'
base_id = 'com.hackclub.burrow'
app_idpath = '/com/hackclub/' + app_name + '/'
if get_option('buildtype') == 'release'
cargo_opt += ['--release']
rust_target = 'release'
app_id = base_id
else
rust_target = 'debug'
app_id = base_id + '-' + 'devel'
endif
# Imports
i18n = import('i18n')
gnome = import('gnome')
# External Dependencies
dependency('gtk4', version: '>= 4.12')
dependency('libadwaita-1', version: '>= 1.4')
glib_compile_resources = find_program('glib-compile-resources', required: true)
glib_compile_schemas = find_program('glib-compile-schemas', required: true)
desktop_file_validate = find_program('desktop-file-validate', required: false)
appstream_util = find_program('appstream-util', required: false)
fc_cache = find_program('fc-cache', required: false)
# Our Sources
subdir('po')
subdir('data')
subdir('src')
# Gnome Post Install
gnome.post_install(
glib_compile_schemas: true,
gtk_update_icon_cache: true,
update_desktop_database: true,
)

View file

@ -1,4 +1 @@
data/com.hackclub.Burrow.desktop.in data/app.desktop.in.in
data/com.hackclub.Burrow.appdata.xml.in
data/com.hackclub.Burrow.gschema.xml
src/window.ui

View file

@ -1 +1 @@
i18n.gettext('burrow-gtk', preset: 'glib') i18n.gettext(app_name, preset: 'glib')

1
burrow-gtk/src/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
config.rs

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);
}
}
}

View file

@ -0,0 +1,8 @@
#[allow(unused)]
pub const ID: &str = @ID@;
#[allow(unused)]
pub const VERSION: &str = @VERSION@;
#[allow(unused)]
pub const LOCALEDIR: &str = @LOCALEDIR@;
#[allow(unused)]
pub const GETTEXT_PACKAGE: &str = @GETTEXT_PACKAGE@;

80
burrow-gtk/src/diag.rs Normal file
View file

@ -0,0 +1,80 @@
use super::*;
use std::{fmt::Display, fs, process::Command};
const SYSTEMD_SOCKET_LOC: &str = "/etc/systemd/system/burrow.socket";
const SYSTEMD_SERVICE_LOC: &str = "/etc/systemd/system/burrow.service";
// I don't like this type very much.
#[derive(Debug, Clone, Copy)]
pub enum StatusTernary {
True,
False,
NA,
}
// Realistically, we may not explicitly "support" non-systemd platforms which would simply this
// code greatly.
// Along with replacing [`StatusTernary`] with good old [`bool`].
#[derive(Debug, Clone, Copy)]
pub enum SystemSetup {
Systemd,
Other,
}
impl SystemSetup {
pub fn new() -> Self {
if Command::new("systemctl").arg("--version").output().is_ok() {
SystemSetup::Systemd
} else {
SystemSetup::Other
}
}
pub fn is_service_installed(&self) -> Result<StatusTernary> {
match self {
SystemSetup::Systemd => Ok(fs::metadata(SYSTEMD_SERVICE_LOC).is_ok().into()),
SystemSetup::Other => Ok(StatusTernary::NA),
}
}
pub fn is_socket_installed(&self) -> Result<StatusTernary> {
match self {
SystemSetup::Systemd => Ok(fs::metadata(SYSTEMD_SOCKET_LOC).is_ok().into()),
SystemSetup::Other => Ok(StatusTernary::NA),
}
}
pub fn is_socket_enabled(&self) -> Result<StatusTernary> {
match self {
SystemSetup::Systemd => {
let output = Command::new("systemctl")
.arg("is-enabled")
.arg("burrow.socket")
.output()?
.stdout;
let output = String::from_utf8(output)?;
Ok((output == "enabled\n").into())
}
SystemSetup::Other => Ok(StatusTernary::NA),
}
}
}
impl From<bool> for StatusTernary {
fn from(value: bool) -> Self {
if value {
StatusTernary::True
} else {
StatusTernary::False
}
}
}
impl Display for SystemSetup {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
SystemSetup::Systemd => "Systemd",
SystemSetup::Other => "Other",
})
}
}

View file

@ -1,87 +1,11 @@
use adw::prelude::*; use anyhow::Result;
use burrow::{DaemonClient, DaemonCommand, DaemonStartOptions};
use gtk::Align;
use relm4::{
component::{AsyncComponent, AsyncComponentParts, AsyncComponentSender},
prelude::*,
};
struct App {} pub mod components;
mod diag;
#[derive(Debug)] // Generated using meson
enum Msg { mod config;
Start,
Stop,
}
#[relm4::component(async)]
impl AsyncComponent for App {
type Init = ();
type Input = Msg;
type Output = ();
type CommandOutput = ();
view! {
adw::Window {
set_title: Some("Simple app"),
set_default_size: (640, 480),
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_spacing: 5,
set_margin_all: 5,
set_valign: Align::Center,
gtk::Label {
set_label: "Burrow GTK Switch",
},
gtk::Switch {
set_halign: Align::Center,
set_hexpand: false,
set_vexpand: false,
connect_active_notify => move |switch|
sender.input(if switch.is_active() { Msg::Start } else { Msg::Stop })
},
}
}
}
async fn init(
_: Self::Init,
root: Self::Root,
sender: AsyncComponentSender<Self>,
) -> AsyncComponentParts<Self> {
let model = App {};
let widgets = view_output!();
AsyncComponentParts { model, widgets }
}
async fn update(
&mut self,
msg: Self::Input,
_sender: AsyncComponentSender<Self>,
_root: &Self::Root,
) {
match msg {
Msg::Start => {
let mut client = DaemonClient::new().await.unwrap();
client
.send_command(DaemonCommand::Start(DaemonStartOptions::default()))
.await
.unwrap();
}
Msg::Stop => {
let mut client = DaemonClient::new().await.unwrap();
client.send_command(DaemonCommand::Stop).await.unwrap();
}
}
}
}
fn main() { fn main() {
let app = RelmApp::new("com.hackclub.burrow"); components::App::run();
app.run_async::<App>(());
} }

View file

@ -0,0 +1,34 @@
# config.rs.in
global_conf = configuration_data()
global_conf.set_quoted('ID', app_id)
global_conf.set_quoted('VERSION', meson.project_version())
global_conf.set_quoted('LOCALEDIR', localedir)
global_conf.set_quoted('GETTEXT_PACKAGE', app_name)
config = configure_file(
input: 'config.rs.in',
output: 'config.rs',
configuration: global_conf,
)
run_command(
'cp',
meson.project_build_root() / 'src' / 'config.rs',
meson.project_source_root() / 'src',
check: true,
)
# Cargo Build
cargo_build = custom_target(
'cargo-build',
build_by_default: true,
build_always_stale: true,
output: meson.project_name(),
console: true,
install: true,
install_dir: get_option('bindir'),
command: [
'env', cargo_env,
cargo_bin, 'build',
cargo_opt, '&&', 'cp', 'target' / rust_target / meson.project_name(), '@OUTPUT@',
]
)