diff options
| author | Nippy <nippy@rp1.hu> | 2026-05-09 13:33:09 +0200 |
|---|---|---|
| committer | Nippy <nippy@rp1.hu> | 2026-05-09 13:33:09 +0200 |
| commit | a2b924d7e9492b978ad6dc33b6c1ffcb304b4bc5 (patch) | |
| tree | 1a8769217f84bfe9d6216fafaadaf8cdd876d457 /raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/plugins | |
| parent | 34b9c34f982b2596cfa9e368a2d7f74e9a17477c (diff) | |
| download | RaveOS-PKGBUILD-a2b924d7e9492b978ad6dc33b6c1ffcb304b4bc5.tar.gz RaveOS-PKGBUILD-a2b924d7e9492b978ad6dc33b6c1ffcb304b4bc5.zip | |
raveos update
Diffstat (limited to 'raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/plugins')
5 files changed, 0 insertions, 1510 deletions
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/plugins/manager.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/plugins/manager.go deleted file mode 100644 index 1c0ddf5..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/plugins/manager.go +++ /dev/null @@ -1,588 +0,0 @@ -package plugins - -import ( - "crypto/sha256" - "encoding/hex" - "encoding/json" - "fmt" - "os" - "path/filepath" - "strings" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/log" - "github.com/spf13/afero" -) - -type Manager struct { - fs afero.Fs - pluginsDir string - gitClient GitClient -} - -func NewManager() (*Manager, error) { - return NewManagerWithFs(afero.NewOsFs()) -} - -func NewManagerWithFs(fs afero.Fs) (*Manager, error) { - pluginsDir := getPluginsDir() - return &Manager{ - fs: fs, - pluginsDir: pluginsDir, - gitClient: &realGitClient{}, - }, nil -} - -func getPluginsDir() string { - configDir, err := os.UserConfigDir() - if err != nil { - log.Error("failed to get user config dir", "err", err) - return "" - } - return filepath.Join(configDir, "DankMaterialShell", "plugins") -} - -func (m *Manager) IsInstalled(plugin Plugin) (bool, error) { - path, err := m.findInstalledPath(plugin.ID) - if err != nil { - return false, err - } - return path != "", nil -} - -func (m *Manager) findInstalledPath(pluginID string) (string, error) { - // Check user plugins directory - path, err := m.findInDir(m.pluginsDir, pluginID) - if err != nil { - return "", err - } - if path != "" { - return path, nil - } - - // Check system plugins directory - systemDir := "/etc/xdg/quickshell/dms-plugins" - return m.findInDir(systemDir, pluginID) -} - -func (m *Manager) findInDir(dir, pluginID string) (string, error) { - // First, check if folder with exact ID name exists - exactPath := filepath.Join(dir, pluginID) - if exists, _ := afero.DirExists(m.fs, exactPath); exists { - return exactPath, nil - } - - // Scan all folders and check plugin.json for matching ID - exists, err := afero.DirExists(m.fs, dir) - if err != nil || !exists { - return "", nil - } - - entries, err := afero.ReadDir(m.fs, dir) - if err != nil { - return "", nil - } - - for _, entry := range entries { - name := entry.Name() - if name == ".repos" || strings.HasSuffix(name, ".meta") { - continue - } - - fullPath := filepath.Join(dir, name) - isPlugin := entry.IsDir() || entry.Mode()&os.ModeSymlink != 0 - if !isPlugin { - if info, err := m.fs.Stat(fullPath); err == nil && info.IsDir() { - isPlugin = true - } - } - - if isPlugin && m.getPluginID(fullPath) == pluginID { - return fullPath, nil - } - } - - return "", nil -} - -func (m *Manager) Install(plugin Plugin) error { - pluginPath := filepath.Join(m.pluginsDir, plugin.ID) - - exists, err := afero.DirExists(m.fs, pluginPath) - if err != nil { - return fmt.Errorf("failed to check if plugin exists: %w", err) - } - - if exists { - return fmt.Errorf("plugin already installed: %s", plugin.Name) - } - - if err := m.fs.MkdirAll(m.pluginsDir, 0o755); err != nil { - return fmt.Errorf("failed to create plugins directory: %w", err) - } - - reposDir := filepath.Join(m.pluginsDir, ".repos") - if err := m.fs.MkdirAll(reposDir, 0o755); err != nil { - return fmt.Errorf("failed to create repos directory: %w", err) - } - - if plugin.Path != "" { - repoName := m.getRepoName(plugin.Repo) - repoPath := filepath.Join(reposDir, repoName) - - repoExists, err := afero.DirExists(m.fs, repoPath) - if err != nil { - return fmt.Errorf("failed to check if repo exists: %w", err) - } - - if !repoExists { - if err := m.gitClient.PlainClone(repoPath, plugin.Repo); err != nil { - m.fs.RemoveAll(repoPath) //nolint:errcheck - return fmt.Errorf("failed to clone repository: %w", err) - } - } else { - // Pull latest changes if repo already exists - if err := m.gitClient.Pull(repoPath); err != nil { - // If pull fails (e.g., corrupted shallow clone), delete and re-clone - if err := m.fs.RemoveAll(repoPath); err != nil { - return fmt.Errorf("failed to remove corrupted repository: %w", err) - } - - if err := m.gitClient.PlainClone(repoPath, plugin.Repo); err != nil { - return fmt.Errorf("failed to re-clone repository: %w", err) - } - } - } - - sourcePath := filepath.Join(repoPath, plugin.Path) - sourceExists, err := afero.DirExists(m.fs, sourcePath) - if err != nil { - return fmt.Errorf("failed to check plugin path: %w", err) - } - if !sourceExists { - return fmt.Errorf("plugin path does not exist in repository: %s", plugin.Path) - } - - if err := m.createSymlink(sourcePath, pluginPath); err != nil { - return fmt.Errorf("failed to create symlink: %w", err) - } - - metaPath := pluginPath + ".meta" - metaContent := fmt.Sprintf("repo=%s\npath=%s\nrepodir=%s", plugin.Repo, plugin.Path, repoName) - if err := afero.WriteFile(m.fs, metaPath, []byte(metaContent), 0o644); err != nil { - return fmt.Errorf("failed to write metadata: %w", err) - } - } else { - if err := m.gitClient.PlainClone(pluginPath, plugin.Repo); err != nil { - m.fs.RemoveAll(pluginPath) //nolint:errcheck - return fmt.Errorf("failed to clone plugin: %w", err) - } - } - - return nil -} - -func (m *Manager) getRepoName(repoURL string) string { - hash := sha256.Sum256([]byte(repoURL)) - return hex.EncodeToString(hash[:])[:16] -} - -func (m *Manager) createSymlink(source, dest string) error { - if symlinkFs, ok := m.fs.(afero.Symlinker); ok { - return symlinkFs.SymlinkIfPossible(source, dest) - } - return os.Symlink(source, dest) -} - -func (m *Manager) Update(plugin Plugin) error { - pluginPath, err := m.findInstalledPath(plugin.ID) - if err != nil { - return fmt.Errorf("failed to find plugin: %w", err) - } - - if pluginPath == "" { - return fmt.Errorf("plugin not installed: %s", plugin.Name) - } - - if strings.HasPrefix(pluginPath, "/etc/xdg/quickshell/dms-plugins") { - return fmt.Errorf("cannot update system plugin: %s", plugin.Name) - } - - metaPath := pluginPath + ".meta" - metaExists, err := afero.Exists(m.fs, metaPath) - if err != nil { - return fmt.Errorf("failed to check metadata: %w", err) - } - - if metaExists { - reposDir := filepath.Join(m.pluginsDir, ".repos") - repoName := m.getRepoName(plugin.Repo) - repoPath := filepath.Join(reposDir, repoName) - - // Try to pull, if it fails (e.g., shallow clone corruption), delete and re-clone - if err := m.gitClient.Pull(repoPath); err != nil { - // Repository is likely corrupted or has issues, delete and re-clone - if err := m.fs.RemoveAll(repoPath); err != nil { - return fmt.Errorf("failed to remove corrupted repository: %w", err) - } - - if err := m.gitClient.PlainClone(repoPath, plugin.Repo); err != nil { - return fmt.Errorf("failed to re-clone repository: %w", err) - } - } - } else { - // Try to pull, if it fails, delete and re-clone - if err := m.gitClient.Pull(pluginPath); err != nil { - if err := m.fs.RemoveAll(pluginPath); err != nil { - return fmt.Errorf("failed to remove corrupted plugin: %w", err) - } - - if err := m.gitClient.PlainClone(pluginPath, plugin.Repo); err != nil { - return fmt.Errorf("failed to re-clone plugin: %w", err) - } - } - } - - return nil -} - -func (m *Manager) Uninstall(plugin Plugin) error { - pluginPath, err := m.findInstalledPath(plugin.ID) - if err != nil { - return fmt.Errorf("failed to find plugin: %w", err) - } - - if pluginPath == "" { - return fmt.Errorf("plugin not installed: %s", plugin.Name) - } - - if strings.HasPrefix(pluginPath, "/etc/xdg/quickshell/dms-plugins") { - return fmt.Errorf("cannot uninstall system plugin: %s", plugin.Name) - } - - metaPath := pluginPath + ".meta" - metaExists, err := afero.Exists(m.fs, metaPath) - if err != nil { - return fmt.Errorf("failed to check metadata: %w", err) - } - - if metaExists { - reposDir := filepath.Join(m.pluginsDir, ".repos") - repoName := m.getRepoName(plugin.Repo) - repoPath := filepath.Join(reposDir, repoName) - - shouldCleanup, err := m.shouldCleanupRepo(repoPath, plugin.Repo, plugin.ID) - if err != nil { - return fmt.Errorf("failed to check repo cleanup: %w", err) - } - - if err := m.fs.Remove(pluginPath); err != nil { - return fmt.Errorf("failed to remove symlink: %w", err) - } - - if err := m.fs.Remove(metaPath); err != nil { - return fmt.Errorf("failed to remove metadata: %w", err) - } - - if shouldCleanup { - if err := m.fs.RemoveAll(repoPath); err != nil { - return fmt.Errorf("failed to cleanup repository: %w", err) - } - } - } else { - if err := m.fs.RemoveAll(pluginPath); err != nil { - return fmt.Errorf("failed to remove plugin: %w", err) - } - } - - return nil -} - -func (m *Manager) shouldCleanupRepo(repoPath, repoURL, excludePlugin string) (bool, error) { - installed, err := m.ListInstalled() - if err != nil { - return false, err - } - - registry, err := NewRegistry() - if err != nil { - return false, err - } - - allPlugins, err := registry.List() - if err != nil { - return false, err - } - - for _, id := range installed { - if id == excludePlugin { - continue - } - - for _, p := range allPlugins { - if p.ID == id && p.Repo == repoURL && p.Path != "" { - return false, nil - } - } - } - - return true, nil -} - -func (m *Manager) ListInstalled() ([]string, error) { - installedMap := make(map[string]bool) - - exists, err := afero.DirExists(m.fs, m.pluginsDir) - if err != nil { - return nil, err - } - - if exists { - entries, err := afero.ReadDir(m.fs, m.pluginsDir) - if err != nil { - return nil, fmt.Errorf("failed to read plugins directory: %w", err) - } - - for _, entry := range entries { - name := entry.Name() - if name == ".repos" || strings.HasSuffix(name, ".meta") { - continue - } - - fullPath := filepath.Join(m.pluginsDir, name) - isPlugin := false - - if entry.IsDir() { - isPlugin = true - } else if entry.Mode()&os.ModeSymlink != 0 { - isPlugin = true - } else { - info, err := m.fs.Stat(fullPath) - if err == nil && info.IsDir() { - isPlugin = true - } - } - - if isPlugin { - // Read plugin.json to get the actual plugin ID - pluginID := m.getPluginID(fullPath) - if pluginID != "" { - installedMap[pluginID] = true - } - } - } - } - - systemPluginsDir := "/etc/xdg/quickshell/dms-plugins" - systemExists, err := afero.DirExists(m.fs, systemPluginsDir) - if err == nil && systemExists { - entries, err := afero.ReadDir(m.fs, systemPluginsDir) - if err == nil { - for _, entry := range entries { - if entry.IsDir() { - fullPath := filepath.Join(systemPluginsDir, entry.Name()) - // Read plugin.json to get the actual plugin ID - pluginID := m.getPluginID(fullPath) - if pluginID != "" { - installedMap[pluginID] = true - } - } - } - } - } - - var installed []string - for name := range installedMap { - installed = append(installed, name) - } - - return installed, nil -} - -// getPluginID reads the plugin.json file and returns the plugin ID -func (m *Manager) getPluginID(pluginPath string) string { - manifest := m.getPluginManifest(pluginPath) - if manifest == nil { - return "" - } - return manifest.ID -} - -func (m *Manager) getPluginManifest(pluginPath string) *pluginManifest { - manifestPath := filepath.Join(pluginPath, "plugin.json") - data, err := afero.ReadFile(m.fs, manifestPath) - if err != nil { - return nil - } - - var manifest pluginManifest - if err := json.Unmarshal(data, &manifest); err != nil { - return nil - } - - return &manifest -} - -type pluginManifest struct { - ID string `json:"id"` - Name string `json:"name"` -} - -func (m *Manager) GetPluginsDir() string { - return m.pluginsDir -} - -func (m *Manager) UninstallByIDOrName(idOrName string) error { - pluginPath, err := m.findInstalledPathByIDOrName(idOrName) - if err != nil { - return err - } - if pluginPath == "" { - return fmt.Errorf("plugin not found: %s", idOrName) - } - - if strings.HasPrefix(pluginPath, "/etc/xdg/quickshell/dms-plugins") { - return fmt.Errorf("cannot uninstall system plugin: %s", idOrName) - } - - metaPath := pluginPath + ".meta" - metaExists, _ := afero.Exists(m.fs, metaPath) - - if metaExists { - if err := m.fs.Remove(pluginPath); err != nil { - return fmt.Errorf("failed to remove symlink: %w", err) - } - if err := m.fs.Remove(metaPath); err != nil { - return fmt.Errorf("failed to remove metadata: %w", err) - } - } else { - if err := m.fs.RemoveAll(pluginPath); err != nil { - return fmt.Errorf("failed to remove plugin: %w", err) - } - } - - return nil -} - -func (m *Manager) UpdateByIDOrName(idOrName string) error { - pluginPath, err := m.findInstalledPathByIDOrName(idOrName) - if err != nil { - return err - } - if pluginPath == "" { - return fmt.Errorf("plugin not found: %s", idOrName) - } - - if strings.HasPrefix(pluginPath, "/etc/xdg/quickshell/dms-plugins") { - return fmt.Errorf("cannot update system plugin: %s", idOrName) - } - - metaPath := pluginPath + ".meta" - metaExists, _ := afero.Exists(m.fs, metaPath) - - if metaExists { - // Plugin is from monorepo, but we don't know the repo URL without registry - // Just try to pull from existing .git in the symlink target - return fmt.Errorf("cannot update monorepo plugin without registry info: %s", idOrName) - } - - // Standalone plugin - just pull - if err := m.gitClient.Pull(pluginPath); err != nil { - return fmt.Errorf("failed to update plugin: %w", err) - } - - return nil -} - -func (m *Manager) findInstalledPathByIDOrName(idOrName string) (string, error) { - path, err := m.findInDirByIDOrName(m.pluginsDir, idOrName) - if err != nil { - return "", err - } - if path != "" { - return path, nil - } - - systemDir := "/etc/xdg/quickshell/dms-plugins" - return m.findInDirByIDOrName(systemDir, idOrName) -} - -func (m *Manager) findInDirByIDOrName(dir, idOrName string) (string, error) { - // Check exact folder name match first - exactPath := filepath.Join(dir, idOrName) - if exists, _ := afero.DirExists(m.fs, exactPath); exists { - return exactPath, nil - } - - exists, err := afero.DirExists(m.fs, dir) - if err != nil || !exists { - return "", nil - } - - entries, err := afero.ReadDir(m.fs, dir) - if err != nil { - return "", nil - } - - for _, entry := range entries { - name := entry.Name() - if name == ".repos" || strings.HasSuffix(name, ".meta") { - continue - } - - fullPath := filepath.Join(dir, name) - isPlugin := entry.IsDir() || entry.Mode()&os.ModeSymlink != 0 - if !isPlugin { - if info, err := m.fs.Stat(fullPath); err == nil && info.IsDir() { - isPlugin = true - } - } - - if !isPlugin { - continue - } - - manifest := m.getPluginManifest(fullPath) - if manifest == nil { - continue - } - - if manifest.ID == idOrName || manifest.Name == idOrName { - return fullPath, nil - } - } - - return "", nil -} - -func (m *Manager) HasUpdates(pluginID string, plugin Plugin) (bool, error) { - pluginPath, err := m.findInstalledPath(pluginID) - if err != nil { - return false, fmt.Errorf("failed to find plugin: %w", err) - } - - if pluginPath == "" { - return false, fmt.Errorf("plugin not installed: %s", pluginID) - } - - if strings.HasPrefix(pluginPath, "/etc/xdg/quickshell/dms-plugins") { - return false, nil - } - - metaPath := pluginPath + ".meta" - metaExists, err := afero.Exists(m.fs, metaPath) - if err != nil { - return false, fmt.Errorf("failed to check metadata: %w", err) - } - - if metaExists { - // Plugin is from a monorepo, check the repo directory - reposDir := filepath.Join(m.pluginsDir, ".repos") - repoName := m.getRepoName(plugin.Repo) - repoPath := filepath.Join(reposDir, repoName) - - return m.gitClient.HasUpdates(repoPath) - } - - // Plugin is a standalone repo - return m.gitClient.HasUpdates(pluginPath) -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/plugins/manager_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/plugins/manager_test.go deleted file mode 100644 index 485479e..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/plugins/manager_test.go +++ /dev/null @@ -1,247 +0,0 @@ -package plugins - -import ( - "os" - "path/filepath" - "testing" - - "github.com/spf13/afero" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func setupTestManager(t *testing.T) (*Manager, afero.Fs, string) { - fs := afero.NewMemMapFs() - pluginsDir := "/test-plugins" - manager := &Manager{ - fs: fs, - pluginsDir: pluginsDir, - gitClient: &mockGitClient{}, - } - return manager, fs, pluginsDir -} - -func TestNewManager(t *testing.T) { - manager, err := NewManager() - assert.NoError(t, err) - assert.NotNil(t, manager) - assert.NotEmpty(t, manager.pluginsDir) -} - -func TestGetPluginsDir(t *testing.T) { - t.Run("uses XDG_CONFIG_HOME when set", func(t *testing.T) { - oldConfig := os.Getenv("XDG_CONFIG_HOME") - defer func() { - if oldConfig != "" { - os.Setenv("XDG_CONFIG_HOME", oldConfig) - } else { - os.Unsetenv("XDG_CONFIG_HOME") - } - }() - - os.Setenv("XDG_CONFIG_HOME", "/tmp/test-config") - dir := getPluginsDir() - assert.Equal(t, "/tmp/test-config/DankMaterialShell/plugins", dir) - }) - - t.Run("falls back to home directory", func(t *testing.T) { - oldConfig := os.Getenv("XDG_CONFIG_HOME") - defer func() { - if oldConfig != "" { - os.Setenv("XDG_CONFIG_HOME", oldConfig) - } else { - os.Unsetenv("XDG_CONFIG_HOME") - } - }() - - os.Unsetenv("XDG_CONFIG_HOME") - dir := getPluginsDir() - assert.Contains(t, dir, ".config/DankMaterialShell/plugins") - }) -} - -func TestIsInstalled(t *testing.T) { - t.Run("returns true when plugin is installed", func(t *testing.T) { - manager, fs, pluginsDir := setupTestManager(t) - - plugin := Plugin{ID: "test-plugin", Name: "TestPlugin"} - pluginPath := filepath.Join(pluginsDir, plugin.ID) - err := fs.MkdirAll(pluginPath, 0o755) - require.NoError(t, err) - - installed, err := manager.IsInstalled(plugin) - assert.NoError(t, err) - assert.True(t, installed) - }) - - t.Run("returns false when plugin is not installed", func(t *testing.T) { - manager, _, _ := setupTestManager(t) - - plugin := Plugin{ID: "non-existent", Name: "NonExistent"} - installed, err := manager.IsInstalled(plugin) - assert.NoError(t, err) - assert.False(t, installed) - }) -} - -func TestInstall(t *testing.T) { - t.Run("installs plugin successfully", func(t *testing.T) { - manager, fs, pluginsDir := setupTestManager(t) - - plugin := Plugin{ - ID: "test-plugin", - Name: "TestPlugin", - Repo: "https://github.com/test/plugin", - } - - cloneCalled := false - mockGit := &mockGitClient{ - cloneFunc: func(path string, url string) error { - cloneCalled = true - assert.Equal(t, filepath.Join(pluginsDir, plugin.ID), path) - assert.Equal(t, plugin.Repo, url) - return fs.MkdirAll(path, 0o755) - }, - } - manager.gitClient = mockGit - - err := manager.Install(plugin) - assert.NoError(t, err) - assert.True(t, cloneCalled) - - exists, _ := afero.DirExists(fs, filepath.Join(pluginsDir, plugin.ID)) - assert.True(t, exists) - }) - - t.Run("returns error when plugin already installed", func(t *testing.T) { - manager, fs, pluginsDir := setupTestManager(t) - - plugin := Plugin{ID: "test-plugin", Name: "TestPlugin"} - pluginPath := filepath.Join(pluginsDir, plugin.ID) - err := fs.MkdirAll(pluginPath, 0o755) - require.NoError(t, err) - - err = manager.Install(plugin) - assert.Error(t, err) - assert.Contains(t, err.Error(), "already installed") - }) - - t.Run("installs monorepo plugin with symlink", func(t *testing.T) { - t.Skip("Skipping symlink test as MemMapFs doesn't support symlinks") - }) -} - -func TestManagerUpdate(t *testing.T) { - t.Run("updates plugin successfully", func(t *testing.T) { - manager, fs, pluginsDir := setupTestManager(t) - - plugin := Plugin{ID: "test-plugin", Name: "TestPlugin"} - pluginPath := filepath.Join(pluginsDir, plugin.ID) - err := fs.MkdirAll(pluginPath, 0o755) - require.NoError(t, err) - - pullCalled := false - mockGit := &mockGitClient{ - pullFunc: func(path string) error { - pullCalled = true - assert.Equal(t, pluginPath, path) - return nil - }, - } - manager.gitClient = mockGit - - err = manager.Update(plugin) - assert.NoError(t, err) - assert.True(t, pullCalled) - }) - - t.Run("returns error when plugin not installed", func(t *testing.T) { - manager, _, _ := setupTestManager(t) - - plugin := Plugin{ID: "non-existent", Name: "NonExistent"} - err := manager.Update(plugin) - assert.Error(t, err) - assert.Contains(t, err.Error(), "not installed") - }) -} - -func TestUninstall(t *testing.T) { - t.Run("uninstalls plugin successfully", func(t *testing.T) { - manager, fs, pluginsDir := setupTestManager(t) - - plugin := Plugin{ID: "test-plugin", Name: "TestPlugin"} - pluginPath := filepath.Join(pluginsDir, plugin.ID) - err := fs.MkdirAll(pluginPath, 0o755) - require.NoError(t, err) - - err = manager.Uninstall(plugin) - assert.NoError(t, err) - - exists, _ := afero.DirExists(fs, pluginPath) - assert.False(t, exists) - }) - - t.Run("returns error when plugin not installed", func(t *testing.T) { - manager, _, _ := setupTestManager(t) - - plugin := Plugin{ID: "non-existent", Name: "NonExistent"} - err := manager.Uninstall(plugin) - assert.Error(t, err) - assert.Contains(t, err.Error(), "not installed") - }) -} - -func TestListInstalled(t *testing.T) { - t.Run("lists installed plugins", func(t *testing.T) { - manager, fs, pluginsDir := setupTestManager(t) - - err := fs.MkdirAll(filepath.Join(pluginsDir, "Plugin1"), 0o755) - require.NoError(t, err) - err = afero.WriteFile(fs, filepath.Join(pluginsDir, "Plugin1", "plugin.json"), []byte(`{"id":"Plugin1"}`), 0o644) - require.NoError(t, err) - - err = fs.MkdirAll(filepath.Join(pluginsDir, "Plugin2"), 0o755) - require.NoError(t, err) - err = afero.WriteFile(fs, filepath.Join(pluginsDir, "Plugin2", "plugin.json"), []byte(`{"id":"Plugin2"}`), 0o644) - require.NoError(t, err) - - installed, err := manager.ListInstalled() - assert.NoError(t, err) - assert.Len(t, installed, 2) - assert.Contains(t, installed, "Plugin1") - assert.Contains(t, installed, "Plugin2") - }) - - t.Run("returns empty list when no plugins installed", func(t *testing.T) { - manager, _, _ := setupTestManager(t) - - installed, err := manager.ListInstalled() - assert.NoError(t, err) - assert.Empty(t, installed) - }) - - t.Run("ignores files and .repos directory", func(t *testing.T) { - manager, fs, pluginsDir := setupTestManager(t) - - err := fs.MkdirAll(pluginsDir, 0o755) - require.NoError(t, err) - err = fs.MkdirAll(filepath.Join(pluginsDir, "Plugin1"), 0o755) - require.NoError(t, err) - err = afero.WriteFile(fs, filepath.Join(pluginsDir, "Plugin1", "plugin.json"), []byte(`{"id":"Plugin1"}`), 0o644) - require.NoError(t, err) - err = fs.MkdirAll(filepath.Join(pluginsDir, ".repos"), 0o755) - require.NoError(t, err) - err = afero.WriteFile(fs, filepath.Join(pluginsDir, "README.md"), []byte("test"), 0o644) - require.NoError(t, err) - - installed, err := manager.ListInstalled() - assert.NoError(t, err) - assert.Len(t, installed, 1) - assert.Equal(t, "Plugin1", installed[0]) - }) -} - -func TestManagerGetPluginsDir(t *testing.T) { - manager, _, pluginsDir := setupTestManager(t) - assert.Equal(t, pluginsDir, manager.GetPluginsDir()) -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/plugins/registry.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/plugins/registry.go deleted file mode 100644 index 1728e72..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/plugins/registry.go +++ /dev/null @@ -1,258 +0,0 @@ -package plugins - -import ( - "encoding/json" - "fmt" - "os" - "path/filepath" - "strings" - - "github.com/go-git/go-git/v6" - "github.com/spf13/afero" -) - -const registryRepo = "https://github.com/AvengeMedia/dms-plugin-registry.git" - -type Plugin struct { - ID string `json:"id"` - Name string `json:"name"` - Capabilities []string `json:"capabilities"` - Category string `json:"category"` - Repo string `json:"repo"` - Path string `json:"path,omitempty"` - Author string `json:"author"` - Description string `json:"description"` - Dependencies []string `json:"dependencies,omitempty"` - Compositors []string `json:"compositors"` - Distro []string `json:"distro"` - Screenshot string `json:"screenshot,omitempty"` - RequiresDMS string `json:"requires_dms,omitempty"` - Featured bool `json:"featured,omitempty"` -} - -type GitClient interface { - PlainClone(path string, url string) error - Pull(path string) error - HasUpdates(path string) (bool, 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 -} - -func (g *realGitClient) HasUpdates(path string) (bool, error) { - repo, err := git.PlainOpen(path) - if err != nil { - return false, err - } - - // Fetch remote changes - err = repo.Fetch(&git.FetchOptions{}) - if err != nil && err.Error() != "already up-to-date" { - // If fetch fails, we can't determine if there are updates - // Return false and the error - return false, err - } - - // Get the HEAD reference - head, err := repo.Head() - if err != nil { - return false, err - } - - // Get the remote HEAD reference (typically origin/HEAD or origin/main or origin/master) - remote, err := repo.Remote("origin") - if err != nil { - return false, err - } - - refs, err := remote.List(&git.ListOptions{}) - if err != nil { - return false, err - } - - // Find the default branch remote ref - var remoteHead string - for _, ref := range refs { - if ref.Name().IsBranch() { - // Try common branch names - if ref.Name().Short() == "main" || ref.Name().Short() == "master" { - remoteHead = ref.Hash().String() - break - } - } - } - - // If we couldn't find a remote HEAD, assume no updates - if remoteHead == "" { - return false, nil - } - - // Compare local HEAD with remote HEAD - return head.Hash().String() != remoteHead, nil -} - -type Registry struct { - fs afero.Fs - cacheDir string - plugins []Plugin - 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 { - // Try to pull, if it fails (e.g., shallow clone corruption), delete and re-clone - if err := r.git.Pull(r.cacheDir); err != nil { - // Repository is likely corrupted or has issues, delete and re-clone - 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.loadPlugins() -} - -func (r *Registry) loadPlugins() error { - pluginsDir := filepath.Join(r.cacheDir, "plugins") - - entries, err := afero.ReadDir(r.fs, pluginsDir) - if err != nil { - return fmt.Errorf("failed to read plugins directory: %w", err) - } - - r.plugins = []Plugin{} - - for _, entry := range entries { - if entry.IsDir() || filepath.Ext(entry.Name()) != ".json" { - continue - } - - data, err := afero.ReadFile(r.fs, filepath.Join(pluginsDir, entry.Name())) - if err != nil { - continue - } - - var plugin Plugin - if err := json.Unmarshal(data, &plugin); err != nil { - continue - } - - if plugin.ID == "" { - plugin.ID = strings.TrimSuffix(entry.Name(), ".json") - } - - r.plugins = append(r.plugins, plugin) - } - - return nil -} - -func (r *Registry) List() ([]Plugin, error) { - if len(r.plugins) == 0 { - if err := r.Update(); err != nil { - return nil, err - } - } - - return SortByFirstParty(r.plugins), nil -} - -func (r *Registry) Search(query string) ([]Plugin, error) { - allPlugins, err := r.List() - if err != nil { - return nil, err - } - - if query == "" { - return allPlugins, nil - } - - return SortByFirstParty(FuzzySearch(query, allPlugins)), nil -} - -func (r *Registry) Get(idOrName string) (*Plugin, error) { - plugins, err := r.List() - if err != nil { - return nil, err - } - - // First, try to find by ID (preferred method) - for _, p := range plugins { - if p.ID == idOrName { - return &p, nil - } - } - - // Fallback to name for backward compatibility - for _, p := range plugins { - if p.Name == idOrName { - return &p, nil - } - } - - return nil, fmt.Errorf("plugin not found: %s", idOrName) -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/plugins/registry_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/plugins/registry_test.go deleted file mode 100644 index e535e6f..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/plugins/registry_test.go +++ /dev/null @@ -1,326 +0,0 @@ -package plugins - -import ( - "encoding/json" - "path/filepath" - "testing" - - "github.com/spf13/afero" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -type mockGitClient struct { - cloneFunc func(path string, url string) error - pullFunc func(path string) error - hasUpdatesFunc func(path string) (bool, error) -} - -func (m *mockGitClient) PlainClone(path string, url string) error { - if m.cloneFunc != nil { - return m.cloneFunc(path, url) - } - return nil -} - -func (m *mockGitClient) Pull(path string) error { - if m.pullFunc != nil { - return m.pullFunc(path) - } - return nil -} - -func (m *mockGitClient) HasUpdates(path string) (bool, error) { - if m.hasUpdatesFunc != nil { - return m.hasUpdatesFunc(path) - } - return false, nil -} - -func TestNewRegistry(t *testing.T) { - registry, err := NewRegistry() - assert.NoError(t, err) - assert.NotNil(t, registry) - assert.NotEmpty(t, registry.cacheDir) -} - -func TestGetCacheDir(t *testing.T) { - cacheDir := getCacheDir() - assert.Contains(t, cacheDir, "/tmp/dankdots-plugin-registry") -} - -func setupTestRegistry(t *testing.T) (*Registry, afero.Fs, string) { - fs := afero.NewMemMapFs() - tmpDir := "/test-cache" - registry := &Registry{ - fs: fs, - cacheDir: tmpDir, - plugins: []Plugin{}, - git: &mockGitClient{}, - } - return registry, fs, tmpDir -} - -func createTestPlugin(t *testing.T, fs afero.Fs, dir string, filename string, plugin Plugin) { - pluginsDir := filepath.Join(dir, "plugins") - err := fs.MkdirAll(pluginsDir, 0o755) - require.NoError(t, err) - - data, err := json.Marshal(plugin) - require.NoError(t, err) - - err = afero.WriteFile(fs, filepath.Join(pluginsDir, filename), data, 0o644) - require.NoError(t, err) -} - -func TestLoadPlugins(t *testing.T) { - t.Run("loads valid plugin files", func(t *testing.T) { - registry, fs, tmpDir := setupTestRegistry(t) - - plugin1 := Plugin{ - Name: "TestPlugin1", - Capabilities: []string{"dankbar-widget"}, - Category: "monitoring", - Repo: "https://github.com/test/plugin1", - Author: "Test Author", - Description: "Test plugin 1", - Compositors: []string{"niri"}, - Distro: []string{"any"}, - } - - plugin2 := Plugin{ - Name: "TestPlugin2", - Capabilities: []string{"system-tray"}, - Category: "utilities", - Repo: "https://github.com/test/plugin2", - Author: "Another Author", - Description: "Test plugin 2", - Dependencies: []string{"dep1", "dep2"}, - Compositors: []string{"hyprland", "niri"}, - Distro: []string{"arch"}, - Screenshot: "https://example.com/screenshot.png", - } - - createTestPlugin(t, fs, tmpDir, "plugin1.json", plugin1) - createTestPlugin(t, fs, tmpDir, "plugin2.json", plugin2) - - err := registry.loadPlugins() - assert.NoError(t, err) - assert.Len(t, registry.plugins, 2) - - assert.Equal(t, "TestPlugin1", registry.plugins[0].Name) - assert.Equal(t, "TestPlugin2", registry.plugins[1].Name) - assert.Equal(t, []string{"dankbar-widget"}, registry.plugins[0].Capabilities) - assert.Equal(t, []string{"dep1", "dep2"}, registry.plugins[1].Dependencies) - }) - - t.Run("skips non-json files", func(t *testing.T) { - registry, fs, tmpDir := setupTestRegistry(t) - - pluginsDir := filepath.Join(tmpDir, "plugins") - err := fs.MkdirAll(pluginsDir, 0o755) - require.NoError(t, err) - - err = afero.WriteFile(fs, filepath.Join(pluginsDir, "README.md"), []byte("# Test"), 0o644) - require.NoError(t, err) - - plugin := Plugin{ - Name: "ValidPlugin", - Capabilities: []string{"test"}, - Category: "test", - Repo: "https://github.com/test/test", - Author: "Test", - Description: "Test", - Compositors: []string{"niri"}, - Distro: []string{"any"}, - } - createTestPlugin(t, fs, tmpDir, "valid.json", plugin) - - err = registry.loadPlugins() - assert.NoError(t, err) - assert.Len(t, registry.plugins, 1) - assert.Equal(t, "ValidPlugin", registry.plugins[0].Name) - }) - - t.Run("skips directories", func(t *testing.T) { - registry, fs, tmpDir := setupTestRegistry(t) - - pluginsDir := filepath.Join(tmpDir, "plugins") - err := fs.MkdirAll(filepath.Join(pluginsDir, "subdir"), 0o755) - require.NoError(t, err) - - plugin := Plugin{ - Name: "ValidPlugin", - Capabilities: []string{"test"}, - Category: "test", - Repo: "https://github.com/test/test", - Author: "Test", - Description: "Test", - Compositors: []string{"niri"}, - Distro: []string{"any"}, - } - createTestPlugin(t, fs, tmpDir, "valid.json", plugin) - - err = registry.loadPlugins() - assert.NoError(t, err) - assert.Len(t, registry.plugins, 1) - }) - - t.Run("skips invalid json files", func(t *testing.T) { - registry, fs, tmpDir := setupTestRegistry(t) - - pluginsDir := filepath.Join(tmpDir, "plugins") - err := fs.MkdirAll(pluginsDir, 0o755) - require.NoError(t, err) - - err = afero.WriteFile(fs, filepath.Join(pluginsDir, "invalid.json"), []byte("{invalid json}"), 0o644) - require.NoError(t, err) - - plugin := Plugin{ - Name: "ValidPlugin", - Capabilities: []string{"test"}, - Category: "test", - Repo: "https://github.com/test/test", - Author: "Test", - Description: "Test", - Compositors: []string{"niri"}, - Distro: []string{"any"}, - } - createTestPlugin(t, fs, tmpDir, "valid.json", plugin) - - err = registry.loadPlugins() - assert.NoError(t, err) - assert.Len(t, registry.plugins, 1) - assert.Equal(t, "ValidPlugin", registry.plugins[0].Name) - }) - - t.Run("returns error when plugins directory missing", func(t *testing.T) { - registry, _, _ := setupTestRegistry(t) - - err := registry.loadPlugins() - assert.Error(t, err) - assert.Contains(t, err.Error(), "failed to read plugins directory") - }) -} - -func TestList(t *testing.T) { - t.Run("returns cached plugins if available", func(t *testing.T) { - registry, _, _ := setupTestRegistry(t) - - plugin := Plugin{ - Name: "CachedPlugin", - Capabilities: []string{"test"}, - Category: "test", - Repo: "https://github.com/test/test", - Author: "Test", - Description: "Test", - Compositors: []string{"niri"}, - Distro: []string{"any"}, - } - - registry.plugins = []Plugin{plugin} - - plugins, err := registry.List() - assert.NoError(t, err) - assert.Len(t, plugins, 1) - assert.Equal(t, "CachedPlugin", plugins[0].Name) - }) - - t.Run("updates and loads plugins when cache is empty", func(t *testing.T) { - registry, fs, _ := setupTestRegistry(t) - - plugin := Plugin{ - Name: "NewPlugin", - Capabilities: []string{"test"}, - Category: "test", - Repo: "https://github.com/test/test", - Author: "Test", - Description: "Test", - Compositors: []string{"niri"}, - Distro: []string{"any"}, - } - - mockGit := &mockGitClient{ - cloneFunc: func(path string, url string) error { - createTestPlugin(t, fs, path, "plugin.json", plugin) - return nil - }, - } - registry.git = mockGit - - plugins, err := registry.List() - assert.NoError(t, err) - assert.Len(t, plugins, 1) - assert.Equal(t, "NewPlugin", plugins[0].Name) - }) -} - -func TestUpdate(t *testing.T) { - t.Run("clones repository when cache doesn't exist", func(t *testing.T) { - registry, fs, tmpDir := setupTestRegistry(t) - - plugin := Plugin{ - Name: "RepoPlugin", - Capabilities: []string{"test"}, - Category: "test", - Repo: "https://github.com/test/test", - Author: "Test", - Description: "Test", - Compositors: []string{"niri"}, - Distro: []string{"any"}, - } - - cloneCalled := false - mockGit := &mockGitClient{ - cloneFunc: func(path string, url string) error { - cloneCalled = true - assert.Equal(t, registryRepo, url) - assert.Equal(t, tmpDir, path) - createTestPlugin(t, fs, path, "plugin.json", plugin) - return nil - }, - } - registry.git = mockGit - - err := registry.Update() - assert.NoError(t, err) - assert.True(t, cloneCalled) - assert.Len(t, registry.plugins, 1) - assert.Equal(t, "RepoPlugin", registry.plugins[0].Name) - }) - - t.Run("pulls updates when cache exists", func(t *testing.T) { - registry, fs, tmpDir := setupTestRegistry(t) - - plugin := Plugin{ - Name: "UpdatedPlugin", - Capabilities: []string{"test"}, - Category: "test", - Repo: "https://github.com/test/test", - Author: "Test", - Description: "Test", - Compositors: []string{"niri"}, - Distro: []string{"any"}, - } - - err := fs.MkdirAll(tmpDir, 0o755) - require.NoError(t, err) - - pullCalled := false - mockGit := &mockGitClient{ - pullFunc: func(path string) error { - pullCalled = true - assert.Equal(t, tmpDir, path) - createTestPlugin(t, fs, path, "plugin.json", plugin) - return nil - }, - } - registry.git = mockGit - - err = registry.Update() - assert.NoError(t, err) - assert.True(t, pullCalled) - assert.Len(t, registry.plugins, 1) - assert.Equal(t, "UpdatedPlugin", registry.plugins[0].Name) - }) -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/plugins/search.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/plugins/search.go deleted file mode 100644 index 833158f..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/plugins/search.go +++ /dev/null @@ -1,91 +0,0 @@ -package plugins - -import ( - "sort" - "strings" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/utils" -) - -func FuzzySearch(query string, plugins []Plugin) []Plugin { - if query == "" { - return plugins - } - - queryLower := strings.ToLower(query) - return utils.Filter(plugins, func(p Plugin) bool { - return fuzzyMatch(queryLower, strings.ToLower(p.Name)) || - fuzzyMatch(queryLower, strings.ToLower(p.Category)) || - fuzzyMatch(queryLower, strings.ToLower(p.Description)) || - fuzzyMatch(queryLower, strings.ToLower(p.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 FilterByCategory(category string, plugins []Plugin) []Plugin { - if category == "" { - return plugins - } - categoryLower := strings.ToLower(category) - return utils.Filter(plugins, func(p Plugin) bool { - return strings.ToLower(p.Category) == categoryLower - }) -} - -func FilterByCompositor(compositor string, plugins []Plugin) []Plugin { - if compositor == "" { - return plugins - } - compositorLower := strings.ToLower(compositor) - return utils.Filter(plugins, func(p Plugin) bool { - return utils.Any(p.Compositors, func(c string) bool { - return strings.ToLower(c) == compositorLower - }) - }) -} - -func FilterByCapability(capability string, plugins []Plugin) []Plugin { - if capability == "" { - return plugins - } - capabilityLower := strings.ToLower(capability) - return utils.Filter(plugins, func(p Plugin) bool { - return utils.Any(p.Capabilities, func(c string) bool { - return strings.ToLower(c) == capabilityLower - }) - }) -} - -func SortByFirstParty(plugins []Plugin) []Plugin { - sort.SliceStable(plugins, func(i, j int) bool { - if plugins[i].Featured != plugins[j].Featured { - return plugins[i].Featured - } - isFirstPartyI := strings.HasPrefix(plugins[i].Repo, "https://github.com/AvengeMedia") - isFirstPartyJ := strings.HasPrefix(plugins[j].Repo, "https://github.com/AvengeMedia") - if isFirstPartyI != isFirstPartyJ { - return isFirstPartyI - } - return false - }) - return plugins -} - -func FindByIDOrName(idOrName string, plugins []Plugin) *Plugin { - if p, found := utils.Find(plugins, func(p Plugin) bool { return p.ID == idOrName }); found { - return &p - } - if p, found := utils.Find(plugins, func(p Plugin) bool { return p.Name == idOrName }); found { - return &p - } - return nil -} |