Commit d64a898a authored by Roman Alifanov's avatar Roman Alifanov

service: switch to plugin architecture for theme backends

parent 6dfee745
# Ximper Unified Theme Switcher # Ximper Unified Theme Switcher
**Version:** 0.1.0 **Version:** 0.2.0
**Authors:** Ximper, Fiersik
**Authors:** Ximper, Fiersik
## Description ## 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 ## 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). 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 ## 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 **Copyright © 2024-2026 Etersoft**
- [ ] 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).
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') ...@@ -5,8 +5,6 @@ content = find_program('content')
service_ct_sources = files( service_ct_sources = files(
'src/config.ct', 'src/config.ct',
'src/kvantum.ct',
'src/gtk3.ct',
'src/themes.ct', 'src/themes.ct',
'src/main.ct', 'src/main.ct',
) )
...@@ -21,6 +19,25 @@ service_bin = custom_target( ...@@ -21,6 +19,25 @@ service_bin = custom_target(
install_mode: 'r-xr-xr-x', 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( install_data(
'./data/ximper-unified-theme-switcher.service', './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 { func prefix (): string { return "GTK3" }
schemas = gsettings ("list-schemas") func default_light (): string { return "adw-gtk3" }
if regex.match (schemas, GTK3_SCHEMA) { func default_dark (): string { return "adw-gtk3-dark" }
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 is_available (): bool { func is_available (): bool {
return check_gsettings_schema () schemas = gsettings ("list-schemas")
} if regex.match (schemas, GTK3_SCHEMA) {
keys = gsettings ("list-keys", GTK3_SCHEMA)
func init_defaults (cfg_path: string) { if regex.match (keys, "color-scheme") {
if is_empty (cfg_get (cfg_path, "GTK3_DARK_THEME")) { return true
cfg_set (cfg_path, "GTK3_LIGHT_THEME", this.DEFAULT_LIGHT) }
cfg_set (cfg_path, "GTK3_DARK_THEME", this.DEFAULT_DARK)
} }
return false
} }
func update (mode: string, cfg_path: string) { func apply (theme: string) {
print ("Updating GTK3 theme to {mode}...") print ("Updating GTK3 theme: {theme}")
if mode == "light" { gsettings ("set", GTK3_SCHEMA, "gtk-theme", theme)
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)
}
} }
} }
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 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) { func check_and_update_themes (new_theme: string) {
cfg_path = cfg_check () cfg_path = cfg_check ()
themes_init_defaults (cfg_path) themes_init_defaults (cfg_path)
......
# Реестр тем — добавьте новый экземпляр в массив PLUGIN_DIR = "/usr/share/ximper-unified-theme-switcher/plugins"
kv_theme = KvantumTheme () HOME_PLUGIN_DIR = env.HOME .. "/.local/share/ximper-unified-theme-switcher/plugins"
gtk_theme = Gtk3Theme ()
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) { func themes_init_defaults (cfg_path: string) {
foreach t in THEMES { foreach name in plugin.list ().split (" ") {
t.init_defaults (cfg_path) 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) { func themes_update (mode: string, cfg_path: string) {
foreach t in THEMES { suffix = "_LIGHT_THEME"
if t.is_available () { if mode == "dark" {
t.update (mode, cfg_path) 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 ...@@ -29,11 +29,29 @@ BuildRequires: meson
%package service %package service
Summary: Service for Ximper unified theme switcher Summary: Service for Ximper unified theme switcher
Group: System/Configuration/Other Group: System/Configuration/Other
Requires: Kvantum libgio Requires: libgio
%description service %description service
Service for Ximper unified theme switcher. 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 %package gui
Summary: GUI for Ximper unified theme switcher Summary: GUI for Ximper unified theme switcher
Group: System/Configuration/Other Group: System/Configuration/Other
...@@ -46,6 +64,8 @@ GUI for Ximper unified theme switcher. ...@@ -46,6 +64,8 @@ GUI for Ximper unified theme switcher.
Summary: Default set of themes for Ximper linux Summary: Default set of themes for Ximper linux
Group: System/Configuration/Other Group: System/Configuration/Other
Requires: %name-service Requires: %name-service
Requires: %name-plugin-kvantum
Requires: %name-plugin-gtk3
Requires: gtk3-theme-adw-gtk3 Requires: gtk3-theme-adw-gtk3
Requires: kvantum-theme-kvlibadwaita Requires: kvantum-theme-kvlibadwaita
...@@ -66,8 +86,15 @@ Default set of themes for Ximper linux distro ...@@ -66,8 +86,15 @@ Default set of themes for Ximper linux distro
%files service %files service
%_bindir/%name-service %_bindir/%name-service
%_user_unitdir/%{name}* %_user_unitdir/%{name}*
%dir %_datadir/%name/plugins
%dir %_sysconfdir/%name %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 %files gui -f %name-gui.lang
%_bindir/%name-gui %_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