282 lines
8.3 KiB
Go
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
|
|
}
|