From e748e9f905c66ceb3b845950fa95a38f0340cf6f Mon Sep 17 00:00:00 2001 From: Ilya Zlobintsev Date: Mon, 10 Aug 2026 14:16:12 +0300 Subject: [PATCH] feat: enable autostart during the service setup (#1146) --- lact-gui/i18n/en/lact_gui.ftl | 2 ++ lact-gui/src/app.rs | 8 +++-- lact-gui/src/service_setup.rs | 52 ++++++++++++++++++++++++--- lact-gui/src/service_setup/systemd.rs | 17 ++++++--- 4 files changed, 68 insertions(+), 11 deletions(-) diff --git a/lact-gui/i18n/en/lact_gui.ftl b/lact-gui/i18n/en/lact_gui.ftl index a55052ec..ec94d15f 100644 --- a/lact-gui/i18n/en/lact_gui.ftl +++ b/lact-gui/i18n/en/lact_gui.ftl @@ -254,6 +254,8 @@ service-logs = Service Logs service-start = Start service-stop = Stop service-restart = Restart +service-autostart = Autostart on boot +service-autostart-disable = Also disable autostart version-mismatch-description = Version mismatch between GUI and Daemon ({$gui_version}-{$gui_commit} vs {$daemon_version}-{$daemon_commit})! diff --git a/lact-gui/src/app.rs b/lact-gui/src/app.rs index 8c93dbdd..9dded515 100644 --- a/lact-gui/src/app.rs +++ b/lact-gui/src/app.rs @@ -666,10 +666,13 @@ impl AppModel { self.overdrive_dialog.emit(OverdriveDialogMsg::Show); } AppMsg::ShowServiceSetupDialog => { + let (manager_proxy, unit_proxy) = systemd::connect_unit_proxy().await?; + let params = ServiceSetupDialogParams { parent: root.clone().upcast(), initial_client: Ok((self.daemon_client.clone(), self.system_info.clone())), - unit_proxy: systemd::connect_unit_proxy().await?, + manager_proxy, + unit_proxy, }; let controller = ServiceSetupDialog::builder() .launch(params) @@ -1431,10 +1434,11 @@ async fn create_connection( Ok(client) => (client, false), Err(err) => { let configured_client = match connect_unit_proxy().await { - Ok(unit_proxy) => { + Ok((manager_proxy, unit_proxy)) => { let params = ServiceSetupDialogParams { parent: root.clone().upcast(), initial_client: Err(err), + manager_proxy, unit_proxy, }; let service_setup = ServiceSetupDialog::builder().launch(params).into_stream(); diff --git a/lact-gui/src/service_setup.rs b/lact-gui/src/service_setup.rs index 4d010f2e..dd80e7f2 100644 --- a/lact-gui/src/service_setup.rs +++ b/lact-gui/src/service_setup.rs @@ -3,12 +3,13 @@ pub mod systemd; use crate::I18N; use crate::app::components::info_row::{InfoRow, InfoRowExt}; use crate::app::utils::ext::FlowBoxExt; -use crate::service_setup::systemd::{START_MODE_REPLACE, UnitProxy}; +use crate::service_setup::systemd::{ManagerProxy, START_MODE_REPLACE, UNIT_NAME, UnitProxy}; use adw::prelude::*; use anyhow::{Context as _, anyhow}; use i18n_embed_fl::fl; use lact_client::DaemonClient; use lact_schema::{SystemInfo, VersionInfo}; +use relm4::binding::{BoolBinding, ConnectBinding as _}; use relm4::{ AsyncComponentSender, RelmWidgetExt, css::{self, ERROR, SUCCESS, WARNING}, @@ -22,15 +23,19 @@ use tracing::debug; pub struct ServiceSetupDialog { connection_status: ConnectionStatus, + manager_proxy: ManagerProxy<'static>, unit_proxy: UnitProxy<'static>, service_logs: gtk::TextBuffer, service_state: String, + autostart_on_start: BoolBinding, + autostart_on_stop: BoolBinding, setup_error: Option, } pub struct ServiceSetupDialogParams { pub parent: gtk::ApplicationWindow, pub initial_client: anyhow::Result<(DaemonClient, SystemInfo)>, + pub manager_proxy: ManagerProxy<'static>, pub unit_proxy: UnitProxy<'static>, } @@ -188,17 +193,35 @@ impl AsyncComponent for ServiceSetupDialog { set_hexpand: true, set_halign: gtk::Align::End, - gtk::Button { + adw::SplitButton { set_label: &fl!(I18N, "service-start"), connect_clicked => ServiceSetupDialogMsg::StartService, add_css_class: "suggested-action", + + #[wrap(Some)] + set_popover = >k::Popover { + gtk::CheckButton { + set_label: Some(&fl!(I18N, "service-autostart")), + bind: &model.autostart_on_start, + }, + }, + #[watch] set_visible: model.service_state != systemd::UNIT_STATE_ACTIVE, }, - gtk::Button { + adw::SplitButton { set_label: &fl!(I18N, "service-stop"), connect_clicked => ServiceSetupDialogMsg::StopService, + + #[wrap(Some)] + set_popover = >k::Popover { + gtk::CheckButton { + set_label: Some(&fl!(I18N, "service-autostart-disable")), + bind: &model.autostart_on_stop, + }, + }, + #[watch] set_visible: model.service_state == systemd::UNIT_STATE_ACTIVE, }, @@ -253,7 +276,10 @@ impl AsyncComponent for ServiceSetupDialog { let model = Self { connection_status, + manager_proxy: params.manager_proxy, unit_proxy: params.unit_proxy, + autostart_on_start: BoolBinding::new(true), + autostart_on_stop: BoolBinding::new(true), service_logs: gtk::TextBuffer::builder() .text(service_logs_handle.await.unwrap()) .build(), @@ -289,12 +315,30 @@ impl ServiceSetupDialog { match msg { ServiceSetupDialogMsg::Reconnect => (), ServiceSetupDialogMsg::StartService => { - self.unit_proxy.start(START_MODE_REPLACE).await?; + // Note: this order is important, doing it the other way around causes 2 polkit prompts + if self.autostart_on_start.value() { + self.manager_proxy + .enable_unit_files(&[UNIT_NAME], false, true) + .await + .context("could not enable unit")?; + } + + self.unit_proxy + .start(START_MODE_REPLACE) + .await + .context("could not start unit")?; } ServiceSetupDialogMsg::RestartService => { self.unit_proxy.restart(START_MODE_REPLACE).await?; } ServiceSetupDialogMsg::StopService => { + if self.autostart_on_stop.value() { + self.manager_proxy + .disable_unit_files(&[UNIT_NAME], false) + .await + .context("could not disable unit")?; + } + self.unit_proxy.stop(START_MODE_REPLACE).await?; } ServiceSetupDialogMsg::Close => { diff --git a/lact-gui/src/service_setup/systemd.rs b/lact-gui/src/service_setup/systemd.rs index def4ab87..6b14743c 100644 --- a/lact-gui/src/service_setup/systemd.rs +++ b/lact-gui/src/service_setup/systemd.rs @@ -3,13 +3,13 @@ use anyhow::{Context, bail}; use relm4::tokio::process::Command; use zbus::{proxy, zvariant::OwnedObjectPath}; -const UNIT_NAME: &str = "lactd.service"; +pub const UNIT_NAME: &str = "lactd.service"; pub const UNIT_STATE_ACTIVE: &str = "active"; pub const START_MODE_REPLACE: &str = "replace"; -pub async fn connect_unit_proxy() -> anyhow::Result> { +pub async fn connect_unit_proxy() -> anyhow::Result<(ManagerProxy<'static>, UnitProxy<'static>)> { let conn = zbus::Connection::system() .await .context("Could not establish DBus connection")?; @@ -19,7 +19,7 @@ pub async fn connect_unit_proxy() -> anyhow::Result> { .context("Could not connect to systemd manager interface")?; let path = manager - .get_unit(UNIT_NAME) + .load_unit(UNIT_NAME) .await .context("Could not get lact systemd unit")?; @@ -29,7 +29,7 @@ pub async fn connect_unit_proxy() -> anyhow::Result> { .await .context("Could not connect to systemd unit interface")?; - Ok(unit) + Ok((manager, unit)) } #[proxy( @@ -39,7 +39,7 @@ pub async fn connect_unit_proxy() -> anyhow::Result> { )] pub trait Manager { #[zbus(allow_interactive_auth)] - fn get_unit(&self, name: &str) -> zbus::Result; + fn load_unit(&self, name: &str) -> zbus::Result; #[zbus(allow_interactive_auth)] fn enable_unit_files( @@ -48,6 +48,13 @@ pub trait Manager { runtime: bool, force: bool, ) -> zbus::Result<(bool, Vec<(String, String, String)>)>; + + #[zbus(allow_interactive_auth)] + fn disable_unit_files( + &self, + files: &[&str], + runtime: bool, + ) -> zbus::Result>; } #[proxy(