Commit 016024e2 authored by Roman Alifanov's avatar Roman Alifanov

convert bash script to go code with AI

parent e724dd85
module unified_theme_switcher_service
go 1.25.0
require (
github.com/fsnotify/fsnotify v1.9.0
github.com/godbus/dbus/v5 v5.1.0
github.com/urfave/cli/v3 v3.4.1
)
require golang.org/x/sys v0.13.0 // indirect
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/urfave/cli/v3 v3.4.1 h1:1M9UOCy5bLmGnuu1yn3t3CB4rG79Rtoxuv1sPhnm6qM=
github.com/urfave/cli/v3 v3.4.1/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
#!/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
package main
import (
"fmt"
"os"
"path/filepath"
"strings"
)
func createHomeConfig() (string, error) {
home, err := os.UserHomeDir()
if err != nil {
return "", err
}
configDirPath := filepath.Join(home, configDir)
configPath := filepath.Join(configDirPath, configFileName)
if err := os.MkdirAll(configDirPath, 0755); err != nil {
return "", err
}
if _, err := os.Stat(systemConfig); err == nil {
data, err := os.ReadFile(systemConfig)
if err != nil {
return "", err
}
if err := os.WriteFile(configPath, data, 0644); err != nil {
return "", err
}
} else {
if _, err := os.Create(configPath); err != nil {
return "", err
}
}
return configPath, createDefaultConfigValues(configPath)
}
func createDefaultConfigValues(configPath string) error {
if _, err := os.Stat(configPath); os.IsNotExist(err) {
return fmt.Errorf("config file %s does not exist", configPath)
}
if val, _ := readConfigValue(configPath, "KV_DARK_THEME"); val == "" {
setConfigValue(configPath, "KV_LIGHT_THEME", "KvGnome")
setConfigValue(configPath, "KV_DARK_THEME", "KvGnomeDark")
}
if val, _ := readConfigValue(configPath, "GTK3_DARK_THEME"); val == "" {
setConfigValue(configPath, "GTK3_LIGHT_THEME", "adw-gtk3")
setConfigValue(configPath, "GTK3_DARK_THEME", "adw-gtk3-dark")
}
return nil
}
func isWritable(path string) bool {
info, err := os.Stat(path)
if err != nil {
return false
}
return info.Mode().Perm()&0200 != 0
}
func readConfigValue(configPath, key string) (string, error) {
data, err := os.ReadFile(configPath)
if err != nil {
return "", err
}
lines := strings.SplitSeq(string(data), "\n")
for line := range lines {
if after, ok := strings.CutPrefix(line, key+"="); ok {
return after, nil
}
}
return "", nil
}
func setConfigValue(configPath, key, value string) error {
data, err := os.ReadFile(configPath)
if err != nil && !os.IsNotExist(err) {
return err
}
lines := strings.Split(string(data), "\n")
found := false
for i, line := range lines {
if strings.HasPrefix(line, key+"=") {
lines[i] = fmt.Sprintf("%s=%s", key, value)
found = true
break
}
}
if !found {
lines = append(lines, fmt.Sprintf("%s=%s", key, value))
}
content := strings.Join(lines, "\n")
return os.WriteFile(configPath, []byte(content), 0644)
}
func getConfigPath() (string, error) {
homeConfig := filepath.Join(os.Getenv("HOME"), configDir, configFileName)
if _, err := os.Stat(homeConfig); err == nil {
return homeConfig, nil
}
if _, err := os.Stat(systemConfig); err == nil {
return systemConfig, nil
}
return createHomeConfig()
}
func getWriteConfigPath() (string, error) {
homeConfig := filepath.Join(os.Getenv("HOME"), configDir, configFileName)
if _, err := os.Stat(homeConfig); err == nil {
if !isWritable(homeConfig) {
return "", fmt.Errorf("home config %s is not writable", homeConfig)
}
return homeConfig, nil
}
return createHomeConfig()
}
package main
import (
"context"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/fsnotify/fsnotify"
"github.com/godbus/dbus/v5"
"github.com/urfave/cli/v3"
)
const (
configDir = ".config/ximper-unified-theme-switcher"
configFileName = "themes"
systemConfig = "/etc/ximper-unified-theme-switcher/themes"
schema = "org.gnome.desktop.interface"
key = "color-scheme"
)
func checkGSettingsSchema() bool {
cmd := exec.Command("gsettings", "list-schemas")
output, err := cmd.Output()
if err != nil || !strings.Contains(string(output), schema) {
fmt.Fprintln(os.Stderr, "Schema", schema, "not found.")
return false
}
cmd = exec.Command("gsettings", "list-keys", schema)
output, err = cmd.Output()
if err != nil || !strings.Contains(string(output), key) {
fmt.Fprintln(os.Stderr, "Schema", schema, "exists, but KEY", key, "not found.")
return false
}
fmt.Println("Schema", schema, "and KEY", key, "exist.")
return true
}
func updateKvantumTheme(theme, configPath string) error {
var themeKey string
switch theme {
case "light":
themeKey = "KV_LIGHT_THEME"
case "dark":
themeKey = "KV_DARK_THEME"
default:
return fmt.Errorf("invalid theme mode: %s", theme)
}
themeName, err := readConfigValue(configPath, themeKey)
if err != nil || themeName == "" {
return fmt.Errorf("no theme found for %s mode", theme)
}
fmt.Println("Updating Kvantum theme to", themeName, theme)
cmd := exec.Command("kvantummanager", "--set", themeName)
return cmd.Run()
}
func updateGTK3Theme(theme, configPath string) error {
var themeKey string
switch theme {
case "light":
themeKey = "GTK3_LIGHT_THEME"
case "dark":
themeKey = "GTK3_DARK_THEME"
default:
return fmt.Errorf("invalid theme mode: %s", theme)
}
themeName, err := readConfigValue(configPath, themeKey)
if err != nil || themeName == "" {
return fmt.Errorf("no GTK3 theme found for %s mode", theme)
}
fmt.Println("Updating GTK3 theme to", themeName, theme)
cmd := exec.Command("gsettings", "set", schema, "gtk-theme", themeName)
return cmd.Run()
}
func checkAndUpdateThemes(newTheme string, writeFile bool) error {
configPath, err := getConfigPath()
if err != nil {
return err
}
if writeFile {
writeConfigPath, err := getWriteConfigPath()
if err != nil {
return err
}
setConfigValue(writeConfigPath, "CURRENT_THEME", newTheme)
}
switch newTheme {
case "default", "light", "prefer-light":
if err := updateKvantumTheme("light", configPath); err != nil {
return err
}
return updateGTK3Theme("light", configPath)
case "dark", "prefer-dark":
if err := updateKvantumTheme("dark", configPath); err != nil {
return err
}
return updateGTK3Theme("dark", configPath)
default:
return fmt.Errorf("invalid theme: %s", newTheme)
}
}
func watchFileChanges(configPath string) error {
watcher, err := fsnotify.NewWatcher()
if err != nil {
return fmt.Errorf("failed to create watcher: %v", err)
}
defer watcher.Close()
dir := filepath.Dir(configPath)
if err := watcher.Add(dir); err != nil {
return fmt.Errorf("failed to add watcher to directory %s: %v", dir, err)
}
fmt.Println("Watching config file for changes:", configPath)
lastTheme := ""
for {
select {
case event, ok := <-watcher.Events:
if !ok {
return nil
}
if filepath.Clean(event.Name) == filepath.Clean(configPath) &&
event.Op&(fsnotify.Write|fsnotify.Create|fsnotify.Rename) != 0 {
fmt.Println("Detected change in config file:", event)
newTheme, err := readConfigValue(configPath, "CURRENT_THEME")
if err != nil {
fmt.Println("Error reading CURRENT_THEME:", err)
continue
}
if newTheme == "" {
fmt.Println("CURRENT_THEME is empty, skipping")
continue
}
if newTheme == lastTheme {
continue
}
lastTheme = newTheme
fmt.Println("Applying new theme:", newTheme)
if err := checkAndUpdateThemes(newTheme, true); err != nil {
fmt.Println("Error updating themes:", err)
}
}
case err, ok := <-watcher.Errors:
if !ok {
return nil
}
fmt.Println("Watcher error:", err)
}
}
}
func watchDBusSignal() error {
conn, err := dbus.SessionBus()
if err != nil {
return err
}
defer conn.Close()
if err := conn.AddMatchSignal(
dbus.WithMatchInterface("org.freedesktop.portal.Settings"),
dbus.WithMatchMember("SettingChanged"),
); err != nil {
return err
}
signals := make(chan *dbus.Signal, 10)
conn.Signal(signals)
var lastTheme string
for signal := range signals {
if signal.Name != "org.freedesktop.portal.Settings.SettingChanged" {
continue
}
cmd := exec.Command("gsettings", "get", schema, key)
output, err := cmd.Output()
if err != nil {
fmt.Println("Error getting gsettings:", err)
continue
}
newTheme := strings.Trim(strings.TrimSpace(string(output)), "'")
if newTheme == "" || newTheme == lastTheme {
continue
}
lastTheme = newTheme
fmt.Println("D-Bus signal detected. Applying theme:", newTheme)
if err := checkAndUpdateThemes(newTheme, false); err != nil {
fmt.Println("Error updating themes:", err)
}
}
return nil
}
func main() {
cmd := &cli.Command{
Name: "ximper-unified-theme-switcher",
Usage: "Unified theme switcher for desktop environments",
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "force-file-reaction",
Aliases: []string{"f"},
Usage: "Force reaction to config file changes",
},
&cli.BoolFlag{
Name: "help",
Aliases: []string{"h"},
Usage: "Show this help message",
},
},
Action: func(ctx context.Context, cmd *cli.Command) error {
if cmd.Bool("help") {
fmt.Println("Usage:", cmd.Name, "[options]")
fmt.Println("Options:")
fmt.Println(" --help, -h Show this help message")
fmt.Println(" --force-file-reaction, -f Force reaction to config file changes")
return nil
}
forceFileReaction := cmd.Bool("force-file-reaction")
configPath, err := getConfigPath()
if err != nil {
return fmt.Errorf("failed to get config path: %v", err)
}
if forceFileReaction || !checkGSettingsSchema() {
fmt.Println("reaction mode: after changes in config file")
if _, err := createHomeConfig(); err != nil {
return fmt.Errorf("failed to create home config: %v", err)
}
return watchFileChanges(configPath)
}
fmt.Println("reaction mode: after dbus signal")
return watchDBusSignal()
},
}
if err := cmd.Run(context.Background(), os.Args); err != nil {
fmt.Println("Error:", err)
os.Exit(1)
}
}
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