create: refactoring and renaming to preset

parent e7044e5b
...@@ -6,37 +6,45 @@ import ( ...@@ -6,37 +6,45 @@ import (
"ximperconf/utils" "ximperconf/utils"
) )
type CreateEnv struct { type presetEnv struct {
ConfigDir string Profiles map[string]PresetProfile
} }
type HyprEnv struct { type hyprEnv struct {
Config string Config string
SystemModulesDir string SystemModulesDir string
UserModulesDir string UserModulesDir string
PluginsDir string PluginsDir string
} }
type RepoEnv struct { type repoEnv struct {
DeferredInfoURL string DeferredInfoURL string
} }
type Environment struct { type environment struct {
Version string Version string
IsRoot bool IsRoot bool
Create *CreateEnv Preset *presetEnv
Hyprland *HyprEnv Hyprland *hyprEnv
Repo *RepoEnv Repo *repoEnv
} }
var Env Environment var Env environment
func InitConfig() error { func InitConfig() error {
Env.Version = "0.1.0" Env.Version = "0.1.0"
Env.IsRoot = os.Geteuid() == 0 Env.IsRoot = os.Geteuid() == 0
Env.Create = &CreateEnv{ presetCfg, err := loadPresetConfig("/etc/ximperdistro/ximperconf/preset.d/")
ConfigDir: "/etc/ximperdistro/ximperconf/create.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") { if utils.FileExists("/usr/bin/hyprland") {
...@@ -46,7 +54,7 @@ func InitConfig() error { ...@@ -46,7 +54,7 @@ func InitConfig() error {
pluginsDir := "/usr/lib64/hyprland" pluginsDir := "/usr/lib64/hyprland"
confFile := filepath.Join(userModules, "hyprland.conf") confFile := filepath.Join(userModules, "hyprland.conf")
Env.Hyprland = &HyprEnv{ Env.Hyprland = &hyprEnv{
Config: confFile, Config: confFile,
UserModulesDir: userModules, UserModulesDir: userModules,
SystemModulesDir: optionalPath(systemModules), SystemModulesDir: optionalPath(systemModules),
...@@ -54,7 +62,7 @@ func InitConfig() error { ...@@ -54,7 +62,7 @@ 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/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
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 @@ ...@@ -2,7 +2,7 @@
function __fish_ximperconf_no_subcommand --description 'Test if there has been any subcommand yet' function __fish_ximperconf_no_subcommand --description 'Test if there has been any subcommand yet'
for i in (commandline -opc) 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 return 1
end end
end end
...@@ -18,7 +18,7 @@ function __fish_ximperconf_complete ...@@ -18,7 +18,7 @@ function __fish_ximperconf_complete
end end
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 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)' 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_ ...@@ -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 -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 -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 -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 -x -c ximperconf -n '__fish_ximperconf_no_subcommand' -a 'preset' -d 'Manage preset configuration profiles'
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 preset' -f -l help -s h -d 'show help'
complete -c ximperconf -n '__fish_seen_subcommand_from create' -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 -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 -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' 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 ...@@ -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 -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 -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 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 -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 -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' 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 ...@@ -2,6 +2,8 @@ package hyprland
import ( import (
"ximperconf/config" "ximperconf/config"
"ximperconf/ui"
"ximperconf/utils"
"bufio" "bufio"
"context" "context"
...@@ -49,24 +51,19 @@ func CheckHyprland(ctx context.Context, cmd *cli.Command) error { ...@@ -49,24 +51,19 @@ func CheckHyprland(ctx context.Context, cmd *cli.Command) error {
return nil return nil
} }
func hyprlandGetModuleFile(module string, user bool) string { func HyprlandGetModuleFile(module string, user bool) string {
if user { if user {
return filepath.Join(config.Env.Hyprland.UserModulesDir, module+".conf") return filepath.Join(config.Env.Hyprland.UserModulesDir, module+".conf")
} }
return filepath.Join(config.Env.Hyprland.SystemModulesDir, 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 { func hyprlandModuleStatus(module string, user bool) string {
sysFile, lineFull, lineTilde := hyprlandModuleLines(module, user) sysFile, lineFull, lineTilde := hyprlandModuleLines(module, user)
// Конфиг Hyprland отсутствует // Конфиг Hyprland отсутствует
if !fileExists(config.Env.Hyprland.Config) { if !utils.FileExists(config.Env.Hyprland.Config) {
if fileExists(sysFile) { if utils.FileExists(sysFile) {
return "unused" return "unused"
} }
return "missing" return "missing"
...@@ -74,7 +71,7 @@ func hyprlandModuleStatus(module string, user bool) string { ...@@ -74,7 +71,7 @@ func hyprlandModuleStatus(module string, user bool) string {
f, err := os.Open(config.Env.Hyprland.Config) f, err := os.Open(config.Env.Hyprland.Config)
if err != nil { if err != nil {
if fileExists(sysFile) { if utils.FileExists(sysFile) {
return "unused" return "unused"
} }
return "missing" return "missing"
...@@ -104,12 +101,12 @@ func hyprlandModuleStatus(module string, user bool) string { ...@@ -104,12 +101,12 @@ func hyprlandModuleStatus(module string, user bool) string {
} }
// подключён, но файла нет // подключён, но файла нет
if foundActive && !fileExists(sysFile) { if foundActive && !utils.FileExists(sysFile) {
return "missing" return "missing"
} }
// подключён и файл есть // подключён и файл есть
if foundActive && fileExists(sysFile) { if foundActive && utils.FileExists(sysFile) {
return "enabled" return "enabled"
} }
...@@ -119,7 +116,7 @@ func hyprlandModuleStatus(module string, user bool) string { ...@@ -119,7 +116,7 @@ func hyprlandModuleStatus(module string, user bool) string {
} }
// файл есть, но строка отсутствует // файл есть, но строка отсутствует
if fileExists(sysFile) && !foundActive && !foundCommented { if utils.FileExists(sysFile) && !foundActive && !foundCommented {
return "unused" return "unused"
} }
...@@ -127,6 +124,22 @@ func hyprlandModuleStatus(module string, user bool) string { ...@@ -127,6 +124,22 @@ func hyprlandModuleStatus(module string, user bool) string {
return "missing" 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 { func HyprlandModuleStatusCommand(ctx context.Context, cmd *cli.Command) error {
status := hyprlandModuleStatus(cmd.Args().Get(0), cmd.Bool("user")) status := hyprlandModuleStatus(cmd.Args().Get(0), cmd.Bool("user"))
fmt.Println(status) fmt.Println(status)
...@@ -141,7 +154,7 @@ func HyprlandSetModule(action string, module string, user bool, onlyNew bool) (s ...@@ -141,7 +154,7 @@ func HyprlandSetModule(action string, module string, user bool, onlyNew bool) (s
var out string var out string
if !fileExists(sysFile) { if !utils.FileExists(sysFile) {
return "", fmt.Errorf("модуль '%s' не найден: %s", module, sysFile) return "", fmt.Errorf("модуль '%s' не найден: %s", module, sysFile)
} }
...@@ -305,31 +318,24 @@ func hyprlandInfoModules(user bool, filter string) { ...@@ -305,31 +318,24 @@ func hyprlandInfoModules(user bool, filter string) {
return return
} }
color.Blue("Modules:") items := make([]ui.TreeItem, 0, len(modules))
statusMap := map[string]struct { for _, module := range modules {
symbol string status := hyprlandModuleStatusStruct(module, user)
color func(format string, a ...interface{}) string items = append(items, ui.TreeItem{
}{ Name: module,
"enabled": {symbol: "●", color: color.GreenString}, Status: status,
"disabled": {symbol: "○", color: color.RedString}, Description: "",
"missing": {symbol: "!", color: color.YellowString}, })
"unused": {symbol: "-", color: color.HiBlackString},
} }
for i, module := range modules { ui.RenderTree(ui.RenderTreeOptions{
status := hyprlandModuleStatus(module, user) Title: "Modules",
info := statusMap[status] Items: items,
Style: ui.DefaultTreeStyle,
coloredStatus := info.color(info.symbol) StatusMap: ui.ColorStatusMap,
coloredModule := info.color(module) Sort: true,
})
prefix := "├──"
if i == len(modules)-1 {
prefix = "└──"
}
fmt.Printf("%s %s %s\n", prefix, coloredStatus, coloredModule)
}
} }
func HyprlandInfoModulesCommand(ctx context.Context, cmd *cli.Command) error { func HyprlandInfoModulesCommand(ctx context.Context, cmd *cli.Command) error {
...@@ -338,7 +344,7 @@ 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) { func hyprlandModuleLines(module string, user bool) (sysFile, lineFull, lineTilde string) {
sysFile = hyprlandGetModuleFile(module, user) sysFile = HyprlandGetModuleFile(module, user)
lineFull = "source = " + sysFile lineFull = "source = " + sysFile
lineTilde = "source = ~" + strings.TrimPrefix(sysFile, HomeDir) lineTilde = "source = ~" + strings.TrimPrefix(sysFile, HomeDir)
return return
......
...@@ -2,6 +2,7 @@ package hyprland ...@@ -2,6 +2,7 @@ package hyprland
import ( import (
"ximperconf/config" "ximperconf/config"
"ximperconf/ui"
"ximperconf/utils" "ximperconf/utils"
"context" "context"
...@@ -70,6 +71,17 @@ func hyprlandPluginStatus(plugin string) string { ...@@ -70,6 +71,17 @@ func hyprlandPluginStatus(plugin string) string {
return "unloaded" 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 { func hyprlandPluginList(filter string) []string {
entries, err := os.ReadDir(config.Env.Hyprland.PluginsDir) entries, err := os.ReadDir(config.Env.Hyprland.PluginsDir)
if err != nil { if err != nil {
...@@ -118,31 +130,24 @@ func hyprlandPluginInfo(filter string) { ...@@ -118,31 +130,24 @@ func hyprlandPluginInfo(filter string) {
return return
} }
color.Blue("Plugins:") items := make([]ui.TreeItem, 0, len(plugins))
statusMap := map[string]struct { for _, plugin := range plugins {
symbol string status := hyprlandPluginStatusStruct(plugin)
color func(string, ...interface{}) string items = append(items, ui.TreeItem{
}{ Name: plugin,
"loaded": {"●", color.GreenString}, Status: status,
"unloaded": {"○", color.RedString}, Description: "",
})
} }
for i, name := range plugins { ui.RenderTree(ui.RenderTreeOptions{
status := hyprlandPluginStatus(name) Title: "Plugins",
info := statusMap[status] Items: items,
Style: ui.DefaultTreeStyle,
prefix := "├──" StatusMap: ui.ColorStatusMap,
if i == len(plugins)-1 { Sort: true,
prefix = "└──" })
}
fmt.Printf("%s %s %s\n",
prefix,
info.color(info.symbol),
info.color(name),
)
}
} }
func HyprlandPluginListCommand(ctx context.Context, cmd *cli.Command) error { func HyprlandPluginListCommand(ctx context.Context, cmd *cli.Command) error {
......
...@@ -2,8 +2,9 @@ package hyprland ...@@ -2,8 +2,9 @@ package hyprland
import ( import (
"context" "context"
"sort"
"ximperconf/config" "ximperconf/config"
"ximperconf/ui"
"ximperconf/utils"
"bufio" "bufio"
"fmt" "fmt"
...@@ -21,7 +22,7 @@ var ( ...@@ -21,7 +22,7 @@ var (
) )
func hyprlandVarList() ([]string, error) { func hyprlandVarList() ([]string, error) {
if !fileExists(config.Env.Hyprland.Config) { if !utils.FileExists(config.Env.Hyprland.Config) {
color.Red("Конфигурация не найдена: %s", config.Env.Hyprland.Config) color.Red("Конфигурация не найдена: %s", config.Env.Hyprland.Config)
return nil, nil return nil, nil
} }
...@@ -51,7 +52,7 @@ func HyprlandVarListCommand(ctx context.Context, cmd *cli.Command) error { ...@@ -51,7 +52,7 @@ func HyprlandVarListCommand(ctx context.Context, cmd *cli.Command) error {
} }
func hyprlandVarInfo() (map[string]string, 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) color.Red("Конфигурация не найдена: %s", config.Env.Hyprland.Config)
os.Exit(1) os.Exit(1)
} }
...@@ -79,7 +80,6 @@ func hyprlandVarInfo() (map[string]string, error) { ...@@ -79,7 +80,6 @@ func hyprlandVarInfo() (map[string]string, error) {
} }
func HyprlandVarInfoCommand(ctx context.Context, cmd *cli.Command) error { func HyprlandVarInfoCommand(ctx context.Context, cmd *cli.Command) error {
info, _ := hyprlandVarInfo() info, _ := hyprlandVarInfo()
if len(info) == 0 { if len(info) == 0 {
...@@ -87,32 +87,26 @@ func HyprlandVarInfoCommand(ctx context.Context, cmd *cli.Command) error { ...@@ -87,32 +87,26 @@ func HyprlandVarInfoCommand(ctx context.Context, cmd *cli.Command) error {
return nil return nil
} }
color.Blue("Vars:") items := make([]ui.TreeItem, 0, len(info))
for name, value := range info {
keys := make([]string, 0, len(info)) desc := value
for k := range info { if desc == "" {
keys = append(keys, k) desc = "<empty>"
} }
sort.Strings(keys) items = append(items, ui.TreeItem{
Name: name,
for i, k := range keys { Status: ui.StatusNo,
prefix := "├──" Description: desc,
if i == len(keys)-1 { })
prefix = "└──"
} }
v := info[k] ui.RenderTree(ui.RenderTreeOptions{
fmt.Printf("%s %s: %s\n", Title: "Vars",
prefix, Items: items,
color.YellowString(k), Style: ui.DefaultTreeStyle,
func() string { StatusMap: ui.ColorStatusMap,
if v == "" { Sort: true,
return color.HiBlackString("<empty>") })
}
return color.GreenString(v)
}(),
)
}
return nil return nil
} }
...@@ -124,7 +118,7 @@ func HyprlandVarGet(name string) (string, error) { ...@@ -124,7 +118,7 @@ func HyprlandVarGet(name string) (string, error) {
if !regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`).MatchString(name) { if !regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`).MatchString(name) {
return "", fmt.Errorf("недопустимое имя переменной: %s", 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) return "", fmt.Errorf("конфигурация не найдена: %s", config.Env.Hyprland.Config)
} }
...@@ -172,7 +166,7 @@ func HyprlandVarSet(name, newValue string) (string, error) { ...@@ -172,7 +166,7 @@ func HyprlandVarSet(name, newValue string) (string, error) {
} }
path := config.Env.Hyprland.Config path := config.Env.Hyprland.Config
if !fileExists(path) { if !utils.FileExists(path) {
_ = os.MkdirAll(filepath.Dir(path), 0755) _ = os.MkdirAll(filepath.Dir(path), 0755)
_ = os.WriteFile(path, []byte{}, 0644) _ = os.WriteFile(path, []byte{}, 0644)
} }
...@@ -249,7 +243,7 @@ func hyprlandVarUnset(name string) error { ...@@ -249,7 +243,7 @@ func hyprlandVarUnset(name string) error {
color.Red("Недопустимое имя переменной: %s", name) color.Red("Недопустимое имя переменной: %s", name)
os.Exit(1) os.Exit(1)
} }
if !fileExists(config.Env.Hyprland.Config) { if !utils.FileExists(config.Env.Hyprland.Config) {
color.Red("Конфигурация не найдена: %s", config.Env.Hyprland.Config) color.Red("Конфигурация не найдена: %s", config.Env.Hyprland.Config)
os.Exit(1) os.Exit(1)
} }
......
...@@ -4,8 +4,8 @@ import ( ...@@ -4,8 +4,8 @@ import (
"context" "context"
"os" "os"
"ximperconf/config" "ximperconf/config"
"ximperconf/create"
"ximperconf/hyprland" "ximperconf/hyprland"
"ximperconf/preset"
"ximperconf/repo" "ximperconf/repo"
"ximperconf/system" "ximperconf/system"
...@@ -25,7 +25,7 @@ func main() { ...@@ -25,7 +25,7 @@ func main() {
Version: config.Env.Version, Version: config.Env.Version,
Commands: []*cli.Command{ Commands: []*cli.Command{
repo.CommandList(), repo.CommandList(),
create.CommandList(), preset.CommandList(),
hyprland.CommandList(), hyprland.CommandList(),
system.CommandList(), system.CommandList(),
{ {
......
package create package preset
import ( import (
"context" "context"
...@@ -10,8 +10,17 @@ import ( ...@@ -10,8 +10,17 @@ import (
func CommandList() *cli.Command { func CommandList() *cli.Command {
return &cli.Command{ return &cli.Command{
Name: "create", Name: "preset",
Usage: "Creating a configuration", Usage: "Manage preset configuration profiles",
Commands: []*cli.Command{
{
Name: "info",
Usage: "Show information about a preset profiles",
Action: presetInfoCommand,
},
{
Name: "apply",
Usage: "Apply a profile",
Flags: []cli.Flag{ Flags: []cli.Flag{
&cli.BoolFlag{ &cli.BoolFlag{
Name: "dry-run", Name: "dry-run",
...@@ -19,19 +28,24 @@ func CommandList() *cli.Command { ...@@ -19,19 +28,24 @@ func CommandList() *cli.Command {
Aliases: []string{"d"}, Aliases: []string{"d"},
Value: false, Value: false,
}, },
&cli.BoolFlag{
Name: "auto",
Usage: "Automatic profile selection",
Value: false,
}, },
Action: presetApplyCommand,
ShellComplete: ShellCompleteProfiles,
},
{
Name: "apply-all",
Usage: "Apply all available profiles",
Flags: []cli.Flag{
&cli.BoolFlag{ &cli.BoolFlag{
Name: "available", Name: "dry-run",
Usage: "available profiles selected", Usage: "Show what would be created without making changes.",
Aliases: []string{"d"},
Value: false, Value: false,
}, },
}, },
Action: CreateConfCommand, Action: presetApplyAllCommand,
ShellComplete: ShellCompleteProfiles, },
},
} }
} }
...@@ -40,13 +54,10 @@ func ShellCompleteProfiles(ctx context.Context, cmd *cli.Command) { ...@@ -40,13 +54,10 @@ func ShellCompleteProfiles(ctx context.Context, cmd *cli.Command) {
return return
} }
cfg, err := loadConfig(config.Env.Create.ConfigDir) profiles := config.Env.Preset.Profiles
if err != nil {
return
}
for profileName, profile := range cfg.Profiles { for profileName, profile := range profiles {
if ProfileAvailable(profile) { if profileAvailable(profile) {
fmt.Println(profileName) 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