Commit a1a7c166 authored by Roman Alifanov's avatar Roman Alifanov

service: rewrite from Bash to Zig with native sd-bus and inotify

parent e724dd85
.idea
_build/
build/
service/.zig-cache/
service/zig-out/
service/service/
project('ximper-unified-theme-switcher-gui',
project('ximper-unified-theme-switcher-gui', 'c',
version: '0.1.0',
meson_version: '>= 0.62.0',
default_options: [ 'warning_level=2', 'werror=false', ],
......
#!/bin/bash
# shellcheck source=/usr/bin/shell-config
source shell-config
CONFIG_FILE="/etc/ximper-unified-theme-switcher/themes"
HOME_CONFIG_DIR="$HOME/.config/ximper-unified-theme-switcher"
HOME_CONFIG_FILE="$HOME_CONFIG_DIR/themes"
FORCE_FILE_REACTION=false
# Функция для создания конфига в домашнем каталоге
create_home_config() {
mkdir -p "$HOME_CONFIG_DIR"
touch "$HOME_CONFIG_FILE"
create_default_config_values "$HOME_CONFIG_FILE"
echo "$HOME_CONFIG_FILE"
}
create_default_config_values() {
local CFG_PATH
CFG_PATH="$1"
if [ -w "$CFG_PATH" ]; then
if [ -z "$(shell_config_get "$CFG_PATH" KV_DARK_THEME)" ]; then
shell_config_set "$CFG_PATH" CURRENT_THEME "dark"
fi
if [ -z "$(shell_config_get "$CFG_PATH" KV_DARK_THEME)" ]; then
shell_config_set "$CFG_PATH" KV_LIGHT_THEME "KvGnome"
shell_config_set "$CFG_PATH" KV_DARK_THEME "KvGnomeDark"
fi
if [ -z "$(shell_config_get "$CFG_PATH" GTK3_DARK_THEME)" ]; then
shell_config_set "$CFG_PATH" GTK3_LIGHT_THEME "adw-gtk3"
shell_config_set "$CFG_PATH" GTK3_DARK_THEME "adw-gtk3-dark"
fi
fi
}
cfg_check() {
# Проверка наличия конфига в /etc
if [ -f "$CONFIG_FILE" ]; then
if [ -f "$HOME_CONFIG_FILE" ]; then
echo "$HOME_CONFIG_FILE"
else
echo "$CONFIG_FILE"
fi
elif [ -f "$HOME_CONFIG_FILE" ]; then
echo "$HOME_CONFIG_FILE"
else
create_home_config
fi
}
check_gsettings_schema() {
local SCHEMA="org.gnome.desktop.interface"
local KEY="color-scheme"
if gsettings list-schemas | grep -q "$SCHEMA"; then
if gsettings list-keys "$SCHEMA" | grep -q "$KEY"; then
echo "Schema '$SCHEMA' and KEY '$KEY' exist."
return 0
else
echo "Schema '$SCHEMA' exists, but KEY '$KEY' not found."
return 1
fi
else
echo "Schema '$SCHEMA' not found."
return 1
fi
}
update_kv_theme() {
echo "Updating Kvantum theme to $1..."
echo "Cfg path: $2..."
if [ "$1" == "light" ]; then
kvantummanager --set "$(shell_config_get "$2" KV_LIGHT_THEME)"
elif [ "$1" == "dark" ]; then
kvantummanager --set "$(shell_config_get "$2" KV_DARK_THEME)"
fi
}
update_gtk3_theme() {
echo "Updating GTK3 theme to $1..."
echo "Cfg path: $2..."
if [ "$1" == "light" ]; then
gsettings set org.gnome.desktop.interface gtk-theme "$(shell_config_get "$2" GTK3_LIGHT_THEME)"
elif [ "$1" == "dark" ]; then
gsettings set org.gnome.desktop.interface gtk-theme "$(shell_config_get "$2" GTK3_DARK_THEME)"
fi
}
# Function to check and update the theme
check_and_update_themes() {
local NEW_THEME
NEW_THEME="$1"
local CFG_PATH
CFG_PATH=$(cfg_check)
create_default_config_values
shell_config_set "$CFG_PATH" CURRENT_THEME "$NEW_THEME"
case "$NEW_THEME" in
"default"|"light"|"prefer-light")
update_kv_theme "light" "$CFG_PATH"
update_gtk3_theme "light" "$CFG_PATH"
;;
"dark"|"prefer-dark")
update_kv_theme "dark" "$CFG_PATH"
update_gtk3_theme "dark" "$CFG_PATH"
;;
esac
}
OPTS=$(getopt -o h --long help,force-file-reaction -- "$@") || {
echo "Ошибка обработки опций."
exit 1
}
eval set -- "$OPTS"
while true; do
case "$1" in
-h|--help)
echo "Usage: $0 [options]"
echo "Options:"
echo " --help Show this help message"
echo " --force-file-reaction Force reaction to config file changes"
exit 0
;;
--force-file-reaction)
FORCE_FILE_REACTION=true
shift
;;
--)
shift
break
;;
*)
echo "Неизвестный аргумент: $1"
exit 1
;;
esac
done
main() {
if [ "$FORCE_FILE_REACTION" = true ] || ! check_gsettings_schema; then
echo "reaction mode: after changes in config file"
create_home_config
local CONFIG_FILE
CONFIG_FILE=$(cfg_check)
while true; do
inotifywait -e attrib "$CONFIG_FILE" | \
while read -r directory events filename; do
NEW_THEME=$(shell_config_get "$CONFIG_FILE" CURRENT_THEME)
if [ -n "$NEW_THEME" ]; then
echo "Config file updated: $NEW_THEME"
check_and_update_themes "$NEW_THEME"
fi
done
done
else
echo "reaction mode: after dbus signal"
dbus-monitor "interface='org.freedesktop.portal.Settings',member=SettingChanged" | \
while IFS= read -r LINE; do
local THEME
local NEW_THEME
local CURRENT_TIME
local LAST_UPDATE
THEME=$(echo "$LINE" | grep -E -o 'string "(prefer-dark|default)"')
NEW_THEME=$(gsettings get org.gnome.desktop.interface color-scheme | awk -F"'" '{print $2}')
CURRENT_TIME=$(date +%s%3N)
if [ -n "$THEME" ] && [ $((CURRENT_TIME - LAST_UPDATE)) -ge 150 ]; then
LAST_UPDATE=$CURRENT_TIME
echo "D-Bus signal detected: $THEME"
check_and_update_themes "$NEW_THEME"
fi
done
fi
}
main
\ No newline at end of file
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const exe = b.addExecutable(.{
.name = "ximper-unified-theme-switcher-service",
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
.link_libc = true,
}),
});
exe.linkSystemLibrary("libsystemd");
// Install directly to prefix, not prefix/bin
const install = b.addInstallArtifact(exe, .{
.dest_dir = .{ .override = .prefix },
});
b.getInstallStep().dependOn(&install.step);
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| {
run_cmd.addArgs(args);
}
const run_step = b.step("run", "Run the service");
run_step.dependOn(&run_cmd.step);
}
prefix = get_option('prefix')
sysconfdir = join_paths(prefix, get_option('sysconfdir'))
zig = find_program('zig')
service_dir = meson.current_source_dir()
build_dir = meson.current_build_dir()
service_bin = 'ximper-unified-theme-switcher-service'
install_data(
'./bin/ximper-unified-theme-switcher-service',
install_dir: get_option('bindir'),
install_mode: 'r-xr-xr-x'
custom_target(
'zig-build',
output: service_bin,
command: [
zig, 'build',
'--build-file', service_dir / 'build.zig',
'-Doptimize=ReleaseSmall',
'-p', build_dir,
],
console: true,
install: true,
install_dir: get_option('bindir'),
)
install_data(
'./data/ximper-unified-theme-switcher.service',
install_dir: join_paths(prefix, 'lib', 'systemd', 'user')
'data/ximper-unified-theme-switcher.service',
install_dir: get_option('prefix') / 'lib' / 'systemd' / 'user',
)
cfg_dir = join_paths(sysconfdir, 'ximper-unified-theme-switcher')
install_data(
'./ximper_distro_conf/themes',
install_dir: cfg_dir
'ximper_distro_conf/themes',
install_dir: get_option('sysconfdir') / 'ximper-unified-theme-switcher',
)
message('Created directory: ' + cfg_dir)
const std = @import("std");
pub const Config = struct {
pub const SYSTEM_PATH = "/etc/ximper-unified-theme-switcher/themes";
pub const HOME_DIR = ".config/ximper-unified-theme-switcher";
pub const HOME_FILE = "themes";
pub const DEFAULT_KV_LIGHT = "KvGnome";
pub const DEFAULT_KV_DARK = "KvGnomeDark";
pub const DEFAULT_GTK3_LIGHT = "adw-gtk3";
pub const DEFAULT_GTK3_DARK = "adw-gtk3-dark";
allocator: std.mem.Allocator,
path: []const u8,
pub fn init(allocator: std.mem.Allocator) !Config {
const path = try findOrCreateConfig(allocator);
var config = Config{ .allocator = allocator, .path = path };
try config.ensureDefaults();
return config;
}
pub fn deinit(self: *Config) void {
self.allocator.free(self.path);
}
fn getHomePath(allocator: std.mem.Allocator) ![]const u8 {
const home = std.posix.getenv("HOME") orelse return error.NoHomeDir;
return std.fs.path.join(allocator, &.{ home, HOME_DIR, HOME_FILE });
}
fn getHomeDir(allocator: std.mem.Allocator) ![]const u8 {
const home = std.posix.getenv("HOME") orelse return error.NoHomeDir;
return std.fs.path.join(allocator, &.{ home, HOME_DIR });
}
fn findOrCreateConfig(allocator: std.mem.Allocator) ![]const u8 {
const home_path = try getHomePath(allocator);
defer allocator.free(home_path);
if (std.fs.cwd().access(home_path, .{})) |_| {
return allocator.dupe(u8, home_path);
} else |_| {}
if (std.fs.cwd().access(SYSTEM_PATH, .{})) |_| {
return allocator.dupe(u8, SYSTEM_PATH);
} else |_| {}
const home_dir = try getHomeDir(allocator);
defer allocator.free(home_dir);
std.fs.cwd().makePath(home_dir) catch {};
const file = try std.fs.cwd().createFile(home_path, .{});
file.close();
return allocator.dupe(u8, home_path);
}
pub fn getValue(self: *Config, key: []const u8) !?[]const u8 {
const file = std.fs.cwd().openFile(self.path, .{}) catch return null;
defer file.close();
var buf: [4096]u8 = undefined;
const content = file.readAll(&buf) catch return null;
var lines = std.mem.splitScalar(u8, buf[0..content], '\n');
while (lines.next()) |line| {
if (std.mem.indexOf(u8, line, "=")) |idx| {
const k = std.mem.trim(u8, line[0..idx], " \t");
if (std.mem.eql(u8, k, key)) {
const v = std.mem.trim(u8, line[idx + 1 ..], " \t\r");
return try self.allocator.dupe(u8, v);
}
}
}
return null;
}
pub fn setValue(self: *Config, key: []const u8, value: []const u8) !void {
var lines_list: std.ArrayListUnmanaged(u8) = .{};
defer lines_list.deinit(self.allocator);
var found = false;
if (std.fs.cwd().openFile(self.path, .{})) |file| {
defer file.close();
var buf: [4096]u8 = undefined;
const content = file.readAll(&buf) catch 0;
var iter = std.mem.splitScalar(u8, buf[0..content], '\n');
while (iter.next()) |line| {
if (line.len == 0) continue;
if (std.mem.indexOf(u8, line, "=")) |idx| {
const k = std.mem.trim(u8, line[0..idx], " \t");
if (std.mem.eql(u8, k, key)) {
try lines_list.appendSlice(self.allocator, key);
try lines_list.append(self.allocator, '=');
try lines_list.appendSlice(self.allocator, value);
try lines_list.append(self.allocator, '\n');
found = true;
continue;
}
}
try lines_list.appendSlice(self.allocator, line);
try lines_list.append(self.allocator, '\n');
}
} else |_| {}
if (!found) {
try lines_list.appendSlice(self.allocator, key);
try lines_list.append(self.allocator, '=');
try lines_list.appendSlice(self.allocator, value);
try lines_list.append(self.allocator, '\n');
}
const file = try std.fs.cwd().createFile(self.path, .{});
defer file.close();
_ = try file.write(lines_list.items);
}
pub fn ensureDefaults(self: *Config) !void {
if (try self.getValue("CURRENT_THEME") == null) {
try self.setValue("CURRENT_THEME", "dark");
}
if (try self.getValue("KV_LIGHT_THEME") == null) {
try self.setValue("KV_LIGHT_THEME", DEFAULT_KV_LIGHT);
}
if (try self.getValue("KV_DARK_THEME") == null) {
try self.setValue("KV_DARK_THEME", DEFAULT_KV_DARK);
}
if (try self.getValue("GTK3_LIGHT_THEME") == null) {
try self.setValue("GTK3_LIGHT_THEME", DEFAULT_GTK3_LIGHT);
}
if (try self.getValue("GTK3_DARK_THEME") == null) {
try self.setValue("GTK3_DARK_THEME", DEFAULT_GTK3_DARK);
}
}
};
const std = @import("std");
const Config = @import("config.zig").Config;
const ThemeManager = @import("theme.zig").ThemeManager;
const monitor = @import("monitor.zig");
fn printHelp() void {
std.debug.print(
\\Usage: ximper-unified-theme-switcher-service [options]
\\Options:
\\ -h, --help Show this help message
\\ --force-file-reaction Force reaction to config file changes
\\
, .{});
}
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
var force_file_reaction = false;
var args = try std.process.argsWithAllocator(allocator);
defer args.deinit();
_ = args.skip();
while (args.next()) |arg| {
if (std.mem.eql(u8, arg, "-h") or std.mem.eql(u8, arg, "--help")) {
printHelp();
return;
} else if (std.mem.eql(u8, arg, "--force-file-reaction")) {
force_file_reaction = true;
} else {
std.debug.print("Unknown argument: {s}\n", .{arg});
return error.InvalidArgument;
}
}
var config = try Config.init(allocator);
defer config.deinit();
var theme_manager = ThemeManager{ .config = &config };
if (force_file_reaction) {
var file_monitor = monitor.FileMonitor{ .config = &config, .theme_manager = &theme_manager };
try file_monitor.start();
} else {
var dbus_monitor = monitor.DBusMonitor{ .config = &config, .theme_manager = &theme_manager };
try dbus_monitor.start();
}
}
const std = @import("std");
const c = @cImport({
@cInclude("systemd/sd-bus.h");
});
const Config = @import("config.zig").Config;
const ThemeManager = @import("theme.zig").ThemeManager;
pub const DBusMonitor = struct {
config: *Config,
theme_manager: *ThemeManager,
pub fn start(self: *DBusMonitor) !void {
std.debug.print("reaction mode: after dbus signal\n", .{});
var bus: ?*c.sd_bus = null;
var ret = c.sd_bus_open_user(&bus);
if (ret < 0) {
std.debug.print("Failed to connect to user bus: {d}\n", .{ret});
return error.DBusConnectionFailed;
}
defer _ = c.sd_bus_unref(bus);
ret = c.sd_bus_match_signal(
bus,
null,
"org.freedesktop.portal.Desktop",
"/org/freedesktop/portal/desktop",
"org.freedesktop.portal.Settings",
"SettingChanged",
&signalCallback,
self,
);
if (ret < 0) {
std.debug.print("Failed to add match: {d}\n", .{ret});
return error.DBusMatchFailed;
}
while (true) {
ret = c.sd_bus_process(bus, null);
if (ret < 0) break;
if (ret > 0) continue;
ret = c.sd_bus_wait(bus, std.math.maxInt(u64));
if (ret < 0) break;
}
}
fn signalCallback(msg: ?*c.sd_bus_message, userdata: ?*anyopaque, _: ?*c.sd_bus_error) callconv(.c) c_int {
const self: *DBusMonitor = @ptrCast(@alignCast(userdata));
var namespace: [*c]const u8 = null;
var key: [*c]const u8 = null;
if (c.sd_bus_message_read(msg, "ss", &namespace, &key) < 0) return 0;
const ns_slice = std.mem.span(namespace);
const key_slice = std.mem.span(key);
if (!std.mem.eql(u8, ns_slice, "org.freedesktop.appearance")) return 0;
if (!std.mem.eql(u8, key_slice, "color-scheme")) return 0;
// Enter variant
if (c.sd_bus_message_enter_container(msg, 'v', "u") < 0) return 0;
var color_scheme: u32 = 0;
if (c.sd_bus_message_read(msg, "u", &color_scheme) < 0) return 0;
_ = c.sd_bus_message_exit_container(msg);
const theme: []const u8 = switch (color_scheme) {
1 => "prefer-dark",
2 => "prefer-light",
else => "default",
};
std.debug.print("D-Bus signal detected: {s}\n", .{theme});
self.theme_manager.apply(theme);
return 0;
}
};
pub const FileMonitor = struct {
config: *Config,
theme_manager: *ThemeManager,
last_theme: ?[]const u8 = null,
pub fn start(self: *FileMonitor) !void {
std.debug.print("reaction mode: after changes in config file\n", .{});
self.last_theme = try self.config.getValue("CURRENT_THEME");
const fd = std.posix.inotify_init1(std.os.linux.IN.CLOEXEC) catch |err| {
std.debug.print("Failed to init inotify: {}\n", .{err});
return err;
};
defer std.posix.close(fd);
const path_z = try self.config.allocator.dupeZ(u8, self.config.path);
defer self.config.allocator.free(path_z);
_ = std.posix.inotify_add_watch(fd, path_z, std.os.linux.IN.CLOSE_WRITE) catch |err| {
std.debug.print("Failed to add watch: {}\n", .{err});
return err;
};
var buf: [4096]u8 align(@alignOf(std.os.linux.inotify_event)) = undefined;
while (true) {
const len = std.posix.read(fd, &buf) catch break;
if (len == 0) break;
// Process events
var offset: usize = 0;
while (offset < len) {
const event: *const std.os.linux.inotify_event = @ptrCast(@alignCast(&buf[offset]));
offset += @sizeOf(std.os.linux.inotify_event) + event.len;
self.handleFileChange();
}
}
}
fn handleFileChange(self: *FileMonitor) void {
const new_theme = self.config.getValue("CURRENT_THEME") catch null;
if (new_theme) |theme| {
defer self.config.allocator.free(theme);
if (self.last_theme) |last| {
if (std.mem.eql(u8, theme, last)) return;
self.config.allocator.free(last);
}
self.last_theme = self.config.allocator.dupe(u8, theme) catch null;
std.debug.print("Config file updated: {s}\n", .{theme});
self.theme_manager.apply(theme);
}
}
};
const std = @import("std");
const Config = @import("config.zig").Config;
pub const ThemeManager = struct {
config: *Config,
pub fn apply(self: *ThemeManager, color_scheme: []const u8) void {
const mode: []const u8 = if (std.mem.eql(u8, color_scheme, "prefer-dark") or
std.mem.eql(u8, color_scheme, "dark"))
"dark"
else
"light";
self.config.setValue("CURRENT_THEME", color_scheme) catch {};
self.applyKvantum(mode);
self.applyGtk3(mode);
}
fn applyKvantum(self: *ThemeManager, mode: []const u8) void {
std.debug.print("Updating Kvantum theme to {s}…\n", .{mode});
const key = if (std.mem.eql(u8, mode, "light")) "KV_LIGHT_THEME" else "KV_DARK_THEME";
const theme = (self.config.getValue(key) catch null) orelse return;
defer self.config.allocator.free(theme);
const theme_z = self.config.allocator.dupeZ(u8, theme) catch return;
defer self.config.allocator.free(theme_z);
var child = std.process.Child.init(&.{ "kvantummanager", "--set", theme_z }, self.config.allocator);
_ = child.spawnAndWait() catch {};
}
fn applyGtk3(self: *ThemeManager, mode: []const u8) void {
std.debug.print("Updating GTK3 theme to {s}…\n", .{mode});
const key = if (std.mem.eql(u8, mode, "light")) "GTK3_LIGHT_THEME" else "GTK3_DARK_THEME";
const theme = (self.config.getValue(key) catch null) orelse return;
defer self.config.allocator.free(theme);
const theme_z = self.config.allocator.dupeZ(u8, theme) catch return;
defer self.config.allocator.free(theme_z);
var child = std.process.Child.init(&.{ "gsettings", "set", "org.gnome.desktop.interface", "gtk-theme", theme_z }, self.config.allocator);
_ = child.spawnAndWait() catch {};
}
};
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