1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-24 21:42:51 -05:00

core: add slices, paths, exec utils

This commit is contained in:
bbedward
2025-12-09 15:28:19 -05:00
parent e307de83e2
commit aeacf109eb
44 changed files with 931 additions and 625 deletions

View File

@@ -211,45 +211,13 @@ func runBrightnessSet(cmd *cobra.Command, args []string) {
exponential, _ := cmd.Flags().GetBool("exponential") exponential, _ := cmd.Flags().GetBool("exponential")
exponent, _ := cmd.Flags().GetFloat64("exponent") exponent, _ := cmd.Flags().GetFloat64("exponent")
// For backlight/leds devices, try logind backend first (requires D-Bus connection)
parts := strings.SplitN(deviceID, ":", 2) parts := strings.SplitN(deviceID, ":", 2)
if len(parts) == 2 && (parts[0] == "backlight" || parts[0] == "leds") { if len(parts) == 2 && (parts[0] == "backlight" || parts[0] == "leds") {
subsystem := parts[0] if ok := tryLogindBrightness(parts[0], parts[1], deviceID, percent, exponential, exponent); ok {
name := parts[1]
// Initialize backends needed for logind approach
sysfs, err := brightness.NewSysfsBackend()
if err != nil {
log.Debugf("NewSysfsBackend failed: %v", err)
} else {
logind, err := brightness.NewLogindBackend()
if err != nil {
log.Debugf("NewLogindBackend failed: %v", err)
} else {
defer logind.Close()
// Get device info to convert percent to value
dev, err := sysfs.GetDevice(deviceID)
if err == nil {
// Calculate hardware value using the same logic as Manager.setViaSysfsWithLogind
value := sysfs.PercentToValueWithExponent(percent, dev, exponential, exponent)
// Call logind with hardware value
if err := logind.SetBrightness(subsystem, name, uint32(value)); err == nil {
log.Debugf("set %s to %d%% (%d) via logind", deviceID, percent, value)
fmt.Printf("Set %s to %d%%\n", deviceID, percent)
return return
} else {
log.Debugf("logind.SetBrightness failed: %v", err)
}
} else {
log.Debugf("sysfs.GetDeviceByID failed: %v", err)
}
}
} }
} }
// Fallback to direct sysfs (requires write permissions)
sysfs, err := brightness.NewSysfsBackend() sysfs, err := brightness.NewSysfsBackend()
if err == nil { if err == nil {
if err := sysfs.SetBrightnessWithExponent(deviceID, percent, exponential, exponent); err == nil { if err := sysfs.SetBrightnessWithExponent(deviceID, percent, exponential, exponent); err == nil {
@@ -280,6 +248,37 @@ func runBrightnessSet(cmd *cobra.Command, args []string) {
log.Fatalf("Failed to set brightness for device: %s", deviceID) log.Fatalf("Failed to set brightness for device: %s", deviceID)
} }
func tryLogindBrightness(subsystem, name, deviceID string, percent int, exponential bool, exponent float64) bool {
sysfs, err := brightness.NewSysfsBackend()
if err != nil {
log.Debugf("NewSysfsBackend failed: %v", err)
return false
}
logind, err := brightness.NewLogindBackend()
if err != nil {
log.Debugf("NewLogindBackend failed: %v", err)
return false
}
defer logind.Close()
dev, err := sysfs.GetDevice(deviceID)
if err != nil {
log.Debugf("sysfs.GetDeviceByID failed: %v", err)
return false
}
value := sysfs.PercentToValueWithExponent(percent, dev, exponential, exponent)
if err := logind.SetBrightness(subsystem, name, uint32(value)); err != nil {
log.Debugf("logind.SetBrightness failed: %v", err)
return false
}
log.Debugf("set %s to %d%% (%d) via logind", deviceID, percent, value)
fmt.Printf("Set %s to %d%%\n", deviceID, percent)
return true
}
func getBrightnessDevices(includeDDC bool) []string { func getBrightnessDevices(includeDDC bool) []string {
allDevices := getAllBrightnessDevices(includeDDC) allDevices := getAllBrightnessDevices(includeDDC)

View File

@@ -152,6 +152,24 @@ var pluginsUninstallCmd = &cobra.Command{
}, },
} }
var pluginsUpdateCmd = &cobra.Command{
Use: "update <plugin-id>",
Short: "Update a plugin by ID",
Long: "Update an installed DMS plugin using its ID (e.g., 'myPlugin'). Plugin names are also supported.",
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 := updatePluginCLI(args[0]); err != nil {
log.Fatalf("Error updating plugin: %v", err)
}
},
}
func runVersion(cmd *cobra.Command, args []string) { func runVersion(cmd *cobra.Command, args []string) {
printASCII() printASCII()
fmt.Printf("%s\n", formatVersion(Version)) fmt.Printf("%s\n", formatVersion(Version))
@@ -408,39 +426,14 @@ func uninstallPluginCLI(idOrName string) error {
return fmt.Errorf("failed to create registry: %w", err) return fmt.Errorf("failed to create registry: %w", err)
} }
pluginList, err := registry.List() pluginList, _ := registry.List()
if err != nil { plugin := plugins.FindByIDOrName(idOrName, pluginList)
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)
}
if plugin != nil {
installed, err := manager.IsInstalled(*plugin) installed, err := manager.IsInstalled(*plugin)
if err != nil { if err != nil {
return fmt.Errorf("failed to check install status: %w", err) return fmt.Errorf("failed to check install status: %w", err)
} }
if !installed { if !installed {
return fmt.Errorf("plugin not installed: %s", plugin.Name) return fmt.Errorf("plugin not installed: %s", plugin.Name)
} }
@@ -449,11 +442,57 @@ func uninstallPluginCLI(idOrName string) error {
if err := manager.Uninstall(*plugin); err != nil { if err := manager.Uninstall(*plugin); err != nil {
return fmt.Errorf("failed to uninstall plugin: %w", err) return fmt.Errorf("failed to uninstall plugin: %w", err)
} }
fmt.Printf("Plugin uninstalled successfully: %s\n", plugin.Name) fmt.Printf("Plugin uninstalled successfully: %s\n", plugin.Name)
return nil 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 getCommonCommands() []*cobra.Command { func getCommonCommands() []*cobra.Command {
return []*cobra.Command{ return []*cobra.Command{
versionCmd, versionCmd,

View File

@@ -15,6 +15,7 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros" "github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs" "github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
"github.com/AvengeMedia/DankMaterialShell/core/internal/version" "github.com/AvengeMedia/DankMaterialShell/core/internal/version"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@@ -121,10 +122,10 @@ func updateArchLinux() error {
var helper string var helper string
var updateCmd *exec.Cmd var updateCmd *exec.Cmd
if commandExists("yay") { if utils.CommandExists("yay") {
helper = "yay" helper = "yay"
updateCmd = exec.Command("yay", "-S", packageName) updateCmd = exec.Command("yay", "-S", packageName)
} else if commandExists("paru") { } else if utils.CommandExists("paru") {
helper = "paru" helper = "paru"
updateCmd = exec.Command("paru", "-S", packageName) updateCmd = exec.Command("paru", "-S", packageName)
} else { } else {

View File

@@ -10,6 +10,7 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/greeter" "github.com/AvengeMedia/DankMaterialShell/core/internal/greeter"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"golang.org/x/text/cases" "golang.org/x/text/cases"
"golang.org/x/text/language" "golang.org/x/text/language"
@@ -448,7 +449,7 @@ func enableGreeter() error {
fmt.Println("Detecting installed compositors...") fmt.Println("Detecting installed compositors...")
compositors := greeter.DetectCompositors() compositors := greeter.DetectCompositors()
if commandExists("sway") { if utils.CommandExists("sway") {
compositors = append(compositors, "sway") compositors = append(compositors, "sway")
} }

View File

@@ -23,7 +23,7 @@ func init() {
updateCmd.AddCommand(updateCheckCmd) updateCmd.AddCommand(updateCheckCmd)
// Add subcommands to plugins // Add subcommands to plugins
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd) pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
// Add common commands to root // Add common commands to root
rootCmd.AddCommand(getCommonCommands()...) rootCmd.AddCommand(getCommonCommands()...)

View File

@@ -21,7 +21,7 @@ func init() {
greeterCmd.AddCommand(greeterSyncCmd, greeterEnableCmd, greeterStatusCmd) greeterCmd.AddCommand(greeterSyncCmd, greeterEnableCmd, greeterStatusCmd)
// Add subcommands to plugins // Add subcommands to plugins
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd) pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
// Add common commands to root // Add common commands to root
rootCmd.AddCommand(getCommonCommands()...) rootCmd.AddCommand(getCommonCommands()...)

View File

@@ -104,7 +104,6 @@ func getAllDMSPIDs() []int {
continue continue
} }
// Check if the child process is still alive
proc, err := os.FindProcess(childPID) proc, err := os.FindProcess(childPID)
if err != nil { if err != nil {
os.Remove(pidFile) os.Remove(pidFile)
@@ -112,18 +111,15 @@ func getAllDMSPIDs() []int {
} }
if err := proc.Signal(syscall.Signal(0)); err != nil { if err := proc.Signal(syscall.Signal(0)); err != nil {
// Process is dead, remove stale PID file
os.Remove(pidFile) os.Remove(pidFile)
continue continue
} }
pids = append(pids, childPID) pids = append(pids, childPID)
// Also get the parent PID from the filename
parentPIDStr := strings.TrimPrefix(entry.Name(), "danklinux-") parentPIDStr := strings.TrimPrefix(entry.Name(), "danklinux-")
parentPIDStr = strings.TrimSuffix(parentPIDStr, ".pid") parentPIDStr = strings.TrimSuffix(parentPIDStr, ".pid")
if parentPID, err := strconv.Atoi(parentPIDStr); err == nil { if parentPID, err := strconv.Atoi(parentPIDStr); err == nil {
// Check if parent is still alive
if parentProc, err := os.FindProcess(parentPID); err == nil { if parentProc, err := os.FindProcess(parentPID); err == nil {
if err := parentProc.Signal(syscall.Signal(0)); err == nil { if err := parentProc.Signal(syscall.Signal(0)); err == nil {
pids = append(pids, parentPID) pids = append(pids, parentPID)
@@ -225,7 +221,6 @@ func runShellInteractive(session bool) {
return return
} }
// All other signals: clean shutdown
log.Infof("\nReceived signal %v, shutting down...", sig) log.Infof("\nReceived signal %v, shutting down...", sig)
cancel() cancel()
cmd.Process.Signal(syscall.SIGTERM) cmd.Process.Signal(syscall.SIGTERM)
@@ -282,7 +277,6 @@ func restartShell() {
} }
func killShell() { func killShell() {
// Get all tracked DMS PIDs from PID files
pids := getAllDMSPIDs() pids := getAllDMSPIDs()
if len(pids) == 0 { if len(pids) == 0 {
@@ -293,14 +287,12 @@ func killShell() {
currentPid := os.Getpid() currentPid := os.Getpid()
uniquePids := make(map[int]bool) uniquePids := make(map[int]bool)
// Deduplicate and filter out current process
for _, pid := range pids { for _, pid := range pids {
if pid != currentPid { if pid != currentPid {
uniquePids[pid] = true uniquePids[pid] = true
} }
} }
// Kill all tracked processes
for pid := range uniquePids { for pid := range uniquePids {
proc, err := os.FindProcess(pid) proc, err := os.FindProcess(pid)
if err != nil { if err != nil {
@@ -308,7 +300,6 @@ func killShell() {
continue continue
} }
// Check if process is still alive before killing
if err := proc.Signal(syscall.Signal(0)); err != nil { if err := proc.Signal(syscall.Signal(0)); err != nil {
continue continue
} }
@@ -320,7 +311,6 @@ func killShell() {
} }
} }
// Clean up any remaining PID files
dir := getRuntimeDir() dir := getRuntimeDir()
entries, err := os.ReadDir(dir) entries, err := os.ReadDir(dir)
if err != nil { if err != nil {
@@ -337,7 +327,6 @@ func killShell() {
func runShellDaemon(session bool) { func runShellDaemon(session bool) {
isSessionManaged = session isSessionManaged = session
// Check if this is the daemon child process by looking for the hidden flag
isDaemonChild := false isDaemonChild := false
for _, arg := range os.Args { for _, arg := range os.Args {
if arg == "--daemon-child" { if arg == "--daemon-child" {

View File

@@ -6,12 +6,6 @@ import (
"strings" "strings"
) )
func commandExists(cmd string) bool {
_, err := exec.LookPath(cmd)
return err == nil
}
// findCommandPath returns the absolute path to a command in PATH
func findCommandPath(cmd string) (string, error) { func findCommandPath(cmd string) (string, error) {
path, err := exec.LookPath(cmd) path, err := exec.LookPath(cmd)
if err != nil { if err != nil {

View File

@@ -5,19 +5,14 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
) )
// LocateDMSConfig searches for DMS installation following XDG Base Directory specification
func LocateDMSConfig() (string, error) { func LocateDMSConfig() (string, error) {
var primaryPaths []string var primaryPaths []string
configHome := os.Getenv("XDG_CONFIG_HOME") configHome := utils.XDGConfigHome()
if configHome == "" {
if homeDir, err := os.UserHomeDir(); err == nil {
configHome = filepath.Join(homeDir, ".config")
}
}
if configHome != "" { if configHome != "" {
primaryPaths = append(primaryPaths, filepath.Join(configHome, "quickshell", "dms")) primaryPaths = append(primaryPaths, filepath.Join(configHome, "quickshell", "dms"))
} }

View File

@@ -113,13 +113,14 @@ func RGBToHSV(rgb RGB) HSV {
delta := max - min delta := max - min
var h float64 var h float64
if delta == 0 { switch {
case delta == 0:
h = 0 h = 0
} else if max == rgb.R { case max == rgb.R:
h = math.Mod((rgb.G-rgb.B)/delta, 6.0) / 6.0 h = math.Mod((rgb.G-rgb.B)/delta, 6.0) / 6.0
} else if max == rgb.G { case max == rgb.G:
h = ((rgb.B-rgb.R)/delta + 2.0) / 6.0 h = ((rgb.B-rgb.R)/delta + 2.0) / 6.0
} else { default:
h = ((rgb.R-rgb.G)/delta + 4.0) / 6.0 h = ((rgb.R-rgb.G)/delta + 4.0) / 6.0
} }

View File

@@ -112,31 +112,11 @@ func (a *ArchDistribution) DetectDependenciesWithTerminal(ctx context.Context, w
} }
func (a *ArchDistribution) detectXDGPortal() deps.Dependency { func (a *ArchDistribution) detectXDGPortal() deps.Dependency {
status := deps.StatusMissing return a.detectPackage("xdg-desktop-portal-gtk", "Desktop integration portal for GTK", a.packageInstalled("xdg-desktop-portal-gtk"))
if a.packageInstalled("xdg-desktop-portal-gtk") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "xdg-desktop-portal-gtk",
Status: status,
Description: "Desktop integration portal for GTK",
Required: true,
}
} }
func (a *ArchDistribution) detectAccountsService() deps.Dependency { func (a *ArchDistribution) detectAccountsService() deps.Dependency {
status := deps.StatusMissing return a.detectPackage("accountsservice", "D-Bus interface for user account query and manipulation", a.packageInstalled("accountsservice"))
if a.packageInstalled("accountsservice") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "accountsservice",
Status: status,
Description: "D-Bus interface for user account query and manipulation",
Required: true,
}
} }
func (a *ArchDistribution) packageInstalled(pkg string) bool { func (a *ArchDistribution) packageInstalled(pkg string) bool {

View File

@@ -76,47 +76,42 @@ func ExecSudoCommand(ctx context.Context, sudoPassword string, command string) *
return exec.CommandContext(ctx, "bash", "-c", cmdStr) return exec.CommandContext(ctx, "bash", "-c", cmdStr)
} }
// Common dependency detection methods func (b *BaseDistribution) detectCommand(name, description string) deps.Dependency {
func (b *BaseDistribution) detectGit() deps.Dependency {
status := deps.StatusMissing status := deps.StatusMissing
if b.commandExists("git") { if b.commandExists(name) {
status = deps.StatusInstalled status = deps.StatusInstalled
} }
return deps.Dependency{ return deps.Dependency{
Name: "git", Name: name,
Status: status, Status: status,
Description: "Version control system", Description: description,
Required: true, Required: true,
} }
} }
func (b *BaseDistribution) detectPackage(name, description string, installed bool) deps.Dependency {
status := deps.StatusMissing
if installed {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: name,
Status: status,
Description: description,
Required: true,
}
}
func (b *BaseDistribution) detectGit() deps.Dependency {
return b.detectCommand("git", "Version control system")
}
func (b *BaseDistribution) detectMatugen() deps.Dependency { func (b *BaseDistribution) detectMatugen() deps.Dependency {
status := deps.StatusMissing return b.detectCommand("matugen", "Material Design color generation tool")
if b.commandExists("matugen") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "matugen",
Status: status,
Description: "Material Design color generation tool",
Required: true,
}
} }
func (b *BaseDistribution) detectDgop() deps.Dependency { func (b *BaseDistribution) detectDgop() deps.Dependency {
status := deps.StatusMissing return b.detectCommand("dgop", "Desktop portal management tool")
if b.commandExists("dgop") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "dgop",
Status: status,
Description: "Desktop portal management tool",
Required: true,
}
} }
func (b *BaseDistribution) detectDMS() deps.Dependency { func (b *BaseDistribution) detectDMS() deps.Dependency {

View File

@@ -7,6 +7,7 @@ import (
"testing" "testing"
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps" "github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
) )
func TestBaseDistribution_detectDMS_NotInstalled(t *testing.T) { func TestBaseDistribution_detectDMS_NotInstalled(t *testing.T) {
@@ -36,7 +37,7 @@ func TestBaseDistribution_detectDMS_NotInstalled(t *testing.T) {
} }
func TestBaseDistribution_detectDMS_Installed(t *testing.T) { func TestBaseDistribution_detectDMS_Installed(t *testing.T) {
if !commandExists("git") { if !utils.CommandExists("git") {
t.Skip("git not available") t.Skip("git not available")
} }
@@ -80,7 +81,7 @@ func TestBaseDistribution_detectDMS_Installed(t *testing.T) {
} }
func TestBaseDistribution_detectDMS_NeedsUpdate(t *testing.T) { func TestBaseDistribution_detectDMS_NeedsUpdate(t *testing.T) {
if !commandExists("git") { if !utils.CommandExists("git") {
t.Skip("git not available") t.Skip("git not available")
} }
@@ -164,11 +165,6 @@ func TestBaseDistribution_NewBaseDistribution(t *testing.T) {
} }
} }
func commandExists(cmd string) bool {
_, err := exec.LookPath(cmd)
return err == nil
}
func TestBaseDistribution_versionCompare(t *testing.T) { func TestBaseDistribution_versionCompare(t *testing.T) {
logChan := make(chan string, 10) logChan := make(chan string, 10)
defer close(logChan) defer close(logChan)

View File

@@ -75,45 +75,15 @@ func (d *DebianDistribution) DetectDependenciesWithTerminal(ctx context.Context,
} }
func (d *DebianDistribution) detectXDGPortal() deps.Dependency { func (d *DebianDistribution) detectXDGPortal() deps.Dependency {
status := deps.StatusMissing return d.detectPackage("xdg-desktop-portal-gtk", "Desktop integration portal for GTK", d.packageInstalled("xdg-desktop-portal-gtk"))
if d.packageInstalled("xdg-desktop-portal-gtk") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "xdg-desktop-portal-gtk",
Status: status,
Description: "Desktop integration portal for GTK",
Required: true,
}
} }
func (d *DebianDistribution) detectXwaylandSatellite() deps.Dependency { func (d *DebianDistribution) detectXwaylandSatellite() deps.Dependency {
status := deps.StatusMissing return d.detectCommand("xwayland-satellite", "Xwayland support")
if d.commandExists("xwayland-satellite") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "xwayland-satellite",
Status: status,
Description: "Xwayland support",
Required: true,
}
} }
func (d *DebianDistribution) detectAccountsService() deps.Dependency { func (d *DebianDistribution) detectAccountsService() deps.Dependency {
status := deps.StatusMissing return d.detectPackage("accountsservice", "D-Bus interface for user account query and manipulation", d.packageInstalled("accountsservice"))
if d.packageInstalled("accountsservice") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "accountsservice",
Status: status,
Description: "D-Bus interface for user account query and manipulation",
Required: true,
}
} }
func (d *DebianDistribution) packageInstalled(pkg string) bool { func (d *DebianDistribution) packageInstalled(pkg string) bool {

View File

@@ -97,17 +97,7 @@ func (f *FedoraDistribution) DetectDependenciesWithTerminal(ctx context.Context,
} }
func (f *FedoraDistribution) detectXDGPortal() deps.Dependency { func (f *FedoraDistribution) detectXDGPortal() deps.Dependency {
status := deps.StatusMissing return f.detectPackage("xdg-desktop-portal-gtk", "Desktop integration portal for GTK", f.packageInstalled("xdg-desktop-portal-gtk"))
if f.packageInstalled("xdg-desktop-portal-gtk") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "xdg-desktop-portal-gtk",
Status: status,
Description: "Desktop integration portal for GTK",
Required: true,
}
} }
func (f *FedoraDistribution) packageInstalled(pkg string) bool { func (f *FedoraDistribution) packageInstalled(pkg string) bool {

View File

@@ -113,45 +113,15 @@ func (g *GentooDistribution) DetectDependenciesWithTerminal(ctx context.Context,
} }
func (g *GentooDistribution) detectXDGPortal() deps.Dependency { func (g *GentooDistribution) detectXDGPortal() deps.Dependency {
status := deps.StatusMissing return g.detectPackage("xdg-desktop-portal-gtk", "Desktop integration portal for GTK", g.packageInstalled("sys-apps/xdg-desktop-portal-gtk"))
if g.packageInstalled("sys-apps/xdg-desktop-portal-gtk") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "xdg-desktop-portal-gtk",
Status: status,
Description: "Desktop integration portal for GTK",
Required: true,
}
} }
func (g *GentooDistribution) detectXwaylandSatellite() deps.Dependency { func (g *GentooDistribution) detectXwaylandSatellite() deps.Dependency {
status := deps.StatusMissing return g.detectPackage("xwayland-satellite", "Xwayland support", g.packageInstalled("gui-apps/xwayland-satellite"))
if g.packageInstalled("gui-apps/xwayland-satellite") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "xwayland-satellite",
Status: status,
Description: "Xwayland support",
Required: true,
}
} }
func (g *GentooDistribution) detectAccountsService() deps.Dependency { func (g *GentooDistribution) detectAccountsService() deps.Dependency {
status := deps.StatusMissing return g.detectPackage("accountsservice", "D-Bus interface for user account query and manipulation", g.packageInstalled("sys-apps/accountsservice"))
if g.packageInstalled("sys-apps/accountsservice") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "accountsservice",
Status: status,
Description: "D-Bus interface for user account query and manipulation",
Required: true,
}
} }
func (g *GentooDistribution) packageInstalled(pkg string) bool { func (g *GentooDistribution) packageInstalled(pkg string) bool {

View File

@@ -87,17 +87,7 @@ func (o *OpenSUSEDistribution) DetectDependenciesWithTerminal(ctx context.Contex
} }
func (o *OpenSUSEDistribution) detectXDGPortal() deps.Dependency { func (o *OpenSUSEDistribution) detectXDGPortal() deps.Dependency {
status := deps.StatusMissing return o.detectPackage("xdg-desktop-portal-gtk", "Desktop integration portal for GTK", o.packageInstalled("xdg-desktop-portal-gtk"))
if o.packageInstalled("xdg-desktop-portal-gtk") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "xdg-desktop-portal-gtk",
Status: status,
Description: "Desktop integration portal for GTK",
Required: true,
}
} }
func (o *OpenSUSEDistribution) packageInstalled(pkg string) bool { func (o *OpenSUSEDistribution) packageInstalled(pkg string) bool {

View File

@@ -85,45 +85,15 @@ func (u *UbuntuDistribution) DetectDependenciesWithTerminal(ctx context.Context,
} }
func (u *UbuntuDistribution) detectXDGPortal() deps.Dependency { func (u *UbuntuDistribution) detectXDGPortal() deps.Dependency {
status := deps.StatusMissing return u.detectPackage("xdg-desktop-portal-gtk", "Desktop integration portal for GTK", u.packageInstalled("xdg-desktop-portal-gtk"))
if u.packageInstalled("xdg-desktop-portal-gtk") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "xdg-desktop-portal-gtk",
Status: status,
Description: "Desktop integration portal for GTK",
Required: true,
}
} }
func (u *UbuntuDistribution) detectXwaylandSatellite() deps.Dependency { func (u *UbuntuDistribution) detectXwaylandSatellite() deps.Dependency {
status := deps.StatusMissing return u.detectCommand("xwayland-satellite", "Xwayland support")
if u.commandExists("xwayland-satellite") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "xwayland-satellite",
Status: status,
Description: "Xwayland support",
Required: true,
}
} }
func (u *UbuntuDistribution) detectAccountsService() deps.Dependency { func (u *UbuntuDistribution) detectAccountsService() deps.Dependency {
status := deps.StatusMissing return u.detectPackage("accountsservice", "D-Bus interface for user account query and manipulation", u.packageInstalled("accountsservice"))
if u.packageInstalled("accountsservice") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "accountsservice",
Status: status,
Description: "D-Bus interface for user account query and manipulation",
Required: true,
}
} }
func (u *UbuntuDistribution) packageInstalled(pkg string) bool { func (u *UbuntuDistribution) packageInstalled(pkg string) bool {

View File

@@ -286,6 +286,13 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, loadInstalledPlugins return m, loadInstalledPlugins
} }
return m, nil return m, nil
case pluginUpdatedMsg:
if msg.err != nil {
m.installedPluginsError = msg.err.Error()
} else {
m.installedPluginsError = ""
}
return m, nil
case pluginInstalledMsg: case pluginInstalledMsg:
if msg.err != nil { if msg.err != nil {
m.pluginsError = msg.err.Error() m.pluginsError = msg.err.Error()

View File

@@ -75,14 +75,13 @@ type MenuItem struct {
func NewModel(version string) Model { func NewModel(version string) Model {
detector, _ := NewDetector() detector, _ := NewDetector()
dependencies := detector.GetInstalledComponents()
// Use the proper detection method for both window managers var dependencies []DependencyInfo
hyprlandInstalled, niriInstalled, err := detector.GetWindowManagerStatus() var hyprlandInstalled, niriInstalled bool
if err != nil {
// Fallback to false if detection fails if detector != nil {
hyprlandInstalled = false dependencies = detector.GetInstalledComponents()
niriInstalled = false hyprlandInstalled, niriInstalled, _ = detector.GetWindowManagerStatus()
} }
m := Model{ m := Model{
@@ -201,6 +200,13 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, loadInstalledPlugins return m, loadInstalledPlugins
} }
return m, nil return m, nil
case pluginUpdatedMsg:
if msg.err != nil {
m.installedPluginsError = msg.err.Error()
} else {
m.installedPluginsError = ""
}
return m, nil
case pluginInstalledMsg: case pluginInstalledMsg:
if msg.err != nil { if msg.err != nil {
m.pluginsError = msg.err.Error() m.pluginsError = msg.err.Error()

View File

@@ -227,6 +227,11 @@ func (m Model) updatePluginInstalledDetail(msg tea.KeyMsg) (tea.Model, tea.Cmd)
plugin := m.installedPluginsList[m.selectedInstalledIndex] plugin := m.installedPluginsList[m.selectedInstalledIndex]
return m, uninstallPlugin(plugin) return m, uninstallPlugin(plugin)
} }
case "p":
if m.selectedInstalledIndex < len(m.installedPluginsList) {
plugin := m.installedPluginsList[m.selectedInstalledIndex]
return m, updatePlugin(plugin)
}
} }
return m, nil return m, nil
} }
@@ -246,6 +251,11 @@ type pluginInstalledMsg struct {
err error err error
} }
type pluginUpdatedMsg struct {
pluginName string
err error
}
func loadInstalledPlugins() tea.Msg { func loadInstalledPlugins() tea.Msg {
manager, err := plugins.NewManager() manager, err := plugins.NewManager()
if err != nil { if err != nil {
@@ -337,3 +347,31 @@ func uninstallPlugin(plugin pluginInfo) tea.Cmd {
return pluginUninstalledMsg{pluginName: plugin.Name} return pluginUninstalledMsg{pluginName: plugin.Name}
} }
} }
func updatePlugin(plugin pluginInfo) tea.Cmd {
return func() tea.Msg {
manager, err := plugins.NewManager()
if err != nil {
return pluginUpdatedMsg{pluginName: plugin.Name, err: err}
}
p := plugins.Plugin{
ID: plugin.ID,
Name: plugin.Name,
Category: plugin.Category,
Author: plugin.Author,
Description: plugin.Description,
Repo: plugin.Repo,
Path: plugin.Path,
Capabilities: plugin.Capabilities,
Compositors: plugin.Compositors,
Dependencies: plugin.Dependencies,
}
if err := manager.Update(p); err != nil {
return pluginUpdatedMsg{pluginName: plugin.Name, err: err}
}
return pluginUpdatedMsg{pluginName: plugin.Name}
}
}

View File

@@ -11,6 +11,7 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/config" "github.com/AvengeMedia/DankMaterialShell/core/internal/config"
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros" "github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
) )
// DetectDMSPath checks for DMS installation following XDG Base Directory specification // DetectDMSPath checks for DMS installation following XDG Base Directory specification
@@ -22,10 +23,10 @@ func DetectDMSPath() (string, error) {
func DetectCompositors() []string { func DetectCompositors() []string {
var compositors []string var compositors []string
if commandExists("niri") { if utils.CommandExists("niri") {
compositors = append(compositors, "niri") compositors = append(compositors, "niri")
} }
if commandExists("Hyprland") { if utils.CommandExists("Hyprland") {
compositors = append(compositors, "Hyprland") compositors = append(compositors, "Hyprland")
} }
@@ -62,7 +63,7 @@ func PromptCompositorChoice(compositors []string) (string, error) {
// EnsureGreetdInstalled checks if greetd is installed and installs it if not // EnsureGreetdInstalled checks if greetd is installed and installs it if not
func EnsureGreetdInstalled(logFunc func(string), sudoPassword string) error { func EnsureGreetdInstalled(logFunc func(string), sudoPassword string) error {
if commandExists("greetd") { if utils.CommandExists("greetd") {
logFunc("✓ greetd is already installed") logFunc("✓ greetd is already installed")
return nil return nil
} }
@@ -144,7 +145,7 @@ func EnsureGreetdInstalled(logFunc func(string), sudoPassword string) error {
// CopyGreeterFiles installs the dms-greeter wrapper and sets up cache directory // CopyGreeterFiles installs the dms-greeter wrapper and sets up cache directory
func CopyGreeterFiles(dmsPath, compositor string, logFunc func(string), sudoPassword string) error { func CopyGreeterFiles(dmsPath, compositor string, logFunc func(string), sudoPassword string) error {
// Check if dms-greeter is already in PATH // Check if dms-greeter is already in PATH
if commandExists("dms-greeter") { if utils.CommandExists("dms-greeter") {
logFunc("✓ dms-greeter wrapper already installed") logFunc("✓ dms-greeter wrapper already installed")
} else { } else {
// Install the wrapper script // Install the wrapper script
@@ -204,7 +205,7 @@ func CopyGreeterFiles(dmsPath, compositor string, logFunc func(string), sudoPass
// SetupParentDirectoryACLs sets ACLs on parent directories to allow traversal // SetupParentDirectoryACLs sets ACLs on parent directories to allow traversal
func SetupParentDirectoryACLs(logFunc func(string), sudoPassword string) error { func SetupParentDirectoryACLs(logFunc func(string), sudoPassword string) error {
if !commandExists("setfacl") { if !utils.CommandExists("setfacl") {
logFunc("⚠ Warning: setfacl command not found. ACL support may not be available on this filesystem.") logFunc("⚠ Warning: setfacl command not found. ACL support may not be available on this filesystem.")
logFunc(" If theme sync doesn't work, you may need to install acl package:") logFunc(" If theme sync doesn't work, you may need to install acl package:")
logFunc(" - Fedora/RHEL: sudo dnf install acl") logFunc(" - Fedora/RHEL: sudo dnf install acl")
@@ -419,7 +420,7 @@ user = "greeter"
// Determine wrapper command path // Determine wrapper command path
wrapperCmd := "dms-greeter" wrapperCmd := "dms-greeter"
if !commandExists("dms-greeter") { if !utils.CommandExists("dms-greeter") {
wrapperCmd = "/usr/local/bin/dms-greeter" wrapperCmd = "/usr/local/bin/dms-greeter"
} }
@@ -486,8 +487,3 @@ func runSudoCmd(sudoPassword string, command string, args ...string) error {
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
return cmd.Run() return cmd.Run()
} }
func commandExists(cmd string) bool {
_, err := exec.LookPath(cmd)
return err == nil
}

View File

@@ -5,6 +5,8 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
) )
type DiscoveryConfig struct { type DiscoveryConfig struct {
@@ -14,13 +16,7 @@ type DiscoveryConfig struct {
func DefaultDiscoveryConfig() *DiscoveryConfig { func DefaultDiscoveryConfig() *DiscoveryConfig {
var searchPaths []string var searchPaths []string
configHome := os.Getenv("XDG_CONFIG_HOME") configHome := utils.XDGConfigHome()
if configHome == "" {
if homeDir, err := os.UserHomeDir(); err == nil {
configHome = filepath.Join(homeDir, ".config")
}
}
if configHome != "" { if configHome != "" {
searchPaths = append(searchPaths, filepath.Join(configHome, "DankMaterialShell", "cheatsheets")) searchPaths = append(searchPaths, filepath.Join(configHome, "DankMaterialShell", "cheatsheets"))
} }
@@ -43,7 +39,7 @@ func (d *DiscoveryConfig) FindJSONFiles() ([]string, error) {
var files []string var files []string
for _, searchPath := range d.SearchPaths { for _, searchPath := range d.SearchPaths {
expandedPath, err := expandPath(searchPath) expandedPath, err := utils.ExpandPath(searchPath)
if err != nil { if err != nil {
continue continue
} }
@@ -74,20 +70,6 @@ func (d *DiscoveryConfig) FindJSONFiles() ([]string, error) {
return files, nil return files, nil
} }
func expandPath(path string) (string, error) {
expandedPath := os.ExpandEnv(path)
if strings.HasPrefix(expandedPath, "~") {
home, err := os.UserHomeDir()
if err != nil {
return "", err
}
expandedPath = filepath.Join(home, expandedPath[1:])
}
return filepath.Clean(expandedPath), nil
}
type JSONProviderFactory func(filePath string) (Provider, error) type JSONProviderFactory func(filePath string) (Provider, error)
var jsonProviderFactory JSONProviderFactory var jsonProviderFactory JSONProviderFactory

View File

@@ -4,6 +4,8 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"testing" "testing"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
) )
func TestDefaultDiscoveryConfig(t *testing.T) { func TestDefaultDiscoveryConfig(t *testing.T) {
@@ -272,13 +274,13 @@ func TestExpandPathInDiscovery(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
result, err := expandPath(tt.input) result, err := utils.ExpandPath(tt.input)
if err != nil { if err != nil {
t.Fatalf("expandPath failed: %v", err) t.Fatalf("expandPath failed: %v", err)
} }
if result != tt.expected { if result != tt.expected {
t.Errorf("expandPath(%q) = %q, want %q", tt.input, result, tt.expected) t.Errorf("utils.ExpandPath(%q) = %q, want %q", tt.input, result, tt.expected)
} }
}) })
} }

View File

@@ -5,6 +5,8 @@ import (
"path/filepath" "path/filepath"
"regexp" "regexp"
"strings" "strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
) )
const ( const (
@@ -42,15 +44,10 @@ func NewHyprlandParser() *HyprlandParser {
} }
func (p *HyprlandParser) ReadContent(directory string) error { func (p *HyprlandParser) ReadContent(directory string) error {
expandedDir := os.ExpandEnv(directory) expandedDir, err := utils.ExpandPath(directory)
expandedDir = filepath.Clean(expandedDir)
if strings.HasPrefix(expandedDir, "~") {
home, err := os.UserHomeDir()
if err != nil { if err != nil {
return err return err
} }
expandedDir = filepath.Join(home, expandedDir[1:])
}
info, err := os.Stat(expandedDir) info, err := os.Stat(expandedDir)
if err != nil { if err != nil {

View File

@@ -5,9 +5,9 @@ import (
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds" "github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
) )
type JSONFileProvider struct { type JSONFileProvider struct {
@@ -20,7 +20,7 @@ func NewJSONFileProvider(filePath string) (*JSONFileProvider, error) {
return nil, fmt.Errorf("file path cannot be empty") return nil, fmt.Errorf("file path cannot be empty")
} }
expandedPath, err := expandPath(filePath) expandedPath, err := utils.ExpandPath(filePath)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to expand path: %w", err) return nil, fmt.Errorf("failed to expand path: %w", err)
} }
@@ -117,17 +117,3 @@ func (j *JSONFileProvider) GetCheatSheet() (*keybinds.CheatSheet, error) {
Binds: categorizedBinds, Binds: categorizedBinds,
}, nil }, nil
} }
func expandPath(path string) (string, error) {
expandedPath := os.ExpandEnv(path)
if strings.HasPrefix(expandedPath, "~") {
home, err := os.UserHomeDir()
if err != nil {
return "", err
}
expandedPath = filepath.Join(home, expandedPath[1:])
}
return filepath.Clean(expandedPath), nil
}

View File

@@ -4,6 +4,8 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"testing" "testing"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
) )
func TestNewJSONFileProvider(t *testing.T) { func TestNewJSONFileProvider(t *testing.T) {
@@ -266,13 +268,13 @@ func TestExpandPath(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
result, err := expandPath(tt.input) result, err := utils.ExpandPath(tt.input)
if err != nil { if err != nil {
t.Fatalf("expandPath failed: %v", err) t.Fatalf("expandPath failed: %v", err)
} }
if result != tt.expected { if result != tt.expected {
t.Errorf("expandPath(%q) = %q, want %q", tt.input, result, tt.expected) t.Errorf("utils.ExpandPath(%q) = %q, want %q", tt.input, result, tt.expected)
} }
}) })
} }

View File

@@ -5,6 +5,8 @@ import (
"path/filepath" "path/filepath"
"regexp" "regexp"
"strings" "strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
) )
const ( const (
@@ -34,15 +36,10 @@ func NewMangoWCParser() *MangoWCParser {
} }
func (p *MangoWCParser) ReadContent(path string) error { func (p *MangoWCParser) ReadContent(path string) error {
expandedPath := os.ExpandEnv(path) expandedPath, err := utils.ExpandPath(path)
expandedPath = filepath.Clean(expandedPath)
if strings.HasPrefix(expandedPath, "~") {
home, err := os.UserHomeDir()
if err != nil { if err != nil {
return err return err
} }
expandedPath = filepath.Join(home, expandedPath[1:])
}
info, err := os.Stat(expandedPath) info, err := os.Stat(expandedPath)
if err != nil { if err != nil {

View File

@@ -9,6 +9,7 @@ import (
"strings" "strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds" "github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
"github.com/sblinch/kdl-go" "github.com/sblinch/kdl-go"
"github.com/sblinch/kdl-go/document" "github.com/sblinch/kdl-go/document"
) )
@@ -29,15 +30,7 @@ func NewNiriProvider(configDir string) *NiriProvider {
} }
func defaultNiriConfigDir() string { func defaultNiriConfigDir() string {
if configHome := os.Getenv("XDG_CONFIG_HOME"); configHome != "" { return filepath.Join(utils.XDGConfigHome(), "niri")
return filepath.Join(configHome, "niri")
}
home, err := os.UserHomeDir()
if err != nil {
return ""
}
return filepath.Join(home, ".config", "niri")
} }
func (n *NiriProvider) Name() string { func (n *NiriProvider) Name() string {

View File

@@ -5,6 +5,8 @@ import (
"path/filepath" "path/filepath"
"regexp" "regexp"
"strings" "strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
) )
const ( const (
@@ -42,15 +44,10 @@ func NewSwayParser() *SwayParser {
} }
func (p *SwayParser) ReadContent(path string) error { func (p *SwayParser) ReadContent(path string) error {
expandedPath := os.ExpandEnv(path) expandedPath, err := utils.ExpandPath(path)
expandedPath = filepath.Clean(expandedPath)
if strings.HasPrefix(expandedPath, "~") {
home, err := os.UserHomeDir()
if err != nil { if err != nil {
return err return err
} }
expandedPath = filepath.Join(home, expandedPath[1:])
}
info, err := os.Stat(expandedPath) info, err := os.Stat(expandedPath)
if err != nil { if err != nil {

View File

@@ -13,6 +13,7 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/dank16" "github.com/AvengeMedia/DankMaterialShell/core/internal/dank16"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
) )
var ( var (
@@ -277,7 +278,7 @@ func appendConfig(opts *Options, cfgFile *os.File, checkCmd, fileName string) {
if _, err := os.Stat(configPath); err != nil { if _, err := os.Stat(configPath); err != nil {
return return
} }
if checkCmd != "skip" && !commandExists(checkCmd) { if checkCmd != "skip" && !utils.CommandExists(checkCmd) {
return return
} }
data, err := os.ReadFile(configPath) data, err := os.ReadFile(configPath)
@@ -293,7 +294,7 @@ func appendTerminalConfig(opts *Options, cfgFile *os.File, tmpDir, checkCmd, fil
if _, err := os.Stat(configPath); err != nil { if _, err := os.Stat(configPath); err != nil {
return return
} }
if checkCmd != "skip" && !commandExists(checkCmd) { if checkCmd != "skip" && !utils.CommandExists(checkCmd) {
return return
} }
data, err := os.ReadFile(configPath) data, err := os.ReadFile(configPath)
@@ -390,11 +391,6 @@ func extractTOMLSection(content, startMarker, endMarker string) string {
return content[startIdx : startIdx+endIdx] return content[startIdx : startIdx+endIdx]
} }
func commandExists(name string) bool {
_, err := exec.LookPath(name)
return err == nil
}
func checkMatugenVersion() { func checkMatugenVersion() {
matugenVersionOnce.Do(func() { matugenVersionOnce.Do(func() {
cmd := exec.Command("matugen", "--version") cmd := exec.Command("matugen", "--version")

View File

@@ -9,6 +9,7 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
"github.com/spf13/afero" "github.com/spf13/afero"
) )
@@ -32,33 +33,70 @@ func NewManagerWithFs(fs afero.Fs) (*Manager, error) {
} }
func getPluginsDir() string { func getPluginsDir() string {
configHome := os.Getenv("XDG_CONFIG_HOME") return filepath.Join(utils.XDGConfigHome(), "DankMaterialShell", "plugins")
if configHome == "" {
homeDir, err := os.UserHomeDir()
if err != nil {
return filepath.Join(os.TempDir(), "DankMaterialShell", "plugins")
}
configHome = filepath.Join(homeDir, ".config")
}
return filepath.Join(configHome, "DankMaterialShell", "plugins")
} }
func (m *Manager) IsInstalled(plugin Plugin) (bool, error) { func (m *Manager) IsInstalled(plugin Plugin) (bool, error) {
pluginPath := filepath.Join(m.pluginsDir, plugin.ID) path, err := m.findInstalledPath(plugin.ID)
exists, err := afero.DirExists(m.fs, pluginPath)
if err != nil { if err != nil {
return false, err return false, err
} }
if exists { return path != "", nil
return true, nil
} }
systemPluginPath := filepath.Join("/etc/xdg/quickshell/dms-plugins", plugin.ID) func (m *Manager) findInstalledPath(pluginID string) (string, error) {
systemExists, err := afero.DirExists(m.fs, systemPluginPath) // Check user plugins directory
path, err := m.findInDir(m.pluginsDir, pluginID)
if err != nil { if err != nil {
return false, err return "", err
} }
return systemExists, nil 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 { func (m *Manager) Install(plugin Plugin) error {
@@ -151,25 +189,19 @@ func (m *Manager) createSymlink(source, dest string) error {
} }
func (m *Manager) Update(plugin Plugin) error { func (m *Manager) Update(plugin Plugin) error {
pluginPath := filepath.Join(m.pluginsDir, plugin.ID) pluginPath, err := m.findInstalledPath(plugin.ID)
exists, err := afero.DirExists(m.fs, pluginPath)
if err != nil { if err != nil {
return fmt.Errorf("failed to check if plugin exists: %w", err) return fmt.Errorf("failed to find plugin: %w", err)
} }
if !exists { if pluginPath == "" {
systemPluginPath := filepath.Join("/etc/xdg/quickshell/dms-plugins", plugin.ID)
systemExists, err := afero.DirExists(m.fs, systemPluginPath)
if err != nil {
return fmt.Errorf("failed to check if plugin exists: %w", err)
}
if systemExists {
return fmt.Errorf("cannot update system plugin: %s", plugin.Name)
}
return fmt.Errorf("plugin not installed: %s", plugin.Name) 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" metaPath := pluginPath + ".meta"
metaExists, err := afero.Exists(m.fs, metaPath) metaExists, err := afero.Exists(m.fs, metaPath)
if err != nil { if err != nil {
@@ -209,25 +241,19 @@ func (m *Manager) Update(plugin Plugin) error {
} }
func (m *Manager) Uninstall(plugin Plugin) error { func (m *Manager) Uninstall(plugin Plugin) error {
pluginPath := filepath.Join(m.pluginsDir, plugin.ID) pluginPath, err := m.findInstalledPath(plugin.ID)
exists, err := afero.DirExists(m.fs, pluginPath)
if err != nil { if err != nil {
return fmt.Errorf("failed to check if plugin exists: %w", err) return fmt.Errorf("failed to find plugin: %w", err)
} }
if !exists { if pluginPath == "" {
systemPluginPath := filepath.Join("/etc/xdg/quickshell/dms-plugins", plugin.ID)
systemExists, err := afero.DirExists(m.fs, systemPluginPath)
if err != nil {
return fmt.Errorf("failed to check if plugin exists: %w", err)
}
if systemExists {
return fmt.Errorf("cannot uninstall system plugin: %s", plugin.Name)
}
return fmt.Errorf("plugin not installed: %s", plugin.Name) 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" metaPath := pluginPath + ".meta"
metaExists, err := afero.Exists(m.fs, metaPath) metaExists, err := afero.Exists(m.fs, metaPath)
if err != nil { if err != nil {
@@ -369,47 +395,174 @@ func (m *Manager) ListInstalled() ([]string, error) {
// getPluginID reads the plugin.json file and returns the plugin ID // getPluginID reads the plugin.json file and returns the plugin ID
func (m *Manager) getPluginID(pluginPath string) string { 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") manifestPath := filepath.Join(pluginPath, "plugin.json")
data, err := afero.ReadFile(m.fs, manifestPath) data, err := afero.ReadFile(m.fs, manifestPath)
if err != nil { if err != nil {
return "" return nil
} }
var manifest struct { var manifest pluginManifest
ID string `json:"id"`
}
if err := json.Unmarshal(data, &manifest); err != nil { if err := json.Unmarshal(data, &manifest); err != nil {
return "" return nil
} }
return manifest.ID return &manifest
}
type pluginManifest struct {
ID string `json:"id"`
Name string `json:"name"`
} }
func (m *Manager) GetPluginsDir() string { func (m *Manager) GetPluginsDir() string {
return m.pluginsDir 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) { func (m *Manager) HasUpdates(pluginID string, plugin Plugin) (bool, error) {
pluginPath := filepath.Join(m.pluginsDir, pluginID) pluginPath, err := m.findInstalledPath(pluginID)
exists, err := afero.DirExists(m.fs, pluginPath)
if err != nil { if err != nil {
return false, fmt.Errorf("failed to check if plugin exists: %w", err) return false, fmt.Errorf("failed to find plugin: %w", err)
} }
if !exists { if pluginPath == "" {
systemPluginPath := filepath.Join("/etc/xdg/quickshell/dms-plugins", pluginID)
systemExists, err := afero.DirExists(m.fs, systemPluginPath)
if err != nil {
return false, fmt.Errorf("failed to check system plugin: %w", err)
}
if systemExists {
return false, nil
}
return false, fmt.Errorf("plugin not installed: %s", pluginID) return false, fmt.Errorf("plugin not installed: %s", pluginID)
} }
// Check if there's a .meta file (plugin installed from a monorepo) if strings.HasPrefix(pluginPath, "/etc/xdg/quickshell/dms-plugins") {
return false, nil
}
metaPath := pluginPath + ".meta" metaPath := pluginPath + ".meta"
metaExists, err := afero.Exists(m.fs, metaPath) metaExists, err := afero.Exists(m.fs, metaPath)
if err != nil { if err != nil {

View File

@@ -3,6 +3,8 @@ package plugins
import ( import (
"sort" "sort"
"strings" "strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
) )
func FuzzySearch(query string, plugins []Plugin) []Plugin { func FuzzySearch(query string, plugins []Plugin) []Plugin {
@@ -11,18 +13,12 @@ func FuzzySearch(query string, plugins []Plugin) []Plugin {
} }
queryLower := strings.ToLower(query) queryLower := strings.ToLower(query)
var results []Plugin return utils.Filter(plugins, func(p Plugin) bool {
return fuzzyMatch(queryLower, strings.ToLower(p.Name)) ||
for _, plugin := range plugins { fuzzyMatch(queryLower, strings.ToLower(p.Category)) ||
if fuzzyMatch(queryLower, strings.ToLower(plugin.Name)) || fuzzyMatch(queryLower, strings.ToLower(p.Description)) ||
fuzzyMatch(queryLower, strings.ToLower(plugin.Category)) || fuzzyMatch(queryLower, strings.ToLower(p.Author))
fuzzyMatch(queryLower, strings.ToLower(plugin.Description)) || })
fuzzyMatch(queryLower, strings.ToLower(plugin.Author)) {
results = append(results, plugin)
}
}
return results
} }
func fuzzyMatch(query, text string) bool { func fuzzyMatch(query, text string) bool {
@@ -39,57 +35,34 @@ func FilterByCategory(category string, plugins []Plugin) []Plugin {
if category == "" { if category == "" {
return plugins return plugins
} }
var results []Plugin
categoryLower := strings.ToLower(category) categoryLower := strings.ToLower(category)
return utils.Filter(plugins, func(p Plugin) bool {
for _, plugin := range plugins { return strings.ToLower(p.Category) == categoryLower
if strings.ToLower(plugin.Category) == categoryLower { })
results = append(results, plugin)
}
}
return results
} }
func FilterByCompositor(compositor string, plugins []Plugin) []Plugin { func FilterByCompositor(compositor string, plugins []Plugin) []Plugin {
if compositor == "" { if compositor == "" {
return plugins return plugins
} }
var results []Plugin
compositorLower := strings.ToLower(compositor) compositorLower := strings.ToLower(compositor)
return utils.Filter(plugins, func(p Plugin) bool {
for _, plugin := range plugins { return utils.Any(p.Compositors, func(c string) bool {
for _, comp := range plugin.Compositors { return strings.ToLower(c) == compositorLower
if strings.ToLower(comp) == compositorLower { })
results = append(results, plugin) })
break
}
}
}
return results
} }
func FilterByCapability(capability string, plugins []Plugin) []Plugin { func FilterByCapability(capability string, plugins []Plugin) []Plugin {
if capability == "" { if capability == "" {
return plugins return plugins
} }
var results []Plugin
capabilityLower := strings.ToLower(capability) capabilityLower := strings.ToLower(capability)
return utils.Filter(plugins, func(p Plugin) bool {
for _, plugin := range plugins { return utils.Any(p.Capabilities, func(c string) bool {
for _, cap := range plugin.Capabilities { return strings.ToLower(c) == capabilityLower
if strings.ToLower(cap) == capabilityLower { })
results = append(results, plugin) })
break
}
}
}
return results
} }
func SortByFirstParty(plugins []Plugin) []Plugin { func SortByFirstParty(plugins []Plugin) []Plugin {
@@ -103,3 +76,13 @@ func SortByFirstParty(plugins []Plugin) []Plugin {
}) })
return plugins 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
}

View File

@@ -9,7 +9,10 @@ import (
"io" "io"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"time" "time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
) )
func BufferToImage(buf *ShmBuffer) *image.RGBA { func BufferToImage(buf *ShmBuffer) *image.RGBA {
@@ -116,68 +119,28 @@ func GetOutputDir() string {
} }
func getXDGPicturesDir() string { func getXDGPicturesDir() string {
configDir := os.Getenv("XDG_CONFIG_HOME") userDirsFile := filepath.Join(utils.XDGConfigHome(), "user-dirs.dirs")
if configDir == "" {
home := os.Getenv("HOME")
if home == "" {
return ""
}
configDir = filepath.Join(home, ".config")
}
userDirsFile := filepath.Join(configDir, "user-dirs.dirs")
data, err := os.ReadFile(userDirsFile) data, err := os.ReadFile(userDirsFile)
if err != nil { if err != nil {
return "" return ""
} }
for _, line := range splitLines(string(data)) { for _, line := range strings.Split(string(data), "\n") {
if len(line) == 0 || line[0] == '#' { if len(line) == 0 || line[0] == '#' {
continue continue
} }
const prefix = "XDG_PICTURES_DIR=" const prefix = "XDG_PICTURES_DIR="
if len(line) > len(prefix) && line[:len(prefix)] == prefix { if !strings.HasPrefix(line, prefix) {
path := line[len(prefix):] continue
path = trimQuotes(path)
path = expandHome(path)
return path
}
} }
path := strings.Trim(line[len(prefix):], "\"")
expanded, err := utils.ExpandPath(path)
if err != nil {
return "" return ""
} }
return expanded
func splitLines(s string) []string {
var lines []string
start := 0
for i := 0; i < len(s); i++ {
if s[i] == '\n' {
lines = append(lines, s[start:i])
start = i + 1
} }
} return ""
if start < len(s) {
lines = append(lines, s[start:])
}
return lines
}
func trimQuotes(s string) string {
if len(s) >= 2 && s[0] == '"' && s[len(s)-1] == '"' {
return s[1 : len(s)-1]
}
return s
}
func expandHome(path string) string {
if len(path) >= 5 && path[:5] == "$HOME" {
home := os.Getenv("HOME")
return home + path[5:]
}
if len(path) >= 1 && path[0] == '~' {
home := os.Getenv("HOME")
return home + path[1:]
}
return path
} }
func WriteToFile(buf *ShmBuffer, path string, format Format, quality int) error { func WriteToFile(buf *ShmBuffer, path string, format Format, quality int) error {

View File

@@ -6,6 +6,8 @@ import (
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
) )
type ThemeColors struct { type ThemeColors struct {
@@ -72,15 +74,7 @@ func loadColorsFile() *ColorScheme {
} }
func getColorsFilePath() string { func getColorsFilePath() string {
cacheDir := os.Getenv("XDG_CACHE_HOME") return filepath.Join(utils.XDGCacheHome(), "DankMaterialShell", "dms-colors.json")
if cacheDir == "" {
home := os.Getenv("HOME")
if home == "" {
return ""
}
cacheDir = filepath.Join(home, ".cache")
}
return filepath.Join(cacheDir, "DankMaterialShell", "dms-colors.json")
} }
func isLightMode() bool { func isLightMode() bool {

View File

@@ -15,52 +15,49 @@ func HandleUninstall(conn net.Conn, req models.Request) {
return return
} }
registry, err := plugins.NewRegistry()
if err != nil {
models.RespondError(conn, req.ID, fmt.Sprintf("failed to create registry: %v", err))
return
}
pluginList, err := registry.List()
if err != nil {
models.RespondError(conn, req.ID, fmt.Sprintf("failed to list plugins: %v", err))
return
}
var plugin *plugins.Plugin
for _, p := range pluginList {
if p.Name == name {
plugin = &p
break
}
}
if plugin == nil {
models.RespondError(conn, req.ID, fmt.Sprintf("plugin not found: %s", name))
return
}
manager, err := plugins.NewManager() manager, err := plugins.NewManager()
if err != nil { if err != nil {
models.RespondError(conn, req.ID, fmt.Sprintf("failed to create manager: %v", err)) models.RespondError(conn, req.ID, fmt.Sprintf("failed to create manager: %v", err))
return return
} }
// First try to find in registry (by name or ID)
registry, err := plugins.NewRegistry()
if err != nil {
models.RespondError(conn, req.ID, fmt.Sprintf("failed to create registry: %v", err))
return
}
pluginList, _ := registry.List()
plugin := plugins.FindByIDOrName(name, pluginList)
// If found in registry, use that
if plugin != nil {
installed, err := manager.IsInstalled(*plugin) installed, err := manager.IsInstalled(*plugin)
if err != nil { if err != nil {
models.RespondError(conn, req.ID, fmt.Sprintf("failed to check if plugin is installed: %v", err)) models.RespondError(conn, req.ID, fmt.Sprintf("failed to check if plugin is installed: %v", err))
return return
} }
if !installed { if !installed {
models.RespondError(conn, req.ID, fmt.Sprintf("plugin not installed: %s", name)) models.RespondError(conn, req.ID, fmt.Sprintf("plugin not installed: %s", name))
return return
} }
if err := manager.Uninstall(*plugin); err != nil { if err := manager.Uninstall(*plugin); err != nil {
models.RespondError(conn, req.ID, fmt.Sprintf("failed to uninstall plugin: %v", err)) models.RespondError(conn, req.ID, fmt.Sprintf("failed to uninstall plugin: %v", err))
return return
} }
models.Respond(conn, req.ID, SuccessResult{
Success: true,
Message: fmt.Sprintf("plugin uninstalled: %s", plugin.Name),
})
return
}
// Not in registry - try to find and uninstall from installed plugins directly
if err := manager.UninstallByIDOrName(name); err != nil {
models.RespondError(conn, req.ID, fmt.Sprintf("plugin not found: %s", name))
return
}
models.Respond(conn, req.ID, SuccessResult{ models.Respond(conn, req.ID, SuccessResult{
Success: true, Success: true,

View File

@@ -15,52 +15,47 @@ func HandleUpdate(conn net.Conn, req models.Request) {
return return
} }
registry, err := plugins.NewRegistry()
if err != nil {
models.RespondError(conn, req.ID, fmt.Sprintf("failed to create registry: %v", err))
return
}
pluginList, err := registry.List()
if err != nil {
models.RespondError(conn, req.ID, fmt.Sprintf("failed to list plugins: %v", err))
return
}
var plugin *plugins.Plugin
for _, p := range pluginList {
if p.Name == name {
plugin = &p
break
}
}
if plugin == nil {
models.RespondError(conn, req.ID, fmt.Sprintf("plugin not found: %s", name))
return
}
manager, err := plugins.NewManager() manager, err := plugins.NewManager()
if err != nil { if err != nil {
models.RespondError(conn, req.ID, fmt.Sprintf("failed to create manager: %v", err)) models.RespondError(conn, req.ID, fmt.Sprintf("failed to create manager: %v", err))
return return
} }
registry, err := plugins.NewRegistry()
if err != nil {
models.RespondError(conn, req.ID, fmt.Sprintf("failed to create registry: %v", err))
return
}
pluginList, _ := registry.List()
plugin := plugins.FindByIDOrName(name, pluginList)
if plugin != nil {
installed, err := manager.IsInstalled(*plugin) installed, err := manager.IsInstalled(*plugin)
if err != nil { if err != nil {
models.RespondError(conn, req.ID, fmt.Sprintf("failed to check if plugin is installed: %v", err)) models.RespondError(conn, req.ID, fmt.Sprintf("failed to check if plugin is installed: %v", err))
return return
} }
if !installed { if !installed {
models.RespondError(conn, req.ID, fmt.Sprintf("plugin not installed: %s", name)) models.RespondError(conn, req.ID, fmt.Sprintf("plugin not installed: %s", name))
return return
} }
if err := manager.Update(*plugin); err != nil { if err := manager.Update(*plugin); err != nil {
models.RespondError(conn, req.ID, fmt.Sprintf("failed to update plugin: %v", err)) models.RespondError(conn, req.ID, fmt.Sprintf("failed to update plugin: %v", err))
return return
} }
models.Respond(conn, req.ID, SuccessResult{
Success: true,
Message: fmt.Sprintf("plugin updated: %s", plugin.Name),
})
return
}
// Not in registry - try to update from installed plugins directly
if err := manager.UpdateByIDOrName(name); err != nil {
models.RespondError(conn, req.ID, fmt.Sprintf("plugin not found: %s", name))
return
}
models.Respond(conn, req.ID, SuccessResult{ models.Respond(conn, req.ID, SuccessResult{
Success: true, Success: true,

View File

@@ -0,0 +1,8 @@
package utils
import "os/exec"
func CommandExists(cmd string) bool {
_, err := exec.LookPath(cmd)
return err == nil
}

View File

@@ -0,0 +1,52 @@
package utils
import (
"os"
"path/filepath"
"strings"
)
func ExpandPath(path string) (string, error) {
expanded := os.ExpandEnv(path)
expanded = filepath.Clean(expanded)
if strings.HasPrefix(expanded, "~") {
home, err := os.UserHomeDir()
if err != nil {
return "", err
}
expanded = filepath.Join(home, expanded[1:])
}
return expanded, nil
}
func XDGConfigHome() string {
if configHome := os.Getenv("XDG_CONFIG_HOME"); configHome != "" {
return configHome
}
if home, err := os.UserHomeDir(); err == nil {
return filepath.Join(home, ".config")
}
return filepath.Join(os.TempDir(), ".config")
}
func XDGCacheHome() string {
if cacheHome := os.Getenv("XDG_CACHE_HOME"); cacheHome != "" {
return cacheHome
}
if home, err := os.UserHomeDir(); err == nil {
return filepath.Join(home, ".cache")
}
return filepath.Join(os.TempDir(), ".cache")
}
func XDGDataHome() string {
if dataHome := os.Getenv("XDG_DATA_HOME"); dataHome != "" {
return dataHome
}
if home, err := os.UserHomeDir(); err == nil {
return filepath.Join(home, ".local", "share")
}
return filepath.Join(os.TempDir(), ".local", "share")
}

View File

@@ -0,0 +1,106 @@
package utils
import (
"os"
"path/filepath"
"testing"
)
func TestExpandPathTilde(t *testing.T) {
home, err := os.UserHomeDir()
if err != nil {
t.Skip("no home directory")
}
result, err := ExpandPath("~/test")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
expected := filepath.Join(home, "test")
if result != expected {
t.Errorf("expected %s, got %s", expected, result)
}
}
func TestExpandPathEnvVar(t *testing.T) {
t.Setenv("TEST_PATH_VAR", "/custom/path")
result, err := ExpandPath("$TEST_PATH_VAR/subdir")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result != "/custom/path/subdir" {
t.Errorf("expected /custom/path/subdir, got %s", result)
}
}
func TestExpandPathAbsolute(t *testing.T) {
result, err := ExpandPath("/absolute/path")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result != "/absolute/path" {
t.Errorf("expected /absolute/path, got %s", result)
}
}
func TestXDGConfigHomeDefault(t *testing.T) {
t.Setenv("XDG_CONFIG_HOME", "")
home, err := os.UserHomeDir()
if err != nil {
t.Skip("no home directory")
}
result := XDGConfigHome()
expected := filepath.Join(home, ".config")
if result != expected {
t.Errorf("expected %s, got %s", expected, result)
}
}
func TestXDGConfigHomeCustom(t *testing.T) {
t.Setenv("XDG_CONFIG_HOME", "/custom/config")
result := XDGConfigHome()
if result != "/custom/config" {
t.Errorf("expected /custom/config, got %s", result)
}
}
func TestXDGCacheHomeDefault(t *testing.T) {
t.Setenv("XDG_CACHE_HOME", "")
home, err := os.UserHomeDir()
if err != nil {
t.Skip("no home directory")
}
result := XDGCacheHome()
expected := filepath.Join(home, ".cache")
if result != expected {
t.Errorf("expected %s, got %s", expected, result)
}
}
func TestXDGCacheHomeCustom(t *testing.T) {
t.Setenv("XDG_CACHE_HOME", "/custom/cache")
result := XDGCacheHome()
if result != "/custom/cache" {
t.Errorf("expected /custom/cache, got %s", result)
}
}
func TestXDGDataHomeDefault(t *testing.T) {
t.Setenv("XDG_DATA_HOME", "")
home, err := os.UserHomeDir()
if err != nil {
t.Skip("no home directory")
}
result := XDGDataHome()
expected := filepath.Join(home, ".local", "share")
if result != expected {
t.Errorf("expected %s, got %s", expected, result)
}
}
func TestXDGDataHomeCustom(t *testing.T) {
t.Setenv("XDG_DATA_HOME", "/custom/data")
result := XDGDataHome()
if result != "/custom/data" {
t.Errorf("expected /custom/data, got %s", result)
}
}

View File

@@ -0,0 +1,56 @@
package utils
func Filter[T any](items []T, predicate func(T) bool) []T {
var result []T
for _, item := range items {
if predicate(item) {
result = append(result, item)
}
}
return result
}
func Find[T any](items []T, predicate func(T) bool) (T, bool) {
for _, item := range items {
if predicate(item) {
return item, true
}
}
var zero T
return zero, false
}
func Map[T, U any](items []T, transform func(T) U) []U {
result := make([]U, len(items))
for i, item := range items {
result[i] = transform(item)
}
return result
}
func Contains[T comparable](items []T, target T) bool {
for _, item := range items {
if item == target {
return true
}
}
return false
}
func Any[T any](items []T, predicate func(T) bool) bool {
for _, item := range items {
if predicate(item) {
return true
}
}
return false
}
func All[T any](items []T, predicate func(T) bool) bool {
for _, item := range items {
if !predicate(item) {
return false
}
}
return true
}

View File

@@ -0,0 +1,72 @@
package utils
import (
"testing"
)
func TestFilter(t *testing.T) {
nums := []int{1, 2, 3, 4, 5}
evens := Filter(nums, func(n int) bool { return n%2 == 0 })
if len(evens) != 2 || evens[0] != 2 || evens[1] != 4 {
t.Errorf("expected [2, 4], got %v", evens)
}
}
func TestFilterEmpty(t *testing.T) {
result := Filter([]int{1, 2, 3}, func(n int) bool { return n > 10 })
if len(result) != 0 {
t.Errorf("expected empty slice, got %v", result)
}
}
func TestFind(t *testing.T) {
nums := []int{1, 2, 3, 4, 5}
val, found := Find(nums, func(n int) bool { return n == 3 })
if !found || val != 3 {
t.Errorf("expected 3, got %v (found=%v)", val, found)
}
}
func TestFindNotFound(t *testing.T) {
nums := []int{1, 2, 3}
val, found := Find(nums, func(n int) bool { return n == 99 })
if found || val != 0 {
t.Errorf("expected zero value not found, got %v (found=%v)", val, found)
}
}
func TestMap(t *testing.T) {
nums := []int{1, 2, 3}
doubled := Map(nums, func(n int) int { return n * 2 })
if len(doubled) != 3 || doubled[0] != 2 || doubled[1] != 4 || doubled[2] != 6 {
t.Errorf("expected [2, 4, 6], got %v", doubled)
}
}
func TestMapTypeConversion(t *testing.T) {
nums := []int{1, 2, 3}
strs := Map(nums, func(n int) string { return string(rune('a' + n - 1)) })
if strs[0] != "a" || strs[1] != "b" || strs[2] != "c" {
t.Errorf("expected [a, b, c], got %v", strs)
}
}
func TestContains(t *testing.T) {
nums := []int{1, 2, 3}
if !Contains(nums, 2) {
t.Error("expected to contain 2")
}
if Contains(nums, 99) {
t.Error("expected not to contain 99")
}
}
func TestAny(t *testing.T) {
nums := []int{1, 2, 3, 4, 5}
if !Any(nums, func(n int) bool { return n > 4 }) {
t.Error("expected any > 4")
}
if Any(nums, func(n int) bool { return n > 10 }) {
t.Error("expected none > 10")
}
}

View File

@@ -7,6 +7,7 @@ import (
"testing" "testing"
mocks_version "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/version" mocks_version "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/version"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
) )
func TestCompareVersions(t *testing.T) { func TestCompareVersions(t *testing.T) {
@@ -150,7 +151,7 @@ func TestGetCurrentDMSVersion_NotInstalled(t *testing.T) {
} }
func TestGetCurrentDMSVersion_GitTag(t *testing.T) { func TestGetCurrentDMSVersion_GitTag(t *testing.T) {
if !commandExists("git") { if !utils.CommandExists("git") {
t.Skip("git not available") t.Skip("git not available")
} }
@@ -183,7 +184,7 @@ func TestGetCurrentDMSVersion_GitTag(t *testing.T) {
} }
func TestGetCurrentDMSVersion_GitBranch(t *testing.T) { func TestGetCurrentDMSVersion_GitBranch(t *testing.T) {
if !commandExists("git") { if !utils.CommandExists("git") {
t.Skip("git not available") t.Skip("git not available")
} }
@@ -314,11 +315,6 @@ func TestVersionInfo_HasUpdate_Tag(t *testing.T) {
} }
} }
func commandExists(cmd string) bool {
_, err := exec.LookPath(cmd)
return err == nil
}
func TestGetLatestDMSVersion_FallbackParsing(t *testing.T) { func TestGetLatestDMSVersion_FallbackParsing(t *testing.T) {
jsonResponse := `{ jsonResponse := `{
"tag_name": "v0.1.17", "tag_name": "v0.1.17",

View File

@@ -890,18 +890,52 @@ Item {
id: rowLayout id: rowLayout
Row { Row {
spacing: 4 spacing: 4
visible: loadedIcons.length > 0 || SettingsData.showWorkspaceIndex visible: loadedIcons.length > 0 || SettingsData.showWorkspaceIndex || loadedHasIcon
StyledText {
topPadding: 2 Item {
rightPadding: isActive ? 4 : 0 visible: loadedHasIcon && loadedIconData?.type === "icon"
visible: SettingsData.showWorkspaceIndex width: wsIcon.width + (isActive && loadedIcons.length > 0 ? 4 : 0)
text: { height: 18
return root.getWorkspaceIndex(modelData);
DankIcon {
id: wsIcon
anchors.verticalCenter: parent.verticalCenter
name: loadedIconData?.value ?? ""
size: Theme.barTextSize(barThickness, barConfig?.fontScale)
color: (isActive || isUrgent) ? Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) : isPlaceholder ? Theme.surfaceTextAlpha : Theme.surfaceTextMedium
weight: (isActive && !isPlaceholder) ? 500 : 400
} }
}
Item {
visible: loadedHasIcon && loadedIconData?.type === "text"
width: wsText.implicitWidth + (isActive && loadedIcons.length > 0 ? 4 : 0)
height: 18
StyledText {
id: wsText
anchors.verticalCenter: parent.verticalCenter
text: loadedIconData?.value ?? ""
color: (isActive || isUrgent) ? Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) : isPlaceholder ? Theme.surfaceTextAlpha : Theme.surfaceTextMedium color: (isActive || isUrgent) ? Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) : isPlaceholder ? Theme.surfaceTextAlpha : Theme.surfaceTextMedium
font.pixelSize: Theme.barTextSize(barThickness, barConfig?.fontScale) font.pixelSize: Theme.barTextSize(barThickness, barConfig?.fontScale)
font.weight: (isActive && !isPlaceholder) ? Font.DemiBold : Font.Normal font.weight: (isActive && !isPlaceholder) ? Font.DemiBold : Font.Normal
} }
}
Item {
visible: SettingsData.showWorkspaceIndex && !loadedHasIcon
width: wsIndexText.implicitWidth + (isActive && loadedIcons.length > 0 ? 4 : 0)
height: 18
StyledText {
id: wsIndexText
anchors.verticalCenter: parent.verticalCenter
text: root.getWorkspaceIndex(modelData)
color: (isActive || isUrgent) ? Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) : isPlaceholder ? Theme.surfaceTextAlpha : Theme.surfaceTextMedium
font.pixelSize: Theme.barTextSize(barThickness, barConfig?.fontScale)
font.weight: (isActive && !isPlaceholder) ? Font.DemiBold : Font.Normal
}
}
Repeater { Repeater {
model: ScriptModel { model: ScriptModel {
@@ -973,7 +1007,25 @@ Item {
id: columnLayout id: columnLayout
Column { Column {
spacing: 4 spacing: 4
visible: loadedIcons.length > 0 visible: loadedIcons.length > 0 || loadedHasIcon
DankIcon {
visible: loadedHasIcon && loadedIconData?.type === "icon"
anchors.horizontalCenter: parent.horizontalCenter
name: loadedIconData?.value ?? ""
size: Theme.barTextSize(barThickness, barConfig?.fontScale)
color: (isActive || isUrgent) ? Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) : isPlaceholder ? Theme.surfaceTextAlpha : Theme.surfaceTextMedium
weight: (isActive && !isPlaceholder) ? 500 : 400
}
StyledText {
visible: loadedHasIcon && loadedIconData?.type === "text"
anchors.horizontalCenter: parent.horizontalCenter
text: loadedIconData?.value ?? ""
color: (isActive || isUrgent) ? Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) : isPlaceholder ? Theme.surfaceTextAlpha : Theme.surfaceTextMedium
font.pixelSize: Theme.barTextSize(barThickness, barConfig?.fontScale)
font.weight: (isActive && !isPlaceholder) ? Font.DemiBold : Font.Normal
}
Repeater { Repeater {
model: ScriptModel { model: ScriptModel {