preset: refactor preset system and YAML config format

parent 5b29e43c
...@@ -3,13 +3,8 @@ package config ...@@ -3,13 +3,8 @@ package config
import ( import (
"os" "os"
"path/filepath" "path/filepath"
"ximperconf/utils"
) )
type presetEnv struct {
Profiles map[string]PresetProfile
}
type hyprEnv struct { type hyprEnv struct {
Config string Config string
SystemModulesDir string SystemModulesDir string
...@@ -24,7 +19,7 @@ type repoEnv struct { ...@@ -24,7 +19,7 @@ type repoEnv struct {
type environment struct { type environment struct {
Version string Version string
IsRoot bool IsRoot bool
Preset *presetEnv Profiles map[string]PresetProfile
Hyprland *hyprEnv Hyprland *hyprEnv
Repo *repoEnv Repo *repoEnv
} }
...@@ -35,19 +30,14 @@ func InitConfig() error { ...@@ -35,19 +30,14 @@ func InitConfig() error {
Env.Version = "0.1.0" Env.Version = "0.1.0"
Env.IsRoot = os.Geteuid() == 0 Env.IsRoot = os.Geteuid() == 0
presetCfg, err := loadPresetConfig("/etc/ximperdistro/ximperconf/preset.d/") profiles, err := loadPresetProfiles("/etc/ximperdistro/ximperconf/preset.d/")
if err != nil { if err != nil {
// Если ошибка, пустой map Env.Profiles = map[string]PresetProfile{}
Env.Preset = &presetEnv{
Profiles: map[string]PresetProfile{},
}
} else { } else {
Env.Preset = &presetEnv{ Env.Profiles = profiles
Profiles: presetCfg.Profiles,
}
} }
if utils.FileExists("/usr/bin/hyprland") { if FileExists("/usr/bin/hyprland") {
home, _ := os.UserHomeDir() home, _ := os.UserHomeDir()
userModules := filepath.Join(home, ".config", "hypr") userModules := filepath.Join(home, ".config", "hypr")
systemModules := "/etc/ximperdistro/hyprland/hypr" systemModules := "/etc/ximperdistro/hyprland/hypr"
...@@ -63,14 +53,19 @@ func InitConfig() error { ...@@ -63,14 +53,19 @@ func InitConfig() error {
} }
Env.Repo = &repoEnv{ Env.Repo = &repoEnv{
DeferredInfoURL: "https://download.etersoft.ru/pub/Etersoft/Sisyphus/Deferred_Info.html", DeferredInfoURL: "https://download.etersoft.ru/pub/Ximper/Deferred_Info.html",
} }
return nil return nil
} }
func FileExists(path string) bool {
_, err := os.Stat(path)
return err == nil
}
func optionalPath(path string) string { func optionalPath(path string) string {
if utils.FileExists(path) { if FileExists(path) {
return path return path
} }
return "" return ""
......
...@@ -9,40 +9,39 @@ import ( ...@@ -9,40 +9,39 @@ import (
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
type copyEntry struct { type CopyEntry struct {
Src string `yaml:"src"` Src string `yaml:"src"`
Dest string `yaml:"dest"` Dest string `yaml:"dest"`
} }
type linkEntry struct { type LinkEntry struct {
Src string `yaml:"src"`
Dest string `yaml:"dest"`
Apps []string `yaml:"apps,omitempty"` Apps []string `yaml:"apps,omitempty"`
App string `yaml:"app,omitempty"`
Sys string `yaml:"sys"`
User string `yaml:"user"`
} }
type releaseReplaceEntry struct { type ReplaceEntry struct {
Src string `yaml:"src"` Src string `yaml:"src"`
Dest string `yaml:"dest"` Dest string `yaml:"dest"`
} }
type grubEntry struct { type GrubEntry struct {
FixResolution bool `yaml:"fix-resolution,omitempty"` FixResolution bool `yaml:"fix-resolution,omitempty"`
Update bool `yaml:"update,omitempty"` Update bool `yaml:"update,omitempty"`
} }
type systemEntry struct { type SystemEntry struct {
Grub grubEntry `yaml:"grub,omitempty"` Grub GrubEntry `yaml:"grub,omitempty"`
} }
type hyprVar struct { type HyprVar struct {
Var string `yaml:"var"` Name string `yaml:"name"`
Value string `yaml:"value"` Value string `yaml:"value"`
Force bool `yaml:"force,omitempty"` Force bool `yaml:"force,omitempty"`
} }
type hyprModule struct { type HyprModule struct {
Module string `yaml:"module"` Name string `yaml:"name"`
Action string `yaml:"action"` Action string `yaml:"action"`
User bool `yaml:"user,omitempty"` User bool `yaml:"user,omitempty"`
NewOnly bool `yaml:"new-only,omitempty"` NewOnly bool `yaml:"new-only,omitempty"`
...@@ -51,35 +50,26 @@ type hyprModule struct { ...@@ -51,35 +50,26 @@ type hyprModule struct {
Init bool `yaml:"init,omitempty"` Init bool `yaml:"init,omitempty"`
} }
type hyprOptions struct { type HyprlandEntry struct {
SyncSystemLayouts bool `yaml:"sync-system-layouts,omitempty"` SyncLayouts bool `yaml:"sync-layouts,omitempty"`
} Vars []HyprVar `yaml:"vars,omitempty"`
Modules []HyprModule `yaml:"modules,omitempty"`
type hyprlandEntry struct {
Option hyprOptions `yaml:"options,omitempty"`
Vars []hyprVar `yaml:"vars,omitempty"`
Modules []hyprModule `yaml:"modules,omitempty"`
} }
type PresetProfile struct { type PresetProfile struct {
Binary string `yaml:"binary,omitempty"` Binary string `yaml:"binary,omitempty"`
Description string `yaml:"description,omitempty"` Description string `yaml:"description,omitempty"`
Root bool `yaml:"root,omitempty"` Root bool `yaml:"root,omitempty"`
Copy []copyEntry `yaml:"copy,omitempty"` Priority int `yaml:"priority,omitempty"`
Links []linkEntry `yaml:"links,omitempty"` Copy []CopyEntry `yaml:"copy,omitempty"`
ReleaseReplace []releaseReplaceEntry `yaml:"release-replace,omitempty"` Links []LinkEntry `yaml:"links,omitempty"`
System systemEntry `yaml:"system,omitempty"` Replace []ReplaceEntry `yaml:"replace,omitempty"`
System SystemEntry `yaml:"system,omitempty"`
// ----- hyprland ----- Hyprland HyprlandEntry `yaml:"hyprland,omitempty"`
Hyprland hyprlandEntry `yaml:"hyprland,omitempty"`
} }
type Presets struct { func loadPresetProfiles(dir string) (map[string]PresetProfile, error) {
Profiles map[string]PresetProfile `yaml:"profiles"` profiles := map[string]PresetProfile{}
}
func loadPresetConfig(dir string) (*Presets, error) {
cfg := &Presets{Profiles: map[string]PresetProfile{}}
entries, err := os.ReadDir(dir) entries, err := os.ReadDir(dir)
if err != nil { if err != nil {
...@@ -108,9 +98,8 @@ func loadPresetConfig(dir string) (*Presets, error) { ...@@ -108,9 +98,8 @@ func loadPresetConfig(dir string) (*Presets, error) {
} }
profileName := strings.TrimSuffix(name, filepath.Ext(name)) profileName := strings.TrimSuffix(name, filepath.Ext(name))
profiles[profileName] = p
cfg.Profiles[profileName] = p
} }
return cfg, nil return profiles, nil
} }
...@@ -4,7 +4,6 @@ import ( ...@@ -4,7 +4,6 @@ import (
"ximperconf/config" "ximperconf/config"
"ximperconf/locale" "ximperconf/locale"
"ximperconf/ui" "ximperconf/ui"
"ximperconf/utils"
"context" "context"
"errors" "errors"
...@@ -267,7 +266,7 @@ func HyprlandModuleEditCommand(ctx context.Context, cmd *cli.Command) error { ...@@ -267,7 +266,7 @@ func HyprlandModuleEditCommand(ctx context.Context, cmd *cli.Command) error {
return fmt.Errorf(locale.T("module '%s' not found: %s"), module, modulefile) return fmt.Errorf(locale.T("module '%s' not found: %s"), module, modulefile)
} }
editor := utils.GetEditor() editor := getEditor()
editCmd := exec.Command(editor, modulefile) editCmd := exec.Command(editor, modulefile)
editCmd.Stdin = os.Stdin editCmd.Stdin = os.Stdin
...@@ -344,3 +343,10 @@ func HyprlandModuleShowCommand(ctx context.Context, cmd *cli.Command) error { ...@@ -344,3 +343,10 @@ func HyprlandModuleShowCommand(ctx context.Context, cmd *cli.Command) error {
return nil return nil
} }
func getEditor() string {
if editor := os.Getenv("EDITOR"); editor != "" {
return editor
}
return "micro"
}
...@@ -13,7 +13,6 @@ import ( ...@@ -13,7 +13,6 @@ import (
"time" "time"
"ximperconf/config" "ximperconf/config"
"ximperconf/locale" "ximperconf/locale"
"ximperconf/utils"
) )
type HyprlandManager struct { type HyprlandManager struct {
...@@ -193,10 +192,10 @@ func (m *HyprlandManager) GetModuleInfo(module string, user bool) HyprModule { ...@@ -193,10 +192,10 @@ func (m *HyprlandManager) GetModuleInfo(module string, user bool) HyprModule {
} }
} }
FileExists := utils.FileExists(sysFile) FileExists := config.FileExists(sysFile)
// Конфиг Hyprland отсутствует // Конфиг Hyprland отсутствует
if !utils.FileExists(config.Env.Hyprland.Config) { if !config.FileExists(config.Env.Hyprland.Config) {
if FileExists { if FileExists {
return HyprModule{ return HyprModule{
Name: module, Name: module,
...@@ -796,7 +795,7 @@ func (m *HyprlandManager) GetPluginFile(name string) (string, error) { ...@@ -796,7 +795,7 @@ func (m *HyprlandManager) GetPluginFile(name string) (string, error) {
} }
path := filepath.Join(m.PluginsDir, name+".so") path := filepath.Join(m.PluginsDir, name+".so")
if !utils.FileExists(path) { if !config.FileExists(path) {
return "", errors.New(locale.T("plugin not found")) return "", errors.New(locale.T("plugin not found"))
} }
......
...@@ -5,7 +5,6 @@ import ( ...@@ -5,7 +5,6 @@ import (
"ximperconf/config" "ximperconf/config"
"ximperconf/locale" "ximperconf/locale"
"ximperconf/ui" "ximperconf/ui"
"ximperconf/utils"
"fmt" "fmt"
"os" "os"
...@@ -121,7 +120,7 @@ func hyprlandVarUnset(name string) error { ...@@ -121,7 +120,7 @@ func hyprlandVarUnset(name string) error {
color.Red(locale.T("Invalid variable name: %s"), name) color.Red(locale.T("Invalid variable name: %s"), name)
os.Exit(1) os.Exit(1)
} }
if !utils.FileExists(config.Env.Hyprland.Config) { if !config.FileExists(config.Env.Hyprland.Config) {
color.Red(locale.T("Configuration not found: %s"), config.Env.Hyprland.Config) color.Red(locale.T("Configuration not found: %s"), config.Env.Hyprland.Config)
os.Exit(1) os.Exit(1)
} }
......
...@@ -7,7 +7,8 @@ hyprland/var-actions.go ...@@ -7,7 +7,8 @@ hyprland/var-actions.go
main.go main.go
preset/actions.go preset/actions.go
preset/commands.go preset/commands.go
preset/tools.go preset/helpers.go
preset/result.go
repo/actions.go repo/actions.go
repo/commands.go repo/commands.go
system/commands.go system/commands.go
......
...@@ -6,7 +6,7 @@ msgid "" ...@@ -6,7 +6,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: ximperconf\n" "Project-Id-Version: ximperconf\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-02-14 18:34+0300\n" "POT-Creation-Date: 2026-02-14 21:56+0300\n"
"PO-Revision-Date: 2026-02-14 17:13+0300\n" "PO-Revision-Date: 2026-02-14 17:13+0300\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Russian\n" "Language-Team: Russian\n"
...@@ -229,21 +229,21 @@ msgstr "выгрузить плагин" ...@@ -229,21 +229,21 @@ msgstr "выгрузить плагин"
msgid "toggle plugin" msgid "toggle plugin"
msgstr "переключить плагин" msgstr "переключить плагин"
#: hyprland/keyboard-actions.go:42 #: hyprland/keyboard-actions.go:43
msgid "failed to get system layouts: %w" msgid "failed to get system layouts: %w"
msgstr "не удалось получить системные раскладки: %w" msgstr "не удалось получить системные раскладки: %w"
#: hyprland/keyboard-actions.go:49 #: hyprland/keyboard-actions.go:50
msgid "failed to update kb_layout in Hyprland: %w" msgid "failed to update kb_layout in Hyprland: %w"
msgstr "не удалось обновить kb_layout в Hyprland: %w" msgstr "не удалось обновить kb_layout в Hyprland: %w"
#: hyprland/keyboard-actions.go:51 #: hyprland/keyboard-actions.go:52
msgid "Layout updated!" msgid "Layout updated!"
msgstr "Раскладка обновлена!" msgstr "Раскладка обновлена!"
#: hyprland/keyboard-actions.go:53 #: hyprland/keyboard-actions.go:54
msgid "layout is already set, use '--force' for forced update" msgid "layout is already set, use '--force' for forced update"
msgstr "" msgstr "раскладка уже установлена, используйте '--force' для принудительного обновления"
"раскладка уже установлена, используйте '--force' для принудительного " "раскладка уже установлена, используйте '--force' для принудительного "
"обновления" "обновления"
...@@ -284,7 +284,7 @@ msgstr "модуль '%s' не изменён" ...@@ -284,7 +284,7 @@ msgstr "модуль '%s' не изменён"
msgid "module '%s' is already disabled" msgid "module '%s' is already disabled"
msgstr "модуль '%s' уже отключён" msgstr "модуль '%s' уже отключён"
#: hyprland/manager.go:416 preset/actions.go:240 preset/actions.go:267 #: hyprland/manager.go:416
#, c-format #, c-format
msgid "Module '%s' disabled" msgid "Module '%s' disabled"
msgstr "Модуль '%s' отключён" msgstr "Модуль '%s' отключён"
...@@ -294,12 +294,12 @@ msgstr "Модуль '%s' отключён" ...@@ -294,12 +294,12 @@ msgstr "Модуль '%s' отключён"
msgid "module '%s' not found in config" msgid "module '%s' not found in config"
msgstr "модуль '%s' не найден в конфигурации" msgstr "модуль '%s' не найден в конфигурации"
#: hyprland/manager.go:428 preset/actions.go:242 preset/actions.go:269 #: hyprland/manager.go:428
#, c-format #, c-format
msgid "Module '%s' removed" msgid "Module '%s' removed"
msgstr "Модуль '%s' удалён" msgstr "Модуль '%s' удалён"
#: hyprland/manager.go:434 preset/actions.go:238 preset/actions.go:265 #: hyprland/manager.go:434
#, c-format #, c-format
msgid "Module '%s' enabled" msgid "Module '%s' enabled"
msgstr "Модуль '%s' включён" msgstr "Модуль '%s' включён"
...@@ -343,7 +343,7 @@ msgstr "плагин не загружен" ...@@ -343,7 +343,7 @@ msgstr "плагин не загружен"
msgid "Plugin '%s' unloaded" msgid "Plugin '%s' unloaded"
msgstr "Плагин '%s' выгружен" msgstr "Плагин '%s' выгружен"
#: hyprland/manager.go:886 hyprland/manager.go:925 hyprland/var-actions.go:117 #: hyprland/manager.go:886 hyprland/manager.go:925 hyprland/var-actions.go:116
msgid "specify variable name" msgid "specify variable name"
msgstr "укажите имя переменной" msgstr "укажите имя переменной"
...@@ -371,43 +371,43 @@ msgstr "Переменная '%s' установлена: %s" ...@@ -371,43 +371,43 @@ msgstr "Переменная '%s' установлена: %s"
msgid "VARS block created, variable '%s' set: %s" msgid "VARS block created, variable '%s' set: %s"
msgstr "Блок VARS создан, переменная '%s' установлена: %s" msgstr "Блок VARS создан, переменная '%s' установлена: %s"
#: hyprland/manager.go:935 hyprland/var-actions.go:145 #: hyprland/manager.go:935 hyprland/var-actions.go:144
#, c-format #, c-format
msgid "variable '%s' not found" msgid "variable '%s' not found"
msgstr "переменная '%s' не найдена" msgstr "переменная '%s' не найдена"
#: hyprland/plugin-actions.go:53 #: hyprland/plugin-actions.go:54
msgid "no available plugins" msgid "no available plugins"
msgstr "нет доступных плагинов" msgstr "нет доступных плагинов"
#: hyprland/plugin-actions.go:80 #: hyprland/plugin-actions.go:81
msgid "Plugins" msgid "Plugins"
msgstr "Плагины" msgstr "Плагины"
#: hyprland/var-actions.go:52 #: hyprland/var-actions.go:51
msgid "No variables in configuration" msgid "No variables in configuration"
msgstr "Нет переменных в конфигурации" msgstr "Нет переменных в конфигурации"
#: hyprland/var-actions.go:77 #: hyprland/var-actions.go:76
msgid "Vars" msgid "Vars"
msgstr "Переменные" msgstr "Переменные"
#: hyprland/var-actions.go:94 #: hyprland/var-actions.go:93
#, c-format #, c-format
msgid "variable '%s' is not set" msgid "variable '%s' is not set"
msgstr "переменная '%s' не установлена" msgstr "переменная '%s' не установлена"
#: hyprland/var-actions.go:121 #: hyprland/var-actions.go:120
#, c-format #, c-format
msgid "Invalid variable name: %s" msgid "Invalid variable name: %s"
msgstr "Недопустимое имя переменной: %s" msgstr "Недопустимое имя переменной: %s"
#: hyprland/var-actions.go:125 #: hyprland/var-actions.go:124
#, c-format #, c-format
msgid "Configuration not found: %s" msgid "Configuration not found: %s"
msgstr "Конфигурация не найдена: %s" msgstr "Конфигурация не найдена: %s"
#: hyprland/var-actions.go:149 #: hyprland/var-actions.go:148
#, c-format #, c-format
msgid "Variable '%s' removed" msgid "Variable '%s' removed"
msgstr "Переменная '%s' удалена" msgstr "Переменная '%s' удалена"
...@@ -420,194 +420,194 @@ msgstr "Инструмент настройки Ximper Linux" ...@@ -420,194 +420,194 @@ msgstr "Инструмент настройки Ximper Linux"
msgid "show help" msgid "show help"
msgstr "показать справку" msgstr "показать справку"
#: preset/actions.go:77 #: preset/actions.go:30
msgid "copy"
msgstr "копия"
#: preset/actions.go:40
msgid "link"
msgstr "ссылка"
#: preset/actions.go:64
msgid "replace"
msgstr "замена"
#: preset/actions.go:70
#, c-format #, c-format
msgid "Not found: '%s'" msgid "Not found: '%s'"
msgstr "Не найден: '%s'" msgstr "Не найден: '%s'"
#: preset/actions.go:85 #: preset/actions.go:75
msgid "Failed to read %s: %v" msgid "Failed to read %s: %v"
msgstr "Не удалось прочитать %s: %v" msgstr "Не удалось прочитать %s: %v"
#: preset/actions.go:93 #: preset/actions.go:79
#, c-format #, c-format
msgid "Locked: %s" msgid "Locked: %s"
msgstr "Заблокирован: %s" msgstr "Заблокирован: %s"
#: preset/actions.go:105 #: preset/actions.go:88
#, c-format #, c-format
msgid "Up to date: '%s'" msgid "Up to date: '%s'"
msgstr "Актуально: '%s'" msgstr "Актуально: '%s'"
#: preset/actions.go:120 #: preset/actions.go:97
msgid "Replace error %s: %v" msgid "Replace error %s: %v"
msgstr "Ошибка замены %s: %v" msgstr "Ошибка замены %s: %v"
#: preset/actions.go:136 preset/tools.go:45 #: preset/actions.go:111 preset/actions.go:113 preset/actions.go:119
#: preset/actions.go:123 preset/actions.go:125
msgid "system"
msgstr "система"
#: preset/actions.go:119
msgid "GRUB error: %v"
msgstr "Ошибка GRUB: %v"
#: preset/actions.go:123 system/grub-actions.go:86
msgid "GRUB_GFXMODE fixed"
msgstr "GRUB_GFXMODE исправлен"
#: preset/actions.go:125 system/grub-actions.go:89
msgid "update-grub completed"
msgstr "update-grub выполнен"
#: preset/actions.go:133 preset/actions.go:139 preset/actions.go:144
#: preset/actions.go:146
msgid "var"
msgstr "переменная"
#: preset/actions.go:133
#, c-format #, c-format
msgid "Already exists: %s" msgid "Already exists: %s"
msgstr "Уже существует: %s" msgstr "Уже существует: %s"
#: preset/actions.go:153 #: preset/actions.go:144
msgid "Error setting %s: %v" msgid "Error setting %s: %v"
msgstr "Ошибка установки %s: %v" msgstr "Ошибка установки %s: %v"
#: preset/actions.go:170 #: preset/actions.go:154 preset/actions.go:159 preset/actions.go:167
#: preset/actions.go:173 preset/actions.go:181 preset/actions.go:184
#: preset/actions.go:190 preset/actions.go:195 preset/actions.go:197
msgid "module"
msgstr "модуль"
#: preset/actions.go:154
msgid "Skipped module without name" msgid "Skipped module without name"
msgstr "Пропущен модуль без имени" msgstr "Пропущен модуль без имени"
#: preset/actions.go:178 #: preset/actions.go:159
#, c-format #, c-format
msgid "Module '%s': unsupported action" msgid "Module '%s': unsupported action"
msgstr "Модуль '%s': неподдерживаемый action" msgstr "Модуль '%s': неподдерживаемый action"
#: preset/actions.go:190 #: preset/actions.go:167
#, c-format #, c-format
msgid "Module '%s': command '%s' failed" msgid "Module '%s': condition not met"
msgstr "Модуль '%s': команда '%s' завершилась с ошибкой" msgstr "Модуль '%s': условие не выполнено"
#: preset/actions.go:198 #: preset/actions.go:173
#, c-format
msgid "Module '%s': command '%s' returned no data"
msgstr "Модуль '%s': команда '%s' не вернула данных"
#: preset/actions.go:208
#, c-format #, c-format
msgid "Module '%s': binary '%s' not found" msgid "Module '%s': binary '%s' not found"
msgstr "Модуль '%s': бинарник '%s' не найден" msgstr "Модуль '%s': бинарник '%s' не найден"
#: preset/actions.go:220 preset/actions.go:226 #: preset/actions.go:181 preset/actions.go:184
#, c-format #, c-format
msgid "Created empty module: %s" msgid "Created empty module: %s"
msgstr "Создан пустой модуль: %s" msgstr "Создан пустой модуль: %s"
#: preset/actions.go:258 #: preset/actions.go:195
msgid "Error (%s): %v" msgid "Error (%s): %v"
msgstr "Ошибка (%s): %v" msgstr "Ошибка (%s): %v"
#: preset/actions.go:286 #: preset/actions.go:208 preset/actions.go:214 preset/actions.go:222
#: preset/actions.go:225 preset/actions.go:227
msgid "layout"
msgstr "раскладка"
#: preset/actions.go:208
msgid "Sync Hyprland layouts" msgid "Sync Hyprland layouts"
msgstr "Синхронизация раскладок Hyprland" msgstr "Синхронизация раскладок Hyprland"
#: preset/actions.go:295 #: preset/actions.go:214
msgid "Failed to get system layouts" msgid "Failed to get system layouts"
msgstr "Не удалось получить системные раскладки" msgstr "Не удалось получить системные раскладки"
#: preset/actions.go:308 #: preset/actions.go:222
#, c-format
msgid "XCB layout: %s"
msgstr "Раскладка XCB: %s"
#: preset/actions.go:312
#, c-format
msgid "Hyprland layout: %s"
msgstr "Раскладка Hyprland: %s"
#: preset/actions.go:319
msgid "Failed to update kb_layout" msgid "Failed to update kb_layout"
msgstr "Не удалось обновить kb_layout" msgstr "Не удалось обновить kb_layout"
#: preset/actions.go:325 #: preset/actions.go:225
#, c-format #, c-format
msgid "Layout updated: %s" msgid "Layout updated: %s"
msgstr "Раскладка обновлена: %s" msgstr "Раскладка обновлена: %s"
#: preset/actions.go:330 #: preset/actions.go:227
#, c-format #, c-format
msgid "Layout already set: %s" msgid "Layout already set: %s"
msgstr "Раскладка уже установлена: %s" msgstr "Раскладка уже установлена: %s"
#: preset/actions.go:354 #: preset/actions.go:270
msgid "GRUB error: %v"
msgstr "Ошибка GRUB: %v"
#: preset/actions.go:359 system/grub-actions.go:86
msgid "GRUB_GFXMODE fixed"
msgstr "GRUB_GFXMODE исправлен"
#: preset/actions.go:364 system/grub-actions.go:89
msgid "update-grub completed"
msgstr "update-grub выполнен"
#: preset/actions.go:392
#, c-format #, c-format
msgid "Creating profile: %s" msgid "Profile %s not found"
msgstr "Создаётся профиль: %s" msgstr "Профиль %s не найден"
#: preset/actions.go:394 #: preset/actions.go:275
#, c-format #, c-format
msgid "Profile %s unavailable" msgid "Profile %s unavailable"
msgstr "Профиль %s недоступен" msgstr "Профиль %s недоступен"
#: preset/actions.go:408 preset/actions.go:468 #: preset/actions.go:287 preset/actions.go:338
#, c-format
msgid "Applying profile: %s"
msgstr "Применяем профиль: %s"
#: preset/actions.go:321 preset/helpers.go:73
msgid "no available profiles" msgid "no available profiles"
msgstr "нет доступных профилей" msgstr "нет доступных профилей"
#: preset/actions.go:430 #: preset/commands.go:13
msgid "Profiles" msgid "Show what would be created without making changes"
msgstr "Профили" msgstr "Показать что будет создано без внесения изменений"
#: preset/actions.go:451
#, c-format
msgid "Profile %s not found"
msgstr "Профиль %s не найден"
#: preset/actions.go:472 #: preset/commands.go:18
msgid "Creating all available profiles..." msgid "Force overwrite existing files (backup to .bak)"
msgstr "Создаём все доступные профили..." msgstr "Принудительная перезапись файлов (бэкап в .bak)"
#: preset/commands.go:15 #: preset/commands.go:26
msgid "Manage preset configuration profiles" msgid "Manage preset configuration profiles"
msgstr "Управление профилями конфигурации" msgstr "Управление профилями конфигурации"
#: preset/commands.go:19 #: preset/commands.go:30
msgid "Show information about preset profiles" msgid "Show information about preset profiles"
msgstr "Показать информацию о профилях" msgstr "Показать информацию о профилях"
#: preset/commands.go:27 #: preset/commands.go:38
msgid "Apply a profile" msgid "Apply a profile"
msgstr "Применить профиль" msgstr "Применить профиль"
#: preset/commands.go:31 preset/commands.go:45 #: preset/commands.go:45
msgid "Show what would be created without making changes"
msgstr "Показать что будет создано без внесения изменений"
#: preset/commands.go:41
msgid "Apply all available profiles" msgid "Apply all available profiles"
msgstr "Применить все доступные профили" msgstr "Применить все доступные профили"
#: preset/tools.go:51 #: preset/helpers.go:93
#, c-format msgid "Profiles"
msgid "Error: %s" msgstr "Профили"
msgstr "Ошибка: %s"
#: preset/tools.go:85
msgid "Copying"
msgstr "Копируем"
#: preset/tools.go:86
msgid "Creating links"
msgstr "Создаём ссылки"
#: preset/tools.go:87
msgid "Updating"
msgstr "Обновляем"
#: preset/tools.go:88
msgid "Configuring system"
msgstr "Настраиваем систему"
#: preset/tools.go:89 #: preset/result.go:74
msgid "Creating Hyprland variables" msgid "done"
msgstr "Создаём переменные Hyprland" msgstr "выполнено"
#: preset/tools.go:90 #: preset/result.go:77
msgid "Enabling Hyprland modules" msgid "skipped"
msgstr "Подключаем модули Hyprland" msgstr "пропущено"
#: preset/tools.go:91 #: preset/result.go:80
msgid "Syncing Hyprland layout" msgid "error"
msgstr "Синхронизируем раскладку Hyprland" msgid_plural "errors"
msgstr[0] "ошибка"
msgstr[1] "ошибки"
msgstr[2] "ошибок"
#: repo/actions.go:31 repo/actions.go:38 #: repo/actions.go:31 repo/actions.go:38
msgid "Error fetching data" msgid "Error fetching data"
...@@ -718,3 +718,39 @@ msgstr "Глобальные параметры:" ...@@ -718,3 +718,39 @@ msgstr "Глобальные параметры:"
#: ui/help.go:125 #: ui/help.go:125
msgid "default" msgid "default"
msgstr "по умолчанию" msgstr "по умолчанию"
#, c-format
#~ msgid "Module '%s': command '%s' failed"
#~ msgstr "Модуль '%s': команда '%s' завершилась с ошибкой"
#, c-format
#~ msgid "Hyprland layout: %s"
#~ msgstr "Раскладка Hyprland: %s"
#~ msgid "Creating all available profiles..."
#~ msgstr "Создаём все доступные профили..."
#, c-format
#~ msgid "Error: %s"
#~ msgstr "Ошибка: %s"
#~ msgid "Copying"
#~ msgstr "Копируем"
#~ msgid "Creating links"
#~ msgstr "Создаём ссылки"
#~ msgid "Updating"
#~ msgstr "Обновляем"
#~ msgid "Configuring system"
#~ msgstr "Настраиваем систему"
#~ msgid "Creating Hyprland variables"
#~ msgstr "Создаём переменные Hyprland"
#~ msgid "Enabling Hyprland modules"
#~ msgstr "Подключаем модули Hyprland"
#~ msgid "Syncing Hyprland layout"
#~ msgstr "Синхронизируем раскладку Hyprland"
...@@ -8,7 +8,7 @@ msgid "" ...@@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: ximperconf\n" "Project-Id-Version: ximperconf\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-02-14 18:34+0300\n" "POT-Creation-Date: 2026-02-14 21:56+0300\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
...@@ -16,6 +16,7 @@ msgstr "" ...@@ -16,6 +16,7 @@ msgstr ""
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
#: hyprland/actions.go:27 #: hyprland/actions.go:27
msgid "Configuration fixed" msgid "Configuration fixed"
...@@ -229,19 +230,19 @@ msgstr "" ...@@ -229,19 +230,19 @@ msgstr ""
msgid "toggle plugin" msgid "toggle plugin"
msgstr "" msgstr ""
#: hyprland/keyboard-actions.go:42 #: hyprland/keyboard-actions.go:43
msgid "failed to get system layouts: %w" msgid "failed to get system layouts: %w"
msgstr "" msgstr ""
#: hyprland/keyboard-actions.go:49 #: hyprland/keyboard-actions.go:50
msgid "failed to update kb_layout in Hyprland: %w" msgid "failed to update kb_layout in Hyprland: %w"
msgstr "" msgstr ""
#: hyprland/keyboard-actions.go:51 #: hyprland/keyboard-actions.go:52
msgid "Layout updated!" msgid "Layout updated!"
msgstr "" msgstr ""
#: hyprland/keyboard-actions.go:53 #: hyprland/keyboard-actions.go:54
msgid "layout is already set, use '--force' for forced update" msgid "layout is already set, use '--force' for forced update"
msgstr "" msgstr ""
...@@ -282,7 +283,7 @@ msgstr "" ...@@ -282,7 +283,7 @@ msgstr ""
msgid "module '%s' is already disabled" msgid "module '%s' is already disabled"
msgstr "" msgstr ""
#: hyprland/manager.go:416 preset/actions.go:240 preset/actions.go:267 #: hyprland/manager.go:416
#, c-format #, c-format
msgid "Module '%s' disabled" msgid "Module '%s' disabled"
msgstr "" msgstr ""
...@@ -292,12 +293,12 @@ msgstr "" ...@@ -292,12 +293,12 @@ msgstr ""
msgid "module '%s' not found in config" msgid "module '%s' not found in config"
msgstr "" msgstr ""
#: hyprland/manager.go:428 preset/actions.go:242 preset/actions.go:269 #: hyprland/manager.go:428
#, c-format #, c-format
msgid "Module '%s' removed" msgid "Module '%s' removed"
msgstr "" msgstr ""
#: hyprland/manager.go:434 preset/actions.go:238 preset/actions.go:265 #: hyprland/manager.go:434
#, c-format #, c-format
msgid "Module '%s' enabled" msgid "Module '%s' enabled"
msgstr "" msgstr ""
...@@ -339,7 +340,7 @@ msgstr "" ...@@ -339,7 +340,7 @@ msgstr ""
msgid "Plugin '%s' unloaded" msgid "Plugin '%s' unloaded"
msgstr "" msgstr ""
#: hyprland/manager.go:886 hyprland/manager.go:925 hyprland/var-actions.go:117 #: hyprland/manager.go:886 hyprland/manager.go:925 hyprland/var-actions.go:116
msgid "specify variable name" msgid "specify variable name"
msgstr "" msgstr ""
...@@ -367,43 +368,43 @@ msgstr "" ...@@ -367,43 +368,43 @@ msgstr ""
msgid "VARS block created, variable '%s' set: %s" msgid "VARS block created, variable '%s' set: %s"
msgstr "" msgstr ""
#: hyprland/manager.go:935 hyprland/var-actions.go:145 #: hyprland/manager.go:935 hyprland/var-actions.go:144
#, c-format #, c-format
msgid "variable '%s' not found" msgid "variable '%s' not found"
msgstr "" msgstr ""
#: hyprland/plugin-actions.go:53 #: hyprland/plugin-actions.go:54
msgid "no available plugins" msgid "no available plugins"
msgstr "" msgstr ""
#: hyprland/plugin-actions.go:80 #: hyprland/plugin-actions.go:81
msgid "Plugins" msgid "Plugins"
msgstr "" msgstr ""
#: hyprland/var-actions.go:52 #: hyprland/var-actions.go:51
msgid "No variables in configuration" msgid "No variables in configuration"
msgstr "" msgstr ""
#: hyprland/var-actions.go:77 #: hyprland/var-actions.go:76
msgid "Vars" msgid "Vars"
msgstr "" msgstr ""
#: hyprland/var-actions.go:94 #: hyprland/var-actions.go:93
#, c-format #, c-format
msgid "variable '%s' is not set" msgid "variable '%s' is not set"
msgstr "" msgstr ""
#: hyprland/var-actions.go:121 #: hyprland/var-actions.go:120
#, c-format #, c-format
msgid "Invalid variable name: %s" msgid "Invalid variable name: %s"
msgstr "" msgstr ""
#: hyprland/var-actions.go:125 #: hyprland/var-actions.go:124
#, c-format #, c-format
msgid "Configuration not found: %s" msgid "Configuration not found: %s"
msgstr "" msgstr ""
#: hyprland/var-actions.go:149 #: hyprland/var-actions.go:148
#, c-format #, c-format
msgid "Variable '%s' removed" msgid "Variable '%s' removed"
msgstr "" msgstr ""
...@@ -416,194 +417,193 @@ msgstr "" ...@@ -416,194 +417,193 @@ msgstr ""
msgid "show help" msgid "show help"
msgstr "" msgstr ""
#: preset/actions.go:77 #: preset/actions.go:30
msgid "copy"
msgstr ""
#: preset/actions.go:40
msgid "link"
msgstr ""
#: preset/actions.go:64
msgid "replace"
msgstr ""
#: preset/actions.go:70
#, c-format #, c-format
msgid "Not found: '%s'" msgid "Not found: '%s'"
msgstr "" msgstr ""
#: preset/actions.go:85 #: preset/actions.go:75
msgid "Failed to read %s: %v" msgid "Failed to read %s: %v"
msgstr "" msgstr ""
#: preset/actions.go:93 #: preset/actions.go:79
#, c-format #, c-format
msgid "Locked: %s" msgid "Locked: %s"
msgstr "" msgstr ""
#: preset/actions.go:105 #: preset/actions.go:88
#, c-format #, c-format
msgid "Up to date: '%s'" msgid "Up to date: '%s'"
msgstr "" msgstr ""
#: preset/actions.go:120 #: preset/actions.go:97
msgid "Replace error %s: %v" msgid "Replace error %s: %v"
msgstr "" msgstr ""
#: preset/actions.go:136 preset/tools.go:45 #: preset/actions.go:111 preset/actions.go:113 preset/actions.go:119
#: preset/actions.go:123 preset/actions.go:125
msgid "system"
msgstr ""
#: preset/actions.go:119
msgid "GRUB error: %v"
msgstr ""
#: preset/actions.go:123 system/grub-actions.go:86
msgid "GRUB_GFXMODE fixed"
msgstr ""
#: preset/actions.go:125 system/grub-actions.go:89
msgid "update-grub completed"
msgstr ""
#: preset/actions.go:133 preset/actions.go:139 preset/actions.go:144
#: preset/actions.go:146
msgid "var"
msgstr ""
#: preset/actions.go:133
#, c-format #, c-format
msgid "Already exists: %s" msgid "Already exists: %s"
msgstr "" msgstr ""
#: preset/actions.go:153 #: preset/actions.go:144
msgid "Error setting %s: %v" msgid "Error setting %s: %v"
msgstr "" msgstr ""
#: preset/actions.go:170 #: preset/actions.go:154 preset/actions.go:159 preset/actions.go:167
msgid "Skipped module without name" #: preset/actions.go:173 preset/actions.go:181 preset/actions.go:184
#: preset/actions.go:190 preset/actions.go:195 preset/actions.go:197
msgid "module"
msgstr "" msgstr ""
#: preset/actions.go:178 #: preset/actions.go:154
#, c-format msgid "Skipped module without name"
msgid "Module '%s': unsupported action"
msgstr "" msgstr ""
#: preset/actions.go:190 #: preset/actions.go:159
#, c-format #, c-format
msgid "Module '%s': command '%s' failed" msgid "Module '%s': unsupported action"
msgstr "" msgstr ""
#: preset/actions.go:198 #: preset/actions.go:167
#, c-format #, c-format
msgid "Module '%s': command '%s' returned no data" msgid "Module '%s': condition not met"
msgstr "" msgstr ""
#: preset/actions.go:208 #: preset/actions.go:173
#, c-format #, c-format
msgid "Module '%s': binary '%s' not found" msgid "Module '%s': binary '%s' not found"
msgstr "" msgstr ""
#: preset/actions.go:220 preset/actions.go:226 #: preset/actions.go:181 preset/actions.go:184
#, c-format #, c-format
msgid "Created empty module: %s" msgid "Created empty module: %s"
msgstr "" msgstr ""
#: preset/actions.go:258 #: preset/actions.go:195
msgid "Error (%s): %v" msgid "Error (%s): %v"
msgstr "" msgstr ""
#: preset/actions.go:286 #: preset/actions.go:208 preset/actions.go:214 preset/actions.go:222
msgid "Sync Hyprland layouts" #: preset/actions.go:225 preset/actions.go:227
msgstr "" msgid "layout"
#: preset/actions.go:295
msgid "Failed to get system layouts"
msgstr "" msgstr ""
#: preset/actions.go:308 #: preset/actions.go:208
#, c-format msgid "Sync Hyprland layouts"
msgid "XCB layout: %s"
msgstr "" msgstr ""
#: preset/actions.go:312 #: preset/actions.go:214
#, c-format msgid "Failed to get system layouts"
msgid "Hyprland layout: %s"
msgstr "" msgstr ""
#: preset/actions.go:319 #: preset/actions.go:222
msgid "Failed to update kb_layout" msgid "Failed to update kb_layout"
msgstr "" msgstr ""
#: preset/actions.go:325 #: preset/actions.go:225
#, c-format #, c-format
msgid "Layout updated: %s" msgid "Layout updated: %s"
msgstr "" msgstr ""
#: preset/actions.go:330 #: preset/actions.go:227
#, c-format #, c-format
msgid "Layout already set: %s" msgid "Layout already set: %s"
msgstr "" msgstr ""
#: preset/actions.go:354 #: preset/actions.go:270
msgid "GRUB error: %v"
msgstr ""
#: preset/actions.go:359 system/grub-actions.go:86
msgid "GRUB_GFXMODE fixed"
msgstr ""
#: preset/actions.go:364 system/grub-actions.go:89
msgid "update-grub completed"
msgstr ""
#: preset/actions.go:392
#, c-format #, c-format
msgid "Creating profile: %s" msgid "Profile %s not found"
msgstr "" msgstr ""
#: preset/actions.go:394 #: preset/actions.go:275
#, c-format #, c-format
msgid "Profile %s unavailable" msgid "Profile %s unavailable"
msgstr "" msgstr ""
#: preset/actions.go:408 preset/actions.go:468 #: preset/actions.go:287 preset/actions.go:338
msgid "no available profiles" #, c-format
msgid "Applying profile: %s"
msgstr "" msgstr ""
#: preset/actions.go:430 #: preset/actions.go:321 preset/helpers.go:73
msgid "Profiles" msgid "no available profiles"
msgstr "" msgstr ""
#: preset/actions.go:451 #: preset/commands.go:13
#, c-format msgid "Show what would be created without making changes"
msgid "Profile %s not found"
msgstr "" msgstr ""
#: preset/actions.go:472 #: preset/commands.go:18
msgid "Creating all available profiles..." msgid "Force overwrite existing files (backup to .bak)"
msgstr "" msgstr ""
#: preset/commands.go:15 #: preset/commands.go:26
msgid "Manage preset configuration profiles" msgid "Manage preset configuration profiles"
msgstr "" msgstr ""
#: preset/commands.go:19 #: preset/commands.go:30
msgid "Show information about preset profiles" msgid "Show information about preset profiles"
msgstr "" msgstr ""
#: preset/commands.go:27 #: preset/commands.go:38
msgid "Apply a profile" msgid "Apply a profile"
msgstr "" msgstr ""
#: preset/commands.go:31 preset/commands.go:45 #: preset/commands.go:45
msgid "Show what would be created without making changes"
msgstr ""
#: preset/commands.go:41
msgid "Apply all available profiles" msgid "Apply all available profiles"
msgstr "" msgstr ""
#: preset/tools.go:51 #: preset/helpers.go:93
#, c-format msgid "Profiles"
msgid "Error: %s"
msgstr ""
#: preset/tools.go:85
msgid "Copying"
msgstr ""
#: preset/tools.go:86
msgid "Creating links"
msgstr ""
#: preset/tools.go:87
msgid "Updating"
msgstr ""
#: preset/tools.go:88
msgid "Configuring system"
msgstr "" msgstr ""
#: preset/tools.go:89 #: preset/result.go:74
msgid "Creating Hyprland variables" msgid "done"
msgstr "" msgstr ""
#: preset/tools.go:90 #: preset/result.go:77
msgid "Enabling Hyprland modules" msgid "skipped"
msgstr "" msgstr ""
#: preset/tools.go:91 #: preset/result.go:80
msgid "Syncing Hyprland layout" msgid "error"
msgstr "" msgid_plural "errors"
msgstr[0] ""
msgstr[1] ""
#: repo/actions.go:31 repo/actions.go:38 #: repo/actions.go:31 repo/actions.go:38
msgid "Error fetching data" msgid "Error fetching data"
......
...@@ -2,44 +2,42 @@ package preset ...@@ -2,44 +2,42 @@ package preset
import ( import (
"bytes" "bytes"
"slices"
"ximperconf/config"
"ximperconf/hyprland"
"ximperconf/locale"
"ximperconf/system"
"ximperconf/ui"
"context" "context"
"fmt" "fmt"
"os" "os"
"os/exec" "os/exec"
"slices"
"sort"
"strings" "strings"
"ximperconf/config"
"ximperconf/hyprland"
"ximperconf/locale"
"ximperconf/system"
"github.com/fatih/color" "github.com/fatih/color"
"github.com/urfave/cli/v3" "github.com/urfave/cli/v3"
) )
var ( var validActions = []string{"enable", "disable", "remove"}
validActions = []string{"enable", "disable", "remove"}
) // ===== Process functions =====
func processCopies(prof config.PresetProfile, opts opOptions, res *Result) { func processCopies(prof config.PresetProfile, opts opOptions, res *Result) {
for _, c := range prof.Copy { for _, c := range prof.Copy {
src := expandPath(c.Src, "") src := expandPath(c.Src, "")
dest := expandPath(c.Dest, "") dest := expandPath(c.Dest, "")
r := copyIfMissing(src, dest, opts) status, msg := execCopy(src, dest, opts)
AppendOpResult(res, r) res.Add(locale.T("copy"), msg, status)
} }
} }
func processLinks(prof config.PresetProfile, opts opOptions, res *Result) { func processLinks(prof config.PresetProfile, opts opOptions, res *Result) {
for _, entry := range prof.Links { for _, entry := range prof.Links {
handle := func(app string) { handle := func(app string) {
sys := expandPath(entry.Sys, app) src := expandPath(entry.Src, app)
user := expandPath(entry.User, app) dest := expandPath(entry.Dest, app)
r := createLinkIfMissing(sys, user, opts) status, msg := execLink(src, dest, opts)
AppendOpResult(res, r) res.Add(locale.T("link"), msg, status)
} }
if len(entry.Apps) > 0 { if len(entry.Apps) > 0 {
...@@ -51,48 +49,34 @@ func processLinks(prof config.PresetProfile, opts opOptions, res *Result) { ...@@ -51,48 +49,34 @@ func processLinks(prof config.PresetProfile, opts opOptions, res *Result) {
continue continue
} }
if entry.App != "" && !commandExists(entry.App) {
continue
}
handle("") handle("")
} }
} }
func processReleaseReplace(prof config.PresetProfile, opts opOptions, res *Result) { func processReplace(prof config.PresetProfile, opts opOptions, res *Result) {
sysVer := getSystemVersion() sysVer := getSystemVersion()
for _, rr := range prof.ReleaseReplace { for _, rr := range prof.Replace {
src := expandPath(rr.Src, "") src := expandPath(rr.Src, "")
dest := expandPath(rr.Dest, "") dest := expandPath(rr.Dest, "")
item := handleReleaseReplace(src, dest, sysVer, opts) status, msg := handleReplace(src, dest, sysVer, opts)
res.Replaced = append(res.Replaced, item) res.Add(locale.T("replace"), msg, status)
} }
} }
func handleReleaseReplace(src, dest, sysVer string, opts opOptions) ui.TreeItem { func handleReplace(src, dest, sysVer string, opts opOptions) (config.ItemStatus, string) {
if _, err := os.Stat(dest); os.IsNotExist(err) { if _, err := os.Stat(dest); os.IsNotExist(err) {
return ui.TreeItem{ return config.OpStatus.Skipped, fmt.Sprintf(locale.T("Not found: '%s'"), dest)
Name: fmt.Sprintf(locale.T("Not found: '%s'"), dest),
Status: config.OpStatus.Skipped,
}
} }
data, err := os.ReadFile(dest) data, err := os.ReadFile(dest)
if err != nil { if err != nil {
return ui.TreeItem{ return config.OpStatus.Error, fmt.Sprintf(locale.T("Failed to read %s: %v"), dest, err)
Name: fmt.Sprintf(locale.T("Failed to read %s: %v"), dest, err),
Status: config.OpStatus.Error,
}
} }
content := string(data) if strings.Contains(string(data), "XIMPER_LOCK") {
if strings.Contains(content, "XIMPER_LOCK") { return config.OpStatus.Skipped, fmt.Sprintf(locale.T("Locked: %s"), dest)
return ui.TreeItem{
Name: fmt.Sprintf(locale.T("Locked: %s"), dest),
Status: config.OpStatus.Skipped,
}
} }
fileVer := getFileVersionTag(dest) fileVer := getFileVersionTag(dest)
...@@ -100,341 +84,178 @@ func handleReleaseReplace(src, dest, sysVer string, opts opOptions) ui.TreeItem ...@@ -100,341 +84,178 @@ func handleReleaseReplace(src, dest, sysVer string, opts opOptions) ui.TreeItem
fileVer = "0.9.3" fileVer = "0.9.3"
} }
if fileVer == sysVer { if !opts.Force && fileVer == sysVer {
return ui.TreeItem{ return config.OpStatus.Skipped, fmt.Sprintf(locale.T("Up to date: '%s'"), dest)
Name: fmt.Sprintf(locale.T("Up to date: '%s'"), dest),
Status: config.OpStatus.Skipped,
}
} }
if opts.DryRun { if opts.DryRun {
return ui.TreeItem{ return config.OpStatus.DryRun, fmt.Sprintf("%s → %s", src, dest)
Name: fmt.Sprintf("%s → %s", src, dest),
Status: config.OpStatus.DryRun,
}
} }
_ = os.Rename(dest, dest+".old") _ = os.Rename(dest, dest+".bak")
if err := copyFile(src, dest); err != nil { if err := copyFile(src, dest); err != nil {
return ui.TreeItem{ return config.OpStatus.Error, fmt.Sprintf(locale.T("Replace error %s: %v"), dest, err)
Name: fmt.Sprintf(locale.T("Replace error %s: %v"), dest, err), }
Status: config.OpStatus.Error,
return config.OpStatus.Done, fmt.Sprintf("%s (%s → %s)", dest, fileVer, sysVer)
}
func processSystemGrub(prof config.PresetProfile, opts opOptions, res *Result) {
if !prof.System.Grub.FixResolution {
return
}
autoUpdate := prof.System.Grub.Update
if opts.DryRun {
res.Add(locale.T("system"), "GRUB_GFXMODE", config.OpStatus.DryRun)
if autoUpdate {
res.Add(locale.T("system"), "update-grub", config.OpStatus.DryRun)
} }
return
}
if err := system.SystemGrubFixResolution(autoUpdate, false); err != nil {
res.Add(locale.T("system"), fmt.Sprintf(locale.T("GRUB error: %v"), err), config.OpStatus.Error)
return
} }
return ui.TreeItem{ res.Add(locale.T("system"), locale.T("GRUB_GFXMODE fixed"), config.OpStatus.Done)
Name: fmt.Sprintf("%s (%s → %s)", dest, fileVer, sysVer), if autoUpdate {
Status: config.OpStatus.Done, res.Add(locale.T("system"), locale.T("update-grub completed"), config.OpStatus.Done)
} }
} }
func processHyprVars(manager *hyprland.HyprlandManager, prof config.PresetProfile, opts opOptions, res *Result) { func processHyprVars(manager *hyprland.HyprlandManager, prof config.PresetProfile, opts opOptions, res *Result) {
for _, v := range prof.Hyprland.Vars { for _, v := range prof.Hyprland.Vars {
if !v.Force { if !v.Force {
if V := manager.GetVar(v.Var); V != "" { if existing := manager.GetVar(v.Name); existing != "" {
res.HyprVars = append(res.HyprVars, ui.TreeItem{ res.Add(locale.T("var"), fmt.Sprintf(locale.T("Already exists: %s"), v.Name), config.OpStatus.Skipped)
Name: fmt.Sprintf(locale.T("Already exists: %s"), v.Var),
Status: config.OpStatus.Skipped,
})
continue continue
} }
} }
if opts.DryRun { if opts.DryRun {
res.HyprVars = append(res.HyprVars, ui.TreeItem{ res.Add(locale.T("var"), fmt.Sprintf("%s = %s", v.Name, v.Value), config.OpStatus.DryRun)
Name: fmt.Sprintf("%s = %s", v.Var, v.Value),
Status: config.OpStatus.DryRun,
})
continue continue
} }
if _, err := manager.SetVar(v.Var, v.Value); err != nil { if _, err := manager.SetVar(v.Name, v.Value); err != nil {
res.HyprVars = append(res.HyprVars, ui.TreeItem{ res.Add(locale.T("var"), fmt.Sprintf(locale.T("Error setting %s: %v"), v.Name, err), config.OpStatus.Error)
Name: fmt.Sprintf(locale.T("Error setting %s: %v"), v.Var, err),
Status: config.OpStatus.Error,
})
} else { } else {
res.HyprVars = append(res.HyprVars, ui.TreeItem{ res.Add(locale.T("var"), fmt.Sprintf("%s = %s", v.Name, v.Value), config.OpStatus.Done)
Name: fmt.Sprintf("%s = %s", v.Var, v.Value),
Status: config.OpStatus.Done,
})
} }
} }
} }
func processHyprModules(manager *hyprland.HyprlandManager, prof config.PresetProfile, opts opOptions, res *Result) { func processHyprModules(manager *hyprland.HyprlandManager, prof config.PresetProfile, opts opOptions, res *Result) {
for _, m := range prof.Hyprland.Modules { for _, m := range prof.Hyprland.Modules {
if m.Name == "" {
if m.Module == "" { res.Add(locale.T("module"), locale.T("Skipped module without name"), config.OpStatus.Skipped)
res.HyprModules = append(res.HyprModules, ui.TreeItem{
Name: locale.T("Skipped module without name"),
Status: config.OpStatus.Skipped,
})
continue continue
} }
if !slices.Contains(validActions, m.Action) { if !slices.Contains(validActions, m.Action) {
res.HyprModules = append(res.HyprModules, ui.TreeItem{ res.Add(locale.T("module"), fmt.Sprintf(locale.T("Module '%s': unsupported action"), m.Name), config.OpStatus.Skipped)
Name: fmt.Sprintf(locale.T("Module '%s': unsupported action"), m.Module),
Status: config.OpStatus.Skipped,
})
continue continue
} }
// if-exec
if m.IfExec != "" { if m.IfExec != "" {
cmd := exec.Command("sh", "-c", m.IfExec) cmd := exec.Command("sh", "-c", m.IfExec)
output, err := cmd.Output() output, err := cmd.Output()
if err != nil { if err != nil || len(bytes.TrimSpace(output)) == 0 {
res.HyprModules = append(res.HyprModules, ui.TreeItem{ res.Add(locale.T("module"), fmt.Sprintf(locale.T("Module '%s': condition not met"), m.Name), config.OpStatus.Skipped)
Name: fmt.Sprintf(locale.T("Module '%s': command '%s' failed"), m.Module, m.IfExec),
Status: config.OpStatus.Skipped,
})
continue
}
if len(bytes.TrimSpace(output)) == 0 {
res.HyprModules = append(res.HyprModules, ui.TreeItem{
Name: fmt.Sprintf(locale.T("Module '%s': command '%s' returned no data"), m.Module, m.IfExec),
Status: config.OpStatus.Skipped,
})
continue continue
} }
} }
// if-binary
if m.IfBinary != "" && !commandExists(m.IfBinary) { if m.IfBinary != "" && !commandExists(m.IfBinary) {
res.HyprModules = append(res.HyprModules, ui.TreeItem{ res.Add(locale.T("module"), fmt.Sprintf(locale.T("Module '%s': binary '%s' not found"), m.Name, m.IfBinary), config.OpStatus.Skipped)
Name: fmt.Sprintf(locale.T("Module '%s': binary '%s' not found"), m.Module, m.IfBinary),
Status: config.OpStatus.Skipped,
})
continue continue
} }
if m.Init && m.User && m.Action == "enable" { if m.Init && m.User && m.Action == "enable" {
modulefile := manager.GetModuleFile(m.Module, m.User) modulefile := manager.GetModuleFile(m.Name, m.User)
if _, err := os.Stat(modulefile); os.IsNotExist(err) { if _, err := os.Stat(modulefile); os.IsNotExist(err) {
if opts.DryRun { if opts.DryRun {
res.HyprModules = append(res.HyprModules, ui.TreeItem{ res.Add(locale.T("module"), fmt.Sprintf(locale.T("Created empty module: %s"), m.Name), config.OpStatus.DryRun)
Name: fmt.Sprintf(locale.T("Created empty module: %s"), m.Module),
Status: config.OpStatus.DryRun,
})
} else { } else {
_ = os.WriteFile(modulefile, []byte(""), 0o644) _ = os.WriteFile(modulefile, []byte(""), 0o644)
res.HyprModules = append(res.HyprModules, ui.TreeItem{ res.Add(locale.T("module"), fmt.Sprintf(locale.T("Created empty module: %s"), m.Name), config.OpStatus.Done)
Name: fmt.Sprintf(locale.T("Created empty module: %s"), m.Module),
Status: config.OpStatus.Done,
})
} }
} }
} }
// dry-run
if opts.DryRun { if opts.DryRun {
var msg string res.Add(locale.T("module"), fmt.Sprintf("%s: %s", m.Name, m.Action), config.OpStatus.DryRun)
switch m.Action {
case "enable":
msg = fmt.Sprintf(locale.T("Module '%s' enabled"), m.Module)
case "disable":
msg = fmt.Sprintf(locale.T("Module '%s' disabled"), m.Module)
case "remove":
msg = fmt.Sprintf(locale.T("Module '%s' removed"), m.Module)
}
res.HyprModules = append(res.HyprModules, ui.TreeItem{
Name: msg,
Status: config.OpStatus.DryRun,
})
continue continue
} }
if _, err := manager.SetModule( if _, err := manager.SetModule(m.Action, m.Name, m.User, m.NewOnly); err != nil {
m.Action, res.Add(locale.T("module"), fmt.Sprintf(locale.T("Error (%s): %v"), m.Name, err), config.OpStatus.Error)
m.Module,
m.User,
m.NewOnly,
); err != nil {
res.HyprModules = append(res.HyprModules, ui.TreeItem{
Name: fmt.Sprintf(locale.T("Error (%s): %v"), m.Module, err),
Status: config.OpStatus.Error,
})
} else { } else {
var msg string res.Add(locale.T("module"), fmt.Sprintf("%s: %s", m.Name, m.Action), config.OpStatus.Done)
switch m.Action {
case "enable":
msg = fmt.Sprintf(locale.T("Module '%s' enabled"), m.Module)
case "disable":
msg = fmt.Sprintf(locale.T("Module '%s' disabled"), m.Module)
case "remove":
msg = fmt.Sprintf(locale.T("Module '%s' removed"), m.Module)
}
res.HyprModules = append(res.HyprModules, ui.TreeItem{
Name: msg,
Status: config.OpStatus.Done,
})
} }
} }
} }
func processHyprLayoutSync(manager *hyprland.HyprlandManager, prof config.PresetProfile, opts opOptions, res *Result) { func processHyprLayoutSync(manager *hyprland.HyprlandManager, prof config.PresetProfile, opts opOptions, res *Result) {
if !prof.Hyprland.Option.SyncSystemLayouts { if !prof.Hyprland.SyncLayouts {
return return
} }
if opts.DryRun { if opts.DryRun {
res.SyncSystemLayouts = append(res.SyncSystemLayouts, ui.TreeItem{ res.Add(locale.T("layout"), locale.T("Sync Hyprland layouts"), config.OpStatus.DryRun)
Name: locale.T("Sync Hyprland layouts"),
Status: config.OpStatus.DryRun,
})
return return
} }
sysLayouts, err := hyprland.HyprlandGetKeyboardLayouts() sysLayouts, err := hyprland.HyprlandGetKeyboardLayouts()
if err != nil { if err != nil {
res.SyncSystemLayouts = append(res.SyncSystemLayouts, ui.TreeItem{ res.Add(locale.T("layout"), locale.T("Failed to get system layouts"), config.OpStatus.Error)
Name: locale.T("Failed to get system layouts"),
Status: config.OpStatus.Error,
})
return return
} }
hyprLayouts := manager.GetVar("kb_layout") hyprLayouts := manager.GetVar("kb_layout")
if hyprLayouts == "" { if hyprLayouts == "" {
hyprLayouts = "<empty>"
}
res.SyncSystemLayouts = append(res.SyncSystemLayouts, ui.TreeItem{
Name: fmt.Sprintf(locale.T("XCB layout: %s"), sysLayouts),
Status: config.OpStatus.Done,
})
res.SyncSystemLayouts = append(res.SyncSystemLayouts, ui.TreeItem{
Name: fmt.Sprintf(locale.T("Hyprland layout: %s"), hyprLayouts),
Status: config.OpStatus.Done,
})
if hyprLayouts == "<empty>" {
if _, err := manager.SetVar("kb_layout", sysLayouts); err != nil { if _, err := manager.SetVar("kb_layout", sysLayouts); err != nil {
res.SyncSystemLayouts = append(res.SyncSystemLayouts, ui.TreeItem{ res.Add(locale.T("layout"), locale.T("Failed to update kb_layout"), config.OpStatus.Error)
Name: locale.T("Failed to update kb_layout"),
Status: config.OpStatus.Error,
})
return return
} }
res.SyncSystemLayouts = append(res.SyncSystemLayouts, ui.TreeItem{ res.Add(locale.T("layout"), fmt.Sprintf(locale.T("Layout updated: %s"), sysLayouts), config.OpStatus.Done)
Name: fmt.Sprintf(locale.T("Layout updated: %s"), sysLayouts),
Status: config.OpStatus.Done,
})
} else { } else {
res.SyncSystemLayouts = append(res.SyncSystemLayouts, ui.TreeItem{ res.Add(locale.T("layout"), fmt.Sprintf(locale.T("Layout already set: %s"), hyprLayouts), config.OpStatus.Skipped)
Name: fmt.Sprintf(locale.T("Layout already set: %s"), hyprLayouts),
Status: config.OpStatus.Skipped,
})
}
}
func processSystemGrub(prof config.PresetProfile, opts opOptions, res *Result) {
if prof.System.Grub.FixResolution {
autoUpdate := prof.System.Grub.Update
if opts.DryRun {
res.System = append(res.System, ui.TreeItem{
Name: "GRUB_GFXMODE",
Status: config.OpStatus.DryRun,
})
if autoUpdate {
res.System = append(res.System, ui.TreeItem{
Name: "update-grub",
Status: config.OpStatus.DryRun,
})
}
} else {
err := system.SystemGrubFixResolution(autoUpdate, false)
if err != nil {
res.System = append(res.System, ui.TreeItem{
Name: fmt.Sprintf(locale.T("GRUB error: %v"), err),
Status: config.OpStatus.Error,
})
} else {
res.System = append(res.System, ui.TreeItem{
Name: locale.T("GRUB_GFXMODE fixed"),
Status: config.OpStatus.Done,
})
if autoUpdate {
res.System = append(res.System, ui.TreeItem{
Name: locale.T("update-grub completed"),
Status: config.OpStatus.Done,
})
}
}
}
} }
} }
func processProfile(prof config.PresetProfile, dryRun bool, res *Result) { // ===== Orchestration =====
opts := opOptions{DryRun: dryRun}
func processProfile(prof config.PresetProfile, opts opOptions, res *Result, manager *hyprland.HyprlandManager) {
processCopies(prof, opts, res) processCopies(prof, opts, res)
processLinks(prof, opts, res) processLinks(prof, opts, res)
processReleaseReplace(prof, opts, res) processReplace(prof, opts, res)
if config.Env.IsRoot { if config.Env.IsRoot {
processSystemGrub(prof, opts, res) processSystemGrub(prof, opts, res)
} }
manager, err := hyprland.NewHyprlandManager()
if err != nil {
return
}
processHyprVars(manager, prof, opts, res)
processHyprModules(manager, prof, opts, res)
processHyprLayoutSync(manager, prof, opts, res)
}
func createProfile(profileName string, prof config.PresetProfile, dryRun bool, res *Result) { if manager != nil {
color.Green(locale.T("Creating profile: %s"), profileName) processHyprVars(manager, prof, opts, res)
if !profileAvailable(prof) { processHyprModules(manager, prof, opts, res)
color.Red(locale.T("Profile %s unavailable"), profileName) processHyprLayoutSync(manager, prof, opts, res)
return
} }
processProfile(prof, dryRun, res)
} }
func ShowProfilesInfo(jsonFormat bool) { func tryCreateManager() *hyprland.HyprlandManager {
profiles := config.Env.Preset.Profiles manager, err := hyprland.NewHyprlandManager()
if err != nil {
if len(profiles) == 0 { return nil
if jsonFormat {
ui.PrintJSON([]ui.JSONItem{})
return
}
color.Red(locale.T("no available profiles"))
return
}
items := make([]ui.TreeItem, 0, len(profiles))
for name, profile := range profiles {
status := profileStatus(profile)
desc := profile.Description
items = append(items, ui.TreeItem{
Name: name,
Status: status,
Description: desc,
})
}
if jsonFormat {
ui.PrintJSON(ui.TreeItemsToJSON(items))
return
} }
return manager
ui.RenderTree(ui.RenderTreeOptions{
Title: locale.T("Profiles"),
Items: items,
Style: ui.DefaultTreeStyle,
Color: true,
Sort: true,
})
} }
// ===== Command handlers =====
func presetApplyCommand(ctx context.Context, cmd *cli.Command) error { func presetApplyCommand(ctx context.Context, cmd *cli.Command) error {
profileName := cmd.Args().Get(0) profileName := cmd.Args().Get(0)
...@@ -443,42 +264,84 @@ func presetApplyCommand(ctx context.Context, cmd *cli.Command) error { ...@@ -443,42 +264,84 @@ func presetApplyCommand(ctx context.Context, cmd *cli.Command) error {
return nil return nil
} }
dryRun := cmd.Bool("dry-run") profiles := config.Env.Profiles
profiles := config.Env.Preset.Profiles prof, ok := profiles[profileName]
mainProf, ok := profiles[profileName]
if !ok { if !ok {
color.Red(locale.T("Profile %s not found"), profileName) color.Red(locale.T("Profile %s not found"), profileName)
return nil return nil
} }
if !profileAvailable(prof) {
color.Red(locale.T("Profile %s unavailable"), profileName)
return nil
}
opts := opOptions{
DryRun: cmd.Bool("dry-run"),
Force: cmd.Bool("force"),
}
manager := tryCreateManager()
res := &Result{} res := &Result{}
createProfile(profileName, mainProf, dryRun, res)
renderPresetResult(res) color.Green(locale.T("Applying profile: %s"), profileName)
processProfile(prof, opts, res, manager)
res.Render()
if manager != nil && manager.Changed && !opts.DryRun {
manager.Save()
}
return nil return nil
} }
func presetApplyAllCommand(ctx context.Context, cmd *cli.Command) error { type namedProfile struct {
dryRun := cmd.Bool("dry-run") Name string
Profile config.PresetProfile
}
profiles := config.Env.Preset.Profiles func sortedProfiles(profiles map[string]config.PresetProfile) []namedProfile {
sorted := make([]namedProfile, 0, len(profiles))
for name, prof := range profiles {
sorted = append(sorted, namedProfile{Name: name, Profile: prof})
}
sort.Slice(sorted, func(i, j int) bool {
if sorted[i].Profile.Priority != sorted[j].Profile.Priority {
return sorted[i].Profile.Priority < sorted[j].Profile.Priority
}
return sorted[i].Name < sorted[j].Name
})
return sorted
}
func presetApplyAllCommand(ctx context.Context, cmd *cli.Command) error {
profiles := config.Env.Profiles
if len(profiles) == 0 { if len(profiles) == 0 {
color.Red(locale.T("no available profiles")) color.Red(locale.T("no available profiles"))
return nil return nil
} }
color.Cyan(locale.T("Creating all available profiles...") + "\n") opts := opOptions{
DryRun: cmd.Bool("dry-run"),
Force: cmd.Bool("force"),
}
for name, prof := range profiles { manager := tryCreateManager()
if !profileAvailable(prof) {
for _, np := range sortedProfiles(profiles) {
if !profileAvailable(np.Profile) {
continue continue
} }
res := &Result{} res := &Result{}
createProfile(name, prof, dryRun, res) color.Green(locale.T("Applying profile: %s"), np.Name)
renderPresetResult(res) processProfile(np.Profile, opts, res, manager)
res.Render()
}
if manager != nil && manager.Changed && !opts.DryRun {
manager.Save()
} }
return nil return nil
......
package preset package preset
import ( import (
"context"
"fmt"
"ximperconf/config" "ximperconf/config"
"ximperconf/locale" "ximperconf/locale"
"github.com/urfave/cli/v3" "github.com/urfave/cli/v3"
) )
var presetFlags = []cli.Flag{
&cli.BoolFlag{
Name: "dry-run",
Usage: locale.T("Show what would be created without making changes"),
Aliases: []string{"d"},
},
&cli.BoolFlag{
Name: "force",
Usage: locale.T("Force overwrite existing files (backup to .bak)"),
Aliases: []string{"f"},
},
}
func CommandList() *cli.Command { func CommandList() *cli.Command {
return &cli.Command{ return &cli.Command{
Name: "preset", Name: "preset",
...@@ -23,46 +34,18 @@ func CommandList() *cli.Command { ...@@ -23,46 +34,18 @@ func CommandList() *cli.Command {
Action: presetInfoCommand, Action: presetInfoCommand,
}, },
{ {
Name: "apply", Name: "apply",
Usage: locale.T("Apply a profile"), Usage: locale.T("Apply a profile"),
Flags: []cli.Flag{ Flags: presetFlags,
&cli.BoolFlag{
Name: "dry-run",
Usage: locale.T("Show what would be created without making changes"),
Aliases: []string{"d"},
Value: false,
},
},
Action: presetApplyCommand, Action: presetApplyCommand,
ShellComplete: ShellCompleteProfiles, ShellComplete: ShellCompleteProfiles,
}, },
{ {
Name: "apply-all", Name: "apply-all",
Usage: locale.T("Apply all available profiles"), Usage: locale.T("Apply all available profiles"),
Flags: []cli.Flag{ Flags: presetFlags,
&cli.BoolFlag{
Name: "dry-run",
Usage: locale.T("Show what would be created without making changes"),
Aliases: []string{"d"},
Value: false,
},
},
Action: presetApplyAllCommand, Action: presetApplyAllCommand,
}, },
}, },
} }
} }
func ShellCompleteProfiles(ctx context.Context, cmd *cli.Command) {
if cmd.NArg() > 0 {
return
}
profiles := config.Env.Preset.Profiles
for profileName, profile := range profiles {
if profileAvailable(profile) {
fmt.Println(profileName)
}
}
}
...@@ -2,67 +2,35 @@ package preset ...@@ -2,67 +2,35 @@ package preset
import ( import (
"os" "os"
"os/exec"
"path/filepath" "path/filepath"
"strings" "strings"
"ximperconf/config" "ximperconf/config"
) )
type opKind string
const (
OpCopy opKind = "copy"
OpLink opKind = "link"
OpReplace opKind = "replace"
)
type opOptions struct { type opOptions struct {
DryRun bool DryRun bool
Force bool Force bool
} }
type opResult struct { func execCopy(src, dest string, opts opOptions) (config.ItemStatus, string) {
Kind opKind
Status config.ItemStatus
Source string
Target string
Reason string
}
func commandExists(name string) bool {
_, err := exec.LookPath(name)
return err == nil
}
// ===== Copy =====
func copyIfMissing(src, dest string, opts opOptions) opResult {
info, err := os.Stat(src) info, err := os.Stat(src)
if err != nil { if err != nil {
return opResult{ return config.OpStatus.Error, err.Error()
Kind: OpCopy,
Status: config.OpStatus.Error,
Source: src,
Target: dest,
Reason: err.Error(),
}
} }
exists := false
if _, err := os.Stat(dest); err == nil { if _, err := os.Stat(dest); err == nil {
return opResult{ if !opts.Force {
Kind: OpCopy, return config.OpStatus.Skipped, dest + " (exists)"
Status: config.OpStatus.Skipped, }
Target: dest, exists = true
Reason: "already exists", if !opts.DryRun {
_ = os.Rename(dest, dest+".bak")
} }
} }
if opts.DryRun { if opts.DryRun {
return opResult{ return config.OpStatus.DryRun, src + " → " + dest
Kind: OpCopy,
Status: config.OpStatus.DryRun,
Source: src,
Target: dest,
}
} }
if info.IsDir() { if info.IsDir() {
...@@ -72,21 +40,49 @@ func copyIfMissing(src, dest string, opts opOptions) opResult { ...@@ -72,21 +40,49 @@ func copyIfMissing(src, dest string, opts opOptions) opResult {
} }
if err != nil { if err != nil {
return opResult{ return config.OpStatus.Error, err.Error()
Kind: OpCopy, }
Status: config.OpStatus.Error,
Source: src, msg := src + " → " + dest
Target: dest, if exists {
Reason: err.Error(), msg += " (overwritten)"
}
return config.OpStatus.Done, msg
}
func execLink(src, dest string, opts opOptions) (config.ItemStatus, string) {
if _, err := os.Stat(src); os.IsNotExist(err) {
return config.OpStatus.Skipped, src + " → " + dest + " (source missing)"
}
exists := false
if _, err := os.Lstat(dest); err == nil {
if !opts.Force {
return config.OpStatus.Skipped, dest + " (exists)"
}
exists = true
if !opts.DryRun {
_ = os.Rename(dest, dest+".bak")
} }
} }
return opResult{ if opts.DryRun {
Kind: OpCopy, return config.OpStatus.DryRun, src + " → " + dest
Status: config.OpStatus.Done, }
Source: src,
Target: dest, if err := os.MkdirAll(filepath.Dir(dest), 0755); err != nil {
return config.OpStatus.Error, err.Error()
}
if err := os.Symlink(src, dest); err != nil {
return config.OpStatus.Error, err.Error()
} }
msg := src + " → " + dest
if exists {
msg += " (overwritten)"
}
return config.OpStatus.Done, msg
} }
func copyFile(src, dest string) error { func copyFile(src, dest string) error {
...@@ -121,60 +117,8 @@ func copyDir(src, dest string) error { ...@@ -121,60 +117,8 @@ func copyDir(src, dest string) error {
}) })
} }
// ===== Link =====
func createLinkIfMissing(src, dest string, opts opOptions) opResult {
if _, err := os.Stat(src); os.IsNotExist(err) {
return opResult{
Kind: OpLink,
Status: config.OpStatus.Skipped,
Target: dest,
Reason: "source missing",
}
}
if _, err := os.Lstat(dest); err == nil {
return opResult{
Kind: OpLink,
Status: config.OpStatus.Skipped,
Target: dest,
Reason: "already exists",
}
}
if opts.DryRun {
return opResult{
Kind: OpLink,
Status: config.OpStatus.DryRun,
Source: src,
Target: dest,
}
}
if err := os.MkdirAll(filepath.Dir(dest), 0755); err != nil {
return opResult{
Kind: OpLink,
Status: config.OpStatus.Error,
Reason: err.Error(),
}
}
if err := os.Symlink(src, dest); err != nil {
return opResult{
Kind: OpLink,
Status: config.OpStatus.Error,
Reason: err.Error(),
}
}
return opResult{
Kind: OpLink,
Status: config.OpStatus.Done,
Source: src,
Target: dest,
}
}
// ===== Versions ===== // ===== Versions =====
func getSystemVersion() string { func getSystemVersion() string {
data, err := os.ReadFile("/etc/os-release") data, err := os.ReadFile("/etc/os-release")
if err != nil { if err != nil {
......
package preset package preset
import ( import (
"context"
"fmt" "fmt"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"strings" "strings"
"ximperconf/config" "ximperconf/config"
"ximperconf/locale" "ximperconf/locale"
"ximperconf/ui" "ximperconf/ui"
"github.com/urfave/cli/v3"
) )
func profileAvailable(p config.PresetProfile) bool { func profileAvailable(p config.PresetProfile) bool {
allowed := false if p.Binary != "" {
if _, err := exec.LookPath(p.Binary); err != nil {
if p.Binary == "" { return false
allowed = true }
} else if _, err := exec.LookPath(p.Binary); err == nil {
allowed = true
} }
if p.Root != config.Env.IsRoot { if p.Root != config.Env.IsRoot {
allowed = false return false
} }
return allowed return true
} }
func profileStatus(p config.PresetProfile) config.ItemStatus { func profileStatus(p config.PresetProfile) config.ItemStatus {
...@@ -35,28 +35,9 @@ func profileStatus(p config.PresetProfile) config.ItemStatus { ...@@ -35,28 +35,9 @@ func profileStatus(p config.PresetProfile) config.ItemStatus {
return config.ProfileStatus.Unavailable return config.ProfileStatus.Unavailable
} }
func AppendOpResult(res *Result, r opResult) { func commandExists(name string) bool {
item := ui.TreeItem{ _, err := exec.LookPath(name)
Status: r.Status, return err == nil
}
switch {
case r.Status.IsEqual(config.OpStatus.Skipped):
item.Name = fmt.Sprintf(locale.T("Already exists: %s"), r.Target)
case r.Status.IsEqual(config.OpStatus.DryRun):
item.Name = fmt.Sprintf("%s → %s", r.Source, r.Target)
case r.Status.IsEqual(config.OpStatus.Done):
item.Name = fmt.Sprintf("%s → %s", r.Source, r.Target)
case r.Status.IsEqual(config.OpStatus.Error):
item.Name = fmt.Sprintf(locale.T("Error: %s"), r.Reason)
}
switch r.Kind {
case OpCopy:
res.Copies = append(res.Copies, item)
case OpLink:
res.Links = append(res.Links, item)
}
} }
func expandPath(path, name string) string { func expandPath(path, name string) string {
...@@ -81,12 +62,50 @@ func expandPath(path, name string) string { ...@@ -81,12 +62,50 @@ func expandPath(path, name string) string {
return path return path
} }
func renderPresetResult(res *Result) { func ShowProfilesInfo(jsonFormat bool) {
ui.RenderTreeItems(locale.T("Copying"), res.Copies) profiles := config.Env.Profiles
ui.RenderTreeItems(locale.T("Creating links"), res.Links)
ui.RenderTreeItems(locale.T("Updating"), res.Replaced) if len(profiles) == 0 {
ui.RenderTreeItems(locale.T("Configuring system"), res.System) if jsonFormat {
ui.RenderTreeItems(locale.T("Creating Hyprland variables"), res.HyprVars) ui.PrintJSON([]ui.JSONItem{})
ui.RenderTreeItems(locale.T("Enabling Hyprland modules"), res.HyprModules) return
ui.RenderTreeItems(locale.T("Syncing Hyprland layout"), res.SyncSystemLayouts) }
fmt.Println(config.OpStatus.Error.Color(locale.T("no available profiles")))
return
}
items := make([]ui.TreeItem, 0, len(profiles))
for name, profile := range profiles {
items = append(items, ui.TreeItem{
Name: name,
Status: profileStatus(profile),
Description: profile.Description,
})
}
if jsonFormat {
ui.PrintJSON(ui.TreeItemsToJSON(items))
return
}
ui.RenderTree(ui.RenderTreeOptions{
Title: locale.T("Profiles"),
Items: items,
Style: ui.DefaultTreeStyle,
Color: true,
Sort: true,
})
}
func ShellCompleteProfiles(ctx context.Context, cmd *cli.Command) {
if cmd.NArg() > 0 {
return
}
for name, profile := range config.Env.Profiles {
if profileAvailable(profile) {
fmt.Println(name)
}
}
} }
package preset package preset
import "ximperconf/ui" import (
"fmt"
"ximperconf/config"
"ximperconf/locale"
)
type LogEntry struct {
Category string
Name string
Status config.ItemStatus
}
type Result struct { type Result struct {
Copies []ui.TreeItem entries []LogEntry
Links []ui.TreeItem }
Replaced []ui.TreeItem
System []ui.TreeItem func (r *Result) Add(category, name string, status config.ItemStatus) {
HyprVars []ui.TreeItem r.entries = append(r.entries, LogEntry{
HyprModules []ui.TreeItem Category: category,
SyncSystemLayouts []ui.TreeItem Name: name,
Status: status,
})
}
func (r *Result) Render() {
if len(r.entries) == 0 {
return
}
maxCat := 0
for _, e := range r.entries {
if len([]rune(e.Category)) > maxCat {
maxCat = len([]rune(e.Category))
}
}
for _, e := range r.entries {
symbol := e.Status.Symbol
cat := fmt.Sprintf("%-*s", maxCat, e.Category)
if e.Status.Color != nil {
symbol = e.Status.Color(symbol)
cat = e.Status.Color(cat)
}
fmt.Printf(" %s %s %s\n", symbol, cat, e.Name)
}
summary := r.Summary()
if summary != "" {
fmt.Println()
fmt.Printf(" %s\n", summary)
}
}
func (r *Result) Summary() string {
var done, skipped, errors int
for _, e := range r.entries {
switch {
case e.Status.IsEqual(config.OpStatus.Done):
done++
case e.Status.IsEqual(config.OpStatus.Skipped):
skipped++
case e.Status.IsEqual(config.OpStatus.Error):
errors++
case e.Status.IsEqual(config.OpStatus.DryRun):
done++
}
}
parts := []string{}
if done > 0 {
parts = append(parts, fmt.Sprintf("%d %s", done, locale.T("done")))
}
if skipped > 0 {
parts = append(parts, fmt.Sprintf("%d %s", skipped, locale.T("skipped")))
}
if errors > 0 {
parts = append(parts, fmt.Sprintf("%d %s", errors, locale.TN("error", "errors", errors)))
}
if len(parts) == 0 {
return ""
}
result := parts[0]
for i := 1; i < len(parts); i++ {
result += ", " + parts[i]
}
return result
} }
package ui
import (
"github.com/fatih/color"
)
func RenderTreeLines(title string, lines []string) {
if len(lines) == 0 {
return
}
color.Blue("%s", title)
items := make([]TreeItem, len(lines))
for i, line := range lines {
items[i] = TreeItem{
Name: line,
}
}
RenderTree(RenderTreeOptions{
Items: items,
Style: DefaultTreeStyle,
Color: false,
})
}
func RenderTreeItems(title string, items []TreeItem) {
if len(items) == 0 {
return
}
color.Blue("%s", title)
RenderTree(RenderTreeOptions{
Items: items,
Style: DefaultTreeStyle,
Color: true,
})
}
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