Wire Forge-native release infrastructure
Some checks failed
Cache: Publish Nix / Publish Nix Cache (push) Waiting to run
Build Rust / Cargo Test (push) Successful in 4m14s
Build Site / Next.js Build (push) Failing after 6s
Infra: OpenTofu / OpenTofu (grafana) (push) Successful in 5s
Lint Governance / BEP Metadata (push) Successful in 0s
Build: Android / Android Rust Core Stub (push) Failing after 23s

This commit is contained in:
Conrad Kramer 2026-06-07 05:51:12 -07:00
parent 97c569fb35
commit 002bd382e9
199 changed files with 14268 additions and 185 deletions

View file

@ -0,0 +1,515 @@
use super::*;
use anyhow::{Context, Result};
use tokio::time::Duration;
pub struct NetworksScreen {
list_box: gtk::ListBox,
network_status: gtk::Label,
wireguard_text: gtk::TextView,
tailnet_email_entry: gtk::Entry,
tailnet_authority_entry: gtk::Entry,
tailnet_account_entry: gtk::Entry,
tailnet_identity_entry: gtk::Entry,
tailnet_hostname_entry: gtk::Entry,
tailnet_name_entry: gtk::Entry,
custom_authority_switch: gtk::Switch,
tailnet_status: gtk::Label,
tailnet_session_id: Option<String>,
tailnet_running: bool,
}
#[derive(Debug)]
pub enum NetworksScreenMsg {
Refresh,
AddWireGuard,
DiscoverTailnet,
ProbeTailnet,
StartTailnetLogin,
PollTailnetLogin,
CancelTailnetLogin,
AddTailnet,
}
#[relm4::component(pub, async)]
impl AsyncComponent for NetworksScreen {
type Init = ();
type Input = NetworksScreenMsg;
type Output = ();
type CommandOutput = ();
view! {
gtk::ScrolledWindow {
set_vexpand: true,
adw::Clamp {
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_spacing: 12,
set_margin_all: 12,
adw::PreferencesGroup {
set_title: "Stored Networks",
set_description: Some("Networks saved in the Burrow daemon"),
#[name(network_status)]
gtk::Label {
set_xalign: 0.0,
set_wrap: true,
set_label: "Loading networks",
},
#[name(list_box)]
gtk::ListBox {
add_css_class: "boxed-list",
},
gtk::Button {
set_label: "Refresh",
connect_clicked => NetworksScreenMsg::Refresh
},
},
adw::PreferencesGroup {
set_title: "Add WireGuard",
set_description: Some("Import a WireGuard configuration into the daemon"),
gtk::ScrolledWindow {
set_min_content_height: 150,
set_vexpand: false,
#[name(wireguard_text)]
gtk::TextView {
set_monospace: true,
set_wrap_mode: gtk::WrapMode::WordChar,
}
},
gtk::Button {
set_label: "Add WireGuard Network",
connect_clicked => NetworksScreenMsg::AddWireGuard
},
},
adw::PreferencesGroup {
set_title: "Tailnet",
set_description: Some("Discover, probe, sign in, and save Tailnet authorities through the daemon"),
#[name(tailnet_email_entry)]
gtk::Entry {
set_placeholder_text: Some("Email address"),
},
#[name(custom_authority_switch)]
gtk::Switch {
set_halign: Align::Start,
set_active: false,
},
#[name(tailnet_authority_entry)]
gtk::Entry {
set_placeholder_text: Some("Tailnet server URL"),
},
#[name(tailnet_account_entry)]
gtk::Entry {
set_placeholder_text: Some("Account name"),
},
#[name(tailnet_identity_entry)]
gtk::Entry {
set_placeholder_text: Some("Identity name"),
},
#[name(tailnet_hostname_entry)]
gtk::Entry {
set_placeholder_text: Some("Device hostname"),
},
#[name(tailnet_name_entry)]
gtk::Entry {
set_placeholder_text: Some("Tailnet name"),
},
#[name(tailnet_status)]
gtk::Label {
set_xalign: 0.0,
set_wrap: true,
set_selectable: true,
set_label: "Use discovery for the managed Tailscale authority or enter a custom Tailnet server.",
},
gtk::Box {
set_orientation: gtk::Orientation::Horizontal,
set_spacing: 8,
gtk::Button {
set_label: "Discover",
connect_clicked => NetworksScreenMsg::DiscoverTailnet
},
gtk::Button {
set_label: "Probe",
connect_clicked => NetworksScreenMsg::ProbeTailnet
},
gtk::Button {
set_label: "Sign In",
connect_clicked => NetworksScreenMsg::StartTailnetLogin
},
gtk::Button {
set_label: "Cancel",
connect_clicked => NetworksScreenMsg::CancelTailnetLogin
},
},
gtk::Button {
set_label: "Save Tailnet Network",
connect_clicked => NetworksScreenMsg::AddTailnet
},
},
}
}
}
}
async fn init(
_: Self::Init,
root: Self::Root,
sender: AsyncComponentSender<Self>,
) -> AsyncComponentParts<Self> {
let widgets = view_output!();
widgets
.tailnet_authority_entry
.set_text(daemon_api::default_tailnet_authority());
widgets.tailnet_account_entry.set_text("default");
widgets.tailnet_identity_entry.set_text("linux");
let mut model = NetworksScreen {
list_box: widgets.list_box.clone(),
network_status: widgets.network_status.clone(),
wireguard_text: widgets.wireguard_text.clone(),
tailnet_email_entry: widgets.tailnet_email_entry.clone(),
tailnet_authority_entry: widgets.tailnet_authority_entry.clone(),
tailnet_account_entry: widgets.tailnet_account_entry.clone(),
tailnet_identity_entry: widgets.tailnet_identity_entry.clone(),
tailnet_hostname_entry: widgets.tailnet_hostname_entry.clone(),
tailnet_name_entry: widgets.tailnet_name_entry.clone(),
custom_authority_switch: widgets.custom_authority_switch.clone(),
tailnet_status: widgets.tailnet_status.clone(),
tailnet_session_id: None,
tailnet_running: false,
};
model.refresh_networks().await;
AsyncComponentParts { model, widgets }
}
async fn update(
&mut self,
msg: Self::Input,
sender: AsyncComponentSender<Self>,
_root: &Self::Root,
) {
match msg {
NetworksScreenMsg::Refresh => self.refresh_networks().await,
NetworksScreenMsg::AddWireGuard => self.add_wireguard().await,
NetworksScreenMsg::DiscoverTailnet => self.discover_tailnet().await,
NetworksScreenMsg::ProbeTailnet => self.probe_tailnet().await,
NetworksScreenMsg::StartTailnetLogin => self.start_tailnet_login(sender).await,
NetworksScreenMsg::PollTailnetLogin => self.poll_tailnet_login(sender).await,
NetworksScreenMsg::CancelTailnetLogin => self.cancel_tailnet_login().await,
NetworksScreenMsg::AddTailnet => self.add_tailnet().await,
}
}
}
impl NetworksScreen {
async fn refresh_networks(&mut self) {
match daemon_api::list_networks().await {
Ok(networks) => {
self.render_networks(&networks);
self.network_status.set_label(if networks.is_empty() {
"No daemon networks saved yet"
} else {
"Daemon networks are up to date"
});
}
Err(error) => {
self.clear_networks();
self.network_status
.set_label(&format!("Unable to read daemon networks: {error}"));
}
}
}
async fn add_wireguard(&mut self) {
let config = text_view_text(&self.wireguard_text);
if config.trim().is_empty() {
self.network_status
.set_label("Paste a WireGuard configuration before adding a network");
return;
}
match daemon_api::add_wireguard(config).await {
Ok(id) => {
self.network_status
.set_label(&format!("Added WireGuard network #{id}"));
self.wireguard_text.buffer().set_text("");
self.refresh_networks().await;
}
Err(error) => self
.network_status
.set_label(&format!("Unable to add WireGuard network: {error}")),
}
}
async fn discover_tailnet(&mut self) {
let email = self.tailnet_email_entry.text().to_string();
let Ok(email) = daemon_api::require_value(&email, "Email address") else {
self.tailnet_status
.set_label("Enter an email address before discovery");
return;
};
self.tailnet_status.set_label("Discovering Tailnet server");
match daemon_api::discover_tailnet(email).await {
Ok(discovery) => {
self.tailnet_authority_entry.set_text(&discovery.authority);
self.custom_authority_switch.set_active(!discovery.managed);
let issuer = discovery
.oidc_issuer
.map(|issuer| format!(" OIDC issuer: {issuer}."))
.unwrap_or_default();
let kind = if discovery.managed {
"managed authority"
} else {
"custom authority"
};
self.tailnet_status.set_label(&format!(
"Discovered {kind}: {}.{issuer}",
discovery.authority
));
}
Err(error) => self
.tailnet_status
.set_label(&format!("Tailnet discovery failed: {error}")),
}
}
async fn probe_tailnet(&mut self) {
let authority = self.tailnet_authority();
let Ok(authority) = daemon_api::require_value(&authority, "Tailnet server URL") else {
self.tailnet_status
.set_label("Enter a Tailnet server URL before probing");
return;
};
self.tailnet_status.set_label("Checking Tailnet server");
match daemon_api::probe_tailnet(authority).await {
Ok(probe) => {
let detail = probe
.detail
.unwrap_or_else(|| format!("HTTP {}", probe.status_code));
self.tailnet_status
.set_label(&format!("{}: {detail}", probe.summary));
}
Err(error) => self
.tailnet_status
.set_label(&format!("Tailnet probe failed: {error}")),
}
}
async fn start_tailnet_login(&mut self, sender: AsyncComponentSender<Self>) {
let authority = self.tailnet_authority();
let Ok(authority) = daemon_api::require_value(&authority, "Tailnet server URL") else {
self.tailnet_status
.set_label("Enter a Tailnet server URL before sign-in");
return;
};
let account = daemon_api::normalized(&self.tailnet_account_entry.text(), "default");
let identity = daemon_api::normalized(&self.tailnet_identity_entry.text(), "linux");
let hostname = daemon_api::normalized_optional(&self.tailnet_hostname_entry.text());
self.tailnet_status.set_label("Starting Tailnet sign-in");
match daemon_api::start_tailnet_login(authority, account, identity, hostname).await {
Ok(status) => {
self.apply_login_status(&status);
if let Some(auth_url) = status.auth_url.as_deref() {
if let Err(error) = open_auth_url(auth_url) {
self.tailnet_status.set_label(&format!(
"{} Open this URL manually: {auth_url}. Browser launch failed: {error}",
self.tailnet_status.text()
));
}
}
if !status.running {
sender.input(NetworksScreenMsg::PollTailnetLogin);
}
}
Err(error) => self
.tailnet_status
.set_label(&format!("Tailnet sign-in failed: {error}")),
}
}
async fn poll_tailnet_login(&mut self, sender: AsyncComponentSender<Self>) {
let Some(session_id) = self.tailnet_session_id.clone() else {
return;
};
if self.tailnet_running {
return;
}
tokio::time::sleep(Duration::from_secs(1)).await;
match daemon_api::tailnet_login_status(session_id).await {
Ok(status) => {
self.apply_login_status(&status);
if !status.running {
sender.input(NetworksScreenMsg::PollTailnetLogin);
}
}
Err(error) => {
self.tailnet_status
.set_label(&format!("Tailnet sign-in status failed: {error}"));
self.tailnet_session_id = None;
}
}
}
async fn cancel_tailnet_login(&mut self) {
let Some(session_id) = self.tailnet_session_id.clone() else {
self.tailnet_status
.set_label("No Tailnet sign-in is active");
return;
};
match daemon_api::cancel_tailnet_login(session_id).await {
Ok(()) => {
self.tailnet_session_id = None;
self.tailnet_running = false;
self.tailnet_status.set_label("Tailnet sign-in cancelled");
}
Err(error) => self
.tailnet_status
.set_label(&format!("Unable to cancel Tailnet sign-in: {error}")),
}
}
async fn add_tailnet(&mut self) {
let authority = self.tailnet_authority();
let Ok(authority) = daemon_api::require_value(&authority, "Tailnet server URL") else {
self.tailnet_status
.set_label("Enter a Tailnet server URL before saving");
return;
};
if self.tailnet_session_id.is_some() && !self.tailnet_running {
self.tailnet_status
.set_label("Finish browser sign-in before saving this Tailnet network");
return;
}
let account = daemon_api::normalized(&self.tailnet_account_entry.text(), "default");
let identity = daemon_api::normalized(&self.tailnet_identity_entry.text(), "linux");
let hostname = daemon_api::normalized_optional(&self.tailnet_hostname_entry.text());
let tailnet = daemon_api::normalized_optional(&self.tailnet_name_entry.text());
match daemon_api::add_tailnet(authority, account, identity, hostname, tailnet).await {
Ok(id) => {
self.tailnet_status
.set_label(&format!("Saved Tailnet network #{id}"));
self.refresh_networks().await;
}
Err(error) => self
.tailnet_status
.set_label(&format!("Unable to save Tailnet network: {error}")),
}
}
fn apply_login_status(&mut self, status: &daemon_api::TailnetLoginStatus) {
self.tailnet_session_id = Some(status.session_id.clone());
self.tailnet_running = status.running;
let mut parts = Vec::new();
if status.running {
parts.push("Signed in".to_owned());
} else if status.needs_login {
parts.push("Browser sign-in required".to_owned());
} else {
parts.push("Checking sign-in".to_owned());
}
if !status.backend_state.is_empty() {
parts.push(format!("State: {}", status.backend_state));
}
if let Some(tailnet_name) = &status.tailnet_name {
parts.push(format!("Tailnet: {tailnet_name}"));
}
if let Some(self_dns_name) = &status.self_dns_name {
parts.push(self_dns_name.clone());
}
if !status.tailnet_ips.is_empty() {
parts.push(status.tailnet_ips.join(", "));
}
if !status.health.is_empty() {
parts.push(status.health.join(" / "));
}
if let Some(auth_url) = &status.auth_url {
parts.push(format!("Auth URL: {auth_url}"));
}
self.tailnet_status.set_label(&parts.join("\n"));
}
fn tailnet_authority(&self) -> String {
if self.custom_authority_switch.is_active() {
self.tailnet_authority_entry.text().to_string()
} else {
let authority = self.tailnet_authority_entry.text();
if authority.trim().is_empty() {
daemon_api::default_tailnet_authority().to_owned()
} else {
authority.to_string()
}
}
}
fn clear_networks(&self) {
while let Some(child) = self.list_box.first_child() {
self.list_box.remove(&child);
}
}
fn render_networks(&self, networks: &[daemon_api::NetworkSummary]) {
self.clear_networks();
if networks.is_empty() {
let row = adw::ActionRow::builder()
.title("No networks")
.subtitle("Save a WireGuard or Tailnet network to use it here.")
.build();
self.list_box.append(&row);
return;
}
for network in networks {
let title = format!("{} (#{})", network.title, network.id);
let row = adw::ActionRow::builder()
.title(&title)
.subtitle(&network.detail)
.build();
self.list_box.append(&row);
}
}
}
fn text_view_text(text_view: &gtk::TextView) -> String {
let buffer = text_view.buffer();
buffer
.text(&buffer.start_iter(), &buffer.end_iter(), true)
.to_string()
}
fn open_auth_url(url: &str) -> Result<()> {
gtk::gio::AppInfo::launch_default_for_uri(url, None::<&gtk::gio::AppLaunchContext>)
.with_context(|| format!("failed to open {url}"))
}

View file

@ -4,12 +4,10 @@ use std::process::Command;
#[derive(Debug)]
pub struct DaemonGroup {
system_setup: SystemSetup,
daemon_client: Arc<Mutex<Option<DaemonClient>>>,
already_running: bool,
}
pub struct DaemonGroupInit {
pub daemon_client: Arc<Mutex<Option<DaemonClient>>>,
pub system_setup: SystemSetup,
}
@ -30,8 +28,8 @@ impl AsyncComponent for DaemonGroup {
#[name(group)]
adw::PreferencesGroup {
#[watch]
set_sensitive:
(model.system_setup == SystemSetup::AppImage || model.system_setup == SystemSetup::Other) &&
set_sensitive:
(model.system_setup == SystemSetup::AppImage || model.system_setup == SystemSetup::Other) &&
!model.already_running,
set_title: "Local Daemon",
set_description: Some("Run Local Daemon"),
@ -51,8 +49,7 @@ impl AsyncComponent for DaemonGroup {
// Should be impossible to panic here
let model = DaemonGroup {
system_setup: init.system_setup,
daemon_client: init.daemon_client.clone(),
already_running: init.daemon_client.lock().await.is_some(),
already_running: daemon_api::daemon_available().await,
};
let widgets = view_output!();
@ -104,7 +101,7 @@ mv $TEMP /tmp/burrow-detached-daemon"#,
.unwrap();
}
DaemonGroupMsg::DaemonStateChange => {
self.already_running = self.daemon_client.lock().await.is_some();
self.already_running = daemon_api::daemon_available().await;
}
}
}

View file

@ -2,8 +2,6 @@ use super::*;
#[derive(Debug)]
pub struct DiagGroup {
daemon_client: Arc<Mutex<Option<DaemonClient>>>,
system_setup: SystemSetup,
service_installed: StatusTernary,
socket_installed: StatusTernary,
@ -12,14 +10,13 @@ pub struct DiagGroup {
}
pub struct DiagGroupInit {
pub daemon_client: Arc<Mutex<Option<DaemonClient>>>,
pub system_setup: SystemSetup,
}
impl DiagGroup {
async fn new(daemon_client: Arc<Mutex<Option<DaemonClient>>>) -> Result<Self> {
async fn new() -> Result<Self> {
let system_setup = SystemSetup::new();
let daemon_running = daemon_client.lock().await.is_some();
let daemon_running = daemon_api::daemon_available().await;
Ok(Self {
service_installed: system_setup.is_service_installed()?,
@ -27,7 +24,6 @@ impl DiagGroup {
socket_enabled: system_setup.is_socket_enabled()?,
daemon_running,
system_setup,
daemon_client,
})
}
}
@ -95,7 +91,8 @@ impl AsyncComponent for DiagGroup {
sender: AsyncComponentSender<Self>,
) -> AsyncComponentParts<Self> {
// Should be impossible to panic here
let model = DiagGroup::new(init.daemon_client).await.unwrap();
let mut model = DiagGroup::new().await.unwrap();
model.system_setup = init.system_setup;
let widgets = view_output!();
@ -111,7 +108,7 @@ impl AsyncComponent for DiagGroup {
match msg {
DiagGroupMsg::Refresh => {
// Should be impossible to panic here
*self = Self::new(Arc::clone(&self.daemon_client)).await.unwrap();
*self = Self::new().await.unwrap();
}
}
}

View file

@ -6,10 +6,6 @@ pub struct SettingsScreen {
daemon_group: AsyncController<settings::DaemonGroup>,
}
pub struct SettingsScreenInit {
pub daemon_client: Arc<Mutex<Option<DaemonClient>>>,
}
#[derive(Debug, PartialEq, Eq)]
pub enum SettingsScreenMsg {
DaemonStateChange,
@ -17,7 +13,7 @@ pub enum SettingsScreenMsg {
#[relm4::component(pub)]
impl SimpleComponent for SettingsScreen {
type Init = SettingsScreenInit;
type Init = ();
type Input = SettingsScreenMsg;
type Output = ();
@ -27,26 +23,20 @@ impl SimpleComponent for SettingsScreen {
}
fn init(
init: Self::Init,
_init: Self::Init,
root: &Self::Root,
sender: ComponentSender<Self>,
) -> ComponentParts<Self> {
let system_setup = SystemSetup::new();
let diag_group = settings::DiagGroup::builder()
.launch(settings::DiagGroupInit {
system_setup,
daemon_client: Arc::clone(&init.daemon_client),
})
.launch(settings::DiagGroupInit { system_setup })
.forward(sender.input_sender(), |_| {
SettingsScreenMsg::DaemonStateChange
});
let daemon_group = settings::DaemonGroup::builder()
.launch(settings::DaemonGroupInit {
system_setup,
daemon_client: Arc::clone(&init.daemon_client),
})
.launch(settings::DaemonGroupInit { system_setup })
.forward(sender.input_sender(), |_| {
SettingsScreenMsg::DaemonStateChange
});

View file

@ -1,27 +1,22 @@
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>>>,
status_label: gtk::Label,
}
#[derive(Debug, PartialEq, Eq)]
pub enum SwitchScreenMsg {
DaemonReconnect,
DaemonDisconnect,
Refresh,
Start,
Stop,
}
#[relm4::component(pub, async)]
impl AsyncComponent for SwitchScreen {
type Init = SwitchScreenInit;
type Init = ();
type Input = SwitchScreenMsg;
type Output = ();
type CommandOutput = ();
@ -39,7 +34,7 @@ impl AsyncComponent for SwitchScreen {
#[name(setup_banner)]
adw::Banner {
set_title: "Burrow is not running!",
set_title: "Burrow daemon is not reachable",
},
},
@ -52,7 +47,12 @@ impl AsyncComponent for SwitchScreen {
set_vexpand: true,
gtk::Label {
set_label: "Burrow Switch",
set_label: "Burrow Tunnel",
},
#[name(status_label)]
gtk::Label {
set_label: "Checking daemon status",
},
#[name(switch)]
@ -68,91 +68,76 @@ impl AsyncComponent for SwitchScreen {
}
async fn init(
init: Self::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,
let mut model = SwitchScreen {
switch: widgets.switch.clone(),
switch_screen: widgets.switch_screen.clone(),
disconnected_banner: widgets.setup_banner.clone(),
status_label: widgets.status_label.clone(),
};
model.refresh().await;
AsyncComponentParts { model, widgets }
}
async fn update(
&mut self,
msg: Self::Input,
_: AsyncComponentSender<Self>,
_sender: 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;
}
match msg {
SwitchScreenMsg::Refresh => self.refresh().await,
SwitchScreenMsg::Start => {
match daemon_api::start_tunnel().await {
Ok(()) => self.status_label.set_label("Tunnel running"),
Err(error) => self
.status_label
.set_label(&format!("Start failed: {error}")),
}
Self::Input::Stop => {
if let Err(_e) = daemon_client.send_command(DaemonCommand::Stop).await {
disconnected_daemon_client = true;
}
}
_ => {}
self.refresh().await;
}
SwitchScreenMsg::Stop => {
match daemon_api::stop_tunnel().await {
Ok(()) => self.status_label.set_label("Tunnel stopped"),
Err(error) => self
.status_label
.set_label(&format!("Stop failed: {error}")),
}
self.refresh().await;
}
}
}
}
impl SwitchScreen {
async fn refresh(&mut self) {
match daemon_api::tunnel_state().await {
Ok(daemon_api::TunnelState::Running) => {
self.disconnected_banner.set_revealed(false);
self.switch_screen.set_sensitive(true);
self.status_label.set_label("Tunnel running");
self.switch.set_active(true);
}
Ok(daemon_api::TunnelState::Stopped) => {
self.disconnected_banner.set_revealed(false);
self.switch_screen.set_sensitive(true);
self.status_label.set_label("Tunnel stopped");
self.switch.set_active(false);
}
Err(error) => {
self.disconnected_banner.set_revealed(true);
self.switch_screen.set_sensitive(false);
self.status_label
.set_label(&format!("Daemon unavailable: {error}"));
self.switch.set_active(false);
}
} 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);
}
}
}