summaryrefslogtreecommitdiff
path: root/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/plugins/manager.go
diff options
context:
space:
mode:
Diffstat (limited to 'raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/plugins/manager.go')
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/plugins/manager.go588
1 files changed, 588 insertions, 0 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
new file mode 100644
index 0000000..1c0ddf5
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/plugins/manager.go
@@ -0,0 +1,588 @@
+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)
+}