diff --git a/go.mod b/go.mod index 20e430f..659a42d 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,6 @@ module dirmd go 1.26.4 require ( - charm.land/bubbles/v2 v2.2.1 charm.land/bubbletea/v2 v2.0.9 charm.land/huh/v2 v2.0.3 charm.land/lipgloss/v2 v2.0.6 @@ -11,6 +10,7 @@ require ( ) require ( + charm.land/bubbles/v2 v2.2.1 // indirect github.com/atotto/clipboard v0.1.4 // indirect github.com/catppuccin/go v0.2.0 // indirect github.com/charmbracelet/colorprofile v0.4.3 // indirect diff --git a/internal/tui/run.go b/internal/tui/run.go index dd127ab..b8a7319 100644 --- a/internal/tui/run.go +++ b/internal/tui/run.go @@ -3,10 +3,12 @@ package tui import ( "fmt" "os" + "os/exec" "path/filepath" "strconv" "strings" + "charm.land/bubbles/v2/key" "charm.land/huh/v2" "charm.land/huh/v2/spinner" @@ -14,20 +16,17 @@ import ( ) func Run() error { + clearScreen() + var ( inputDir string outputDir string - force bool - verticalSlices bool - skipFrontend bool - frontendOnly bool - omitTest bool - mitMd bool - protonDrive bool + options []string drivePath string maxDepthStr string maxSizeStr string ignorePatterns string + finalConfirm bool ) inputDir = "." @@ -35,33 +34,97 @@ func Run() error { maxSizeStr = "512" defOut := defaultOutputDir() outputDir = defOut + drivePath = "/my-files/md/" + + km := huh.NewDefaultKeyMap() + + km.Input.Next = key.NewBinding(key.WithKeys("tab", "down"), key.WithHelp("tab/↓", "next")) + km.Input.Prev = key.NewBinding(key.WithKeys("shift+tab", "up"), key.WithHelp("shift+tab/↑", "prev")) + km.Input.Submit = key.NewBinding(key.WithKeys("enter"), key.WithHelp("enter", "submit")) + + km.Text.Next = key.NewBinding(key.WithKeys("tab", "down"), key.WithHelp("tab/↓", "next")) + km.Text.Prev = key.NewBinding(key.WithKeys("shift+tab", "up"), key.WithHelp("shift+tab/↑", "prev")) + km.Text.NewLine = key.NewBinding(key.WithKeys("enter"), key.WithHelp("enter", "new line")) + km.Text.Submit = key.NewBinding(key.WithKeys("ctrl+s"), key.WithHelp("ctrl+s", "submit")) + + km.Confirm.Next = key.NewBinding(key.WithKeys("tab", "down"), key.WithHelp("tab/↓", "next")) + km.Confirm.Prev = key.NewBinding(key.WithKeys("shift+tab", "up"), key.WithHelp("shift+tab/↑", "prev")) + km.Confirm.Toggle = key.NewBinding(key.WithKeys("left", "right", "h", "l"), key.WithHelp("←/→", "toggle")) + km.Confirm.Submit = key.NewBinding(key.WithKeys("enter"), key.WithHelp("enter", "confirm")) + km.Confirm.Accept = key.NewBinding(key.WithKeys("y"), key.WithHelp("y", "yes")) + km.Confirm.Reject = key.NewBinding(key.WithKeys("n"), key.WithHelp("n", "no")) + + km.MultiSelect.Next = key.NewBinding(key.WithKeys("tab"), key.WithHelp("tab", "next field")) + km.MultiSelect.Prev = key.NewBinding(key.WithKeys("shift+tab"), key.WithHelp("shift+tab", "prev field")) + km.MultiSelect.Up = key.NewBinding(key.WithKeys("up", "k"), key.WithHelp("↑/k", "up")) + km.MultiSelect.Down = key.NewBinding(key.WithKeys("down", "j"), key.WithHelp("↓/j", "down")) + km.MultiSelect.Toggle = key.NewBinding(key.WithKeys("enter", " "), key.WithHelp("enter/space", "toggle")) + km.MultiSelect.Submit = key.NewBinding(key.WithKeys("tab"), key.WithHelp("tab", "next")) + km.MultiSelect.Filter = key.NewBinding(key.WithKeys("/"), key.WithHelp("/", "filter")) + km.MultiSelect.SelectAll = key.NewBinding(key.WithKeys("a"), key.WithHelp("a", "select all")) + km.MultiSelect.SelectNone = key.NewBinding(key.WithKeys("A"), key.WithHelp("A", "select none")) 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.NewMultiSelect[string](). + Title("Options"). + Description("↑↓ navigate • enter/space toggle • tab next field"). + Options( + huh.NewOption("Force overwrite", "force"), + huh.NewOption("Vertical slices", "vertical-slices"), + huh.NewOption("Skip frontend files", "skip-frontend"), + huh.NewOption("Frontend only", "frontend-only"), + huh.NewOption("Omit test files", "omit-test"), + huh.NewOption("Omit non-README markdown", "omit-md"), + huh.NewOption("Upload to Proton Drive", "proton-drive"), + ). + Value(&options). + Height(10), + huh.NewInput(). + Title("Proton Drive path"). + Description("Used if 'Upload to Proton Drive' is toggled"). + Value(&drivePath). + Placeholder("/my-files/md/"), 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.NewText(). + Title("Ignore patterns (one per line)"). + Description("enter = new line • tab = next field"). + Value(&ignorePatterns). + Placeholder("One pattern per line..."). + Lines(4), + huh.NewConfirm(). + Title("Confirm and proceed?"). + Description("←/→ or y/n to choose • enter to confirm"). + Value(&finalConfirm). + Affirmative("Yes"). + Negative("No"), ), - huh.NewGroup( - huh.NewInput().Title("Remote path on Proton Drive").Value(&drivePath).Placeholder("/my-files/md/"), - ).WithHideFunc(func() bool { - return !protonDrive - }), - ) + ).WithWidth(80).WithKeyMap(km) if err := form.Run(); err != nil { return fmt.Errorf("form: %w", err) } + force := containsOption(options, "force") + verticalSlices := containsOption(options, "vertical-slices") + skipFrontend := containsOption(options, "skip-frontend") + frontendOnly := containsOption(options, "frontend-only") + omitTest := containsOption(options, "omit-test") + omitMd := containsOption(options, "omit-md") + protonDrive := containsOption(options, "proton-drive") + + if skipFrontend && frontendOnly { + return fmt.Errorf("--skip-frontend and --frontend-only are mutually exclusive") + } + + if !finalConfirm { + fmt.Fprintln(os.Stderr, "Cancelled.") + os.Exit(0) + } + maxDepth, err := strconv.Atoi(strings.TrimSpace(maxDepthStr)) if err != nil || maxDepth <= 0 { maxDepth = 20 @@ -83,10 +146,6 @@ func Run() error { } } - 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...) @@ -109,7 +168,7 @@ func Run() error { SkipFrontend: skipFrontend, FrontendOnly: frontendOnly, OmitTest: omitTest, - OmitMd: mitMd, + OmitMd: omitMd, } cmd := buildCommand(cfg) @@ -139,6 +198,21 @@ func Run() error { return nil } +func clearScreen() { + cmd := exec.Command("clear") + cmd.Stdout = os.Stdout + cmd.Run() +} + +func containsOption(opts []string, target string) bool { + for _, o := range opts { + if o == target { + return true + } + } + return false +} + func defaultOutputDir() string { cwd, err := os.Getwd() if err != nil {