implement huh

This commit is contained in:
2026-08-27 22:03:04 +01:00
parent e33682098a
commit 287d60e978
2 changed files with 101 additions and 27 deletions
+1 -1
View File
@@ -3,7 +3,6 @@ module dirmd
go 1.26.4 go 1.26.4
require ( require (
charm.land/bubbles/v2 v2.2.1
charm.land/bubbletea/v2 v2.0.9 charm.land/bubbletea/v2 v2.0.9
charm.land/huh/v2 v2.0.3 charm.land/huh/v2 v2.0.3
charm.land/lipgloss/v2 v2.0.6 charm.land/lipgloss/v2 v2.0.6
@@ -11,6 +10,7 @@ require (
) )
require ( require (
charm.land/bubbles/v2 v2.2.1 // indirect
github.com/atotto/clipboard v0.1.4 // indirect github.com/atotto/clipboard v0.1.4 // indirect
github.com/catppuccin/go v0.2.0 // indirect github.com/catppuccin/go v0.2.0 // indirect
github.com/charmbracelet/colorprofile v0.4.3 // indirect github.com/charmbracelet/colorprofile v0.4.3 // indirect
+100 -26
View File
@@ -3,10 +3,12 @@ package tui
import ( import (
"fmt" "fmt"
"os" "os"
"os/exec"
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings" "strings"
"charm.land/bubbles/v2/key"
"charm.land/huh/v2" "charm.land/huh/v2"
"charm.land/huh/v2/spinner" "charm.land/huh/v2/spinner"
@@ -14,20 +16,17 @@ import (
) )
func Run() error { func Run() error {
clearScreen()
var ( var (
inputDir string inputDir string
outputDir string outputDir string
force bool options []string
verticalSlices bool
skipFrontend bool
frontendOnly bool
omitTest bool
mitMd bool
protonDrive bool
drivePath string drivePath string
maxDepthStr string maxDepthStr string
maxSizeStr string maxSizeStr string
ignorePatterns string ignorePatterns string
finalConfirm bool
) )
inputDir = "." inputDir = "."
@@ -35,33 +34,97 @@ func Run() error {
maxSizeStr = "512" maxSizeStr = "512"
defOut := defaultOutputDir() defOut := defaultOutputDir()
outputDir = defOut 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( form := huh.NewForm(
huh.NewGroup( huh.NewGroup(
huh.NewInput().Title("Input directory").Value(&inputDir).Placeholder("."), huh.NewInput().Title("Input directory").Value(&inputDir).Placeholder("."),
huh.NewInput().Title("Output directory").Value(&outputDir).Placeholder(defOut), huh.NewInput().Title("Output directory").Value(&outputDir).Placeholder(defOut),
huh.NewConfirm().Title("Force overwrite?").Value(&force), huh.NewMultiSelect[string]().
huh.NewConfirm().Title("Vertical slices?").Value(&verticalSlices), Title("Options").
huh.NewConfirm().Title("Skip frontend files?").Value(&skipFrontend), Description("↑↓ navigate • enter/space toggle • tab next field").
huh.NewConfirm().Title("Frontend only?").Value(&frontendOnly), Options(
huh.NewConfirm().Title("Omit test files?").Value(&omitTest), huh.NewOption("Force overwrite", "force"),
huh.NewConfirm().Title("Omit non-README markdown?").Value(&mitMd), huh.NewOption("Vertical slices", "vertical-slices"),
huh.NewConfirm().Title("Upload to Proton Drive?").Value(&protonDrive), 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 depth").Value(&maxDepthStr).Placeholder("20"),
huh.NewInput().Title("Max size (KB)").Value(&maxSizeStr).Placeholder("512"), 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( ).WithWidth(80).WithKeyMap(km)
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 { if err := form.Run(); err != nil {
return fmt.Errorf("form: %w", err) 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)) maxDepth, err := strconv.Atoi(strings.TrimSpace(maxDepthStr))
if err != nil || maxDepth <= 0 { if err != nil || maxDepth <= 0 {
maxDepth = 20 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...) excludedExts := append([]string{}, config.DefaultExts...)
if skipFrontend { if skipFrontend {
excludedExts = append(excludedExts, config.FrontendExts...) excludedExts = append(excludedExts, config.FrontendExts...)
@@ -109,7 +168,7 @@ func Run() error {
SkipFrontend: skipFrontend, SkipFrontend: skipFrontend,
FrontendOnly: frontendOnly, FrontendOnly: frontendOnly,
OmitTest: omitTest, OmitTest: omitTest,
OmitMd: mitMd, OmitMd: omitMd,
} }
cmd := buildCommand(cfg) cmd := buildCommand(cfg)
@@ -139,6 +198,21 @@ func Run() error {
return nil 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 { func defaultOutputDir() string {
cwd, err := os.Getwd() cwd, err := os.Getwd()
if err != nil { if err != nil {