sysd-manager 2.19.2

Application to empower user to manage their <b>systemd units</b> via Graphical User Interface. Not only are you able to make changes to the enablement and running status of each of the units, but you will also be able to view and modify their unit files and check the journal logs.
extern crate dotenv;
extern crate gtk;
extern crate systemd;
extern crate tracing;

mod analyze;
mod consts;
mod errors;
mod systemd_gui;
mod utils;
mod widget;

use crate::systemd_gui::set_is_dark;
use adw::prelude::AdwApplicationExt;
use base::enums::UnitDBusLevel;
use base::{RunMode, consts::APP_ID};
use clap::{Parser, Subcommand};
use dotenv::dotenv;
use gettextrs::gettext;
use gio::glib::translate::FromGlib;
use gtk::{
    gdk,
    gio::{self},
    glib,
    prelude::*,
};
use std::env;
use systemd::data::UnitInfo;
use systemd_gui::new_settings;
use tracing::{debug, error, info, warn};
use tracing_subscriber::EnvFilter;
use widget::{
    app_window::{AppWindow, menu},
    preferences::{
        PreferencesDialog,
        data::{DbusLevel, KEY_PREF_PREFERRED_COLOR_SCHEME, PREFERENCES},
    },
};

const DOMAIN_NAME: &str = "sysd-manager";
fn main() -> glib::ExitCode {
    #[cfg(all(feature = "flatpak", feature = "appimage"))]
    compile_error!("Features flatpack and appimage can't be combined");

    dotenv().ok();

    let timer = tracing_subscriber::fmt::time::ChronoLocal::new("%Y-%m-%d %H:%M:%S%.3f".to_owned());
    tracing_subscriber::fmt()
        .with_timer(timer)
        .with_line_number(true)
        .with_env_filter(EnvFilter::from_default_env())
        .with_ansi(true)
        .init();

    let (unit, command, level, run_mode) = handle_args();

    #[allow(clippy::single_match)]
    match command {
        Some(Command::Test { test }) => {
            info!("End test");

            systemd::runtime().block_on(async move {
                systemd::test(&test, level).await;
            });

            return gtk::glib::ExitCode::SUCCESS;
        }

        _ => {}
    }

    info!("LANGUAGE {:?}", env::var("LANGUAGE"));
    let textdomain_dir = env::var("TEXTDOMAINDIR");
    info!("env TEXTDOMAINDIR {textdomain_dir:?}");
    let locale_dir = if let Ok(domain_dir) = textdomain_dir {
        domain_dir
    } else {
        #[cfg(feature = "flatpak")]
        let domain_dir = "/app/share/locale".to_owned();

        #[cfg(not(feature = "flatpak"))]
        let domain_dir = "/usr/share/locale".to_owned();

        domain_dir
    };

    gettextrs::setlocale(gettextrs::LocaleCategory::LcAll, "");

    // Set up gettext translations
    let path =
        gettextrs::bindtextdomain(DOMAIN_NAME, locale_dir).expect("Unable to bind the text domain");
    info!("bindtextdomain path {path:?}");

    match gettextrs::bind_textdomain_codeset(DOMAIN_NAME, "UTF-8") {
        Ok(v) => info!("bind_textdomain_codeset {v:?}"),
        Err(error) => error!("Unable to set the text domain encoding Error: {error:?}"),
    }

    // Specify the name of the .mo file to use.
    match gettextrs::textdomain(DOMAIN_NAME) {
        Ok(v) => info!("textdomain {:?}", String::from_utf8_lossy(&v)),
        Err(error) => error!("Unable to switch to the text domain Error: {error:?}"),
    }

    // Ask gettext for UTF-8 strings. THIS CRATE CAN'T HANDLE NON-UTF-8 DATA!
    if let Err(error) = gettextrs::bind_textdomain_codeset(DOMAIN_NAME, "UTF-8") {
        error!("bind_textdomain_codeset Error: {error:?}");
    };

    info!("Program starting up");
    // Just a simple log that it's all ok. Need to set env RUST_LOG="info" to see it
    info!("{}", gettext("Program starting up"));

    #[cfg(feature = "flatpak")]
    info!("Flatpak version");

    #[cfg(feature = "appimage")]
    info!("AppImage version");

    debug!("Run mode: {:?}", run_mode);

    //systemd::init(run_mode);

    if let Err(e) = gio::resources_register_include!("sysd-manager.gresource") {
        warn!("Failed to register resources. Error: {e:?}");
    }

    // Create a new application
    let app = adw::Application::builder().application_id(APP_ID).build();

    app.connect_startup(|application| {
        let style_manager = application.style_manager();
        menu::on_startup(application);

        let settings = new_settings();
        let preferred_color_scheme_id = settings.get::<i32>(KEY_PREF_PREFERRED_COLOR_SCHEME);
        let preferred_color_scheme: adw::ColorScheme =
            unsafe { adw::ColorScheme::from_glib(preferred_color_scheme_id) };

        info!("id {preferred_color_scheme_id:?} color {preferred_color_scheme:?}");
        style_manager.set_color_scheme(preferred_color_scheme);
        load_css(&style_manager);
    });

    app.connect_activate(move |application| {
        build_ui(application, unit.as_ref());

        //Start the Proxy after the app is loaded
        #[cfg(not(any(feature = "flatpak", feature = "appimage")))]
        crate::systemd::runtime().spawn(async move {
            systemd::init_proxy_async(run_mode).await;
        });
    });

    //to not transfer args to gtk4
    app.run_with_args::<String>(&[])
}

/// Load the CSS file and add it to the provider
fn load_css(style_manager: &adw::StyleManager) {
    let resource = css_resource_light_dark(style_manager.is_dark());

    load_css_ress(resource);
}

fn css_resource_light_dark(is_dark: bool) -> &'static str {
    if is_dark {
        "/io/github/plrigaux/sysd-manager/style_dark.css"
    } else {
        "/io/github/plrigaux/sysd-manager/style.css"
    }
}

fn load_css_ress(resource: &str) {
    // Load the CSS file and add it to the provider

    let provider = gtk::CssProvider::new();
    provider.load_from_resource(resource);

    // Add the provider to the default screen
    gtk::style_context_add_provider_for_display(
        &gdk::Display::default().expect("Could not connect to a display."),
        &provider,
        gtk::STYLE_PROVIDER_PRIORITY_APPLICATION,
    );
}

fn build_ui(application: &adw::Application, unit: Option<&UnitInfo>) {
    let window = AppWindow::new(application);

    let style_manager = application.style_manager();

    {
        let window = window.clone();

        let is_dark = style_manager.is_dark();
        info!("is dark {is_dark}");

        set_is_dark(is_dark);
        window.set_inter_message(&widget::InterPanelMessage::IsDark(is_dark));

        style_manager.connect_dark_notify(move |style_manager: &adw::StyleManager| {
            let is_dark = style_manager.is_dark();
            info!("is dark {is_dark}");

            let resource = css_resource_light_dark(is_dark);
            load_css_ress(resource);
            window.set_inter_message(&widget::InterPanelMessage::IsDark(is_dark));
            set_is_dark(is_dark);
        });
    }

    window.set_unit(unit);

    window.present();

    if PREFERENCES.is_app_first_connection() {
        info!("Is application first connection");

        let pdialog = PreferencesDialog::new(Some(&window));

        adw::prelude::AdwDialogExt::present(&pdialog, Some(&window));
    }
}

/// A GUI interface to manage systemd units
#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]
struct Args {
    /// Name of the unit
    #[arg()]
    unit: Option<String>,

    /// Specify the user session bus
    #[arg(short, long)]
    user: bool,

    /// Specify the system session bus (This is the implied default)
    #[arg(short, long)]
    system: bool,

    /// Development mode (uses dev proxy service)
    #[arg(short, long, default_value_t = false)]
    dev: bool,

    /// Normal mode (uses normal proxy service)
    #[arg(short, long, default_value_t = false)]
    normal: bool,

    #[command(subcommand)]
    command: Option<Command>,
}

/// Doc comment
#[derive(Subcommand, Debug, Clone, PartialEq)]
enum Command {
    /// Install Flatpak proxy files
    #[cfg(feature = "flatpak")]
    Install,

    /// Test some api call
    Test {
        #[arg( default_value_t = ("null").to_string())]
        test: String,
    },

    /// Run has proxy (used in flatpak distrubution)
    #[cfg(feature = "flatpak")]
    Proxy,
}

fn handle_args() -> (Option<UnitInfo>, Option<Command>, UnitDBusLevel, RunMode) {
    let args = Args::parse();

    let run_mode = RunMode::from_flags(args.dev, args.normal);

    let current_level = PREFERENCES.dbus_level();

    debug!("Current level: {current_level:?}");
    let (app_level, unit_level) = match (args.system, args.user) {
        (true, _) => (DbusLevel::System, UnitDBusLevel::System),
        (false, true) => (DbusLevel::UserSession, UnitDBusLevel::UserSession),
        (false, false) => (current_level, UnitDBusLevel::System),
    };

    let settings = new_settings();
    PREFERENCES.set_and_save_dbus_level(app_level, &settings);

    let Some(unit_name) = args.unit else {
        return (None, args.command, unit_level, run_mode);
    };

    match systemd::fetch_unit(unit_level, &unit_name) {
        Ok(unit) => (Some(unit), args.command, unit_level, run_mode),
        Err(e) => {
            warn!("Cli unit: {e:?}");
            (None, None, unit_level, run_mode)
        }
    }
}