//go:build linux package installer import ( "fmt" "os" "os/exec" "path/filepath" "strings" "gitgud.io/mike/mpv-manager/internal/assets" "gitgud.io/mike/mpv-manager/pkg/config" "gitgud.io/mike/mpv-manager/pkg/constants" "gitgud.io/mike/mpv-manager/pkg/log" "gitgud.io/mike/mpv-manager/pkg/platform" ) type LinuxInstaller struct { *Installer Platform *platform.Platform } func (li *LinuxInstaller) getPrivilegePrefix() []string { return GetPrivilegePrefix() } func NewLinuxInstaller(installer *Installer, p *platform.Platform) *LinuxInstaller { return &LinuxInstaller{ Installer: installer, Platform: p, } } func (li *LinuxInstaller) GetAvailableMethods(filterInstalled bool) []InstallMethod { methods := []InstallMethod{} if li.Platform.IsImmutableDistro() { if filterInstalled && config.IsAppInstalled("Celluloid") { } else { methods = append(methods, InstallMethod{ ID: constants.MethodCelluloidFlatpak, Name: "Celluloid", Description: "Modern MPV frontend via Flatpak from Flathub", Recommended: true, Priority: 1, Icon: constants.MethodIcon[constants.MethodCelluloidFlatpak], Logo: constants.MethodLogo[constants.MethodCelluloidFlatpak], Type: constants.MethodType[constants.MethodCelluloidFlatpak], AppType: constants.MethodAppType[constants.MethodCelluloidFlatpak], Homepage: constants.MethodHomepage[constants.MethodCelluloidFlatpak], }) } if filterInstalled && config.IsAppInstalled("MPV") { } else { methods = append(methods, InstallMethod{ ID: constants.MethodMPVFlatpak, Name: "MPV", Description: "Official MPV via Flatpak from Flathub", Recommended: true, Priority: 2, Icon: constants.MethodIcon[constants.MethodMPVFlatpak], Logo: constants.MethodLogo[constants.MethodMPVFlatpak], Type: constants.MethodType[constants.MethodMPVFlatpak], AppType: constants.MethodAppType[constants.MethodMPVFlatpak], Homepage: constants.MethodHomepage[constants.MethodMPVFlatpak], }) } } else { if filterInstalled && config.IsAppInstalled("Celluloid") { } else { methods = append(methods, InstallMethod{ ID: constants.MethodCelluloidPackage, Name: "Celluloid", Description: "Install via package manager", Recommended: true, Priority: 1, Icon: constants.MethodIcon[constants.MethodCelluloidPackage], Logo: constants.MethodLogo[constants.MethodCelluloidPackage], Type: constants.MethodType[constants.MethodCelluloidPackage], AppType: constants.MethodAppType[constants.MethodCelluloidPackage], Homepage: constants.MethodHomepage[constants.MethodCelluloidPackage], }) } if filterInstalled && config.IsAppInstalled("Celluloid") { } else { methods = append(methods, InstallMethod{ ID: constants.MethodCelluloidFlatpak, Name: "Celluloid", Description: "Modern MPV frontend via Flatpak from Flathub", Recommended: true, Priority: 2, Icon: constants.MethodIcon[constants.MethodCelluloidFlatpak], Logo: constants.MethodLogo[constants.MethodCelluloidFlatpak], Type: constants.MethodType[constants.MethodCelluloidFlatpak], AppType: constants.MethodAppType[constants.MethodCelluloidFlatpak], Homepage: constants.MethodHomepage[constants.MethodCelluloidFlatpak], }) } if filterInstalled && config.IsAppInstalled("MPV") { } else { methods = append(methods, InstallMethod{ ID: constants.MethodMPVPackage, Name: "MPV", Description: "Install via package manager", Recommended: true, Priority: 3, Icon: constants.MethodIcon[constants.MethodMPVPackage], Logo: constants.MethodLogo[constants.MethodMPVPackage], Type: constants.MethodType[constants.MethodMPVPackage], AppType: constants.MethodAppType[constants.MethodMPVPackage], Homepage: constants.MethodHomepage[constants.MethodMPVPackage], }) } if filterInstalled && config.IsAppInstalled("MPV") { } else { methods = append(methods, InstallMethod{ ID: constants.MethodMPVFlatpak, Name: "MPV", Description: "Official MPV via Flatpak from Flathub", Recommended: false, Priority: 4, Icon: constants.MethodIcon[constants.MethodMPVFlatpak], Logo: constants.MethodLogo[constants.MethodMPVFlatpak], Type: constants.MethodType[constants.MethodMPVFlatpak], AppType: constants.MethodAppType[constants.MethodMPVFlatpak], Homepage: constants.MethodHomepage[constants.MethodMPVFlatpak], }) } } return methods } func (li *LinuxInstaller) CheckFlatpakInstalled() bool { _, err := exec.LookPath("flatpak") return err == nil } func (li *LinuxInstaller) CheckFlathubEnabled() bool { cmd := exec.Command("flatpak", "remote-list") output, err := cmd.Output() if err != nil { return false } return strings.Contains(string(output), "flathub") } func (li *LinuxInstaller) EnableFlathub() error { cmd := exec.Command("flatpak", "remote-add", "--if-not-exists", "flathub", "https://flathub.org/repo/flathub.flatpakrepo") return cmd.Run() } func (li *LinuxInstaller) InstallFlatpak(flatpakID string, installUOSC bool) error { if !li.CheckFlatpakInstalled() { log.Warn("Flatpak is not installed, cannot install " + flatpakID) return fmt.Errorf("flatpak is not installed. Please install flatpak first.") } if !li.CheckFlathubEnabled() { fmt.Println("Enabling Flathub...") log.Info("Enabling Flathub repository...") cmd := exec.Command("flatpak", "remote-add", "--if-not-exists", "flathub", "https://flathub.org/repo/flathub.flatpakrepo") cmd.Stdin = os.Stdin if err := cmd.Run(); err != nil { log.Warn("Failed to enable Flathub: " + err.Error()) return fmt.Errorf("failed to enable flathub: %w", err) } log.Info("Flathub enabled successfully") } fmt.Printf("Installing %s via Flatpak...\n", flatpakID) log.Info("Installing " + flatpakID + " via Flatpak from Flathub...") cmd := exec.Command("flatpak", "install", "flathub", flatpakID, "-y") cmd.Stdin = os.Stdin if err := cmd.Run(); err != nil { log.Error("Flatpak install failed for " + flatpakID + ": " + err.Error()) return fmt.Errorf("flatpak install failed: %w", err) } log.Info(flatpakID + " installed successfully via Flatpak") return nil } func (li *LinuxInstaller) CheckInstalled() bool { _, err := exec.LookPath("mpv") return err == nil } func (li *LinuxInstaller) CanUpdate() bool { return li.CheckInstalled() } func (li *LinuxInstaller) GetInstalledVersion() string { cmd := exec.Command("mpv", "--version") output, err := cmd.Output() if err != nil { return "" } lines := strings.Split(string(output), "\n") if len(lines) > 0 { return strings.TrimPrefix(lines[0], "mpv ") } return "" } func (li *LinuxInstaller) Uninstall() error { switch li.Platform.DistroFamily { case "debian": if _, err := exec.LookPath("apt"); err == nil { cmd := exec.Command("sudo", "apt", "remove", "-y", constants.PackageMPV, constants.PackageCelluloid) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr return cmd.Run() } case "rhel": if _, err := exec.LookPath("dnf"); err == nil { cmd := exec.Command("sudo", "dnf", "remove", "-y", constants.PackageMPV, constants.PackageCelluloid) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr return cmd.Run() } case "arch": if _, err := exec.LookPath("pacman"); err == nil { cmd := exec.Command("sudo", "pacman", "-Rns", "--noconfirm", constants.PackageMPV, constants.PackageCelluloid) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr return cmd.Run() } } if _, err := exec.LookPath("flatpak"); err == nil { cmd := exec.Command("flatpak", "uninstall", "-y", constants.FlatpakMPV, constants.FlatpakCelluloid) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.Run() } return os.RemoveAll(li.InstallDir) } func (li *LinuxInstaller) UninstallFlatpak(appID string) error { cmd := exec.Command("flatpak", "uninstall", "-y", appID) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr return cmd.Run() } func (li *LinuxInstaller) UninstallBrew(appName string) error { if _, err := exec.LookPath("brew"); err != nil { return fmt.Errorf("homebrew is not installed") } cmd := exec.Command("brew", "uninstall", appName) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr return cmd.Run() } func (li *LinuxInstaller) InstallFlatpakWithOutput(cr *CommandRunner, flatpakID string, uiType string, isUpdate bool) error { // Auto-install flatpak if not present if !li.CheckFlatpakInstalled() { cr.outputChan <- "Flatpak is not installed. Installing automatically..." // Determine package name based on distro family var flatpakPkg string switch li.Platform.DistroFamily { case "debian": flatpakPkg = "flatpak" case "rhel": flatpakPkg = "flatpak" case "arch": flatpakPkg = "flatpak" default: return fmt.Errorf("unsupported distribution family for auto-installing flatpak: %s", li.Platform.DistroFamily) } cr.outputChan <- fmt.Sprintf("Installing %s via package manager (requires sudo)...", flatpakPkg) // Build the package manager install command pm := NewPackageManager(li.Platform.DistroFamily) installCmd := pm.BuildInstallCommand(flatpakPkg, false) // false = no privilege prefix, we use RunSudoCommand if len(installCmd) == 0 { return fmt.Errorf("failed to build install command for flatpak") } // Use RunSudoCommand which handles keyring password authentication if err := cr.RunSudoCommand(installCmd[0], installCmd[1:]...); err != nil { cr.outputChan <- fmt.Sprintf("Failed to install flatpak: %v", err) return fmt.Errorf("failed to install flatpak: %w", err) } cr.outputChan <- "✓ Flatpak installed successfully" cr.outputChan <- "" cr.outputChan <- "⚠️ NOTICE: Flatpak was just installed." cr.outputChan <- " You may need to restart your session or run 'flatpak update' for" cr.outputChan <- " application icons to appear in your desktop menu." cr.outputChan <- "" // Set pending notice for Web UI SetPendingNotice("Flatpak was just installed. You may need to restart your session or run 'flatpak update' for application icons to appear in your desktop menu.") } if isUpdate { cr.outputChan <- fmt.Sprintf("Updating %s via Flatpak...", flatpakID) cmd := cr.RunCommand("flatpak", "update", flatpakID, "-y") if cmd != nil { return cmd } if isMPVInstall(flatpakID) { cr.outputChan <- constants.MsgConfigNotOverwritten } return nil } // Auto-enable Flathub if not present if !li.CheckFlathubEnabled() { cr.outputChan <- "Enabling Flathub repository..." cmd := cr.RunCommand("flatpak", "remote-add", "--if-not-exists", "flathub", "https://flathub.org/repo/flathub.flatpakrepo") if cmd != nil { return cmd } cr.outputChan <- "✓ Flathub enabled" } cr.outputChan <- fmt.Sprintf("Installing %s via Flatpak...", flatpakID) cmd := cr.RunCommand("flatpak", "install", "flathub", flatpakID, "-y") if cmd != nil { return cmd } if isMPVInstall(flatpakID) && uiType != constants.UITypeNone { homeDir, err := os.UserHomeDir() if err != nil { return err } configDir := GetMPVConfigDirFromHome(homeDir) InstallUISafely(li.Installer, cr, configDir, uiType) } // Configure flatpak filesystem access for user's mpv config directory if isMPVInstall(flatpakID) { if err := li.ConfigureMPVFlatpakWithOutput(cr); err != nil { cr.outputChan <- fmt.Sprintf("Warning: Failed to configure MPV Flatpak: %v", err) } } else if flatpakID == constants.FlatpakCelluloid { if err := li.ConfigureCelluloidFlatpakWithOutput(cr); err != nil { cr.outputChan <- fmt.Sprintf("Warning: Failed to configure Celluloid Flatpak: %v", err) } } return nil } func (li *LinuxInstaller) GetDefaultInstallPath() string { homeDir, _ := os.UserHomeDir() return filepath.Join(homeDir, "Downloads", "mpv") } func (li *LinuxInstaller) NormalizePath(path string) string { return filepath.Clean(path) } func (li *LinuxInstaller) UninstallFlatpakWithOutput(cr *CommandRunner, appID string) error { cr.outputChan <- fmt.Sprintf("Uninstalling %s via Flatpak...", appID) cmd := cr.RunCommand("flatpak", "uninstall", "-y", appID) if err := cmd; err != nil { return fmt.Errorf("flatpak uninstall failed: %w", err) } return nil } func (li *LinuxInstaller) UninstallBrewWithOutput(cr *CommandRunner, appName string) error { if _, err := exec.LookPath("brew"); err != nil { return fmt.Errorf("homebrew is not installed") } cr.outputChan <- fmt.Sprintf("Uninstalling %s via Homebrew...", appName) cmd := cr.RunCommand("brew", "uninstall", appName) if err := cmd; err != nil { return fmt.Errorf("brew uninstall failed: %w", err) } return nil } func (li *LinuxInstaller) EnsureGSettingsInstalled(cr *CommandRunner) error { if _, err := exec.LookPath(constants.CommandGsettings); err == nil { cr.outputChan <- "gsettings is already installed" return nil } cr.outputChan <- "gsettings not found, installing gsettings-desktop-schemas..." packageManager := NewPackageManager(li.Platform.DistroFamily) // Build command WITHOUT sudo prefix - RunSudoCommand handles authentication installCmd := packageManager.BuildInstallCommand("gsettings-desktop-schemas", false) if cr != nil { cr.outputChan <- fmt.Sprintf("Running: sudo %s", strings.Join(installCmd, " ")) } return cr.RunSudoCommand(installCmd[0], installCmd[1:]...) } func (li *LinuxInstaller) ConfigureCelluloidPackageWithOutput(cr *CommandRunner) error { cr.outputChan <- "Configuring Celluloid settings..." cr.outputChan <- strings.Repeat("─", 50) if err := li.EnsureGSettingsInstalled(cr); err != nil { cr.outputChan <- "Warning: Failed to install gsettings, skipping configuration" return nil } cr.outputChan <- "Setting mpv-config-enable to true..." cmd := cr.RunCommand("gsettings", "set", "io.github.celluloid-player.Celluloid", "mpv-config-enable", "true") if err := cmd; err != nil { cr.outputChan <- "Warning: Failed to set gsettings (Celluloid may not be running)" } homeDir, _ := os.UserHomeDir() configDir := GetMPVConfigDirFromHome(homeDir) configPath := filepath.Join(configDir, "mpv.conf") cr.outputChan <- fmt.Sprintf("Setting mpv-config-file to: %s", configPath) cmd = cr.RunCommand("gsettings", "set", "io.github.celluloid-player.Celluloid", "mpv-config-file", configPath) if err := cmd; err != nil { cr.outputChan <- "Warning: Failed to set gsettings (Celluloid may not be running)" } cr.outputChan <- "Celluloid configuration set!" if err := li.Installer.DisableOSCForCelluloid(cr); err != nil { cr.outputChan <- fmt.Sprintf("Warning: Failed to disable OSC: %v", err) } return nil } func (li *LinuxInstaller) ConfigureCelluloidFlatpakWithOutput(cr *CommandRunner) error { cr.outputChan <- "Configuring Celluloid Flatpak..." cr.outputChan <- strings.Repeat("─", 50) cr.outputChan <- "Granting filesystem access to mpv config..." cmd := cr.RunCommand("flatpak", "override", "--user", "--filesystem=xdg-config/mpv:ro", constants.FlatpakCelluloid) if err := cmd; err != nil { cr.outputChan <- "Warning: Failed to set flatpak mpv dir override" } cr.outputChan <- "Setting mpv-config-enable to true..." cmd = cr.RunCommand("flatpak", "run", "--command=gsettings", constants.FlatpakCelluloid, "set", constants.GSettingsCelluloidSchema, "mpv-config-enable", "true") if err := cmd; err != nil { cr.outputChan <- "Warning: Failed to set gsettings mpv config true to flatpak" } homeDir, _ := os.UserHomeDir() configDir := GetMPVConfigDirFromHome(homeDir) configPath := filepath.Join(configDir, "mpv.conf") cr.outputChan <- fmt.Sprintf("Setting mpv-config-file to: %s", configPath) cmd = cr.RunCommand("flatpak", "run", "--command=gsettings", constants.FlatpakCelluloid, "set", constants.GSettingsCelluloidSchema, "mpv-config-file", configPath) if err := cmd; err != nil { cr.outputChan <- "Warning: Failed to set gsettings mpv file path to flatpak" } cr.outputChan <- "Celluloid Flatpak configuration set!" if err := li.Installer.DisableOSCForCelluloid(cr); err != nil { cr.outputChan <- fmt.Sprintf("Warning: Failed to disable OSC: %v", err) } return nil } func (li *LinuxInstaller) ConfigureMPVFlatpakWithOutput(cr *CommandRunner) error { cr.outputChan <- "Configuring MPV Flatpak..." cr.outputChan <- strings.Repeat("─", 50) cr.outputChan <- "Granting filesystem access to mpv config..." cmd := cr.RunCommand("flatpak", "override", "--user", "--filesystem=xdg-config/mpv:ro", constants.FlatpakMPV) if err := cmd; err != nil { cr.outputChan <- "Warning: Failed to set flatpak override" } cr.outputChan <- "MPV Flatpak configuration set!" return nil } func (li *LinuxInstaller) InstallIINAWithOutput(cr *CommandRunner) error { return fmt.Errorf("IINA is not supported on Linux") } func (li *LinuxInstaller) InstallMPCQTWithOutput(cr *CommandRunner, method string) error { return fmt.Errorf("MPC-QT is not supported on Linux") } func (li *LinuxInstaller) InstallMPVAppWithOutput(cr *CommandRunner, uiType string) error { return fmt.Errorf("MPV App is not supported on Linux") } func (li *LinuxInstaller) InstallMPVViaBrewWithOutput(cr *CommandRunner, uiType string, isUpdate bool) error { return fmt.Errorf("Homebrew is not supported on Linux") } func (li *LinuxInstaller) UninstallAppWithOutput(cr *CommandRunner, appName string) error { return fmt.Errorf("App uninstallation is not supported on Linux") } func (li *LinuxInstaller) UpdateMPVAppWithOutput(cr *CommandRunner) error { return fmt.Errorf("MPV App update is not supported on Linux") } func (li *LinuxInstaller) SetupFileAssociationsWithOutput(cr *CommandRunner) error { return fmt.Errorf("File associations are not supported on Linux") } func (li *LinuxInstaller) CreateInstallerShortcutWithOutput(cr *CommandRunner) error { cr.outputChan <- "Installer shortcut creation not needed on Linux" return nil } func (li *LinuxInstaller) CreateInstallerShortcutToBinaryWithOutput(cr *CommandRunner, binaryPath string) error { cr.outputChan <- "Installer shortcut creation not needed on Linux" return nil } // InstallIcon installs the icon to ~/.local/share/icons/hicolor/128x128/apps/ func (li *LinuxInstaller) InstallIcon(cr *CommandRunner) error { homeDir, err := os.UserHomeDir() if err != nil { return fmt.Errorf("failed to get home directory: %w", err) } // Use XDG icon theme directory structure iconThemeDir := filepath.Join(homeDir, constants.LinuxIconsDir, constants.LinuxIconThemeDir) if err := os.MkdirAll(iconThemeDir, 0755); err != nil { return fmt.Errorf("failed to create icon theme directory: %w", err) } iconPath := filepath.Join(iconThemeDir, constants.LinuxIconFileName) // Check if icon already exists if _, err := os.Stat(iconPath); err == nil { cr.outputChan <- "Icon already installed: " + iconPath return nil } cr.outputChan <- "Installing icon to " + iconThemeDir + "..." // Read icon from embedded assets iconData, err := assets.ReadPNGIcon() if err != nil { return fmt.Errorf("failed to read icon from assets: %w", err) } // Write icon to destination if err := os.WriteFile(iconPath, iconData, 0644); err != nil { return fmt.Errorf("failed to write icon file: %w", err) } cr.outputChan <- "✓ Icon installed: " + iconPath // Update icon cache for desktop environments to pick up the new icon if _, err := exec.LookPath("gtk-update-icon-cache"); err == nil { iconsBaseDir := filepath.Join(homeDir, constants.LinuxIconsDir) cmd := exec.Command("gtk-update-icon-cache", "--quiet", "--force", iconsBaseDir) if err := cmd.Run(); err != nil { // Non-critical error, just log it cr.outputChan <- "Note: Failed to update icon cache (non-critical)" } else { cr.outputChan <- "✓ Icon cache updated" } } else if _, err := exec.LookPath("update-icon-caches"); err == nil { // Alternative command for some distros iconsBaseDir := filepath.Join(homeDir, constants.LinuxIconsDir) cmd := exec.Command("update-icon-caches", iconsBaseDir) if err := cmd.Run(); err != nil { // Non-critical error, just log it cr.outputChan <- "Note: Failed to update icon cache (non-critical)" } else { cr.outputChan <- "✓ Icon cache updated" } } return nil } func CreateLinuxWebUIShortcutWithOutput(cr *CommandRunner) error { execPath, err := os.Executable() if err != nil { return fmt.Errorf("failed to get executable path: %w", err) } cr.outputChan <- "Setting up MPV Manager..." homeDir, err := os.UserHomeDir() if err != nil { return fmt.Errorf("failed to get home directory: %w", err) } // Create .config/mpv directory for the binary (standard MPV config location) // Target: ~/.config/mpv/mpv-manager (the binary itself, not a directory) targetDir := filepath.Join(homeDir, ".config", "mpv") targetBinPath := filepath.Join(targetDir, constants.AppName) // Create the target directory if it doesn't exist if err := os.MkdirAll(targetDir, 0755); err != nil { return fmt.Errorf("failed to create directory %s: %w", targetDir, err) } // Check if we need to copy the binary shouldCopy := false // Resolve symlinks to get the real path realExecPath, err := filepath.EvalSymlinks(execPath) if err != nil { realExecPath = execPath // fallback to original if resolution fails } realTargetPath, err := filepath.EvalSymlinks(targetBinPath) if err != nil { // Target doesn't exist, need to copy shouldCopy = true cr.outputChan <- "Installing MPV Manager to " + targetDir + "..." } else { // Target exists, check if it's the same as current binary if realExecPath != realTargetPath { // Different location - check if we should update shouldCopy = true cr.outputChan <- "Updating MPV Manager at " + targetDir + "..." } else { // Already installed in target location cr.outputChan <- "MPV Manager already installed at: " + targetBinPath } } // Copy the binary if needed if shouldCopy { // Read the current executable input, err := os.ReadFile(execPath) if err != nil { return fmt.Errorf("failed to read executable: %w", err) } // Write to target location if err := os.WriteFile(targetBinPath, input, 0755); err != nil { return fmt.Errorf("failed to copy executable to %s: %w", targetBinPath, err) } // Ensure executable permissions if err := os.Chmod(targetBinPath, 0755); err != nil { return fmt.Errorf("failed to set executable permissions: %w", err) } cr.outputChan <- "✓ MPV Manager installed to: " + targetBinPath // Save the bin path to config for future updates if err := config.SetManagerBinPath(targetBinPath); err != nil { cr.outputChan <- fmt.Sprintf("Warning: Failed to save bin path to config: %v", err) } } // Extract the Linux uninstall script to the config directory if err := assets.ExtractUninstallLinuxSh(targetDir); err != nil { cr.outputChan <- fmt.Sprintf("Warning: Failed to extract uninstall script: %v", err) } else { cr.outputChan <- "✓ Uninstall script installed: " + filepath.Join(targetDir, "uninstall-linux.sh") } // Install icon li := &LinuxInstaller{} if err := li.InstallIcon(cr); err != nil { cr.outputChan <- "Warning: Failed to install icon: " + err.Error() } // Build absolute icon path for .desktop file iconPath := filepath.Join(homeDir, constants.LinuxIconsDir, constants.LinuxIconThemeDir, constants.LinuxIconFileName) // Create .desktop file pointing to the installed binary desktopFilePath := filepath.Join(homeDir, ".local", "share", "applications", "mpv-manager.desktop") if err := os.MkdirAll(filepath.Dir(desktopFilePath), 0755); err != nil { return fmt.Errorf("failed to create applications directory: %w", err) } desktopFileContent := `[Desktop Entry] Name=MPV Manager GenericName=MPV Media Player Manager (Web UI) Comment=Install and manage MPV media player via Web UI Exec=` + targetBinPath + ` -m web Icon=` + iconPath + ` Terminal=false Type=Application Categories=Utility;AudioVideo; StartupNotify=true Keywords=mpv;media;player;installer; ` if err := os.WriteFile(desktopFilePath, []byte(desktopFileContent), 0644); err != nil { return fmt.Errorf("failed to write desktop file: %w", err) } desktopDir := filepath.Join(homeDir, "Desktop") desktopShortcutPath := filepath.Join(desktopDir, "mpv-manager.desktop") if err := os.Symlink(desktopFilePath, desktopShortcutPath); err != nil { // Symlink may already exist, try to remove and recreate os.Remove(desktopShortcutPath) if err := os.Symlink(desktopFilePath, desktopShortcutPath); err != nil { cr.outputChan <- "Note: Desktop symlink not created (may need to create manually)" } else { cr.outputChan <- "✓ Desktop shortcut created: " + desktopShortcutPath } } else { cr.outputChan <- "✓ Desktop shortcut created: " + desktopShortcutPath } cr.outputChan <- "✓ Application entry created: " + desktopFilePath cr.outputChan <- "Note: You may need to log out and log back in for shortcuts to appear in your menu." return nil } func (li *LinuxInstaller) CreateWebUIShortcutWithOutput(cr *CommandRunner) error { return CreateLinuxWebUIShortcutWithOutput(cr) } func (li *LinuxInstaller) InstallMPVWithOutput(cr *CommandRunner, method string, uiType string) error { return fmt.Errorf("MPV binary installation is not supported on Linux (use Flatpak or package manager)") } func (li *LinuxInstaller) UpdateMPVWithOutput(cr *CommandRunner, method string) error { return fmt.Errorf("MPV binary update is not supported on Linux (use Flatpak or package manager)") } // RemoveFileAssociationsWithOutput is a no-op on Linux func (li *LinuxInstaller) RemoveFileAssociationsWithOutput(cr *CommandRunner) error { return fmt.Errorf("file associations are not supported on Linux") } // CreateMPVShortcut is a no-op on Linux (shortcuts are not needed) func (li *LinuxInstaller) CreateMPVShortcut(cr *CommandRunner) error { return fmt.Errorf("MPV shortcuts are not supported on Linux") } // UninstallWithOutput is a no-op on Linux func (li *LinuxInstaller) UninstallWithOutput(cr *CommandRunner) error { return fmt.Errorf("MPV binary uninstall is not supported on Linux") } // UninstallMPCQTWithOutput is a no-op on Linux (MPC-QT is Windows-only) func (li *LinuxInstaller) UninstallMPCQTWithOutput(cr *CommandRunner) error { return fmt.Errorf("MPC-QT uninstall is not supported on Linux") }