package tui import ( "fmt" "os" "path/filepath" "strconv" "strings" "charm.land/huh/v2" "charm.land/huh/v2/spinner" "dirmd/internal/config" ) func Run() error { var ( inputDir string outputDir string force bool verticalSlices bool skipFrontend bool frontendOnly bool omitTest bool mitMd bool protonDrive bool drivePath string maxDepthStr string maxSizeStr string ignorePatterns string ) inputDir = "." maxDepthStr = "20" maxSizeStr = "512" defOut := defaultOutputDir() outputDir = defOut form := huh.NewForm( huh.NewGroup( huh.NewInput().Title("Input directory").Value(&inputDir).Placeholder("."), huh.NewInput().Title("Output directory").Value(&outputDir).Placeholder(defOut), huh.NewConfirm().Title("Force overwrite?").Value(&force), huh.NewConfirm().Title("Vertical slices?").Value(&verticalSlices), huh.NewConfirm().Title("Skip frontend files?").Value(&skipFrontend), huh.NewConfirm().Title("Frontend only?").Value(&frontendOnly), huh.NewConfirm().Title("Omit test files?").Value(&omitTest), huh.NewConfirm().Title("Omit non-README markdown?").Value(&mitMd), huh.NewConfirm().Title("Upload to Proton Drive?").Value(&protonDrive), huh.NewInput().Title("Max depth").Value(&maxDepthStr).Placeholder("20"), huh.NewInput().Title("Max size (KB)").Value(&maxSizeStr).Placeholder("512"), huh.NewText().Title("Ignore patterns (one per line)").Value(&ignorePatterns).Placeholder("One pattern per line..."), ), huh.NewGroup( huh.NewInput().Title("Remote path on Proton Drive").Value(&drivePath).Placeholder("/my-files/md/"), ).WithHideFunc(func() bool { return !protonDrive }), ) if err := form.Run(); err != nil { return fmt.Errorf("form: %w", err) } maxDepth, err := strconv.Atoi(strings.TrimSpace(maxDepthStr)) if err != nil || maxDepth <= 0 { maxDepth = 20 } maxSizeKB, err := strconv.Atoi(strings.TrimSpace(maxSizeStr)) if err != nil || maxSizeKB <= 0 { maxSizeKB = 512 } var ignores []string raw := strings.TrimSpace(ignorePatterns) if raw != "" { for _, line := range strings.Split(raw, "\n") { line = strings.TrimSpace(line) if line != "" { ignores = append(ignores, line) } } } if skipFrontend && frontendOnly { return fmt.Errorf("--skip-frontend and --frontend-only are mutually exclusive") } excludedExts := append([]string{}, config.DefaultExts...) if skipFrontend { excludedExts = append(excludedExts, config.FrontendExts...) } allIgnores := append([]string{}, config.DefaultIgnores...) allIgnores = append(allIgnores, ignores...) cfg := &config.Config{ InputPath: inputDir, OutputPath: outputDir, Force: force, MaxSize: int64(maxSizeKB) * 1024, MaxDepth: maxDepth, Ignores: allIgnores, Extensions: excludedExts, ProtonDrive: protonDrive, DrivePath: drivePath, VerticalSlices: verticalSlices, SkipFrontend: skipFrontend, FrontendOnly: frontendOnly, OmitTest: omitTest, OmitMd: mitMd, } cmd := buildCommand(cfg) fmt.Fprintf(os.Stderr, "%s\n", cmd) var result GenerateResult if err := spinner.New(). Title("Generating..."). Action(func() { result = Generate(cfg) }). Run(); err != nil { return fmt.Errorf("interrupted: %w", err) } if result.Error != nil { return result.Error } for _, f := range result.Files { fmt.Fprintf(os.Stdout, " %s\n", f) } if result.Uploaded { fmt.Fprintln(os.Stdout, "Uploaded to Proton Drive") } return nil } func defaultOutputDir() string { cwd, err := os.Getwd() if err != nil { return "/tmp/output_dirmd" } base := filepath.Base(cwd) return filepath.Join("/tmp", base+"_dirmd") } func buildCommand(cfg *config.Config) string { var parts []string parts = append(parts, "dirmd") parts = append(parts, "-o", cfg.OutputPath) if cfg.Force { parts = append(parts, "-f") } if cfg.VerticalSlices { parts = append(parts, "--vertical-slices") } if cfg.SkipFrontend { parts = append(parts, "--skip-frontend") } if cfg.FrontendOnly { parts = append(parts, "--frontend-only") } if cfg.OmitTest { parts = append(parts, "--omit-test") } if cfg.OmitMd { parts = append(parts, "--omit-md") } if cfg.ProtonDrive { parts = append(parts, "--proton-drive") if cfg.DrivePath != "" { parts = append(parts, "--drive-path", cfg.DrivePath) } } if cfg.MaxDepth != 20 { parts = append(parts, "--max-depth", strconv.Itoa(cfg.MaxDepth)) } if cfg.MaxSize != 512*1024 { parts = append(parts, "--max-size", strconv.FormatInt(cfg.MaxSize, 10)) } for _, ig := range extraIgnores(cfg) { parts = append(parts, "--ignore", ig) } parts = append(parts, cfg.InputPath) return strings.Join(parts, " ") } func extraIgnores(cfg *config.Config) []string { defaultSet := make(map[string]bool) for _, d := range config.DefaultIgnores { defaultSet[d] = true } var extra []string for _, ig := range cfg.Ignores { if !defaultSet[ig] { extra = append(extra, ig) } } return extra }