summaryrefslogtreecommitdiff
path: root/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/themes
diff options
context:
space:
mode:
Diffstat (limited to 'raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/themes')
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/themes/manager.go258
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/themes/registry.go309
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/themes/search.go40
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", &registryNum)
+ }
+
+ 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
+}