create: refactoring and renaming to preset

parent e7044e5b
......@@ -6,37 +6,45 @@ import (
"ximperconf/utils"
)
type CreateEnv struct {
ConfigDir string
type presetEnv struct {
Profiles map[string]PresetProfile
}
type HyprEnv struct {
type hyprEnv struct {
Config string
SystemModulesDir string
UserModulesDir string
PluginsDir string
}
type RepoEnv struct {
type repoEnv struct {
DeferredInfoURL string
}
type Environment struct {
type environment struct {
Version string
IsRoot bool
Create *CreateEnv
Hyprland *HyprEnv
Repo *RepoEnv
Preset *presetEnv
Hyprland *hyprEnv
Repo *repoEnv
}
var Env Environment
var Env environment
func InitConfig() error {
Env.Version = "0.1.0"
Env.IsRoot = os.Geteuid() == 0
Env.Create = &CreateEnv{
ConfigDir: "/etc/ximperdistro/ximperconf/create.d/",
presetCfg, err := loadPresetConfig("/etc/ximperdistro/ximperconf/preset.d/")
if err != nil {
// Если ошибка, пустой map
Env.Preset = &presetEnv{
Profiles: map[string]PresetProfile{},
}
} else {
Env.Preset = &presetEnv{
Profiles: presetCfg.Profiles,
}
}
if utils.FileExists("/usr/bin/hyprland") {
......@@ -46,7 +54,7 @@ func InitConfig() error {
pluginsDir := "/usr/lib64/hyprland"
confFile := filepath.Join(userModules, "hyprland.conf")
Env.Hyprland = &HyprEnv{
Env.Hyprland = &hyprEnv{
Config: confFile,
UserModulesDir: userModules,
SystemModulesDir: optionalPath(systemModules),
......@@ -54,7 +62,7 @@ func InitConfig() error {
}
}
Env.Repo = &RepoEnv{
Env.Repo = &repoEnv{
DeferredInfoURL: "https://download.etersoft.ru/pub/Etersoft/Sisyphus/Deferred_Info.html",
}
......
package config
import (
"fmt"
"os"
"path/filepath"
"strings"
"gopkg.in/yaml.v3"
)
type copyEntry struct {
Src string `yaml:"src"`
Dest string `yaml:"dest"`
}
type linkEntry struct {
Apps []string `yaml:"apps,omitempty"`
App string `yaml:"app,omitempty"`
Sys string `yaml:"sys"`
User string `yaml:"user"`
}
type releaseReplaceEntry struct {
Src string `yaml:"src"`
Dest string `yaml:"dest"`
}
type grubEntry struct {
FixResolution bool `yaml:"fix-resolution,omitempty"`
Update bool `yaml:"update,omitempty"`
}
type systemEntry struct {
Grub grubEntry `yaml:"grub,omitempty"`
}
type hyprVar struct {
Var string `yaml:"var"`
Value string `yaml:"value"`
Force bool `yaml:"force,omitempty"`
}
type hyprModule struct {
Module string `yaml:"module"`
User bool `yaml:"user,omitempty"`
NewOnly bool `yaml:"new-only,omitempty"`
IfExec string `yaml:"if-exec,omitempty"`
IfBinary string `yaml:"if-binary,omitempty"`
Init bool `yaml:"init,omitempty"`
}
type hyprOptions struct {
SyncSystemLayouts bool `yaml:"sync-system-layouts,omitempty"`
}
type hyprlandEntry struct {
Option hyprOptions `yaml:"options,omitempty"`
Vars []hyprVar `yaml:"vars,omitempty"`
Modules []hyprModule `yaml:"modules,omitempty"`
}
type PresetProfile struct {
Binary string `yaml:"binary,omitempty"`
Description string `yaml:"description,omitempty"`
Root bool `yaml:"root,omitempty"`
Copy []copyEntry `yaml:"copy,omitempty"`
Links []linkEntry `yaml:"links,omitempty"`
ReleaseReplace []releaseReplaceEntry `yaml:"release-replace,omitempty"`
System systemEntry `yaml:"system,omitempty"`
// ----- hyprland -----
Hyprland hyprlandEntry `yaml:"hyprland,omitempty"`
}
type Presets struct {
Profiles map[string]PresetProfile `yaml:"profiles"`
}
type PresetResult struct {
Links []string
Copies []string
Replaced []string
HyprVars []string
HyprModules []string
SyncSystemLayouts []string
System []string
}
func loadPresetConfig(dir string) (*Presets, error) {
cfg := &Presets{Profiles: map[string]PresetProfile{}}
entries, err := os.ReadDir(dir)
if err != nil {
return nil, err
}
for _, e := range entries {
if e.IsDir() {
continue
}
name := e.Name()
if !strings.HasSuffix(name, ".yaml") && !strings.HasSuffix(name, ".yml") {
continue
}
path := filepath.Join(dir, name)
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("read %s: %w", name, err)
}
var p PresetProfile
if err := yaml.Unmarshal(data, &p); err != nil {
return nil, fmt.Errorf("parse %s: %w", name, err)
}
profileName := strings.TrimSuffix(name, filepath.Ext(name))
cfg.Profiles[profileName] = p
}
return cfg, nil
}
package create
import (
"bytes"
"ximperconf/config"
"ximperconf/hyprland"
"context"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/fatih/color"
"github.com/urfave/cli/v3"
"gopkg.in/yaml.v3"
)
func processProfile(prof Profile, dryRun bool, res *Result) {
// ----- копирование -----
for _, c := range prof.Copy {
src := expandPath(c.Src, "")
dest := expandPath(c.Dest, "")
_ = copyIfMissing(src, dest, dryRun, res)
}
// ----- ссылки -----
for _, entry := range prof.Links {
if len(entry.Apps) > 0 {
for _, app := range entry.Apps {
if commandExists(app) {
sys := expandPath(entry.Sys, app)
user := expandPath(entry.User, app)
_ = createLinkIfMissing(sys, user, dryRun, res)
}
}
continue
}
if entry.App != "" && !commandExists(entry.App) {
continue
}
sys := expandPath(entry.Sys, "")
user := expandPath(entry.User, "")
_ = createLinkIfMissing(sys, user, dryRun, res)
}
// ----- release-replace -----
for _, rr := range prof.ReleaseReplace {
src := expandPath(rr.Src, "")
dest := expandPath(rr.Dest, "")
sysVer := getSystemVersion()
if _, err := os.Stat(dest); os.IsNotExist(err) {
res.Replaced = append(res.Replaced,
fmt.Sprintf("Не найден: '%s' — пропущен", dest))
continue
}
data, err := os.ReadFile(dest)
if err != nil {
res.Replaced = append(res.Replaced,
fmt.Sprintf("Не удалось прочитать %s: %v", dest, err))
continue
}
content := string(data)
if strings.Contains(content, "XIMPER_LOCK") {
res.Replaced = append(res.Replaced,
fmt.Sprintf("Заблокирован: %s — пропущен", dest))
continue
}
needReplace := false
fileVer := getFileVersionTag(dest)
if fileVer == "" {
fileVer = "0.9.3"
}
if fileVer == "" || fileVer != sysVer {
needReplace = true
}
if needReplace {
if dryRun {
res.Replaced = append(res.Replaced,
fmt.Sprintf("[Dry-run] Заменён: %s на %s", dest, src))
continue
}
backup := dest + ".old"
_ = os.Rename(dest, backup)
if err := copyFile(src, dest); err != nil {
res.Replaced = append(res.Replaced,
fmt.Sprintf("Ошибка замены %s: %v", dest, err))
continue
}
res.Replaced = append(res.Replaced,
fmt.Sprintf("Обновлён: %s (%s → %s)", dest, fileVer, sysVer))
} else {
res.Replaced = append(res.Replaced,
fmt.Sprintf("Актуально: '%s' — пропущено", dest))
}
}
// ----- hyprland -----
// --------- vars
for _, v := range prof.Hyprland.Vars {
_, err := hyprland.HyprlandVarGet(v.Name)
if err == nil {
res.HyprVars = append(res.HyprVars,
fmt.Sprintf("Уже существует: '%s' — пропущено", v.Name))
continue
}
if dryRun {
res.HyprVars = append(res.HyprVars,
fmt.Sprintf("[Dry-run] Установлена переменная: %s = %s", v.Name, v.Value))
continue
}
if _, err := hyprland.HyprlandVarSet(v.Name, v.Value); err != nil {
res.HyprVars = append(res.HyprVars,
fmt.Sprintf("Ошибка установки переменной %s: %v", v.Name, err))
} else {
res.HyprVars = append(res.HyprVars,
fmt.Sprintf("Установлена переменная: %s = %s", v.Name, v.Value))
}
}
// --------- modules
for _, m := range prof.Hyprland.Modules {
if m.Module == "" {
res.HyprModules = append(res.HyprModules, "Пропущен модуль без имени")
continue
}
// Если есть if-exec
if m.IfExec != "" {
cmd := exec.Command("sh", "-c", m.IfExec)
output, err := cmd.Output()
if err != nil {
res.HyprModules = append(res.HyprModules,
fmt.Sprintf("Модуль '%s' пропущен: команда '%s' завершилась с ошибкой: %v", m.Module, m.IfExec, err))
continue
}
// Если команда ничего не вывела
if len(bytes.TrimSpace(output)) == 0 {
res.HyprModules = append(res.HyprModules,
fmt.Sprintf("Модуль '%s' пропущен: команда '%s' не вернула данных", m.Module, m.IfExec))
continue
}
}
// Dry-run
if dryRun {
res.HyprModules = append(res.HyprModules,
fmt.Sprintf("[Dry-run] Подключён модуль: %s", m.Module))
continue
}
if _, err := hyprland.HyprlandSetModule("enable", m.Module, m.User, m.NewOnly); err != nil {
res.HyprModules = append(res.HyprModules,
fmt.Sprintf("Ошибка подключения модуля %s: %v", m.Module, err))
} else {
res.HyprModules = append(res.HyprModules,
fmt.Sprintf("Подключён модуль: %s", m.Module))
}
}
// --------- синхронизация раскладок Hyprland
if prof.Hyprland.Option.SyncSystemLayouts && !dryRun {
sysLayouts, err := hyprland.HyprlandGetKeyboardLayouts()
if err != nil {
res.SyncSystemLayouts = append(res.SyncSystemLayouts,
"Не удалось получить системные раскладки")
return
}
hyprLayouts, err := hyprland.HyprlandVarGet("kb_layout")
if err != nil {
res.SyncSystemLayouts = append(res.SyncSystemLayouts,
"Не удалось получить раскладки Hyprland")
return
}
if hyprLayouts == "" {
hyprLayouts = "<empty>"
}
res.SyncSystemLayouts = append(res.SyncSystemLayouts,
fmt.Sprintf("Раскладка XCB: %s", sysLayouts))
res.SyncSystemLayouts = append(res.SyncSystemLayouts,
fmt.Sprintf("Раскладка Hyprland: %s", hyprLayouts))
if hyprLayouts == "<empty>" {
if _, err := hyprland.HyprlandVarSet("kb_layout", sysLayouts); err != nil {
res.SyncSystemLayouts = append(res.SyncSystemLayouts,
"Не удалось обновить kb_layout")
return
}
res.SyncSystemLayouts = append(res.SyncSystemLayouts,
fmt.Sprintf("Раскладка обновлена: %s", sysLayouts))
} else {
res.SyncSystemLayouts = append(res.SyncSystemLayouts,
fmt.Sprintf("Раскладка уже установлена: %s", hyprLayouts))
}
}
}
func loadConfig(dir string) (*Config, error) {
cfg := &Config{Profiles: map[string]Profile{}}
entries, err := os.ReadDir(dir)
if err != nil {
return nil, err
}
for _, e := range entries {
if e.IsDir() {
continue
}
name := e.Name()
if !strings.HasSuffix(name, ".yaml") && !strings.HasSuffix(name, ".yml") {
continue
}
path := filepath.Join(dir, name)
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("read %s: %w", name, err)
}
var p Profile
if err := yaml.Unmarshal(data, &p); err != nil {
return nil, fmt.Errorf("parse %s: %w", name, err)
}
profileName := strings.TrimSuffix(name, filepath.Ext(name))
cfg.Profiles[profileName] = p
}
return cfg, nil
}
func expandPath(path, name string) string {
home := os.Getenv("HOME")
user := os.Getenv("USER")
hostname, _ := os.Hostname()
replacements := map[string]string{
"~": home,
"{home}": home,
"{user}": user,
"{name}": name,
"{hostname}": hostname,
"{config}": filepath.Join(home, ".config"),
"{ximperconf}": "/etc/ximperdistro",
}
for k, v := range replacements {
path = strings.ReplaceAll(path, k, v)
}
return path
}
func commandExists(name string) bool {
_, err := exec.LookPath(name)
return err == nil
}
func copyIfMissing(src, dest string, dryRun bool, res *Result) error {
info, err := os.Stat(src)
if err != nil {
return err
}
if _, err := os.Stat(dest); err == nil {
res.Copies = append(res.Copies,
fmt.Sprintf("Уже существует: %s — пропущено", dest))
return nil
}
if dryRun {
res.Copies = append(res.Copies,
fmt.Sprintf("[Dry-run] Скопировано: %s → %s", src, dest))
return nil
}
if info.IsDir() {
if err := copyDir(src, dest); err != nil {
return err
}
} else {
if err := copyFile(src, dest); err != nil {
return err
}
}
res.Copies = append(res.Copies,
fmt.Sprintf("Скопировано: %s → %s", src, dest))
return nil
}
func copyFile(src, dest string) error {
if err := os.MkdirAll(filepath.Dir(dest), 0755); err != nil {
return err
}
data, err := os.ReadFile(src)
if err != nil {
return err
}
return os.WriteFile(dest, data, 0644)
}
func copyDir(src, dest string) error {
return filepath.Walk(src, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
rel, err := filepath.Rel(src, path)
if err != nil {
return err
}
target := filepath.Join(dest, rel)
if info.IsDir() {
return os.MkdirAll(target, info.Mode())
}
data, err := os.ReadFile(path)
if err != nil {
return err
}
return os.WriteFile(target, data, info.Mode())
})
}
func createLinkIfMissing(src, dest string, dryRun bool, res *Result) error {
if _, err := os.Stat(src); os.IsNotExist(err) {
return nil
}
if _, err := os.Lstat(dest); err == nil {
res.Links = append(res.Links,
fmt.Sprintf("Уже существует: %s — пропущено", dest))
return nil
}
if dryRun {
res.Links = append(res.Links,
fmt.Sprintf("[Dry-run] Создана ссылка: %s → %s", dest, src))
return nil
}
if err := os.MkdirAll(filepath.Dir(dest), 0755); err != nil {
return err
}
if err := os.Symlink(src, dest); err != nil {
return err
}
res.Links = append(res.Links,
fmt.Sprintf("Создана ссылка: %s → %s", dest, src))
return nil
}
func getSystemVersion() string {
data, err := os.ReadFile("/etc/os-release")
if err != nil {
return ""
}
lines := strings.Split(string(data), "\n")
for _, line := range lines {
if strings.HasPrefix(line, "VERSION_ID=") {
return strings.Trim(strings.SplitN(line, "=", 2)[1], `"`)
}
}
for _, line := range lines {
if strings.HasPrefix(line, "VERSION=") {
return strings.Trim(strings.SplitN(line, "=", 2)[1], `"`)
}
}
return ""
}
func getFileVersionTag(path string) string {
data, err := os.ReadFile(path)
if err != nil {
return ""
}
content := string(data)
idx := strings.Index(content, "XIMPER_V")
if idx == -1 {
return ""
}
content = content[idx+len("XIMPER_V"):]
var version strings.Builder
for _, c := range content {
if (c >= '0' && c <= '9') || c == '.' || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') {
version.WriteRune(c)
} else {
break
}
}
return version.String()
}
func printTree(title string, lines []string) {
if len(lines) == 0 {
return
}
color.Blue("%s", title)
for i, line := range lines {
branch := "├──"
if i == len(lines)-1 {
branch = "└──"
}
fmt.Printf("%s %s\n", branch, line)
}
fmt.Println()
}
func createProfile(profileName string, prof Profile, dryRun bool, res *Result) {
color.Green("Создаётся профиль: %s", profileName)
if !ProfileAvailable(prof) {
color.Red("Профиль %s недоступен", profileName)
return
}
processProfile(prof, dryRun, res)
}
func ShowProfilesInfo() {
cfg, err := loadConfig(config.Env.Create.ConfigDir)
if err != nil {
color.Red("Не удалось загрузить конфигурацию: %v", err)
return
}
if len(cfg.Profiles) == 0 {
color.Red("Нет доступных профилей")
return
}
color.Blue("Profiles:")
statusMap := map[bool]struct {
symbol string
color func(format string, a ...interface{}) string
}{
true: {symbol: "●", color: color.GreenString},
false: {symbol: "○", color: color.RedString},
}
i := 0
for name, profile := range cfg.Profiles {
available := ProfileAvailable(profile)
info := statusMap[available]
desc := profile.Description
if desc == "" {
desc = "-"
}
prefix := "├──"
if i == len(cfg.Profiles)-1 {
prefix = "└──"
}
coloredStatus := info.color(info.symbol)
coloredName := info.color(name)
fmt.Printf("%s %s %s — %s\n", prefix, coloredStatus, coloredName, desc)
i++
}
}
func CreateConfCommand(ctx context.Context, cmd *cli.Command) error {
profile := cmd.Args().Get(0)
auto := cmd.Bool("auto")
available := cmd.Bool("available")
if profile == "" && !auto && !available {
ShowProfilesInfo()
return nil
}
if auto {
var err error
profile, err = AutoDetectProfile()
if err != nil {
color.Red("Не удалось определить профиль: %v", err)
return err
}
color.Green("Автоматически выбран профиль: %s", profile)
}
dryRun := cmd.Bool("dry-run")
cfg, err := loadConfig(config.Env.Create.ConfigDir)
if err != nil {
color.Red("Не удалось загрузить конфигурацию: %v", err)
return err
}
if available {
color.Cyan("Создаём все доступные профили...\n")
for name, profile := range cfg.Profiles {
if ProfileAvailable(profile) {
res := &Result{}
createProfile(name, profile, dryRun, res)
printTree("Копируем", res.Copies)
printTree("Создаём ссылки", res.Links)
printTree("Обновляем", res.Replaced)
printTree("Создаём переменные Hyprland", res.HyprVars)
printTree("Подключаем модули Hyprland", res.HyprModules)
printTree("Синхронизируем раскладку Hyprland", res.SyncSystemLayouts)
}
}
return nil
}
mainProf, ok := cfg.Profiles[profile]
if !ok {
color.Red("Профиль %s не найден", profile)
return fmt.Errorf("профиль %s не найден", profile)
}
for _, depName := range mainProf.Depends {
depProf, ok := cfg.Profiles[depName]
if !ok {
color.Yellow("Зависимость %s не найдена, пропускаем", depName)
continue
}
res := &Result{}
createProfile(depName, depProf, dryRun, res)
printTree("Копируем", res.Copies)
printTree("Создаём ссылки", res.Links)
printTree("Обновляем", res.Replaced)
printTree("Создаём переменные Hyprland", res.HyprVars)
printTree("Подключаем модули Hyprland", res.HyprModules)
printTree("Синхронизируем раскладку Hyprland", res.SyncSystemLayouts)
}
res := &Result{}
createProfile(profile, mainProf, dryRun, res)
printTree("Копируем", res.Copies)
printTree("Создаём ссылки", res.Links)
printTree("Обновляем", res.Replaced)
printTree("Создаём переменные Hyprland", res.HyprVars)
printTree("Подключаем модули Hyprland", res.HyprModules)
printTree("Синхронизируем раскладку Hyprland", res.SyncSystemLayouts)
return nil
}
package create
type CopyEntry struct {
Src string `yaml:"src"`
Dest string `yaml:"dest"`
}
type LinkEntry struct {
Apps []string `yaml:"apps,omitempty"`
App string `yaml:"app,omitempty"`
Sys string `yaml:"sys"`
User string `yaml:"user"`
}
type ReleaseReplaceEntry struct {
Src string `yaml:"src"`
Dest string `yaml:"dest"`
}
type HyprVar struct {
Name string `yaml:"name"`
Value string `yaml:"value"`
}
type HyprModule struct {
Module string `yaml:"module"`
User bool `yaml:"user,omitempty"`
NewOnly bool `yaml:"new-only,omitempty"`
IfExec string `yaml:"if-exec,omitempty"`
}
type HyprOptions struct {
SyncSystemLayouts bool `yaml:"sync-system-layouts,omitempty"`
}
type HyprlandEntry struct {
Option HyprOptions `yaml:"options,omitempty"`
Vars []HyprVar `yaml:"rvars,omitempty"`
Modules []HyprModule `yaml:"modules,omitempty"`
}
type Profile struct {
Binary string `yaml:"binary,omitempty"`
Description string `yaml:"description,omitempty"`
Root bool `yaml:"root,omitempty"`
Depends []string `yaml:"depends,omitempty"`
Copy []CopyEntry `yaml:"copy,omitempty"`
Links []LinkEntry `yaml:"links,omitempty"`
ReleaseReplace []ReleaseReplaceEntry `yaml:"release-replace,omitempty"`
// ----- hyprland -----
Hyprland HyprlandEntry `yaml:"hyprland,omitempty"`
}
type Config struct {
Profiles map[string]Profile `yaml:"profiles"`
}
type Result struct {
Links []string
Copies []string
Replaced []string
HyprVars []string
HyprModules []string
SyncSystemLayouts []string
}
package create
import (
"bytes"
"fmt"
"os"
"os/exec"
"strings"
"ximperconf/config"
"github.com/fatih/color"
)
func ProfileAvailable(p Profile) bool {
allowed := false
if p.Binary == "" {
allowed = true
} else if _, err := exec.LookPath(p.Binary); err == nil {
allowed = true
}
if p.Root != config.Env.IsRoot {
allowed = false
}
return allowed
}
func AutoDetectProfile() (string, error) {
cfg, err := loadConfig(config.Env.Create.ConfigDir)
if err != nil {
color.Red("Не удалось загрузить конфигурацию: %v", err)
return "", err
}
for name, profile := range cfg.Profiles {
if profile.Binary == "" || !ProfileAvailable(profile) {
continue
}
cmd := exec.Command("pgrep", "-u", fmt.Sprintf("%d", os.Getuid()), "-x", profile.Binary)
var out bytes.Buffer
cmd.Stdout = &out
if err := cmd.Run(); err != nil {
continue
}
if strings.TrimSpace(out.String()) != "" {
return name, nil
}
}
return "", fmt.Errorf("не найден запущенный профиль")
}
......@@ -2,7 +2,7 @@
function __fish_ximperconf_no_subcommand --description 'Test if there has been any subcommand yet'
for i in (commandline -opc)
if contains -- $i repo create hyprland help h completion
if contains -- $i repo preset hyprland system help h completion
return 1
end
end
......@@ -18,7 +18,7 @@ function __fish_ximperconf_complete
end
end
complete -c ximperconf -n '__fish_seen_subcommand_from create' -f -a '(__fish_ximperconf_complete)'
complete -c ximperconf -n '__fish_seen_subcommand_from preset; and __fish_seen_subcommand_from apply' -f -a '(__fish_ximperconf_complete)'
complete -c ximperconf -n '__fish_seen_subcommand_from hyprland; and __fish_seen_subcommand_from var; and __fish_seen_subcommand_from set' -f -a '(__fish_ximperconf_complete)'
complete -c ximperconf -n '__fish_seen_subcommand_from hyprland; and __fish_seen_subcommand_from var; and __fish_seen_subcommand_from get' -f -a '(__fish_ximperconf_complete)'
......@@ -41,9 +41,16 @@ complete -x -c ximperconf -n '__fish_seen_subcommand_from repo; and __fish_seen_
complete -c ximperconf -n '__fish_seen_subcommand_from repo; and __fish_seen_subcommand_from deferred; and __fish_seen_subcommand_from date' -f -l help -s h -d 'show help'
complete -x -c ximperconf -n '__fish_seen_subcommand_from repo; and __fish_seen_subcommand_from deferred; and not __fish_seen_subcommand_from info date last-update' -a 'last-update' -d 'Date of last update'
complete -c ximperconf -n '__fish_seen_subcommand_from repo; and __fish_seen_subcommand_from deferred; and __fish_seen_subcommand_from last-update' -f -l help -s h -d 'show help'
complete -x -c ximperconf -n '__fish_ximperconf_no_subcommand' -a 'create' -d 'Creating a configuration'
complete -c ximperconf -n '__fish_seen_subcommand_from create' -f -l dry-run -s d -d 'Show what would be created without making changes.'
complete -c ximperconf -n '__fish_seen_subcommand_from create' -f -l help -s h -d 'show help'
complete -x -c ximperconf -n '__fish_ximperconf_no_subcommand' -a 'preset' -d 'Manage preset configuration profiles'
complete -c ximperconf -n '__fish_seen_subcommand_from preset' -f -l help -s h -d 'show help'
complete -x -c ximperconf -n '__fish_seen_subcommand_from preset; and not __fish_seen_subcommand_from info apply apply-all' -a 'info' -d 'Show information about a preset profiles'
complete -c ximperconf -n '__fish_seen_subcommand_from preset; and __fish_seen_subcommand_from info' -f -l help -s h -d 'show help'
complete -x -c ximperconf -n '__fish_seen_subcommand_from preset; and not __fish_seen_subcommand_from info apply apply-all' -a 'apply' -d 'Apply a profile'
complete -c ximperconf -n '__fish_seen_subcommand_from preset; and __fish_seen_subcommand_from apply' -f -l dry-run -s d -d 'Show what would be created without making changes.'
complete -c ximperconf -n '__fish_seen_subcommand_from preset; and __fish_seen_subcommand_from apply' -f -l help -s h -d 'show help'
complete -x -c ximperconf -n '__fish_seen_subcommand_from preset; and not __fish_seen_subcommand_from info apply apply-all' -a 'apply-all' -d 'Apply all available profiles'
complete -c ximperconf -n '__fish_seen_subcommand_from preset; and __fish_seen_subcommand_from apply-all' -f -l dry-run -s d -d 'Show what would be created without making changes.'
complete -c ximperconf -n '__fish_seen_subcommand_from preset; and __fish_seen_subcommand_from apply-all' -f -l help -s h -d 'show help'
complete -x -c ximperconf -n '__fish_ximperconf_no_subcommand' -a 'hyprland' -d 'Hyprland Management'
complete -c ximperconf -n '__fish_seen_subcommand_from hyprland' -f -l help -s h -d 'show help'
complete -x -c ximperconf -n '__fish_seen_subcommand_from hyprland; and not __fish_seen_subcommand_from check sync-xkb-layouts module var plugin' -a 'check' -d 'Check the Hyprland config'
......@@ -92,6 +99,11 @@ complete -x -c ximperconf -n '__fish_seen_subcommand_from hyprland; and __fish_s
complete -c ximperconf -n '__fish_seen_subcommand_from hyprland; and __fish_seen_subcommand_from plugin; and __fish_seen_subcommand_from unload' -f -l help -s h -d 'show help'
complete -x -c ximperconf -n '__fish_seen_subcommand_from hyprland; and __fish_seen_subcommand_from plugin; and not __fish_seen_subcommand_from list status info load unload toggle' -a 'toggle' -d 'toggle plugin'
complete -c ximperconf -n '__fish_seen_subcommand_from hyprland; and __fish_seen_subcommand_from plugin; and __fish_seen_subcommand_from toggle' -f -l help -s h -d 'show help'
complete -c ximperconf -n '__fish_seen_subcommand_from system' -f -l help -s h -d 'show help'
complete -x -c ximperconf -n '__fish_seen_subcommand_from system; and not __fish_seen_subcommand_from fix-grub-resolution' -a 'fix-grub-resolution' -d 'Fix GRUB resolution'
complete -c ximperconf -n '__fish_seen_subcommand_from system; and __fish_seen_subcommand_from fix-grub-resolution' -f -l update -s u -d 'Automatically run update-grub after fixing'
complete -c ximperconf -n '__fish_seen_subcommand_from system; and __fish_seen_subcommand_from fix-grub-resolution' -f -l verbose -s V -d 'Enable verbose output'
complete -c ximperconf -n '__fish_seen_subcommand_from system; and __fish_seen_subcommand_from fix-grub-resolution' -f -l help -s h -d 'show help'
complete -x -c ximperconf -n '__fish_ximperconf_no_subcommand' -a 'help' -d 'show help'
complete -c ximperconf -n '__fish_seen_subcommand_from completion' -f -l help -s h -d 'show help'
complete -x -c ximperconf -n '__fish_seen_subcommand_from completion; and not __fish_seen_subcommand_from help h' -a 'help' -d 'Shows a list of commands or help for one command'
......@@ -2,6 +2,8 @@ package hyprland
import (
"ximperconf/config"
"ximperconf/ui"
"ximperconf/utils"
"bufio"
"context"
......@@ -49,24 +51,19 @@ func CheckHyprland(ctx context.Context, cmd *cli.Command) error {
return nil
}
func hyprlandGetModuleFile(module string, user bool) string {
func HyprlandGetModuleFile(module string, user bool) string {
if user {
return filepath.Join(config.Env.Hyprland.UserModulesDir, module+".conf")
}
return filepath.Join(config.Env.Hyprland.SystemModulesDir, module+".conf")
}
func fileExists(path string) bool {
_, err := os.Stat(path)
return err == nil
}
func hyprlandModuleStatus(module string, user bool) string {
sysFile, lineFull, lineTilde := hyprlandModuleLines(module, user)
// Конфиг Hyprland отсутствует
if !fileExists(config.Env.Hyprland.Config) {
if fileExists(sysFile) {
if !utils.FileExists(config.Env.Hyprland.Config) {
if utils.FileExists(sysFile) {
return "unused"
}
return "missing"
......@@ -74,7 +71,7 @@ func hyprlandModuleStatus(module string, user bool) string {
f, err := os.Open(config.Env.Hyprland.Config)
if err != nil {
if fileExists(sysFile) {
if utils.FileExists(sysFile) {
return "unused"
}
return "missing"
......@@ -104,12 +101,12 @@ func hyprlandModuleStatus(module string, user bool) string {
}
// подключён, но файла нет
if foundActive && !fileExists(sysFile) {
if foundActive && !utils.FileExists(sysFile) {
return "missing"
}
// подключён и файл есть
if foundActive && fileExists(sysFile) {
if foundActive && utils.FileExists(sysFile) {
return "enabled"
}
......@@ -119,7 +116,7 @@ func hyprlandModuleStatus(module string, user bool) string {
}
// файл есть, но строка отсутствует
if fileExists(sysFile) && !foundActive && !foundCommented {
if utils.FileExists(sysFile) && !foundActive && !foundCommented {
return "unused"
}
......@@ -127,6 +124,22 @@ func hyprlandModuleStatus(module string, user bool) string {
return "missing"
}
func hyprlandModuleStatusStruct(module string, user bool) ui.ItemStatus {
status := hyprlandModuleStatus(module, user)
switch status {
case "enabled":
return ui.StatusEnabled
case "disabled":
return ui.StatusDisabled
case "missing":
return ui.StatusMissing
case "unused":
return ui.StatusUnused
}
return ui.StatusUnknown
}
func HyprlandModuleStatusCommand(ctx context.Context, cmd *cli.Command) error {
status := hyprlandModuleStatus(cmd.Args().Get(0), cmd.Bool("user"))
fmt.Println(status)
......@@ -141,7 +154,7 @@ func HyprlandSetModule(action string, module string, user bool, onlyNew bool) (s
var out string
if !fileExists(sysFile) {
if !utils.FileExists(sysFile) {
return "", fmt.Errorf("модуль '%s' не найден: %s", module, sysFile)
}
......@@ -305,31 +318,24 @@ func hyprlandInfoModules(user bool, filter string) {
return
}
color.Blue("Modules:")
items := make([]ui.TreeItem, 0, len(modules))
statusMap := map[string]struct {
symbol string
color func(format string, a ...interface{}) string
}{
"enabled": {symbol: "●", color: color.GreenString},
"disabled": {symbol: "○", color: color.RedString},
"missing": {symbol: "!", color: color.YellowString},
"unused": {symbol: "-", color: color.HiBlackString},
for _, module := range modules {
status := hyprlandModuleStatusStruct(module, user)
items = append(items, ui.TreeItem{
Name: module,
Status: status,
Description: "",
})
}
for i, module := range modules {
status := hyprlandModuleStatus(module, user)
info := statusMap[status]
coloredStatus := info.color(info.symbol)
coloredModule := info.color(module)
prefix := "├──"
if i == len(modules)-1 {
prefix = "└──"
}
fmt.Printf("%s %s %s\n", prefix, coloredStatus, coloredModule)
}
ui.RenderTree(ui.RenderTreeOptions{
Title: "Modules",
Items: items,
Style: ui.DefaultTreeStyle,
StatusMap: ui.ColorStatusMap,
Sort: true,
})
}
func HyprlandInfoModulesCommand(ctx context.Context, cmd *cli.Command) error {
......@@ -338,7 +344,7 @@ func HyprlandInfoModulesCommand(ctx context.Context, cmd *cli.Command) error {
}
func hyprlandModuleLines(module string, user bool) (sysFile, lineFull, lineTilde string) {
sysFile = hyprlandGetModuleFile(module, user)
sysFile = HyprlandGetModuleFile(module, user)
lineFull = "source = " + sysFile
lineTilde = "source = ~" + strings.TrimPrefix(sysFile, HomeDir)
return
......
......@@ -2,6 +2,7 @@ package hyprland
import (
"ximperconf/config"
"ximperconf/ui"
"ximperconf/utils"
"context"
......@@ -70,6 +71,17 @@ func hyprlandPluginStatus(plugin string) string {
return "unloaded"
}
func hyprlandPluginStatusStruct(plugin string) ui.ItemStatus {
status := hyprlandPluginStatus(plugin)
switch status {
case "loaded":
return ui.StatusLoaded
case "unloaded":
return ui.StatusUnloaded
}
return ui.StatusUnknown
}
func hyprlandPluginList(filter string) []string {
entries, err := os.ReadDir(config.Env.Hyprland.PluginsDir)
if err != nil {
......@@ -118,31 +130,24 @@ func hyprlandPluginInfo(filter string) {
return
}
color.Blue("Plugins:")
items := make([]ui.TreeItem, 0, len(plugins))
statusMap := map[string]struct {
symbol string
color func(string, ...interface{}) string
}{
"loaded": {"●", color.GreenString},
"unloaded": {"○", color.RedString},
for _, plugin := range plugins {
status := hyprlandPluginStatusStruct(plugin)
items = append(items, ui.TreeItem{
Name: plugin,
Status: status,
Description: "",
})
}
for i, name := range plugins {
status := hyprlandPluginStatus(name)
info := statusMap[status]
prefix := "├──"
if i == len(plugins)-1 {
prefix = "└──"
}
fmt.Printf("%s %s %s\n",
prefix,
info.color(info.symbol),
info.color(name),
)
}
ui.RenderTree(ui.RenderTreeOptions{
Title: "Plugins",
Items: items,
Style: ui.DefaultTreeStyle,
StatusMap: ui.ColorStatusMap,
Sort: true,
})
}
func HyprlandPluginListCommand(ctx context.Context, cmd *cli.Command) error {
......
......@@ -2,8 +2,9 @@ package hyprland
import (
"context"
"sort"
"ximperconf/config"
"ximperconf/ui"
"ximperconf/utils"
"bufio"
"fmt"
......@@ -21,7 +22,7 @@ var (
)
func hyprlandVarList() ([]string, error) {
if !fileExists(config.Env.Hyprland.Config) {
if !utils.FileExists(config.Env.Hyprland.Config) {
color.Red("Конфигурация не найдена: %s", config.Env.Hyprland.Config)
return nil, nil
}
......@@ -51,7 +52,7 @@ func HyprlandVarListCommand(ctx context.Context, cmd *cli.Command) error {
}
func hyprlandVarInfo() (map[string]string, error) {
if !fileExists(config.Env.Hyprland.Config) {
if !utils.FileExists(config.Env.Hyprland.Config) {
color.Red("Конфигурация не найдена: %s", config.Env.Hyprland.Config)
os.Exit(1)
}
......@@ -79,7 +80,6 @@ func hyprlandVarInfo() (map[string]string, error) {
}
func HyprlandVarInfoCommand(ctx context.Context, cmd *cli.Command) error {
info, _ := hyprlandVarInfo()
if len(info) == 0 {
......@@ -87,33 +87,27 @@ func HyprlandVarInfoCommand(ctx context.Context, cmd *cli.Command) error {
return nil
}
color.Blue("Vars:")
keys := make([]string, 0, len(info))
for k := range info {
keys = append(keys, k)
}
sort.Strings(keys)
for i, k := range keys {
prefix := "├──"
if i == len(keys)-1 {
prefix = "└──"
items := make([]ui.TreeItem, 0, len(info))
for name, value := range info {
desc := value
if desc == "" {
desc = "<empty>"
}
v := info[k]
fmt.Printf("%s %s: %s\n",
prefix,
color.YellowString(k),
func() string {
if v == "" {
return color.HiBlackString("<empty>")
}
return color.GreenString(v)
}(),
)
items = append(items, ui.TreeItem{
Name: name,
Status: ui.StatusNo,
Description: desc,
})
}
ui.RenderTree(ui.RenderTreeOptions{
Title: "Vars",
Items: items,
Style: ui.DefaultTreeStyle,
StatusMap: ui.ColorStatusMap,
Sort: true,
})
return nil
}
......@@ -124,7 +118,7 @@ func HyprlandVarGet(name string) (string, error) {
if !regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`).MatchString(name) {
return "", fmt.Errorf("недопустимое имя переменной: %s", name)
}
if !fileExists(config.Env.Hyprland.Config) {
if !utils.FileExists(config.Env.Hyprland.Config) {
return "", fmt.Errorf("конфигурация не найдена: %s", config.Env.Hyprland.Config)
}
......@@ -172,7 +166,7 @@ func HyprlandVarSet(name, newValue string) (string, error) {
}
path := config.Env.Hyprland.Config
if !fileExists(path) {
if !utils.FileExists(path) {
_ = os.MkdirAll(filepath.Dir(path), 0755)
_ = os.WriteFile(path, []byte{}, 0644)
}
......@@ -249,7 +243,7 @@ func hyprlandVarUnset(name string) error {
color.Red("Недопустимое имя переменной: %s", name)
os.Exit(1)
}
if !fileExists(config.Env.Hyprland.Config) {
if !utils.FileExists(config.Env.Hyprland.Config) {
color.Red("Конфигурация не найдена: %s", config.Env.Hyprland.Config)
os.Exit(1)
}
......
......@@ -4,8 +4,8 @@ import (
"context"
"os"
"ximperconf/config"
"ximperconf/create"
"ximperconf/hyprland"
"ximperconf/preset"
"ximperconf/repo"
"ximperconf/system"
......@@ -25,7 +25,7 @@ func main() {
Version: config.Env.Version,
Commands: []*cli.Command{
repo.CommandList(),
create.CommandList(),
preset.CommandList(),
hyprland.CommandList(),
system.CommandList(),
{
......
package preset
import (
"bytes"
"ximperconf/config"
"ximperconf/hyprland"
"ximperconf/system"
"ximperconf/ui"
"context"
"fmt"
"os"
"os/exec"
"strings"
"github.com/fatih/color"
"github.com/urfave/cli/v3"
)
func processCopies(prof config.PresetProfile, opts opOptions, res *config.PresetResult) {
for _, c := range prof.Copy {
src := expandPath(c.Src, "")
dest := expandPath(c.Dest, "")
r := copyIfMissing(src, dest, opts)
AppendOpResult(res, r)
}
}
func processLinks(prof config.PresetProfile, opts opOptions, res *config.PresetResult) {
for _, entry := range prof.Links {
handle := func(app string) {
sys := expandPath(entry.Sys, app)
user := expandPath(entry.User, app)
r := createLinkIfMissing(sys, user, opts)
AppendOpResult(res, r)
}
if len(entry.Apps) > 0 {
for _, app := range entry.Apps {
if commandExists(app) {
handle(app)
}
}
continue
}
if entry.App != "" && !commandExists(entry.App) {
continue
}
handle("")
}
}
func processReleaseReplace(prof config.PresetProfile, opts opOptions, res *config.PresetResult) {
sysVer := getSystemVersion()
for _, rr := range prof.ReleaseReplace {
src := expandPath(rr.Src, "")
dest := expandPath(rr.Dest, "")
msg := handleReleaseReplace(src, dest, sysVer, opts)
res.Replaced = append(res.Replaced, msg)
}
}
func handleReleaseReplace(src, dest, sysVer string, opts opOptions) string {
if _, err := os.Stat(dest); os.IsNotExist(err) {
return fmt.Sprintf("Не найден: '%s' — пропущен", dest)
}
data, err := os.ReadFile(dest)
if err != nil {
return fmt.Sprintf("Не удалось прочитать %s: %v", dest, err)
}
content := string(data)
if strings.Contains(content, "XIMPER_LOCK") {
return fmt.Sprintf("Заблокирован: %s — пропущен", dest)
}
fileVer := getFileVersionTag(dest)
if fileVer == "" {
fileVer = "0.9.3"
}
if fileVer == sysVer {
return fmt.Sprintf("Актуально: '%s' — пропущено", dest)
}
if opts.DryRun {
return fmt.Sprintf("[Dry-run] Заменён: %s на %s", dest, src)
}
_ = os.Rename(dest, dest+".old")
if err := copyFile(src, dest); err != nil {
return fmt.Sprintf("Ошибка замены %s: %v", dest, err)
}
return fmt.Sprintf("Обновлён: %s (%s → %s)", dest, fileVer, sysVer)
}
func processHyprVars(prof config.PresetProfile, opts opOptions, res *config.PresetResult) {
for _, v := range prof.Hyprland.Vars {
if !v.Force {
if _, err := hyprland.HyprlandVarGet(v.Var); err == nil {
res.HyprVars = append(res.HyprVars,
fmt.Sprintf("Уже существует: '%s' — пропущено", v.Var))
continue
}
}
if opts.DryRun {
res.HyprVars = append(res.HyprVars,
fmt.Sprintf("[Dry-run] Установлена переменная: %s = %s", v.Var, v.Value))
continue
}
if _, err := hyprland.HyprlandVarSet(v.Var, v.Value); err != nil {
res.HyprVars = append(res.HyprVars,
fmt.Sprintf("Ошибка установки %s: %v", v.Var, err))
} else {
res.HyprVars = append(res.HyprVars,
fmt.Sprintf("Установлена переменная: %s = %s", v.Var, v.Value))
}
}
}
func processHyprModules(prof config.PresetProfile, opts opOptions, res *config.PresetResult) {
for _, m := range prof.Hyprland.Modules {
if m.Module == "" {
res.HyprModules = append(res.HyprModules, "Пропущен модуль без имени")
continue
}
// if-exec
if m.IfExec != "" {
cmd := exec.Command("sh", "-c", m.IfExec)
output, err := cmd.Output()
if err != nil {
res.HyprModules = append(res.HyprModules,
fmt.Sprintf("Модуль '%s' пропущен: команда '%s' завершилась с ошибкой",
m.Module, m.IfExec))
continue
}
if len(bytes.TrimSpace(output)) == 0 {
res.HyprModules = append(res.HyprModules,
fmt.Sprintf("Модуль '%s' пропущен: команда '%s' не вернула данных",
m.Module, m.IfExec))
continue
}
}
// if-binary
if m.IfBinary != "" && !commandExists(m.IfBinary) {
res.HyprModules = append(res.HyprModules,
fmt.Sprintf("Модуль '%s' пропущен: бинарник '%s' не найден", m.Module, m.IfBinary))
continue
}
if m.Init && m.User {
modulefile := hyprland.HyprlandGetModuleFile(m.Module, m.User)
if _, err := os.Stat(modulefile); os.IsNotExist(err) {
if opts.DryRun {
res.HyprModules = append(res.HyprModules,
fmt.Sprintf("[Dry-run] Создан пустой модуль: %s", m.Module))
} else {
_ = os.WriteFile(modulefile, []byte(""), 0o644)
res.HyprModules = append(res.HyprModules,
fmt.Sprintf("Создан пустой модуль: %s", m.Module))
}
}
}
// dry-run
if opts.DryRun {
res.HyprModules = append(res.HyprModules,
fmt.Sprintf("[Dry-run] Подключён модуль: %s", m.Module))
continue
}
if _, err := hyprland.HyprlandSetModule(
"enable",
m.Module,
m.User,
m.NewOnly,
); err != nil {
res.HyprModules = append(res.HyprModules,
fmt.Sprintf("Ошибка подключения модуля %s: %v", m.Module, err))
} else {
res.HyprModules = append(res.HyprModules,
fmt.Sprintf("Подключён модуль: %s", m.Module))
}
}
}
func processHyprLayoutSync(prof config.PresetProfile, opts opOptions, res *config.PresetResult) {
if !prof.Hyprland.Option.SyncSystemLayouts {
return
}
if opts.DryRun {
res.SyncSystemLayouts = append(res.SyncSystemLayouts,
"[Dry-run] Синхронизация раскладок Hyprland пропущена")
return
}
sysLayouts, err := hyprland.HyprlandGetKeyboardLayouts()
if err != nil {
res.SyncSystemLayouts = append(res.SyncSystemLayouts,
"Не удалось получить системные раскладки")
return
}
hyprLayouts, err := hyprland.HyprlandVarGet("kb_layout")
if err != nil {
res.SyncSystemLayouts = append(res.SyncSystemLayouts,
"Не удалось получить раскладки Hyprland")
return
}
if hyprLayouts == "" {
hyprLayouts = "<empty>"
}
res.SyncSystemLayouts = append(res.SyncSystemLayouts,
fmt.Sprintf("Раскладка XCB: %s", sysLayouts))
res.SyncSystemLayouts = append(res.SyncSystemLayouts,
fmt.Sprintf("Раскладка Hyprland: %s", hyprLayouts))
if hyprLayouts == "<empty>" {
if _, err := hyprland.HyprlandVarSet("kb_layout", sysLayouts); err != nil {
res.SyncSystemLayouts = append(res.SyncSystemLayouts,
"Не удалось обновить kb_layout")
return
}
res.SyncSystemLayouts = append(res.SyncSystemLayouts,
fmt.Sprintf("Раскладка обновлена: %s", sysLayouts))
} else {
res.SyncSystemLayouts = append(res.SyncSystemLayouts,
fmt.Sprintf("Раскладка уже установлена: %s", hyprLayouts))
}
}
func processSystemGrub(prof config.PresetProfile, opts opOptions, res *config.PresetResult) {
if prof.System.Grub.FixResolution {
autoUpdate := prof.System.Grub.Update
if opts.DryRun {
res.System = append(res.System, "[Dry-run] GRUB_GFXMODE не исправлен")
if autoUpdate {
res.System = append(res.System, "[Dry-run] update-grub не выполнен")
}
} else {
err := system.SystemGrubFixResolution(autoUpdate, false)
if err != nil {
res.System = append(res.System, fmt.Sprintf("Ошибка GRUB: %v", err))
} else {
res.System = append(res.System, "GRUB_GFXMODE исправлен")
if autoUpdate {
res.System = append(res.System, "update-grub выполнен")
}
}
}
}
}
func processProfile(prof config.PresetProfile, dryRun bool, res *config.PresetResult) {
opts := opOptions{DryRun: dryRun}
processCopies(prof, opts, res)
processLinks(prof, opts, res)
processReleaseReplace(prof, opts, res)
if config.Env.IsRoot {
processSystemGrub(prof, opts, res)
}
processHyprVars(prof, opts, res)
processHyprModules(prof, opts, res)
processHyprLayoutSync(prof, opts, res)
}
func createProfile(profileName string, prof config.PresetProfile, dryRun bool, res *config.PresetResult) {
color.Green("Создаётся профиль: %s", profileName)
if !profileAvailable(prof) {
color.Red("Профиль %s недоступен", profileName)
return
}
processProfile(prof, dryRun, res)
}
func ShowProfilesInfo() {
profiles := config.Env.Preset.Profiles
if len(profiles) == 0 {
color.Red("Нет доступных профилей")
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,
})
}
ui.RenderTree(ui.RenderTreeOptions{
Title: "Profiles",
Items: items,
Style: ui.DefaultTreeStyle,
StatusMap: ui.ColorStatusMap,
Sort: true,
})
}
func presetApplyCommand(ctx context.Context, cmd *cli.Command) error {
profileName := cmd.Args().Get(0)
if profileName == "" {
ShowProfilesInfo()
return nil
}
dryRun := cmd.Bool("dry-run")
profiles := config.Env.Preset.Profiles
mainProf, ok := profiles[profileName]
if !ok {
color.Red("Профиль %s не найден", profileName)
return nil
}
res := &config.PresetResult{}
createProfile(profileName, mainProf, dryRun, res)
renderPresetResult(res)
return nil
}
func presetApplyAllCommand(ctx context.Context, cmd *cli.Command) error {
dryRun := cmd.Bool("dry-run")
profiles := config.Env.Preset.Profiles
if len(profiles) == 0 {
color.Red("Нет доступных профилей")
return nil
}
color.Cyan("Создаём все доступные профили...\n")
for name, prof := range profiles {
if !profileAvailable(prof) {
continue
}
res := &config.PresetResult{}
createProfile(name, prof, dryRun, res)
renderPresetResult(res)
}
return nil
}
func presetInfoCommand(ctx context.Context, cmd *cli.Command) error {
ShowProfilesInfo()
return nil
}
package create
package preset
import (
"context"
......@@ -10,28 +10,42 @@ import (
func CommandList() *cli.Command {
return &cli.Command{
Name: "create",
Usage: "Creating a configuration",
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "dry-run",
Usage: "Show what would be created without making changes.",
Aliases: []string{"d"},
Value: false,
Name: "preset",
Usage: "Manage preset configuration profiles",
Commands: []*cli.Command{
{
Name: "info",
Usage: "Show information about a preset profiles",
Action: presetInfoCommand,
},
&cli.BoolFlag{
Name: "auto",
Usage: "Automatic profile selection",
Value: false,
{
Name: "apply",
Usage: "Apply a profile",
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "dry-run",
Usage: "Show what would be created without making changes.",
Aliases: []string{"d"},
Value: false,
},
},
Action: presetApplyCommand,
ShellComplete: ShellCompleteProfiles,
},
&cli.BoolFlag{
Name: "available",
Usage: "available profiles selected",
Value: false,
{
Name: "apply-all",
Usage: "Apply all available profiles",
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "dry-run",
Usage: "Show what would be created without making changes.",
Aliases: []string{"d"},
Value: false,
},
},
Action: presetApplyAllCommand,
},
},
Action: CreateConfCommand,
ShellComplete: ShellCompleteProfiles,
}
}
......@@ -40,13 +54,10 @@ func ShellCompleteProfiles(ctx context.Context, cmd *cli.Command) {
return
}
cfg, err := loadConfig(config.Env.Create.ConfigDir)
if err != nil {
return
}
profiles := config.Env.Preset.Profiles
for profileName, profile := range cfg.Profiles {
if ProfileAvailable(profile) {
for profileName, profile := range profiles {
if profileAvailable(profile) {
fmt.Println(profileName)
}
}
......
package preset
import (
"os"
"os/exec"
"path/filepath"
"strings"
)
type opKind string
type opStatus string
const (
OpCopy opKind = "copy"
OpLink opKind = "link"
OpReplace opKind = "replace"
)
const (
StatusDone opStatus = "done"
StatusSkipped opStatus = "skipped"
StatusDryRun opStatus = "dry-run"
StatusError opStatus = "error"
)
type opOptions struct {
DryRun bool
Force bool
}
type opResult struct {
Kind opKind
Status opStatus
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)
if err != nil {
return opResult{
Kind: OpCopy,
Status: StatusError,
Source: src,
Target: dest,
Reason: err.Error(),
}
}
if _, err := os.Stat(dest); err == nil {
return opResult{
Kind: OpCopy,
Status: StatusSkipped,
Target: dest,
Reason: "already exists",
}
}
if opts.DryRun {
return opResult{
Kind: OpCopy,
Status: StatusDryRun,
Source: src,
Target: dest,
}
}
if info.IsDir() {
err = copyDir(src, dest)
} else {
err = copyFile(src, dest)
}
if err != nil {
return opResult{
Kind: OpCopy,
Status: StatusError,
Source: src,
Target: dest,
Reason: err.Error(),
}
}
return opResult{
Kind: OpCopy,
Status: StatusDone,
Source: src,
Target: dest,
}
}
func copyFile(src, dest string) error {
if err := os.MkdirAll(filepath.Dir(dest), 0755); err != nil {
return err
}
data, err := os.ReadFile(src)
if err != nil {
return err
}
return os.WriteFile(dest, data, 0644)
}
func copyDir(src, dest string) error {
return filepath.Walk(src, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
rel, err := filepath.Rel(src, path)
if err != nil {
return err
}
target := filepath.Join(dest, rel)
if info.IsDir() {
return os.MkdirAll(target, info.Mode())
}
data, err := os.ReadFile(path)
if err != nil {
return err
}
return os.WriteFile(target, data, info.Mode())
})
}
// ===== Link =====
func createLinkIfMissing(src, dest string, opts opOptions) opResult {
if _, err := os.Stat(src); os.IsNotExist(err) {
return opResult{
Kind: OpLink,
Status: StatusSkipped,
Target: dest,
Reason: "source missing",
}
}
if _, err := os.Lstat(dest); err == nil {
return opResult{
Kind: OpLink,
Status: StatusSkipped,
Target: dest,
Reason: "already exists",
}
}
if opts.DryRun {
return opResult{
Kind: OpLink,
Status: StatusDryRun,
Source: src,
Target: dest,
}
}
if err := os.MkdirAll(filepath.Dir(dest), 0755); err != nil {
return opResult{
Kind: OpLink,
Status: StatusError,
Reason: err.Error(),
}
}
if err := os.Symlink(src, dest); err != nil {
return opResult{
Kind: OpLink,
Status: StatusError,
Reason: err.Error(),
}
}
return opResult{
Kind: OpLink,
Status: StatusDone,
Source: src,
Target: dest,
}
}
// ===== Versions =====
func getSystemVersion() string {
data, err := os.ReadFile("/etc/os-release")
if err != nil {
return ""
}
for _, line := range strings.Split(string(data), "\n") {
if strings.HasPrefix(line, "VERSION_ID=") || strings.HasPrefix(line, "VERSION=") {
return strings.Trim(strings.SplitN(line, "=", 2)[1], `"`)
}
}
return ""
}
func getFileVersionTag(path string) string {
data, err := os.ReadFile(path)
if err != nil {
return ""
}
content := string(data)
idx := strings.Index(content, "XIMPER_V")
if idx == -1 {
return ""
}
content = content[idx+len("XIMPER_V"):]
var b strings.Builder
for _, c := range content {
if (c >= '0' && c <= '9') || c == '.' ||
(c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') {
b.WriteRune(c)
} else {
break
}
}
return b.String()
}
package preset
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"ximperconf/config"
"ximperconf/ui"
)
func profileAvailable(p config.PresetProfile) bool {
allowed := false
if p.Binary == "" {
allowed = true
} else if _, err := exec.LookPath(p.Binary); err == nil {
allowed = true
}
if p.Root != config.Env.IsRoot {
allowed = false
}
return allowed
}
func profileStatus(p config.PresetProfile) ui.ItemStatus {
if profileAvailable(p) {
return ui.StatusAvailable
}
return ui.StatusUnavailable
}
func AppendOpResult(res *config.PresetResult, r opResult) {
var msg string
switch r.Status {
case StatusSkipped:
msg = fmt.Sprintf("Уже существует: %s — пропущено", r.Target)
case StatusDryRun:
msg = fmt.Sprintf("[Dry-run] %s → %s", r.Source, r.Target)
case StatusDone:
msg = fmt.Sprintf("Выполнено: %s → %s", r.Source, r.Target)
case StatusError:
msg = fmt.Sprintf("Ошибка: %s", r.Reason)
}
switch r.Kind {
case OpCopy:
res.Copies = append(res.Copies, msg)
case OpLink:
res.Links = append(res.Links, msg)
}
}
func expandPath(path, name string) string {
home := os.Getenv("HOME")
user := os.Getenv("USER")
hostname, _ := os.Hostname()
replacements := map[string]string{
"~": home,
"{home}": home,
"{user}": user,
"{name}": name,
"{hostname}": hostname,
"{config}": filepath.Join(home, ".config"),
"{ximperconf}": "/etc/ximperdistro",
}
for k, v := range replacements {
path = strings.ReplaceAll(path, k, v)
}
return path
}
func renderPresetResult(res *config.PresetResult) {
ui.RenderTreeLines("Копируем", res.Copies)
ui.RenderTreeLines("Создаём ссылки", res.Links)
ui.RenderTreeLines("Обновляем", res.Replaced)
ui.RenderTreeLines("Настраиваем систему", res.System)
ui.RenderTreeLines("Создаём переменные Hyprland", res.HyprVars)
ui.RenderTreeLines("Подключаем модули Hyprland", res.HyprModules)
ui.RenderTreeLines("Синхронизируем раскладку Hyprland", res.SyncSystemLayouts)
}
package ui
import (
"fmt"
"sort"
"github.com/fatih/color"
)
type ItemStatus int
const (
StatusUnknown ItemStatus = iota
StatusNo
StatusEnabled
StatusLoaded
StatusAvailable
StatusDisabled
StatusUnavailable
StatusUnloaded
StatusMissing
StatusUnused
)
type StatusInfo struct {
Symbol string
Color func(format string, a ...interface{}) string
Label string
}
var ColorStatusMap = map[ItemStatus]StatusInfo{
StatusUnknown: {
Symbol: "?",
Color: color.YellowString,
Label: "unknown",
},
StatusNo: {
Symbol: "",
Color: color.YellowString,
Label: "no",
},
StatusEnabled: {
Symbol: "●",
Color: color.GreenString,
Label: "enabled",
},
StatusLoaded: {
Symbol: "●",
Color: color.GreenString,
Label: "loaded",
},
StatusAvailable: {
Symbol: "●",
Color: color.GreenString,
Label: "available",
},
StatusDisabled: {
Symbol: "○",
Color: color.RedString,
Label: "disabled",
},
StatusUnloaded: {
Symbol: "○",
Color: color.RedString,
Label: "unloaded",
},
StatusUnavailable: {
Symbol: "○",
Color: color.RedString,
Label: "unavailable",
},
StatusMissing: {
Symbol: "!",
Color: color.YellowString,
Label: "missing",
},
StatusUnused: {
Symbol: "-",
Color: color.HiBlackString,
Label: "unused",
},
}
var PlainStatusMap = map[ItemStatus]StatusInfo{
StatusEnabled: {Symbol: "", Color: func(format string, a ...interface{}) string { return fmt.Sprintf(format, a...) }, Label: "enabled"},
StatusLoaded: {Symbol: "", Color: func(format string, a ...interface{}) string { return fmt.Sprintf(format, a...) }, Label: "loaded"},
StatusAvailable: {Symbol: "", Color: func(format string, a ...interface{}) string { return fmt.Sprintf(format, a...) }, Label: "available"},
StatusDisabled: {Symbol: "", Color: func(format string, a ...interface{}) string { return fmt.Sprintf(format, a...) }, Label: "disabled"},
StatusUnloaded: {Symbol: "", Color: func(format string, a ...interface{}) string { return fmt.Sprintf(format, a...) }, Label: "unloaded"},
StatusUnavailable: {Symbol: "", Color: func(format string, a ...interface{}) string { return fmt.Sprintf(format, a...) }, Label: "unavailable"},
StatusMissing: {Symbol: "", Color: func(format string, a ...interface{}) string { return fmt.Sprintf(format, a...) }, Label: "missing"},
StatusUnused: {Symbol: "", Color: func(format string, a ...interface{}) string { return fmt.Sprintf(format, a...) }, Label: "unused"},
StatusUnknown: {Symbol: "", Color: func(format string, a ...interface{}) string { return fmt.Sprintf(format, a...) }, Label: "unknown"},
StatusNo: {Symbol: "", Color: func(format string, a ...interface{}) string { return fmt.Sprintf(format, a...) }, Label: "no"},
}
type TreeItem struct {
Name string
Status ItemStatus
Description string
}
type TreeStyle struct {
PrefixMid string
PrefixLast string
Separator string
}
var DefaultTreeStyle = TreeStyle{
PrefixMid: "├──",
PrefixLast: "└──",
Separator: " — ",
}
type RenderTreeOptions struct {
Title string
Items []TreeItem
Style TreeStyle
StatusMap map[ItemStatus]StatusInfo
Sort bool
}
func RenderTree(opts RenderTreeOptions) {
if opts.Style.PrefixMid == "" {
opts.Style = DefaultTreeStyle
}
if opts.StatusMap == nil {
opts.StatusMap = ColorStatusMap
}
if opts.Sort {
sort.Slice(opts.Items, func(i, j int) bool {
return opts.Items[i].Name < opts.Items[j].Name
})
}
if opts.Title != "" {
fmt.Println(opts.Title + ":")
}
for i, it := range opts.Items {
prefix := opts.Style.PrefixMid
if i == len(opts.Items)-1 {
prefix = opts.Style.PrefixLast
}
info, ok := opts.StatusMap[it.Status]
if !ok {
info = opts.StatusMap[StatusUnknown]
}
line := fmt.Sprintf(
"%s %s %s",
prefix,
info.Color(info.Symbol),
info.Color(it.Name),
)
if it.Description != "" {
line += opts.Style.Separator + it.Description
}
fmt.Println(line)
}
}
package ui
import (
"fmt"
"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,
Status: StatusUnknown,
}
}
RenderTree(RenderTreeOptions{
Items: items,
Style: DefaultTreeStyle,
StatusMap: PlainStatusMap,
})
fmt.Println()
}
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