diff options
| author | AlexanderCurl <alexc@alexc.hu> | 2026-04-18 17:46:06 +0100 |
|---|---|---|
| committer | AlexanderCurl <alexc@alexc.hu> | 2026-04-18 17:46:06 +0100 |
| commit | 70ca3fef77ee8bdc6e3ac28589a6fa08c024cc69 (patch) | |
| tree | ab1123d4067c1b086dd6faa7ee4ea643236b565a /raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/themes | |
| parent | 5d94c0a7d44a2255b81815a52a7056a94a39842d (diff) | |
| download | RaveOS-PKGBUILD-70ca3fef77ee8bdc6e3ac28589a6fa08c024cc69.tar.gz RaveOS-PKGBUILD-70ca3fef77ee8bdc6e3ac28589a6fa08c024cc69.zip | |
Replaced file structures for themes in the correct format
Diffstat (limited to 'raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/themes')
3 files changed, 607 insertions, 0 deletions
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/themes/manager.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/themes/manager.go new file mode 100644 index 0000000..2ebe7ae --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/themes/manager.go @@ -0,0 +1,258 @@ +package themes + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/AvengeMedia/DankMaterialShell/core/internal/log" + "github.com/spf13/afero" +) + +type Manager struct { + fs afero.Fs + themesDir string +} + +func NewManager() (*Manager, error) { + return NewManagerWithFs(afero.NewOsFs()) +} + +func NewManagerWithFs(fs afero.Fs) (*Manager, error) { + themesDir := getThemesDir() + return &Manager{ + fs: fs, + themesDir: themesDir, + }, nil +} + +func getThemesDir() string { + configDir, err := os.UserConfigDir() + if err != nil { + log.Error("failed to get user config dir", "err", err) + return "" + } + return filepath.Join(configDir, "DankMaterialShell", "themes") +} + +func (m *Manager) IsInstalled(theme Theme) (bool, error) { + path := m.getInstalledPath(theme.ID) + exists, err := afero.Exists(m.fs, path) + if err != nil { + return false, err + } + return exists, nil +} + +func (m *Manager) getInstalledDir(themeID string) string { + return filepath.Join(m.themesDir, themeID) +} + +func (m *Manager) getInstalledPath(themeID string) string { + return filepath.Join(m.getInstalledDir(themeID), "theme.json") +} + +func (m *Manager) Install(theme Theme, registryThemeDir string) error { + themeDir := m.getInstalledDir(theme.ID) + + exists, err := afero.DirExists(m.fs, themeDir) + if err != nil { + return fmt.Errorf("failed to check if theme exists: %w", err) + } + + if exists { + return fmt.Errorf("theme already installed: %s", theme.Name) + } + + if err := m.fs.MkdirAll(themeDir, 0o755); err != nil { + return fmt.Errorf("failed to create theme directory: %w", err) + } + + data, err := json.MarshalIndent(theme, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal theme: %w", err) + } + + themePath := filepath.Join(themeDir, "theme.json") + if err := afero.WriteFile(m.fs, themePath, data, 0o644); err != nil { + return fmt.Errorf("failed to write theme file: %w", err) + } + + m.copyPreviewFiles(registryThemeDir, themeDir, theme) + return nil +} + +func (m *Manager) copyPreviewFiles(srcDir, dstDir string, theme Theme) { + previews := []string{"preview-dark.svg", "preview-light.svg"} + + if theme.Variants != nil { + for _, v := range theme.Variants.Options { + previews = append(previews, + fmt.Sprintf("preview-%s.svg", v.ID), + fmt.Sprintf("preview-%s-dark.svg", v.ID), + fmt.Sprintf("preview-%s-light.svg", v.ID), + ) + } + } + + for _, preview := range previews { + srcPath := filepath.Join(srcDir, preview) + if exists, _ := afero.Exists(m.fs, srcPath); !exists { + continue + } + data, err := afero.ReadFile(m.fs, srcPath) + if err != nil { + continue + } + dstPath := filepath.Join(dstDir, preview) + _ = afero.WriteFile(m.fs, dstPath, data, 0o644) + } +} + +func (m *Manager) InstallFromRegistry(registry *Registry, themeID string) error { + theme, err := registry.Get(themeID) + if err != nil { + return err + } + + registryThemeDir := registry.GetThemeDir(theme.SourceDir) + return m.Install(*theme, registryThemeDir) +} + +func (m *Manager) Update(theme Theme) error { + themePath := m.getInstalledPath(theme.ID) + + exists, err := afero.Exists(m.fs, themePath) + if err != nil { + return fmt.Errorf("failed to check if theme exists: %w", err) + } + + if !exists { + return fmt.Errorf("theme not installed: %s", theme.Name) + } + + data, err := json.MarshalIndent(theme, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal theme: %w", err) + } + + if err := afero.WriteFile(m.fs, themePath, data, 0o644); err != nil { + return fmt.Errorf("failed to write theme file: %w", err) + } + + return nil +} + +func (m *Manager) Uninstall(theme Theme) error { + return m.UninstallByID(theme.ID) +} + +func (m *Manager) UninstallByID(themeID string) error { + themeDir := m.getInstalledDir(themeID) + + exists, err := afero.DirExists(m.fs, themeDir) + if err != nil { + return fmt.Errorf("failed to check if theme exists: %w", err) + } + + if !exists { + return fmt.Errorf("theme not installed: %s", themeID) + } + + if err := m.fs.RemoveAll(themeDir); err != nil { + return fmt.Errorf("failed to remove theme: %w", err) + } + + return nil +} + +func (m *Manager) ListInstalled() ([]string, error) { + exists, err := afero.DirExists(m.fs, m.themesDir) + if err != nil { + return nil, err + } + + if !exists { + return []string{}, nil + } + + entries, err := afero.ReadDir(m.fs, m.themesDir) + if err != nil { + return nil, fmt.Errorf("failed to read themes directory: %w", err) + } + + var installed []string + for _, entry := range entries { + if !entry.IsDir() { + continue + } + + themeID := entry.Name() + themePath := filepath.Join(m.themesDir, themeID, "theme.json") + if exists, _ := afero.Exists(m.fs, themePath); exists { + installed = append(installed, themeID) + } + } + + return installed, nil +} + +func (m *Manager) GetInstalledTheme(themeID string) (*Theme, error) { + themePath := m.getInstalledPath(themeID) + + data, err := afero.ReadFile(m.fs, themePath) + if err != nil { + return nil, fmt.Errorf("failed to read theme file: %w", err) + } + + var theme Theme + if err := json.Unmarshal(data, &theme); err != nil { + return nil, fmt.Errorf("failed to parse theme file: %w", err) + } + + return &theme, nil +} + +func (m *Manager) HasUpdates(themeID string, registryTheme Theme) (bool, error) { + installed, err := m.GetInstalledTheme(themeID) + if err != nil { + return false, err + } + + return compareVersions(installed.Version, registryTheme.Version) < 0, nil +} + +func compareVersions(installed, registry string) int { + installedParts := strings.Split(installed, ".") + registryParts := strings.Split(registry, ".") + + maxLen := len(installedParts) + if len(registryParts) > maxLen { + maxLen = len(registryParts) + } + + for i := 0; i < maxLen; i++ { + var installedNum, registryNum int + if i < len(installedParts) { + fmt.Sscanf(installedParts[i], "%d", &installedNum) + } + if i < len(registryParts) { + fmt.Sscanf(registryParts[i], "%d", ®istryNum) + } + + if installedNum < registryNum { + return -1 + } + if installedNum > registryNum { + return 1 + } + } + + return 0 +} + +func (m *Manager) GetThemesDir() string { + return m.themesDir +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/themes/registry.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/themes/registry.go new file mode 100644 index 0000000..5a39913 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/themes/registry.go @@ -0,0 +1,309 @@ +package themes + +import ( + "encoding/json" + "errors" + "fmt" + "os" + "path/filepath" + + "github.com/go-git/go-git/v6" + "github.com/spf13/afero" +) + +const registryRepo = "https://github.com/AvengeMedia/dms-plugin-registry.git" + +type ColorScheme struct { + Primary string `json:"primary,omitempty"` + PrimaryText string `json:"primaryText,omitempty"` + PrimaryContainer string `json:"primaryContainer,omitempty"` + Secondary string `json:"secondary,omitempty"` + Surface string `json:"surface,omitempty"` + SurfaceText string `json:"surfaceText,omitempty"` + SurfaceVariant string `json:"surfaceVariant,omitempty"` + SurfaceVariantText string `json:"surfaceVariantText,omitempty"` + SurfaceTint string `json:"surfaceTint,omitempty"` + Background string `json:"background,omitempty"` + BackgroundText string `json:"backgroundText,omitempty"` + Outline string `json:"outline,omitempty"` + SurfaceContainer string `json:"surfaceContainer,omitempty"` + SurfaceContainerHigh string `json:"surfaceContainerHigh,omitempty"` + SurfaceContainerHighest string `json:"surfaceContainerHighest,omitempty"` + Error string `json:"error,omitempty"` + Warning string `json:"warning,omitempty"` + Info string `json:"info,omitempty"` +} + +type ThemeVariant struct { + ID string `json:"id"` + Name string `json:"name"` + Dark ColorScheme `json:"dark,omitempty"` + Light ColorScheme `json:"light,omitempty"` +} + +type ThemeFlavor struct { + ID string `json:"id"` + Name string `json:"name"` + Dark ColorScheme `json:"dark,omitempty"` + Light ColorScheme `json:"light,omitempty"` +} + +type ThemeAccent struct { + ID string `json:"id"` + Name string `json:"name"` + FlavorColors map[string]ColorScheme `json:"-"` +} + +func (a *ThemeAccent) UnmarshalJSON(data []byte) error { + var raw map[string]json.RawMessage + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + a.FlavorColors = make(map[string]ColorScheme) + var mErr error + for key, value := range raw { + switch key { + case "id": + mErr = errors.Join(mErr, json.Unmarshal(value, &a.ID)) + case "name": + mErr = errors.Join(mErr, json.Unmarshal(value, &a.Name)) + default: + var colors ColorScheme + if err := json.Unmarshal(value, &colors); err == nil { + a.FlavorColors[key] = colors + } else { + mErr = errors.Join(mErr, fmt.Errorf("failed to unmarshal flavor colors for key %s: %w", key, err)) + } + } + } + return mErr +} + +func (a ThemeAccent) MarshalJSON() ([]byte, error) { + m := map[string]any{ + "id": a.ID, + "name": a.Name, + } + for k, v := range a.FlavorColors { + m[k] = v + } + return json.Marshal(m) +} + +type MultiVariantDefaults struct { + Dark map[string]string `json:"dark,omitempty"` + Light map[string]string `json:"light,omitempty"` +} + +type ThemeVariants struct { + Type string `json:"type,omitempty"` + Default string `json:"default,omitempty"` + Defaults *MultiVariantDefaults `json:"defaults,omitempty"` + Options []ThemeVariant `json:"options,omitempty"` + Flavors []ThemeFlavor `json:"flavors,omitempty"` + Accents []ThemeAccent `json:"accents,omitempty"` +} + +type Theme struct { + ID string `json:"id"` + Name string `json:"name"` + Version string `json:"version"` + Author string `json:"author"` + Description string `json:"description"` + Dark ColorScheme `json:"dark"` + Light ColorScheme `json:"light"` + Variants *ThemeVariants `json:"variants,omitempty"` + PreviewPath string `json:"-"` + SourceDir string `json:"sourceDir,omitempty"` +} + +type GitClient interface { + PlainClone(path string, url string) error + Pull(path string) error +} + +type realGitClient struct{} + +func (g *realGitClient) PlainClone(path string, url string) error { + _, err := git.PlainClone(path, &git.CloneOptions{ + URL: url, + Progress: os.Stdout, + }) + return err +} + +func (g *realGitClient) Pull(path string) error { + repo, err := git.PlainOpen(path) + if err != nil { + return err + } + + worktree, err := repo.Worktree() + if err != nil { + return err + } + + err = worktree.Pull(&git.PullOptions{}) + if err != nil && err.Error() != "already up-to-date" { + return err + } + + return nil +} + +type Registry struct { + fs afero.Fs + cacheDir string + themes []Theme + git GitClient +} + +func NewRegistry() (*Registry, error) { + return NewRegistryWithFs(afero.NewOsFs()) +} + +func NewRegistryWithFs(fs afero.Fs) (*Registry, error) { + cacheDir := getCacheDir() + return &Registry{ + fs: fs, + cacheDir: cacheDir, + git: &realGitClient{}, + }, nil +} + +func getCacheDir() string { + return filepath.Join(os.TempDir(), "dankdots-plugin-registry") +} + +func (r *Registry) Update() error { + exists, err := afero.DirExists(r.fs, r.cacheDir) + if err != nil { + return fmt.Errorf("failed to check cache directory: %w", err) + } + + if !exists { + if err := r.fs.MkdirAll(filepath.Dir(r.cacheDir), 0o755); err != nil { + return fmt.Errorf("failed to create cache directory: %w", err) + } + + if err := r.git.PlainClone(r.cacheDir, registryRepo); err != nil { + return fmt.Errorf("failed to clone registry: %w", err) + } + } else { + if err := r.git.Pull(r.cacheDir); err != nil { + if err := r.fs.RemoveAll(r.cacheDir); err != nil { + return fmt.Errorf("failed to remove corrupted registry: %w", err) + } + + if err := r.fs.MkdirAll(filepath.Dir(r.cacheDir), 0o755); err != nil { + return fmt.Errorf("failed to create cache directory: %w", err) + } + + if err := r.git.PlainClone(r.cacheDir, registryRepo); err != nil { + return fmt.Errorf("failed to re-clone registry: %w", err) + } + } + } + + return r.loadThemes() +} + +func (r *Registry) loadThemes() error { + themesDir := filepath.Join(r.cacheDir, "themes") + + entries, err := afero.ReadDir(r.fs, themesDir) + if err != nil { + return fmt.Errorf("failed to read themes directory: %w", err) + } + + r.themes = []Theme{} + + for _, entry := range entries { + if !entry.IsDir() { + continue + } + + themeDir := filepath.Join(themesDir, entry.Name()) + themeFile := filepath.Join(themeDir, "theme.json") + + data, err := afero.ReadFile(r.fs, themeFile) + if err != nil { + continue + } + + var theme Theme + if err := json.Unmarshal(data, &theme); err != nil { + continue + } + + if theme.ID == "" { + theme.ID = entry.Name() + } + theme.SourceDir = entry.Name() + + previewPath := filepath.Join(themeDir, "preview.svg") + if exists, _ := afero.Exists(r.fs, previewPath); exists { + theme.PreviewPath = previewPath + } + + r.themes = append(r.themes, theme) + } + + return nil +} + +func (r *Registry) List() ([]Theme, error) { + if len(r.themes) == 0 { + if err := r.Update(); err != nil { + return nil, err + } + } + + return SortByFirstParty(r.themes), nil +} + +func (r *Registry) Search(query string) ([]Theme, error) { + allThemes, err := r.List() + if err != nil { + return nil, err + } + + if query == "" { + return allThemes, nil + } + + return SortByFirstParty(FuzzySearch(query, allThemes)), nil +} + +func (r *Registry) Get(idOrName string) (*Theme, error) { + themes, err := r.List() + if err != nil { + return nil, err + } + + for _, t := range themes { + if t.ID == idOrName { + return &t, nil + } + } + + for _, t := range themes { + if t.Name == idOrName { + return &t, nil + } + } + + return nil, fmt.Errorf("theme not found: %s", idOrName) +} + +func (r *Registry) GetThemeSourcePath(themeID string) string { + return filepath.Join(r.cacheDir, "themes", themeID, "theme.json") +} + +func (r *Registry) GetThemeDir(themeID string) string { + return filepath.Join(r.cacheDir, "themes", themeID) +} + +func SortByFirstParty(themes []Theme) []Theme { + return themes +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/themes/search.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/themes/search.go new file mode 100644 index 0000000..347f791 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/themes/search.go @@ -0,0 +1,40 @@ +package themes + +import ( + "strings" + + "github.com/AvengeMedia/DankMaterialShell/core/internal/utils" +) + +func FuzzySearch(query string, themes []Theme) []Theme { + if query == "" { + return themes + } + + queryLower := strings.ToLower(query) + return utils.Filter(themes, func(t Theme) bool { + return fuzzyMatch(queryLower, strings.ToLower(t.Name)) || + fuzzyMatch(queryLower, strings.ToLower(t.Description)) || + fuzzyMatch(queryLower, strings.ToLower(t.Author)) + }) +} + +func fuzzyMatch(query, text string) bool { + queryIdx := 0 + for _, char := range text { + if queryIdx < len(query) && char == rune(query[queryIdx]) { + queryIdx++ + } + } + return queryIdx == len(query) +} + +func FindByIDOrName(idOrName string, themes []Theme) *Theme { + if t, found := utils.Find(themes, func(t Theme) bool { return t.ID == idOrName }); found { + return &t + } + if t, found := utils.Find(themes, func(t Theme) bool { return t.Name == idOrName }); found { + return &t + } + return nil +} |