thcon 0.13.2

A configurable theme controller that switches multiple apps between light and dark mode
Documentation
use crate::config::Config as ThconConfig;
use crate::operation::Operation;
use crate::themeable::{ConfigError, ConfigState, Themeable};
use crate::AppConfig;
use crate::Disableable;

use std::time::Duration;
use std::vec::Vec;

use anyhow::{Context, Result};
use dbus::arg::Dict;
use dbus::arg::Variant;
use dbus::blocking::Connection;
use gio::SettingsExt;
use log::debug;
use serde::Deserialize;
use xml::reader::{EventReader, XmlEvent};

#[derive(Debug, Deserialize, Disableable, AppConfig)]
pub struct _Config {
    light: String,
    dark: String,
    #[serde(default)]
    disabled: bool,
}

pub struct GnomeTerminal {
    dbus: Connection,
}

impl Default for GnomeTerminal {
    fn default() -> Self {
        Self {
            dbus: Connection::new_session().unwrap(),
        }
    }
}

impl GnomeTerminal {
    fn get_window_ids(&self) -> Result<Vec<String>> {
        let proxy = self.dbus.with_proxy(
            "org.gnome.Terminal",
            "/org/gnome/Terminal/window",
            Duration::from_millis(2500),
        );
        let (xml,): (String,) = proxy
            .method_call("org.freedesktop.DBus.Introspectable", "Introspect", ())
            .context("Unable to retrieve gnome-terminal windows from DBus")?;

        let parser = EventReader::from_str(&xml);
        let mut depth = 0;

        let mut window_ids: Vec<String> = vec![];

        for e in parser {
            match e {
                Ok(XmlEvent::StartElement {
                    name, attributes, ..
                }) => {
                    if depth == 1 && name.local_name == "node" {
                        window_ids.extend(attributes.into_iter().filter_map(|attr| {
                            if attr.name.local_name == "name" {
                                Some(attr.value)
                            } else {
                                None
                            }
                        }));
                    }
                    depth += 1;
                }
                Ok(XmlEvent::EndElement { .. }) => depth -= 1,
                Err(e) => {
                    return Err(e.into());
                }
                _ => {}
            }
        }

        Ok(window_ids)
    }

    fn set_profile(&self, window_id: &str, profile_id: &str) -> Result<()> {
        let proxy = self.dbus.with_proxy(
            "org.gnome.Terminal",
            format!("/org/gnome/Terminal/window/{}", window_id),
            Duration::from_millis(2500),
        );

        let asv = Dict::new(vec![] as Vec<(String, Variant<String>)>);
        let _: () = proxy
            .method_call(
                "org.gtk.Actions",
                "SetState",
                ("profile", Variant(profile_id), asv),
            )
            .with_context(|| {
                format!(
                    "Unable to apply profile '{}' for gnome-terminal window '{}'",
                    profile_id, window_id
                )
            })?;

        Ok(())
    }
}

impl Themeable for GnomeTerminal {
    fn config_state(&self, config: &ThconConfig) -> ConfigState {
        ConfigState::with_manual_config(config.gnome_terminal.as_ref().map(|c| c.inner.as_ref()))
    }

    fn switch(&self, config: &ThconConfig, operation: &Operation) -> Result<()> {
        let config = match self.config_state(config) {
            ConfigState::NoDefault => {
                return Err(ConfigError::RequiresManualConfig("gnome_terminal").into())
            }
            ConfigState::Default => unreachable!(),
            ConfigState::Disabled => return Ok(()),
            ConfigState::Enabled => config.gnome_terminal.as_ref().unwrap().unwrap_inner_left(),
        };

        let theme = match operation {
            Operation::Darken => &config.dark,
            Operation::Lighten => &config.light,
        };

        if let Ok(windows) = self.get_window_ids() {
            debug!(
                "Found {} {}",
                windows.len(),
                if windows.len() == 1 {
                    "window"
                } else {
                    "windows"
                },
            );
            for window_id in windows.iter() {
                self.set_profile(window_id, theme)?;
            }
        }

        let gsettings = gio::Settings::new("org.gnome.Terminal.ProfilesList");
        gsettings
            .set_string("default", theme)
            .map(|_| gio::Settings::sync())
            .with_context(|| format!("Unable to set default gnome-terminal profile '{}'", theme))
    }
}