diff --git a/internal/ui/batch.go b/internal/ui/batch.go new file mode 100644 index 0000000..e23e2d6 --- /dev/null +++ b/internal/ui/batch.go @@ -0,0 +1,276 @@ +package ui + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "zipprine/internal/archiver" + "zipprine/internal/models" + + "github.com/charmbracelet/huh" +) + +func RunBatchCompressFlow() error { + var sourcePaths string + var outputDir string + var archiveTypeStr string + var compressionLevel string + var parallel bool + + form := huh.NewForm( + huh.NewGroup( + huh.NewText(). + Title("📁 Source Paths"). + Description("Enter paths separated by commas (e.g., /path1,/path2,/path3)"). + Value(&sourcePaths). + Validate(func(s string) error { + if s == "" { + return fmt.Errorf("source paths cannot be empty") + } + paths := strings.Split(s, ",") + for _, p := range paths { + p = strings.TrimSpace(p) + if _, err := os.Stat(p); os.IsNotExist(err) { + return fmt.Errorf("path does not exist: %s", p) + } + } + return nil + }), + + huh.NewInput(). + Title("💾 Output Directory"). + Description("Directory to save all archives"). + Value(&outputDir). + Validate(func(s string) error { + if s == "" { + return fmt.Errorf("output directory cannot be empty") + } + return nil + }). + Suggestions(getPathCompletions("")), + ), + + huh.NewGroup( + huh.NewSelect[string](). + Title("🎨 Archive Type"). + Options( + huh.NewOption("ZIP", "ZIP"), + huh.NewOption("TAR.GZ", "TARGZ"), + huh.NewOption("TAR", "TAR"), + ). + Value(&archiveTypeStr), + + huh.NewSelect[string](). + Title("⚡ Compression Level"). + Options( + huh.NewOption("Fast (Level 1)", "1"), + huh.NewOption("Balanced (Level 5)", "5"), + huh.NewOption("Best (Level 9)", "9"), + ). + Value(&compressionLevel), + + huh.NewConfirm(). + Title("🚀 Parallel Processing"). + Description("Process archives in parallel (faster for multiple files)"). + Value(¶llel), + ), + ).WithTheme(huh.ThemeCatppuccin()) + + if err := form.Run(); err != nil { + return err + } + + // Parse paths + paths := strings.Split(sourcePaths, ",") + configs := make([]*models.CompressConfig, 0, len(paths)) + + // Ensure output directory exists + if err := os.MkdirAll(outputDir, 0755); err != nil { + return fmt.Errorf("failed to create output directory: %w", err) + } + + // Create configs for each path + for _, path := range paths { + path = strings.TrimSpace(path) + if path == "" { + continue + } + + basename := filepath.Base(path) + ext := getExtension(models.ArchiveType(archiveTypeStr)) + outputPath := filepath.Join(outputDir, basename+ext) + + level := 5 + fmt.Sscanf(compressionLevel, "%d", &level) + + configs = append(configs, &models.CompressConfig{ + SourcePath: path, + OutputPath: outputPath, + ArchiveType: models.ArchiveType(archiveTypeStr), + CompressionLevel: level, + }) + } + + fmt.Println(InfoStyle.Render(fmt.Sprintf("📦 Batch compressing %d items...", len(configs)))) + fmt.Println() + + // Create batch config + batchConfig := &archiver.BatchCompressConfig{ + Configs: configs, + Parallel: parallel, + MaxWorkers: 4, + OnProgress: func(index, total int, filename string) { + fmt.Printf(" [%d/%d] Processing: %s\n", index, total, filepath.Base(filename)) + }, + OnError: func(index int, filename string, err error) { + fmt.Println(ErrorStyle.Render(fmt.Sprintf(" ❌ Failed: %s - %v", filepath.Base(filename), err))) + }, + OnComplete: func(index int, filename string) { + fmt.Println(SuccessStyle.Render(fmt.Sprintf(" ✅ Completed: %s", filepath.Base(filename)))) + }, + } + + errors := archiver.BatchCompress(batchConfig) + + // Count successes + successCount := 0 + for _, err := range errors { + if err == nil { + successCount++ + } + } + + fmt.Println() + fmt.Println(SuccessStyle.Render(fmt.Sprintf("✨ Batch complete: %d/%d successful", successCount, len(configs)))) + + return nil +} + +func RunBatchExtractFlow() error { + var archivePaths string + var outputDir string + var parallel bool + + form := huh.NewForm( + huh.NewGroup( + huh.NewText(). + Title("📦 Archive Paths"). + Description("Enter archive paths separated by commas"). + Value(&archivePaths). + Validate(func(s string) error { + if s == "" { + return fmt.Errorf("archive paths cannot be empty") + } + paths := strings.Split(s, ",") + for _, p := range paths { + p = strings.TrimSpace(p) + if _, err := os.Stat(p); os.IsNotExist(err) { + return fmt.Errorf("archive does not exist: %s", p) + } + } + return nil + }), + + huh.NewInput(). + Title("💾 Output Directory"). + Description("Directory to extract all archives"). + Value(&outputDir). + Suggestions(getPathCompletions("")), + + huh.NewConfirm(). + Title("🚀 Parallel Processing"). + Description("Extract archives in parallel"). + Value(¶llel), + ), + ).WithTheme(huh.ThemeCatppuccin()) + + if err := form.Run(); err != nil { + return err + } + + // Parse paths + paths := strings.Split(archivePaths, ",") + configs := make([]*models.ExtractConfig, 0, len(paths)) + + // Ensure output directory exists + if err := os.MkdirAll(outputDir, 0755); err != nil { + return fmt.Errorf("failed to create output directory: %w", err) + } + + // Create configs for each archive + for _, path := range paths { + path = strings.TrimSpace(path) + if path == "" { + continue + } + + // Detect archive type + archiveType, err := archiver.DetectArchiveType(path) + if err != nil { + fmt.Println(ErrorStyle.Render(fmt.Sprintf("⚠️ Skipping %s: %v", path, err))) + continue + } + + basename := strings.TrimSuffix(filepath.Base(path), filepath.Ext(path)) + destPath := filepath.Join(outputDir, basename) + + configs = append(configs, &models.ExtractConfig{ + ArchivePath: path, + DestPath: destPath, + ArchiveType: archiveType, + OverwriteAll: true, + PreservePerms: true, + }) + } + + fmt.Println(InfoStyle.Render(fmt.Sprintf("📂 Batch extracting %d archives...", len(configs)))) + fmt.Println() + + // Create batch config + batchConfig := &archiver.BatchExtractConfig{ + Configs: configs, + Parallel: parallel, + MaxWorkers: 4, + OnProgress: func(index, total int, filename string) { + fmt.Printf(" [%d/%d] Extracting: %s\n", index, total, filepath.Base(filename)) + }, + OnError: func(index int, filename string, err error) { + fmt.Println(ErrorStyle.Render(fmt.Sprintf(" ❌ Failed: %s - %v", filepath.Base(filename), err))) + }, + OnComplete: func(index int, filename string) { + fmt.Println(SuccessStyle.Render(fmt.Sprintf(" ✅ Completed: %s", filepath.Base(filename)))) + }, + } + + errors := archiver.BatchExtract(batchConfig) + + // Count successes + successCount := 0 + for _, err := range errors { + if err == nil { + successCount++ + } + } + + fmt.Println() + fmt.Println(SuccessStyle.Render(fmt.Sprintf("✨ Batch complete: %d/%d successful", successCount, len(configs)))) + + return nil +} + +func getExtension(archiveType models.ArchiveType) string { + switch archiveType { + case models.ZIP: + return ".zip" + case models.TARGZ: + return ".tar.gz" + case models.TAR: + return ".tar" + case models.GZIP: + return ".gz" + default: + return ".archive" + } +}