Files
dirmd/internal/tui/run.go
T

282 lines
8.3 KiB
Go

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"
"git.flo.fo/FLO/dirmd/internal/config"
)
func Run() error {
clearScreen()
var (
inputDir string
outputDir string
options []string
drivePath string
maxDepthStr string
maxSizeStr string
ignorePatterns string
finalConfirm bool
)
inputDir = "."
maxDepthStr = "20"
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.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"),
huh.NewOption("Include .dirmd instructions", "instructions"),
).
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)").
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"),
),
).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")
includeInstructions := containsOption(options, "instructions")
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
}
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)
}
}
}
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: omitMd,
IncludeInstructions: includeInstructions,
}
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 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 {
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
}