package tui import ( "errors" "fmt" "io/fs" "os" "path/filepath" "sync" "gitgud.io/mike/mpv-manager/internal/fileops" "gitgud.io/mike/mpv-manager/pkg/config" "gitgud.io/mike/mpv-manager/pkg/log" ) type managerPATHInfo struct { BinDir string BinaryPath string AliasPath string ShellConfig string ShellName string } type reversiblePATHMutation struct { info managerPATHInfo rollback func() error } type pathSnapshot struct { path string exists bool mode fs.FileMode data []byte symlink bool linkTarget string } var managerPATHMu sync.Mutex func getUserBinDir() string { info, err := currentManagerPATHInfo() if err != nil { return "" } return info.BinDir } func applyManagerPATHOperation(removing bool, emit func(string)) error { managerPATHMu.Lock() defer managerPATHMu.Unlock() info, err := currentManagerPATHInfo() if err != nil { return err } if err := os.MkdirAll(info.BinDir, 0o755); err != nil { return fmt.Errorf("create PATH binary directory: %w", err) } release, err := fileops.AcquireAdvisory(filepath.Join(info.BinDir, ".mpv-manager-path-state")) if err != nil { return err } defer release() var mutation reversiblePATHMutation if removing { log.Separator("PATH Removal Operation") emit("Removing MPV Manager command files and PATH registration...") mutation, err = prepareManagerPATHRemoval(info, emit) } else { log.Separator("PATH Addition Operation") source, sourceErr := os.Executable() if sourceErr != nil { return fmt.Errorf("determine current executable: %w", sourceErr) } emit(fmt.Sprintf("Installing command files in %s", info.BinDir)) mutation, err = prepareManagerPATHInstall(info, source, emit) } if err != nil { return err } inPath := !removing binPath := mutation.info.BinaryPath if removing { binPath = "" } if err := config.SetManagerPATHState(inPath, binPath); err != nil { rollbackErr := mutation.rollback() return errors.Join( fmt.Errorf("persist manager PATH state: %w", err), wrapRollbackError(rollbackErr), ) } if removing { emit("MPV Manager was removed from PATH successfully") } else { emit("MPV Manager was added to PATH successfully") if mutation.info.ShellConfig != "" { emit(fmt.Sprintf("Restart your terminal or reload %s", mutation.info.ShellConfig)) } else { emit("Open a new terminal for the PATH change to take effect") } } return nil } func wrapRollbackError(err error) error { if err == nil { return nil } return fmt.Errorf("roll back PATH files after state failure: %w", err) } func managerPATHInstalled() bool { info, err := currentManagerPATHInfo() if err != nil { return false } return managerPATHIsRegistered(info) } func capturePath(path string) (pathSnapshot, error) { snapshot := pathSnapshot{path: path} info, err := os.Lstat(path) if err != nil { if os.IsNotExist(err) { return snapshot, nil } return snapshot, err } snapshot.exists = true snapshot.mode = info.Mode() if info.Mode()&os.ModeSymlink != 0 { snapshot.symlink = true snapshot.linkTarget, err = os.Readlink(path) return snapshot, err } if !info.Mode().IsRegular() { return pathSnapshot{}, fmt.Errorf("refusing to modify non-regular PATH target %s", path) } snapshot.data, err = os.ReadFile(path) return snapshot, err } func restorePath(snapshot pathSnapshot) error { if !snapshot.exists { if err := os.Remove(snapshot.path); err != nil && !os.IsNotExist(err) { return err } return nil } if err := os.MkdirAll(filepath.Dir(snapshot.path), 0o755); err != nil { return err } if snapshot.symlink { if err := os.Remove(snapshot.path); err != nil && !os.IsNotExist(err) { return err } return os.Symlink(snapshot.linkTarget, snapshot.path) } info, err := os.Lstat(snapshot.path) if err == nil && info.Mode().IsRegular() { err = fileops.AtomicWritePreserve(snapshot.path, snapshot.data, snapshot.mode.Perm()) } else { if err == nil { if removeErr := os.Remove(snapshot.path); removeErr != nil { return removeErr } } else if !os.IsNotExist(err) { return err } err = fileops.AtomicWrite(snapshot.path, snapshot.data, snapshot.mode.Perm()) } if err != nil { return err } return os.Chmod(snapshot.path, snapshot.mode.Perm()) } func rollbackPaths(snapshots ...pathSnapshot) error { var rollbackErrors []error for index := len(snapshots) - 1; index >= 0; index-- { if err := restorePath(snapshots[index]); err != nil { rollbackErrors = append(rollbackErrors, fmt.Errorf("restore %s: %w", snapshots[index].path, err)) } } return errors.Join(rollbackErrors...) } func capturePaths(paths ...string) ([]pathSnapshot, error) { snapshots := make([]pathSnapshot, 0, len(paths)) for _, path := range paths { snapshot, err := capturePath(path) if err != nil { return nil, fmt.Errorf("snapshot %s: %w", path, err) } snapshots = append(snapshots, snapshot) } return snapshots, nil }