package config import ( "fmt" "os" "path/filepath" "strings" "gitgud.io/mike/mpv-manager/internal/assets" "gitgud.io/mike/mpv-manager/pkg/constants" "gitgud.io/mike/mpv-manager/pkg/log" ) // GetConfigPath returns the full path to mpv.conf (platform-aware) func GetConfigPath() string { configPath, err := constants.GetMPVConfigPathWithHome() if err != nil { log.Warn(fmt.Sprintf("Failed to get MPV config path: %v", err)) return "" } return configPath } // GetConfigValue reads a config value from mpv.conf and returns as array // Handles comma-separated values, inline comments, and whitespace // Note: For hwdec, the comma is part of the value (e.g., "nvdec-copy,auto"), not a separator func GetConfigValue(key string) []string { configPath := GetConfigPath() if configPath == "" { return []string{} } data, err := os.ReadFile(configPath) if err != nil { if !os.IsNotExist(err) { log.Warn(fmt.Sprintf("Failed to read mpv.conf: %v", err)) } return []string{} } content := string(data) lines := strings.Split(content, "\n") prefix := key + "=" for _, line := range lines { trimmed := strings.TrimSpace(line) if strings.HasPrefix(trimmed, prefix) { // Extract value part valuePart := strings.TrimSpace(strings.TrimPrefix(trimmed, prefix)) // Strip inline comments if commentIndex := strings.Index(valuePart, "#"); commentIndex != -1 { valuePart = strings.TrimSpace(valuePart[:commentIndex]) } // Handle empty value if valuePart == "" { return []string{} } // Strip surrounding quotes if present (mpv.conf allows quoted values) if len(valuePart) >= 2 && ((valuePart[0] == '"' && valuePart[len(valuePart)-1] == '"') || (valuePart[0] == '\'' && valuePart[len(valuePart)-1] == '\'')) { valuePart = valuePart[1 : len(valuePart)-1] } // For hwdec, don't split on comma - the comma is part of the value (e.g., "nvdec-copy,auto") if key == constants.ConfigKeyHardwareDecoder { return []string{valuePart} } // Handle comma-separated arrays for other keys values := strings.Split(valuePart, ",") for i := range values { values[i] = strings.TrimSpace(values[i]) } // Filter out empty values from array var result []string for _, v := range values { if v != "" { result = append(result, v) } } return result } } return []string{} } // shouldQuoteConfigValue determines if a config value should be wrapped in quotes // This is needed for paths and templates that contain special characters func shouldQuoteConfigValue(key, value string) bool { // Keys that typically need quoted values quotedKeys := map[string]bool{ "screenshot-directory": true, "screenshot-template": true, } if !quotedKeys[key] { return false } // Only quote if the value isn't already quoted if len(value) >= 2 && ((value[0] == '"' && value[len(value)-1] == '"') || (value[0] == '\'' && value[len(value)-1] == '\'')) { return false } return true } // SetConfigValue writes a config value to mpv.conf // preserveComments: if true, preserves existing inline comments // values: array of values, will be joined with comma for MPV arrays func SetConfigValue(key string, values []string, preserveComments bool) error { configPath := GetConfigPath() if configPath == "" { return fmt.Errorf("unable to determine MPV config path") } // Create config directory if it doesn't exist configDir := filepath.Dir(configPath) if err := os.MkdirAll(configDir, 0755); err != nil { return fmt.Errorf("failed to create config directory: %w", err) } // Create file from embedded asset if it doesn't exist if _, err := os.Stat(configPath); os.IsNotExist(err) { log.Info("mpv.conf not found, creating from embedded asset") embeddedConfig, err := assets.ReadMPVConfig() if err != nil { return fmt.Errorf("failed to read embedded mpv.conf: %w", err) } if err := os.WriteFile(configPath, embeddedConfig, 0644); err != nil { return fmt.Errorf("failed to create mpv.conf: %w", err) } log.Info(fmt.Sprintf("Created mpv.conf at: %s", configPath)) } // Read existing content data, err := os.ReadFile(configPath) if err != nil { return fmt.Errorf("failed to read mpv.conf: %w", err) } // Build new value var newValue string if len(values) == 0 { newValue = "" } else if len(values) == 1 { newValue = values[0] } else { newValue = strings.Join(values, ",") } // Wrap in quotes for keys that typically need them (paths, templates with special chars) if newValue != "" && shouldQuoteConfigValue(key, newValue) { newValue = `"` + newValue + `"` } // Process lines to update or append lines := strings.Split(string(data), "\n") found := false prefix := key + "=" for i, line := range lines { trimmed := strings.TrimSpace(line) if strings.HasPrefix(trimmed, prefix) { if preserveComments && newValue != "" { // Preserve inline comment equalIndex := strings.Index(line, "=") commentIndex := strings.Index(line, "#") if commentIndex != -1 && commentIndex > equalIndex { comment := line[commentIndex-1:] // Include space before comment lines[i] = line[:equalIndex+1] + newValue + comment } else { lines[i] = line[:equalIndex+1] + newValue } } else { // Replace without comment preservation equalIndex := strings.Index(line, "=") lines[i] = line[:equalIndex+1] + newValue } found = true break } } // Append new line if key not found if !found { lines = append(lines, prefix+newValue) } // Write using atomic file operation newContent := strings.Join(lines, "\n") return writeFileAtomically(configPath, []byte(newContent)) } // ParseConfig parses entire config file into map // Returns map[string][]string where key is the config key and value is array of values func ParseConfig(path string) (map[string][]string, error) { data, err := os.ReadFile(path) if err != nil { if os.IsNotExist(err) { return make(map[string][]string), nil } return nil, fmt.Errorf("failed to read config file: %w", err) } config := make(map[string][]string) lines := strings.Split(string(data), "\n") for _, line := range lines { trimmed := strings.TrimSpace(line) // Skip empty lines and comment-only lines if trimmed == "" || strings.HasPrefix(trimmed, "#") { continue } // Find key=value separator equalIndex := strings.Index(trimmed, "=") if equalIndex == -1 { continue // Skip lines without equals } // Extract key and value key := strings.TrimSpace(trimmed[:equalIndex]) valuePart := strings.TrimSpace(trimmed[equalIndex+1:]) // Strip inline comments from value if commentIndex := strings.Index(valuePart, "#"); commentIndex != -1 { valuePart = strings.TrimSpace(valuePart[:commentIndex]) } // Handle empty value if valuePart == "" { config[key] = []string{} continue } // Handle comma-separated arrays values := strings.Split(valuePart, ",") for i := range values { values[i] = strings.TrimSpace(values[i]) } // Filter out empty values var result []string for _, v := range values { if v != "" { result = append(result, v) } } config[key] = result } return config, nil } // WriteConfig writes entire config map to file using atomic operation // Format: Arrays become "key=value1,value2,value3", singles become "key=value" func WriteConfig(path string, config map[string][]string) error { var lines []string // Build content from map for key, values := range config { if len(values) == 0 { lines = append(lines, key+"=") } else if len(values) == 1 { lines = append(lines, key+"="+values[0]) } else { lines = append(lines, key+"="+strings.Join(values, ",")) } } // Write using atomic file operation newContent := strings.Join(lines, "\n") return writeFileAtomically(path, []byte(newContent)) } // writeFileAtomically writes content to a file using atomic operation // Creates temp file, writes content, then renames to final path func writeFileAtomically(path string, content []byte) error { // Create temp file with .tmp suffix tempPath := path + ".tmp" // Write to temp file if err := os.WriteFile(tempPath, content, 0644); err != nil { return fmt.Errorf("failed to write temp file: %w", err) } // Rename temp file to actual path (atomic operation) if err := os.Rename(tempPath, path); err != nil { // Clean up temp file if rename fails _ = os.Remove(tempPath) return fmt.Errorf("failed to rename temp file: %w", err) } return nil }