Commit d64a898a authored by Roman Alifanov's avatar Roman Alifanov

service: switch to plugin architecture for theme backends

parent 6dfee745
# Ximper Unified Theme Switcher
**Version:** 0.1.0
**Version:** 0.2.0
**Authors:** Ximper, Fiersik
**Authors:** Ximper, Fiersik
## Description
**Ximper Unified Theme Switcher**
Designed to switch themes (Kvantum and GTK3) to suitable ones, simultaneously with changing the style of your system.
Unified Theme Switcher automatically switches themes across multiple toolkits when you change the system color scheme (light/dark). Uses a plugin architecture — each toolkit backend is a separate plugin.
## Architecture
The service is written in [ContenT](https://gitlab.eterfund.ru/ximperlinux/content) language. Plugins are compiled separately via `content build-lib` and loaded at runtime.
## Plugins
### Plugin types
**Config-managed plugins** declare `prefix()`, `default_light()`, `default_dark()`. The service stores `{PREFIX}_LIGHT_THEME` and `{PREFIX}_DARK_THEME` in its config and passes the resolved theme name to `apply()`.
**Self-managed plugins** don't have `prefix()`. They receive just the mode (`"light"` or `"dark"`) in `apply()` and handle everything internally.
### Plugin interface
Every plugin must implement:
| Function | Required | Description |
|----------|----------|-------------|
| `is_available (): bool` | Yes | Check if the backend is available |
| `apply (theme: string)` | Yes | Apply a theme (name or mode) |
| `prefix (): string` | No | Config variable prefix (e.g. `"KV"`) |
| `default_light (): string` | No | Default light theme name |
| `default_dark (): string` | No | Default dark theme name |
### Creating a plugin
**Config-managed** (service stores theme names):
```
namespace my_toolkit {
func prefix (): string { return "MY" }
func default_light (): string { return "MyLight" }
func default_dark (): string { return "MyDark" }
func is_available (): bool {
result = command ("-v", "my-toolkit-manager")
return !is_empty (result)
}
func apply (theme: string) {
print ("Applying: {theme}")
my-toolkit-manager ("--set", theme)
}
}
```
**Self-managed** (plugin handles everything):
```
namespace color_scheme {
func is_available (): bool {
# check if gsettings schema exists
return true
}
func apply (mode: string) {
scheme = "default"
if mode == "dark" {
scheme = "prefer-dark"
}
gsettings ("set", "org.gnome.desktop.interface", "color-scheme", scheme)
}
}
```
### Building and installing a plugin
```bash
# Build
content build-lib my_plugin.ct -o my_plugin.sh
# Install system-wide
sudo cp my_plugin.sh /usr/share/ximper-unified-theme-switcher/plugins/
# Install per-user
mkdir -p ~/.local/share/ximper-unified-theme-switcher/plugins/
cp my_plugin.sh ~/.local/share/ximper-unified-theme-switcher/plugins/
```
See `examples/plugins/` for more examples.
## Building
```bash
meson setup _build
meson compile -C _build
sudo meson install -C _build
```
Requires the [ContenT](https://gitlab.eterfund.ru/ximperlinux/content) compiler.
## Issues and Suggestions
If you have any questions or suggestions, please create them in the [Issues section](https://gitlab.eterfund.ru/ximperlinux/ximper-unified-theme-switcher/issues).
## License
**Copyright © 2024 Etersoft**
---
# TODO
## GUI
- [ ] Notification that the service is off and a suggestion to turn it on.
- [x] Make the interface more user-friendly:
- [ ] Add theme presets for all toolkits?
- [ ] Rename Kvantum (to QT) and GTK3 (to GTK) headers?
## Service
- [ ] Switch themes for GTK4? (often break with libadwaita updates, difficult to implement).
- [ ] Support other environments (e.g., Hyprland, Cinnamon):
- [x] Create a mode where the service monitors changes in the config file instead of the org.gnome.desktop.interface schema (possibly unnecessary).
**Copyright © 2024-2026 Etersoft**
namespace color_scheme {
SCHEMA = "org.gnome.desktop.interface"
func is_available (): bool {
schemas = gsettings ("list-schemas")
if regex.match (schemas, SCHEMA) {
keys = gsettings ("list-keys", SCHEMA)
if regex.match (keys, "color-scheme") {
return true
}
}
return false
}
func apply (mode: string) {
scheme = "default"
if mode == "dark" {
scheme = "prefer-dark"
}
print ("Updating color-scheme: {scheme}")
gsettings ("set", SCHEMA, "color-scheme", scheme)
}
}
namespace cursor_theme {
SCHEMA = "org.gnome.desktop.interface"
func prefix (): string { return "CURSOR" }
func default_light (): string { return "Adwaita" }
func default_dark (): string { return "Adwaita" }
func is_available (): bool {
schemas = gsettings ("list-schemas")
if regex.match (schemas, SCHEMA) {
keys = gsettings ("list-keys", SCHEMA)
if regex.match (keys, "cursor-theme") {
return true
}
}
return false
}
func apply (theme: string) {
print ("Updating cursor theme: {theme}")
gsettings ("set", SCHEMA, "cursor-theme", theme)
}
}
namespace icon_theme {
SCHEMA = "org.gnome.desktop.interface"
func prefix (): string { return "ICON" }
func default_light (): string { return "Adwaita" }
func default_dark (): string { return "Adwaita" }
func is_available (): bool {
schemas = gsettings ("list-schemas")
if regex.match (schemas, SCHEMA) {
keys = gsettings ("list-keys", SCHEMA)
if regex.match (keys, "icon-theme") {
return true
}
}
return false
}
func apply (theme: string) {
print ("Updating icon theme: {theme}")
gsettings ("set", SCHEMA, "icon-theme", theme)
}
}
......@@ -5,8 +5,6 @@ content = find_program('content')
service_ct_sources = files(
'src/config.ct',
'src/kvantum.ct',
'src/gtk3.ct',
'src/themes.ct',
'src/main.ct',
)
......@@ -21,6 +19,25 @@ service_bin = custom_target(
install_mode: 'r-xr-xr-x',
)
plugin_dir = join_paths(get_option('datadir'), 'ximper-unified-theme-switcher', 'plugins')
kvantum_plugin = custom_target(
'plugin-kvantum',
input: files('plugins/kvantum.ct'),
output: 'kvantum.sh',
command: [content, 'build-lib', '@INPUT@', '-o', '@OUTPUT@'],
install: true,
install_dir: plugin_dir,
)
gtk3_plugin = custom_target(
'plugin-gtk3',
input: files('plugins/gtk3.ct'),
output: 'gtk3.sh',
command: [content, 'build-lib', '@INPUT@', '-o', '@OUTPUT@'],
install: true,
install_dir: plugin_dir,
)
install_data(
'./data/ximper-unified-theme-switcher.service',
......
GTK3_SCHEMA = "org.gnome.desktop.interface"
namespace gtk3 {
GTK3_SCHEMA = "org.gnome.desktop.interface"
func check_gsettings_schema (): bool {
schemas = gsettings ("list-schemas")
if regex.match (schemas, GTK3_SCHEMA) {
keys = gsettings ("list-keys", GTK3_SCHEMA)
if regex.match (keys, "color-scheme") {
return true
}
}
return false
}
class Gtk3Theme {
DEFAULT_LIGHT = "adw-gtk3"
DEFAULT_DARK = "adw-gtk3-dark"
func prefix (): string { return "GTK3" }
func default_light (): string { return "adw-gtk3" }
func default_dark (): string { return "adw-gtk3-dark" }
func is_available (): bool {
return check_gsettings_schema ()
}
func init_defaults (cfg_path: string) {
if is_empty (cfg_get (cfg_path, "GTK3_DARK_THEME")) {
cfg_set (cfg_path, "GTK3_LIGHT_THEME", this.DEFAULT_LIGHT)
cfg_set (cfg_path, "GTK3_DARK_THEME", this.DEFAULT_DARK)
schemas = gsettings ("list-schemas")
if regex.match (schemas, GTK3_SCHEMA) {
keys = gsettings ("list-keys", GTK3_SCHEMA)
if regex.match (keys, "color-scheme") {
return true
}
}
return false
}
func update (mode: string, cfg_path: string) {
print ("Updating GTK3 theme to {mode}...")
if mode == "light" {
theme = cfg_get (cfg_path, "GTK3_LIGHT_THEME")
gsettings ("set", GTK3_SCHEMA, "gtk-theme", theme)
} else if mode == "dark" {
theme = cfg_get (cfg_path, "GTK3_DARK_THEME")
gsettings ("set", GTK3_SCHEMA, "gtk-theme", theme)
}
func apply (theme: string) {
print ("Updating GTK3 theme: {theme}")
gsettings ("set", GTK3_SCHEMA, "gtk-theme", theme)
}
}
namespace kvantum {
func prefix (): string { return "KV" }
func default_light (): string { return "KvGnome" }
func default_dark (): string { return "KvGnomeDark" }
func is_available (): bool {
result = command ("-v", "kvantummanager")
return !is_empty (result)
}
func apply (theme: string) {
print ("Updating Kvantum theme: {theme}")
kvantummanager ("--set", theme)
}
}
class KvantumTheme {
DEFAULT_LIGHT = "KvGnome"
DEFAULT_DARK = "KvGnomeDark"
func is_available (): bool {
result = command ("-v", "kvantummanager")
if is_empty (result) {
return false
}
return true
}
func init_defaults (cfg_path: string) {
if is_empty (cfg_get (cfg_path, "KV_DARK_THEME")) {
cfg_set (cfg_path, "KV_LIGHT_THEME", this.DEFAULT_LIGHT)
cfg_set (cfg_path, "KV_DARK_THEME", this.DEFAULT_DARK)
}
}
func update (mode: string, cfg_path: string) {
print ("Updating Kvantum theme to {mode}...")
if mode == "light" {
theme = cfg_get (cfg_path, "KV_LIGHT_THEME")
kvantummanager ("--set", theme)
} else if mode == "dark" {
theme = cfg_get (cfg_path, "KV_DARK_THEME")
kvantummanager ("--set", theme)
}
}
}
FORCE_FILE_REACTION = false
func check_gsettings_schema (): bool {
schemas = gsettings ("list-schemas")
if regex.match (schemas, "org.gnome.desktop.interface") {
keys = gsettings ("list-keys", "org.gnome.desktop.interface")
if regex.match (keys, "color-scheme") {
return true
}
}
return false
}
func check_and_update_themes (new_theme: string) {
cfg_path = cfg_check ()
themes_init_defaults (cfg_path)
......
# Реестр тем — добавьте новый экземпляр в массив
kv_theme = KvantumTheme ()
gtk_theme = Gtk3Theme ()
PLUGIN_DIR = "/usr/share/ximper-unified-theme-switcher/plugins"
HOME_PLUGIN_DIR = env.HOME .. "/.local/share/ximper-unified-theme-switcher/plugins"
THEMES = [kv_theme, gtk_theme]
plugin.load_dir (PLUGIN_DIR)
if fs.exists (HOME_PLUGIN_DIR) {
plugin.load_dir (HOME_PLUGIN_DIR)
}
func themes_init_defaults (cfg_path: string) {
foreach t in THEMES {
t.init_defaults (cfg_path)
foreach name in plugin.list ().split (" ") {
if !is_empty (name) {
if plugin.has (name, "prefix") {
prefix = plugin.call (name, "prefix")
light_key = prefix .. "_LIGHT_THEME"
dark_key = prefix .. "_DARK_THEME"
if is_empty (cfg_get (cfg_path, dark_key)) {
cfg_set (cfg_path, light_key, plugin.call (name, "default_light"))
cfg_set (cfg_path, dark_key, plugin.call (name, "default_dark"))
}
}
}
}
}
func themes_update (mode: string, cfg_path: string) {
foreach t in THEMES {
if t.is_available () {
t.update (mode, cfg_path)
suffix = "_LIGHT_THEME"
if mode == "dark" {
suffix = "_DARK_THEME"
}
foreach name in plugin.list ().split (" ") {
if !is_empty (name) {
available = plugin.call (name, "is_available")
if available == "true" {
if plugin.has (name, "prefix") {
prefix = plugin.call (name, "prefix")
theme = cfg_get (cfg_path, prefix .. suffix)
plugin.call (name, "apply", theme)
} else {
plugin.call (name, "apply", mode)
}
}
}
}
}
......@@ -29,11 +29,29 @@ BuildRequires: meson
%package service
Summary: Service for Ximper unified theme switcher
Group: System/Configuration/Other
Requires: Kvantum libgio
Requires: libgio
%description service
Service for Ximper unified theme switcher.
%package plugin-kvantum
Summary: Kvantum plugin for Ximper unified theme switcher
Group: System/Configuration/Other
Requires: %name-service
Requires: Kvantum
%description plugin-kvantum
Kvantum theme switching plugin for Ximper unified theme switcher.
%package plugin-gtk3
Summary: GTK3 plugin for Ximper unified theme switcher
Group: System/Configuration/Other
Requires: %name-service
Requires: libgio
%description plugin-gtk3
GTK3 theme switching plugin for Ximper unified theme switcher.
%package gui
Summary: GUI for Ximper unified theme switcher
Group: System/Configuration/Other
......@@ -46,6 +64,8 @@ GUI for Ximper unified theme switcher.
Summary: Default set of themes for Ximper linux
Group: System/Configuration/Other
Requires: %name-service
Requires: %name-plugin-kvantum
Requires: %name-plugin-gtk3
Requires: gtk3-theme-adw-gtk3
Requires: kvantum-theme-kvlibadwaita
......@@ -66,8 +86,15 @@ Default set of themes for Ximper linux distro
%files service
%_bindir/%name-service
%_user_unitdir/%{name}*
%dir %_datadir/%name/plugins
%dir %_sysconfdir/%name
%files plugin-kvantum
%_datadir/%name/plugins/kvantum.sh
%files plugin-gtk3
%_datadir/%name/plugins/gtk3.sh
%files gui -f %name-gui.lang
%_bindir/%name-gui
......
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