1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-08-08 14:38:30 -04:00
Files
DankMaterialShell/core/cmd/dms/commands_common.go
T
Artrix b7ec57a534 feat(plugins): add portable plugin lockfile (#2976)
* feat(plugins): add portable plugin lockfile

* plugins/lockfile: allow ssh remotes

---------

Co-authored-by: bbedward <bbedward@gmail.com>
2026-08-07 10:55:57 -04:00

799 lines
21 KiB
Go

package main
import (
"fmt"
"os"
"regexp"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/plugins"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server"
"github.com/AvengeMedia/DankMaterialShell/core/internal/shellembed"
"github.com/spf13/cobra"
)
var versionCmd = &cobra.Command{
Use: "version",
Short: "Show version information",
Run: runVersion,
}
var ipcCmd = &cobra.Command{
Use: "ipc",
Short: "Send IPC commands to running DMS shell",
Long: `Send IPC commands to the running DMS shell.
dms ipc call <target> <function> [args...] invoke a command
dms ipc list list all targets and functions
Full reference: https://danklinux.com/docs/dankmaterialshell/keybinds-ipc`,
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return getShellIPCCompletions(args, toComplete), cobra.ShellCompDirectiveNoFileComp
},
Run: func(cmd *cobra.Command, args []string) {
runShellIPCCommand(args)
},
}
var ipcListCmd = &cobra.Command{
Use: "list",
Short: "List all IPC targets and functions",
Run: func(cmd *cobra.Command, args []string) {
printIPCHelp()
},
}
func init() {
ipcCmd.AddCommand(ipcListCmd)
ipcCmd.SetHelpFunc(func(cmd *cobra.Command, args []string) {
printIPCHelp()
})
pluginsUpdateCmd.Flags().BoolP("all", "a", false, "Update all installed plugins")
pluginsUpdateCmd.Flags().Bool("check", false, "Check for available updates without applying them")
pluginsLockCmd.Flags().StringP("output", "o", "", "Also write the lockfile to this path")
pluginsRestoreCmd.Flags().Bool("prune", false, "Remove managed plugins that are not in the lockfile")
}
var debugSrvCmd = &cobra.Command{
Use: "debug-srv",
Short: "Start the debug server",
Long: "Start the Unix socket debug server for DMS",
Run: func(cmd *cobra.Command, args []string) {
if err := startDebugServer(); err != nil {
log.Fatalf("Error starting debug server: %v", err)
}
},
}
var pluginsCmd = &cobra.Command{
Use: "plugins",
Short: "Manage DMS plugins",
Long: "Browse and manage DMS plugins from the registry",
}
var pluginsBrowseCmd = &cobra.Command{
Use: "browse",
Short: "Browse available plugins",
Long: "Browse available plugins from the DMS plugin registry",
Run: func(cmd *cobra.Command, args []string) {
if err := browsePlugins(); err != nil {
log.Fatalf("Error browsing plugins: %v", err)
}
},
}
var pluginsListCmd = &cobra.Command{
Use: "list",
Short: "List installed plugins",
Long: "List all installed DMS plugins",
Run: func(cmd *cobra.Command, args []string) {
if err := listInstalledPlugins(); err != nil {
log.Fatalf("Error listing plugins: %v", err)
}
},
}
var pluginsInstallCmd = &cobra.Command{
Use: "install <plugin-id>",
Short: "Install a plugin by ID",
Long: "Install a DMS plugin from the registry using its ID (e.g., 'myPlugin'). Plugin names with spaces are also supported for backward compatibility.",
Args: cobra.ExactArgs(1),
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) != 0 {
return nil, cobra.ShellCompDirectiveNoFileComp
}
return getAvailablePluginIDs(), cobra.ShellCompDirectiveNoFileComp
},
Run: func(cmd *cobra.Command, args []string) {
if err := installPluginCLI(args[0]); err != nil {
log.Fatalf("Error installing plugin: %v", err)
}
},
}
var pluginsUninstallCmd = &cobra.Command{
Use: "uninstall <plugin-id>",
Short: "Uninstall a plugin by ID",
Long: "Uninstall a DMS plugin using its ID (e.g., 'myPlugin'). Plugin names with spaces are also supported for backward compatibility.",
Args: cobra.ExactArgs(1),
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) != 0 {
return nil, cobra.ShellCompDirectiveNoFileComp
}
return getInstalledPluginIDs(), cobra.ShellCompDirectiveNoFileComp
},
Run: func(cmd *cobra.Command, args []string) {
if err := uninstallPluginCLI(args[0]); err != nil {
log.Fatalf("Error uninstalling plugin: %v", err)
}
},
}
var pluginsUpdateCmd = &cobra.Command{
Use: "update [plugin-id]",
Short: "Update a plugin by ID, or all plugins",
Long: "Update an installed DMS plugin using its ID (e.g., 'myPlugin'). If --all or -a is specified, all installed plugins will be updated.",
Args: func(cmd *cobra.Command, args []string) error {
updateAll, _ := cmd.Flags().GetBool("all")
if updateAll {
if len(args) > 0 {
return fmt.Errorf("cannot specify plugin ID when using --all/-a")
}
return nil
}
if len(args) != 1 {
return fmt.Errorf("requires exactly 1 arg (plugin ID) or use --all/-a")
}
return nil
},
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) != 0 {
return nil, cobra.ShellCompDirectiveNoFileComp
}
return getInstalledPluginIDs(), cobra.ShellCompDirectiveNoFileComp
},
Run: func(cmd *cobra.Command, args []string) {
checkOnly, _ := cmd.Flags().GetBool("check")
updateAll, _ := cmd.Flags().GetBool("all")
if checkOnly {
if updateAll {
if err := checkAllPluginsCLI(); err != nil {
log.Fatalf("Error checking updates: %v", err)
}
return
}
if err := checkPluginCLI(args[0]); err != nil {
log.Fatalf("Error checking update: %v", err)
}
return
}
if updateAll {
if err := updateAllPluginsCLI(); err != nil {
log.Fatalf("Error updating plugins: %v", err)
}
return
}
if err := updatePluginCLI(args[0]); err != nil {
log.Fatalf("Error updating plugin: %v", err)
}
},
}
var pluginsLockCmd = &cobra.Command{
Use: "lock",
Short: "Record installed plugins and their exact revisions",
Long: "Write a portable plugins.lock.json containing every managed user plugin and its current Git commit.",
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output")
if err := lockPluginsCLI(output); err != nil {
log.Fatalf("Error writing plugin lockfile: %v", err)
}
},
}
var pluginsRestoreCmd = &cobra.Command{
Use: "restore [lockfile]",
Short: "Restore plugins from exact revisions in a lockfile",
Long: "Install or reset all plugins in a portable plugins.lock.json. Existing managed plugins are retained unless --prune is specified.",
Args: cobra.MaximumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
path := ""
if len(args) == 1 {
path = args[0]
}
prune, _ := cmd.Flags().GetBool("prune")
if err := restorePluginsCLI(path, prune); err != nil {
log.Fatalf("Error restoring plugins: %v", err)
}
},
}
func runVersion(cmd *cobra.Command, args []string) {
fmt.Printf("%s\n", formatVersion(Version))
}
// Git builds: dms (git) v0.6.2-XXXX
// Stable releases: dms v0.6.2
func formatVersion(version string) string {
// Arch/Debian/Ubuntu/OpenSUSE git format: 0.6.2+git2264.c5c5ce84
re := regexp.MustCompile(`^([\d.]+)\+git(\d+)\.`)
if matches := re.FindStringSubmatch(version); matches != nil {
return fmt.Sprintf("dms (git) v%s-%s", matches[1], matches[2])
}
// Fedora COPR git format: 0.0.git.2267.d430cae9
re = regexp.MustCompile(`^[\d.]+\.git\.(\d+)\.`)
if matches := re.FindStringSubmatch(version); matches != nil {
baseVersion := getBaseVersion()
return fmt.Sprintf("dms (git) v%s-%s", baseVersion, matches[1])
}
// Stable release format: 0.6.2
re = regexp.MustCompile(`^([\d.]+)$`)
if matches := re.FindStringSubmatch(version); matches != nil {
return fmt.Sprintf("dms v%s", matches[1])
}
return fmt.Sprintf("dms %s", version)
}
var baseVersionRe = regexp.MustCompile(`^([\d.]+)`)
// Installed UI trees, for builds without an embedded UI.
var shellVersionPaths = []string{
"/usr/share/quickshell/dms/VERSION",
"/usr/local/share/quickshell/dms/VERSION",
"/etc/xdg/quickshell/dms/VERSION",
}
func getBaseVersion() string {
if ver := parseBaseVersion(shellembed.Version()); ver != "" {
return ver
}
for _, path := range shellVersionPaths {
content, err := os.ReadFile(path)
if err != nil {
continue
}
if ver := parseBaseVersion(string(content)); ver != "" {
return ver
}
}
return "1.0.2"
}
func parseBaseVersion(raw string) string {
matches := baseVersionRe.FindStringSubmatch(strings.TrimPrefix(strings.TrimSpace(raw), "v"))
if matches == nil {
return ""
}
return matches[1]
}
func startDebugServer() error {
server.CLIVersion = Version
return server.Start(true)
}
func browsePlugins() error {
registry, err := plugins.NewRegistry()
if err != nil {
return fmt.Errorf("failed to create registry: %w", err)
}
manager, err := plugins.NewManager()
if err != nil {
return fmt.Errorf("failed to create manager: %w", err)
}
fmt.Println("Fetching plugin registry...")
pluginList, err := registry.List()
if err != nil {
return fmt.Errorf("failed to list plugins: %w", err)
}
if len(pluginList) == 0 {
fmt.Println("No plugins found in registry.")
return nil
}
feedback := plugins.FetchFeedback()
nameByID := make(map[string]string, len(pluginList))
for _, plugin := range pluginList {
nameByID[plugin.ID] = plugin.Name
}
fmt.Printf("\nAvailable Plugins (%d):\n\n", len(pluginList))
for _, plugin := range pluginList {
installed, _ := manager.IsInstalled(plugin)
installedMarker := ""
if installed {
installedMarker = " [Installed]"
}
fmt.Printf(" %s%s\n", plugin.Name, installedMarker)
fmt.Printf(" ID: %s\n", plugin.ID)
fmt.Printf(" Category: %s\n", plugin.Category)
fmt.Printf(" Author: %s\n", plugin.Author)
fmt.Printf(" Description: %s\n", plugin.Description)
fmt.Printf(" Repository: %s\n", plugin.Repo)
if len(plugin.Capabilities) > 0 {
fmt.Printf(" Capabilities: %s\n", strings.Join(plugin.Capabilities, ", "))
}
if len(plugin.Compositors) > 0 {
fmt.Printf(" Compositors: %s\n", strings.Join(plugin.Compositors, ", "))
}
if len(plugin.Dependencies) > 0 {
fmt.Printf(" Dependencies: %s\n", strings.Join(plugin.Dependencies, ", "))
}
if fb, ok := feedback[plugin.ID]; ok {
fmt.Printf(" Upvotes: %d\n", fb.Upvotes)
if len(fb.Status) > 0 {
fmt.Printf(" Status: %s\n", strings.Join(fb.Status, ", "))
}
if fb.IssueURL != "" {
fmt.Printf(" Discuss: %s\n", fb.IssueURL)
}
if len(fb.Similar) > 0 {
names := make([]string, len(fb.Similar))
for i, id := range fb.Similar {
if name, ok := nameByID[id]; ok {
names[i] = name
} else {
names[i] = id
}
}
fmt.Printf(" Related: %s\n", strings.Join(names, ", "))
}
}
fmt.Println()
}
return nil
}
func listInstalledPlugins() error {
manager, err := plugins.NewManager()
if err != nil {
return fmt.Errorf("failed to create manager: %w", err)
}
registry, err := plugins.NewRegistry()
if err != nil {
return fmt.Errorf("failed to create registry: %w", err)
}
installedNames, err := manager.ListInstalled()
if err != nil {
return fmt.Errorf("failed to list installed plugins: %w", err)
}
if len(installedNames) == 0 {
fmt.Println("No plugins installed.")
return nil
}
allPlugins, err := registry.List()
if err != nil {
return fmt.Errorf("failed to list plugins: %w", err)
}
pluginMap := make(map[string]plugins.Plugin)
for _, p := range allPlugins {
pluginMap[p.ID] = p
}
fmt.Printf("\nInstalled Plugins (%d):\n\n", len(installedNames))
for _, id := range installedNames {
if plugin, ok := pluginMap[id]; ok {
hasUpdateStr := ""
if hasUpdates, _, err := manager.HasUpdates(id, plugin); err == nil && hasUpdates {
hasUpdateStr = " (update available)"
}
fmt.Printf(" %s%s\n", plugin.Name, hasUpdateStr)
fmt.Printf(" ID: %s\n", plugin.ID)
fmt.Printf(" Category: %s\n", plugin.Category)
fmt.Printf(" Author: %s\n", plugin.Author)
fmt.Println()
} else {
fmt.Printf(" %s (not in registry)\n\n", id)
}
}
return nil
}
func installPluginCLI(idOrName string) error {
registry, err := plugins.NewRegistry()
if err != nil {
return fmt.Errorf("failed to create registry: %w", err)
}
manager, err := plugins.NewManager()
if err != nil {
return fmt.Errorf("failed to create manager: %w", err)
}
pluginList, err := registry.List()
if err != nil {
return fmt.Errorf("failed to list plugins: %w", err)
}
// First, try to find by ID (preferred method)
var plugin *plugins.Plugin
for _, p := range pluginList {
if p.ID == idOrName {
plugin = &p
break
}
}
// Fallback to name for backward compatibility
if plugin == nil {
for _, p := range pluginList {
if p.Name == idOrName {
plugin = &p
break
}
}
}
if plugin == nil {
return fmt.Errorf("plugin not found: %s", idOrName)
}
installed, err := manager.IsInstalled(*plugin)
if err != nil {
return fmt.Errorf("failed to check install status: %w", err)
}
if installed {
return fmt.Errorf("plugin already installed: %s", plugin.Name)
}
fmt.Printf("Installing plugin: %s (ID: %s)\n", plugin.Name, plugin.ID)
if err := manager.Install(*plugin); err != nil {
return fmt.Errorf("failed to install plugin: %w", err)
}
fmt.Printf("Plugin installed successfully: %s\n", plugin.Name)
return nil
}
func lockPluginsCLI(outputPath string) error {
manager, err := plugins.NewManager()
if err != nil {
return fmt.Errorf("failed to create manager: %w", err)
}
warnings, err := manager.WriteCurrentLockfile(outputPath)
if err != nil {
return err
}
for _, warning := range warnings {
fmt.Fprintf(os.Stderr, "Warning: %s\n", warning)
}
fmt.Printf("Plugin lockfile written: %s\n", manager.GetLockfilePath())
if outputPath != "" && outputPath != manager.GetLockfilePath() {
fmt.Printf("Plugin lockfile exported: %s\n", outputPath)
}
return nil
}
func restorePluginsCLI(path string, prune bool) error {
manager, err := plugins.NewManager()
if err != nil {
return fmt.Errorf("failed to create manager: %w", err)
}
if err := manager.RestoreFromLockfile(path, prune); err != nil {
return err
}
if path == "" {
path = manager.GetLockfilePath()
}
fmt.Printf("Plugins restored from lockfile: %s\n", path)
return nil
}
func getAvailablePluginIDs() []string {
registry, err := plugins.NewRegistry()
if err != nil {
return nil
}
pluginList, err := registry.List()
if err != nil {
return nil
}
var ids []string
for _, p := range pluginList {
ids = append(ids, p.ID)
}
return ids
}
func getInstalledPluginIDs() []string {
manager, err := plugins.NewManager()
if err != nil {
return nil
}
installed, err := manager.ListInstalled()
if err != nil {
return nil
}
return installed
}
func uninstallPluginCLI(idOrName string) error {
manager, err := plugins.NewManager()
if err != nil {
return fmt.Errorf("failed to create manager: %w", err)
}
registry, err := plugins.NewRegistry()
if err != nil {
return fmt.Errorf("failed to create registry: %w", err)
}
pluginList, _ := registry.List()
plugin := plugins.FindByIDOrName(idOrName, pluginList)
if plugin != nil {
installed, err := manager.IsInstalled(*plugin)
if err != nil {
return fmt.Errorf("failed to check install status: %w", err)
}
if !installed {
return fmt.Errorf("plugin not installed: %s", plugin.Name)
}
fmt.Printf("Uninstalling plugin: %s (ID: %s)\n", plugin.Name, plugin.ID)
if err := manager.Uninstall(*plugin); err != nil {
return fmt.Errorf("failed to uninstall plugin: %w", err)
}
fmt.Printf("Plugin uninstalled successfully: %s\n", plugin.Name)
return nil
}
fmt.Printf("Uninstalling plugin: %s\n", idOrName)
if err := manager.UninstallByIDOrName(idOrName); err != nil {
return err
}
fmt.Printf("Plugin uninstalled successfully: %s\n", idOrName)
return nil
}
func updatePluginCLI(idOrName string) error {
manager, err := plugins.NewManager()
if err != nil {
return fmt.Errorf("failed to create manager: %w", err)
}
registry, err := plugins.NewRegistry()
if err != nil {
return fmt.Errorf("failed to create registry: %w", err)
}
pluginList, _ := registry.List()
plugin := plugins.FindByIDOrName(idOrName, pluginList)
if plugin != nil {
installed, err := manager.IsInstalled(*plugin)
if err != nil {
return fmt.Errorf("failed to check install status: %w", err)
}
if !installed {
return fmt.Errorf("plugin not installed: %s", plugin.Name)
}
fmt.Printf("Updating plugin: %s (ID: %s)\n", plugin.Name, plugin.ID)
if err := manager.Update(*plugin); err != nil {
return fmt.Errorf("failed to update plugin: %w", err)
}
fmt.Printf("Plugin updated successfully: %s\n", plugin.Name)
return nil
}
fmt.Printf("Updating plugin: %s\n", idOrName)
if err := manager.UpdateByIDOrName(idOrName); err != nil {
return err
}
fmt.Printf("Plugin updated successfully: %s\n", idOrName)
return nil
}
func updateAllPluginsCLI() error {
manager, err := plugins.NewManager()
if err != nil {
return fmt.Errorf("failed to create manager: %w", err)
}
registry, err := plugins.NewRegistry()
if err != nil {
return fmt.Errorf("failed to create registry: %w", err)
}
installed, err := manager.ListInstalled()
if err != nil {
return fmt.Errorf("failed to list installed plugins: %w", err)
}
pluginList, _ := registry.List()
var errs []error
for _, pluginID := range installed {
plugin := plugins.FindByIDOrName(pluginID, pluginList)
if plugin != nil {
fmt.Printf("Updating plugin: %s (ID: %s)\n", plugin.Name, plugin.ID)
if err := manager.Update(*plugin); err != nil {
if strings.Contains(err.Error(), "cannot update system plugin") {
fmt.Printf("Skipping system plugin: %s\n", plugin.Name)
} else {
errs = append(errs, fmt.Errorf("failed to update %s: %w", plugin.Name, err))
}
} else {
fmt.Printf("Plugin updated successfully: %s\n", plugin.Name)
}
} else {
fmt.Printf("Updating plugin: %s\n", pluginID)
if err := manager.UpdateByIDOrName(pluginID); err != nil {
if strings.Contains(err.Error(), "cannot update system plugin") {
fmt.Printf("Skipping system plugin: %s\n", pluginID)
} else {
errs = append(errs, fmt.Errorf("failed to update %s: %w", pluginID, err))
}
} else {
fmt.Printf("Plugin updated successfully: %s\n", pluginID)
}
}
}
if len(errs) > 0 {
for _, err := range errs {
fmt.Fprintf(os.Stderr, "%v\n", err)
}
return fmt.Errorf("failed to update some plugins")
}
return nil
}
func checkPluginCLI(idOrName string) error {
manager, err := plugins.NewManager()
if err != nil {
return fmt.Errorf("failed to create manager: %w", err)
}
registry, err := plugins.NewRegistry()
if err != nil {
return fmt.Errorf("failed to create registry: %w", err)
}
pluginList, _ := registry.List()
plugin := plugins.FindByIDOrName(idOrName, pluginList)
if plugin != nil {
installed, err := manager.IsInstalled(*plugin)
if err != nil {
return fmt.Errorf("failed to check install status: %w", err)
}
if !installed {
return fmt.Errorf("plugin not installed: %s", plugin.Name)
}
hasUpdates, _, err := manager.HasUpdates(plugin.ID, *plugin)
if err != nil {
return fmt.Errorf("failed to check updates: %w", err)
}
if hasUpdates {
fmt.Printf("Update available for plugin: %s (ID: %s)\n", plugin.Name, plugin.ID)
} else {
fmt.Printf("Plugin is up to date: %s\n", plugin.Name)
}
return nil
}
dummyPlugin := plugins.Plugin{ID: idOrName}
hasUpdates, _, err := manager.HasUpdates(idOrName, dummyPlugin)
if err != nil {
return fmt.Errorf("failed to check updates: %w", err)
}
if hasUpdates {
fmt.Printf("Update available for plugin: %s\n", idOrName)
} else {
fmt.Printf("Plugin is up to date: %s\n", idOrName)
}
return nil
}
func checkAllPluginsCLI() error {
manager, err := plugins.NewManager()
if err != nil {
return fmt.Errorf("failed to create manager: %w", err)
}
registry, err := plugins.NewRegistry()
if err != nil {
return fmt.Errorf("failed to create registry: %w", err)
}
installed, err := manager.ListInstalled()
if err != nil {
return fmt.Errorf("failed to list installed plugins: %w", err)
}
pluginList, _ := registry.List()
var count int
for _, pluginID := range installed {
plugin := plugins.FindByIDOrName(pluginID, pluginList)
var hasUpdates bool
var name string
if plugin != nil {
name = plugin.Name
hasUpdates, _, _ = manager.HasUpdates(pluginID, *plugin)
} else {
name = pluginID
dummyPlugin := plugins.Plugin{ID: pluginID}
hasUpdates, _, _ = manager.HasUpdates(pluginID, dummyPlugin)
}
if hasUpdates {
fmt.Printf("Update available for plugin: %s (ID: %s)\n", name, pluginID)
count++
}
}
if count > 0 {
fmt.Printf("\nFound %d plugin(s) with available updates.\n", count)
} else {
fmt.Println("All plugins are up to date.")
}
return nil
}
func getCommonCommands() []*cobra.Command {
commands := shellApp.Commands()
return append(commands, []*cobra.Command{
versionCmd,
ipcCmd,
debugSrvCmd,
pluginsCmd,
registryCmd,
dank16Cmd,
brightnessCmd,
dpmsCmd,
keybindsCmd,
greeterCmd,
setupCmd,
colorCmd,
qrCmd,
screenshotCmd,
notifyActionCmd,
notifyCmd,
genericNotifyActionCmd,
matugenCmd,
clipboardCmd,
chromaCmd,
doctorCmd,
configCmd,
dlCmd,
randrCmd,
blurCmd,
trashCmd,
systemCmd,
switchUserCmd,
}...)
}