# MPV.Rocks Installer - Technical & Functional Improvement Plan

**Generated:** February 2026  
**Analyzed by:** GLM-5

---

## Executive Summary

This document outlines technical and functional improvements for the MPV.Rocks Installer, focusing on code quality, architecture, platform support, and reliability. Improvements are prioritized by impact and organized by domain.

---

## 1. Architecture Improvements

### 1.1 Add Context Support (Medium Priority)

**Problem:** Long-running operations cannot be cancelled.

**Recommended Solution:**

```go
// pkg/installer/installer.go
type Installer struct {
    // ... existing fields
    ctx context.Context
}

func (inst *Installer) Install(ctx context.Context, methodID string, installPath string) (*InstallationResult, error) {
    select {
    case <-ctx.Done():
        return nil, ctx.Err()
    default:
        // Continue with installation
    }
    // ... rest of installation
}

// For HTTP requests
func (inst *Installer) downloadFile(ctx context.Context, url string) error {
    req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
    // ...
}
```

**Impact:** Cancellable operations, timeout support, graceful shutdown

### 1.3 Extract Interfaces for Testability (Medium Priority)

**Problem:** Tight coupling makes unit testing difficult.

**Recommended Interfaces:**

```go
// pkg/installer/interfaces.go

// Downloader interface for HTTP operations
type Downloader interface {
    Download(url string, destPath string, progressChan chan<- float64) error
    GetJSON(url string, v interface{}) error
}

// CommandExecutor interface for shell commands
type CommandExecutor interface {
    Run(name string, args ...string) (string, error)
    RunWithOutput(outputChan chan<- string, name string, args ...string) error
}

// PackageManager interface for system packages
type PackageManager interface {
    Install(packageName string) error
    Update() error
    Remove(packageName string) error
    IsInstalled(packageName string) bool
}
```

**Impact:** Mockable dependencies, easier unit testing, flexibility

### 1.4 Split Large Files (Medium Priority)

**Current Large Files:**

| File | Lines | Recommendation |
|------|-------|----------------|
| `pkg/installer/installer.go` | ~890 | Split into `download.go`, `extract.go`, `verify.go` |
| `pkg/installer/linux.go` | ~1080 | Split by package manager |
| `pkg/installer/windows.go` | ~1200 | Split into `install.go`, `uninstall.go`, `shortcuts.go` |
| `pkg/tui/models_update.go` | ~1200 | Split by state handler |

**Example Split:**
```
pkg/installer/
├── installer.go      (core types, interface)
├── download.go       (download operations)
├── extract.go        (archive extraction)
├── verify.go         (checksum verification)
├── common.go         (shared utilities)
├── package_manager.go (existing)
├── windows/
│   ├── install.go
│   ├── uninstall.go
│   ├── shortcuts.go
│   └── file_assoc.go
├── linux/
│   ├── install.go
│   ├── flatpak.go
│   ├── snap.go
│   └── package.go
└── macos/
    ├── install.go
    ├── dmg.go
    └── app_bundle.go
```

---

## 2. Language System Refactoring

### 2.1 Consolidate Data Structures (High Priority)

**Problem:** 5 different data structures for language data create conversion complexity.

**Current:**
```
LocaleEntry → LanguageOption → majorLangItem → web.LanguageOption → JSON
```

**Recommended:**

```go
// pkg/locale/types.go - Unified language type
type Language struct {
    Code        string   `json:"code"`         // e.g., "en", "ja"
    Name        string   `json:"name"`         // e.g., "English", "Japanese"
    NativeName  string   `json:"native_name"`  // e.g., "English", "日本語"
    Flag        string   `json:"flag"`         // Emoji or country code
    IsCommon    bool     `json:"is_common"`    // Major language flag
    Population  int      `json:"population"`   // For sorting
    Region      *Region  `json:"region,omitempty"` // For regional variants
}

type Region struct {
    CountryCode string `json:"country_code"` // e.g., "US", "GB"
    CountryName string `json:"country_name"` // e.g., "United States"
    Locale      string `json:"locale"`       // e.g., "en-US"
    Flag        string `json:"flag"`
}

// Adapter interfaces for TUI and Web
type LanguageAdapter interface {
    ToListItem() string
    ToJSON() ([]byte, error)
}
```

### 2.2 Fix TUI Language Issues

| Issue | Location | Fix |
|-------|----------|-----|
| Dead `saveLanguagePreferences()` | Line 823 | Remove or implement |
| Double Enter key handling | `handleMajorLangUpdate()` | Fix search state logic |
| No retry logic | TUI save | Add 3-retry pattern |

**Retry Logic Implementation:**
```go
// pkg/tui/language_preferences.go
func (m *Model) applyLanguagePreferencesWithRetry() error {
    maxRetries := 3
    delays := []time.Duration{1 * time.Second, 2 * time.Second, 3 * time.Second}
    
    var lastErr error
    for i := 0; i < maxRetries; i++ {
        err := m.applyLanguagePreferences()
        if err == nil {
            return nil
        }
        lastErr = err
        if i < maxRetries-1 {
            time.Sleep(delays[i])
        }
    }
    return fmt.Errorf("failed after %d retries: %w", maxRetries, lastErr)
}
```

### 2.3 Remove Debug Mode from Production ✅ DONE (February 2026)

**Completed:** Fixed `debugMode: true` in `internal/webassets/static/languages.js`

```javascript
// internal/webassets/static/languages.js
// BEFORE:
const state = {
    debugMode: true,  // DEBUG MODE LEFT ENABLED!
    // ...
};

// AFTER:
const state = {
    debugMode: false,  // FIXED
    // ...
};
```

---

## 3. Platform-Specific Improvements

### 3.1 XDG Environment Variables ✅ DONE (February 2026)

**Completed:** Added XDG support to `pkg/constants/paths.go`

```go
// pkg/constants/paths.go - IMPLEMENTED

// getConfigHome returns the user's config directory, respecting XDG_CONFIG_HOME
func getConfigHome() string {
    if xdgConfig := os.Getenv("XDG_CONFIG_HOME"); xdgConfig != "" {
        return xdgConfig
    }
    homeDir, err := os.UserHomeDir()
    if err != nil {
        return ".config" // Fallback
    }
    return filepath.Join(homeDir, ".config")
}

// getDataHome returns the user's data directory, respecting XDG_DATA_HOME
func getDataHome() string {
    if xdgData := os.Getenv("XDG_DATA_HOME"); xdgData != "" {
        return xdgData
    }
    homeDir, err := os.UserHomeDir()
    if err != nil {
        return ".local/share" // Fallback
    }
    return filepath.Join(homeDir, ".local", "share")
}
```

**Additional Change:** Installer config now stored in MPV config directory:
- Linux: `$XDG_CONFIG_HOME/mpv/mpv-manager.json`
- macOS: `~/.config/mpv/mpv-manager.json`
- Windows: `~/mpv/portable_config/mpv-manager.json`

### 3.2 Add zypper Package Manager Support

```go
// pkg/installer/package_manager.go
var PackageManagerConfigs = map[string]PackageManagerCommands{
    // ... existing entries
    "zypper": {
        Install:   []string{"zypper", "--non-interactive", "install"},
        Update:    []string{"zypper", "refresh"},
        Upgrade:   []string{"zypper", "--non-interactive", "update"},
        Remove:    []string{"zypper", "--non-interactive", "remove"},
        Query:     []string{"zypper", "search", "--installed", "--match-exact"},
    },
}
```

### 3.3 Improve Windows CPU Detection

**Problem:** Heuristic-based AVX2 detection is unreliable.

**Solution Options:**

1. **CGO with CPUID intrinsics:**
```go
//go:build windows && cgo

package platform

/*
#include <intrin.h>
void cpuid(int* cpuinfo, int info) {
    __cpuid(cpuinfo, info);
}
*/
import "C"

func hasAVX2() bool {
    var cpuinfo [4]C.int
    C.cpuid(&cpuinfo[0], 7)
    return (cpuinfo[1] & (1 << 5)) != 0 // EBX bit 5 = AVX2
}
```

2. **External helper executable:**
```go
// Build a small helper during release that checks CPU features
// More portable than CGO, avoids cross-compilation issues
func detectCPUFeatures() ([]string, error) {
    cmd := exec.Command(filepath.Join(exeDir, "cpu-detect.exe"))
    output, err := cmd.Output()
    // ...
}
```

3. **Use Windows API:**
```go
// Call IsProcessorFeaturePresent via syscall
func hasAVX2() bool {
    // PF_AVX2_INSTRUCTIONS_AVAILABLE = 40
    ret, _, _ := procIsProcessorFeaturePresent.Call(40)
    return ret != 0
}
```

### 3.4 Add FreeBSD/Other Unix Support

```go
// pkg/platform/platform.go
func getOSType() OSType {
    switch runtime.GOOS {
    case "windows":
        return Windows
    case "darwin":
        return Darwin
    case "linux":
        return Linux
    case "freebsd", "netbsd", "openbsd":
        return UnixBSD // New type
    default:
        return Unsupported
    }
}

// Handle BSD as a supported but limited platform
func (p *Platform) GetInstallMethods() []string {
    switch p.OSType {
    case UnixBSD:
        return []string{"mpv-package"} // Basic package support
    // ...
    }
}
```

---

## 4. Error Handling & Reliability

### 4.1 Standardize Error Types

```go
// pkg/errors/types.go
type InstallError struct {
    Op       string // Operation that failed
    Method   string // Installation method
    Platform string // Target platform
    Err      error  // Underlying error
}

func (e *InstallError) Error() string {
    return fmt.Sprintf("%s failed for %s on %s: %v", e.Op, e.Method, e.Platform, e.Err)
}

func (e *InstallError) Unwrap() error {
    return e.Err
}

// Common error constructors
func NewDownloadError(url string, err error) *InstallError {
    return &InstallError{Op: "download", Method: url, Err: err}
}

func NewExtractionError(archive string, err error) *InstallError {
    return &InstallError{Op: "extract", Method: archive, Err: err}
}
```

### 4.2 Add Retry Mechanisms

```go
// pkg/retry/retry.go
type RetryConfig struct {
    MaxAttempts int
    InitialDelay time.Duration
    MaxDelay     time.Duration
    Multiplier   float64
}

var DefaultRetryConfig = RetryConfig{
    MaxAttempts:  3,
    InitialDelay: 1 * time.Second,
    MaxDelay:     10 * time.Second,
    Multiplier:   2.0,
}

func Do(ctx context.Context, cfg RetryConfig, fn func() error) error {
    var lastErr error
    delay := cfg.InitialDelay
    
    for i := 0; i < cfg.MaxAttempts; i++ {
        err := fn()
        if err == nil {
            return nil
        }
        lastErr = err
        
        if i < cfg.MaxAttempts-1 {
            select {
            case <-ctx.Done():
                return ctx.Err()
            case <-time.After(delay):
            }
            delay = time.Duration(float64(delay) * cfg.Multiplier)
            if delay > cfg.MaxDelay {
                delay = cfg.MaxDelay
            }
        }
    }
    return fmt.Errorf("retry failed after %d attempts: %w", cfg.MaxAttempts, lastErr)
}
```

### 4.3 Graceful Channel Closure

```go
// pkg/installer/common.go
func SafeChannelClose[T any](ch chan T) {
    select {
    case <-ch:
        // Already closed or has data
    default:
        close(ch)
    }
}

// Use defer pattern
func (cr *CommandRunner) RunWithChannels(ctx context.Context) error {
    defer func() {
        close(cr.outputChan)
        close(cr.errorChan)
    }()
    // ... execution
}
```

---

## 5. Error Handling & Reliability

### 6.1 TUI State Machine Tests

```go
// pkg/tui/models_test.go
func TestStateTransitions(t *testing.T) {
    tests := []struct {
        name       string
        initState  State
        input      tea.Msg
        wantState  State
        wantCmd    bool
    }{
        {
            name:      "Enter on main menu goes to install menu",
            initState: StateMainMenu,
            input:     tea.KeyMsg{Type: tea.KeyEnter},
            wantState: StateInstallMenu,
        },
        {
            name:      "Escape on install menu returns to main menu",
            initState: StateInstallMenu,
            input:     tea.KeyMsg{Type: tea.KeyEsc},
            wantState: StateMainMenu,
        },
        // ... more cases
    }
    
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            m := NewModel()
            m.state = tt.initState
            
            newModel, cmd := m.Update(tt.input)
            m = newModel.(*Model)
            
            if m.state != tt.wantState {
                t.Errorf("state = %v, want %v", m.state, tt.wantState)
            }
            if tt.wantCmd && cmd == nil {
                t.Error("expected command, got nil")
            }
        })
    }
}
```

### 6.2 Platform Detection Edge Cases

```go
// pkg/platform/linux_test.go
func TestDistroDetection(t *testing.T) {
    tests := []struct {
        name     string
        osRelease string
        wantFamily string
    }{
        {
            name: "Ubuntu detection",
            osRelease: `ID=ubuntu
VERSION_ID=24.04`,
            wantFamily: "debian",
        },
        {
            name: "Arch detection",
            osRelease: `ID=arch`,
            wantFamily: "arch",
        },
        {
            name: "Empty os-release",
            osRelease: ``,
            wantFamily: "unknown",
        },
        {
            name: "Malformed os-release",
            osRelease: `INVALID`,
            wantFamily: "unknown",
        },
    }
    
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            // Create temp file with os-release content
            tmpFile := createTempOSRelease(t, tt.osRelease)
            defer os.Remove(tmpFile)
            
            family := getDistroFamily(tmpFile)
            if family != tt.wantFamily {
                t.Errorf("family = %v, want %v", family, tt.wantFamily)
            }
        })
    }
}
```

### 6.3 Integration Test Structure

```go
// tests/integration/install_test.go
//go:build integration

package integration

import (
    "context"
    "os"
    "testing"
    "time"
    
    "gitgud.io/mike/mpv-manager/pkg/installer"
    "gitgud.io/mike/mpv-manager/pkg/platform"
)

func TestMPVInstallLinux(t *testing.T) {
    if runtime.GOOS != "linux" {
        t.Skip("Linux only test")
    }
    
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
    defer cancel()
    
    p := platform.Detect()
    inst := installer.New(p)
    
    tmpDir, err := os.MkdirTemp("", "mpv-manager-test-*")
    if err != nil {
        t.Fatal(err)
    }
    defer os.RemoveAll(tmpDir)
    
    result, err := inst.Install(ctx, "mpv-package", tmpDir)
    if err != nil {
        t.Fatalf("Install failed: %v", err)
    }
    
    if !result.Success {
        t.Errorf("Install unsuccessful: %s", result.Message)
    }
}
```

---

## 7. Go 1.26 Modernization

### 7.1 Use `new()` with Initial Values

```go
// Before
duration := 30 * time.Second
timeout := &duration

// After (Go 1.26+)
timeout := new(30 * time.Second)
```

### 7.2 Run `go fix` Modernizers

```bash
# Run all modernizers
go fix ./...

# Or specific modernizers
go fix -modernize ./...
```

### 7.3 Consider Experimental SIMD

For GPU codec lookups in `pkg/platform/gpu.go`:

```go
//go:build go1.26 && amd64

import "simd/archsimd"

// Potential optimization for batch GPU lookups
func batchLookupCodecs(gpuIDs []uint32) [][]string {
    // Use SIMD for parallel lookups
}
```

---

## Implementation Timeline

**Note on Security:** CSRF, CORS, and rate limiting protections are not included in this plan because the Web UI is a locally-run application. The security boundary is local machine access - once an attacker has that, web security measures are irrelevant.

### Week 1-2: Critical Fixes & Platform ✅ COMPLETED

| Task | Effort | Status |
|------|--------|--------|
| XDG environment support | 2h | ✅ DONE |
| Channel closure fixes | 1h | Pending |
| Remove debugMode: true | 5m | ✅ DONE |
| Fix installer config location | 1h | ✅ DONE (now in MPV dir) |
| Fix favicon path | 30m | ✅ DONE |
| Fix language selector checkmarks | 30m | ✅ DONE (Boxicons conflict) |

### Week 3-4: Architecture

| Task | Effort | Owner |
|------|--------|-------|
| Add context support | 4h | |
| Extract interfaces | 4h | |
| Error type standardization | 4h | |

### Week 5-6: Platform & Language

| Task | Effort | Owner |
|------|--------|-------|
| Add zypper support | 2h | |
| Improve Windows CPU detection | 4h | |
| Consolidate language structs | 8h | |
| Fix TUI language issues | 4h | |

### Week 7-8: Testing

| Task | Effort | Owner |
|------|--------|-------|
| TUI state tests | 8h | |
| Platform edge case tests | 4h | |
| Integration tests | 8h | |
| CI/CD improvements | 4h | |

---

## Success Metrics

| Metric | Before | Current | After Target |
|--------|--------|---------|--------------|
| Test coverage (overall) | ~75% | ~75% | 85% |
| Test coverage (pkg/tui) | ~5% | ~5% | 60% |
| XDG compliance | Partial | Full ✅ | Full |
| Language struct count | 5 | 5 | 2-3 |
| Dead code instances | 3+ | 2 | 0 |
| Debug mode in production | Yes | No ✅ | No |
| Config file location | Wrong | MPV dir ✅ | MPV dir |

**Note:** Global config state is intentionally kept - the app is single-user and the singleton pattern is appropriate.
