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
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
+100 -26
View File
@@ -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 {