Commit 09321a7c authored by Vitaly Lipatov's avatar Vitaly Lipatov

service-rs: add Rust implementation of theme switcher service

- Config module using configparser crate for INI format - Theme module for applying Kvantum and GTK3 themes - Monitor module with D-Bus (libdbus) and inotify support - Same CLI interface as Zig version
parent d8fe8785
......@@ -4,3 +4,6 @@ build/
service/.zig-cache/
service/zig-out/
service/service/
# Rust
target/
[package]
name = "ximper-unified-theme-switcher-service"
version = "0.1.0"
edition = "2021"
[dependencies]
dbus = "0.9"
inotify = "0.10"
dirs = "5"
configparser = "3"
[profile.release]
strip = true
lto = true
opt-level = "s"
use std::path::PathBuf;
use configparser::ini::Ini;
const SYSTEM_PATH: &str = "/etc/ximper-unified-theme-switcher/themes";
const HOME_DIR: &str = ".config/ximper-unified-theme-switcher";
const HOME_FILE: &str = "themes";
const DEFAULT_KV_LIGHT: &str = "KvGnome";
const DEFAULT_KV_DARK: &str = "KvGnomeDark";
const DEFAULT_GTK3_LIGHT: &str = "adw-gtk3";
const DEFAULT_GTK3_DARK: &str = "adw-gtk3-dark";
const SECTION: &str = "default";
pub struct Config {
pub path: PathBuf,
ini: Ini,
}
impl Config {
pub fn new() -> std::io::Result<Self> {
let path = Self::find_or_create_config()?;
let mut config = Config {
path,
ini: Ini::new(),
};
config.load()?;
config.ensure_defaults()?;
Ok(config)
}
fn find_or_create_config() -> std::io::Result<PathBuf> {
if let Some(home) = dirs::home_dir() {
let home_path = home.join(HOME_DIR).join(HOME_FILE);
if home_path.exists() {
return Ok(home_path);
}
}
let system_path = PathBuf::from(SYSTEM_PATH);
if system_path.exists() {
return Ok(system_path);
}
if let Some(home) = dirs::home_dir() {
let home_dir = home.join(HOME_DIR);
std::fs::create_dir_all(&home_dir)?;
let home_path = home_dir.join(HOME_FILE);
std::fs::File::create(&home_path)?;
return Ok(home_path);
}
Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
"No home directory",
))
}
fn load(&mut self) -> std::io::Result<()> {
let _ = self.ini.load(&self.path);
Ok(())
}
fn save(&self) -> std::io::Result<()> {
self.ini
.write(&self.path)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))
}
pub fn get(&self, key: &str) -> Option<String> {
self.ini.get(SECTION, key)
}
pub fn set(&mut self, key: &str, value: &str) -> std::io::Result<()> {
self.ini.set(SECTION, key, Some(value.to_string()));
self.save()
}
fn ensure_defaults(&mut self) -> std::io::Result<()> {
let defaults = [
("CURRENT_THEME", "dark"),
("KV_LIGHT_THEME", DEFAULT_KV_LIGHT),
("KV_DARK_THEME", DEFAULT_KV_DARK),
("GTK3_LIGHT_THEME", DEFAULT_GTK3_LIGHT),
("GTK3_DARK_THEME", DEFAULT_GTK3_DARK),
];
let mut changed = false;
for (key, default) in defaults {
if self.ini.get(SECTION, key).is_none() {
self.ini.set(SECTION, key, Some(default.to_string()));
changed = true;
}
}
if changed {
self.save()?;
}
Ok(())
}
pub fn reload(&mut self) -> std::io::Result<()> {
self.load()
}
}
mod config;
mod monitor;
mod theme;
use config::Config;
fn print_help() {
eprintln!(
"Usage: ximper-unified-theme-switcher-service [options]
Options:
-h, --help Show this help message
--force-file-reaction Force reaction to config file changes"
);
}
fn main() {
let args: Vec<String> = std::env::args().skip(1).collect();
let mut force_file_reaction = false;
for arg in &args {
match arg.as_str() {
"-h" | "--help" => {
print_help();
return;
}
"--force-file-reaction" => {
force_file_reaction = true;
}
_ => {
eprintln!("Unknown argument: {}", arg);
std::process::exit(1);
}
}
}
let mut config = match Config::new() {
Ok(c) => c,
Err(e) => {
eprintln!("Failed to initialize config: {}", e);
std::process::exit(1);
}
};
if force_file_reaction {
if let Err(e) = monitor::run_file_monitor(&mut config) {
eprintln!("File monitor error: {}", e);
std::process::exit(1);
}
} else {
if let Err(e) = monitor::run_dbus_monitor(&mut config) {
eprintln!("D-Bus monitor error: {}", e);
std::process::exit(1);
}
}
}
use std::sync::mpsc;
use std::time::Duration;
use dbus::arg::Variant;
use dbus::blocking::Connection;
use dbus::message::MatchRule;
use dbus::Message;
use inotify::{Inotify, WatchMask};
use crate::config::Config;
use crate::theme::ThemeManager;
pub fn run_dbus_monitor(config: &mut Config) -> Result<(), Box<dyn std::error::Error>> {
eprintln!("reaction mode: after dbus signal");
let conn = Connection::new_session()?;
let rule = MatchRule::new_signal("org.freedesktop.portal.Settings", "SettingChanged")
.with_sender("org.freedesktop.portal.Desktop")
.with_path("/org/freedesktop/portal/desktop");
let (tx, rx) = mpsc::channel::<String>();
conn.add_match(rule, move |_: (), _, msg| {
if let Some(theme) = parse_color_scheme_signal(&msg) {
let _ = tx.send(theme);
}
true
})?;
loop {
conn.process(Duration::from_millis(100))?;
if let Ok(theme) = rx.try_recv() {
eprintln!("D-Bus signal detected: {}", theme);
let mut manager = ThemeManager::new(config);
manager.apply(&theme);
}
}
}
fn parse_color_scheme_signal(msg: &Message) -> Option<String> {
let (namespace, key, value): (String, String, Variant<u32>) = msg.read3().ok()?;
if namespace != "org.freedesktop.appearance" || key != "color-scheme" {
return None;
}
let color_scheme = value.0;
Some(
match color_scheme {
1 => "prefer-dark",
2 => "prefer-light",
_ => "default",
}
.to_string(),
)
}
pub fn run_file_monitor(config: &mut Config) -> std::io::Result<()> {
eprintln!("reaction mode: after changes in config file");
let mut last_theme = config.get("CURRENT_THEME");
let mut inotify = Inotify::init()?;
inotify.watches().add(&config.path, WatchMask::CLOSE_WRITE)?;
let mut buffer = [0; 1024];
loop {
let events = inotify.read_events_blocking(&mut buffer)?;
for _ in events {
if let Err(e) = config.reload() {
eprintln!("Failed to reload config: {}", e);
continue;
}
let new_theme = config.get("CURRENT_THEME");
if new_theme != last_theme {
if let Some(ref theme) = new_theme {
eprintln!("Config file updated: {}", theme);
let mut manager = ThemeManager::new(config);
manager.apply(theme);
}
last_theme = new_theme;
}
}
}
}
use std::process::Command;
use crate::config::Config;
pub struct ThemeManager<'a> {
config: &'a mut Config,
}
impl<'a> ThemeManager<'a> {
pub fn new(config: &'a mut Config) -> Self {
Self { config }
}
pub fn apply(&mut self, color_scheme: &str) {
let mode = match color_scheme {
"prefer-dark" | "dark" => "dark",
_ => "light",
};
let _ = self.config.set("CURRENT_THEME", color_scheme);
self.apply_kvantum(mode);
self.apply_gtk3(mode);
}
fn apply_kvantum(&self, mode: &str) {
self.apply_theme("Kvantum", "KV_LIGHT_THEME", "KV_DARK_THEME", mode, &["kvantummanager", "--set"]);
}
fn apply_gtk3(&self, mode: &str) {
self.apply_theme("GTK3", "GTK3_LIGHT_THEME", "GTK3_DARK_THEME", mode, &["gsettings", "set", "org.gnome.desktop.interface", "gtk-theme"]);
}
fn apply_theme(&self, name: &str, light_key: &str, dark_key: &str, mode: &str, cmd: &[&str]) {
eprintln!("Updating {} theme to {}…", name, mode);
let key = if mode == "light" { light_key } else { dark_key };
let Some(theme) = self.config.get(key) else {
return;
};
let Some((&program, args)) = cmd.split_first() else {
return;
};
let _ = Command::new(program)
.args(args)
.arg(&theme)
.status();
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment