preset: refactor preset system and YAML config format

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