use huh
This commit is contained in:
+2
-2
@@ -87,7 +87,8 @@ func init() {
|
||||
rootCmd.PersistentFlags().StringVarP(&outputPath, "output", "o", "", "output directory (required)")
|
||||
rootCmd.Flags().BoolVarP(&appendMode, "append", "a", false, "append to existing index.md")
|
||||
rootCmd.Flags().BoolVarP(&force, "force", "f", false, "overwrite existing output directory")
|
||||
rootCmd.Flags().StringVarP(&singleFile, "input-file", "i", "", "single file input (append mode only)")
|
||||
rootCmd.Flags().StringVar(&singleFile, "input-file", "", "single file input (append mode only)")
|
||||
rootCmd.Flags().BoolVarP(&tuiMode, "interactive", "i", false, "launch interactive TUI configuration wizard")
|
||||
rootCmd.Flags().Int64Var(&maxSize, "max-size", config.DefaultMaxSize, "max file size in bytes")
|
||||
rootCmd.Flags().IntVar(&maxDepth, "max-depth", config.DefaultMaxDepth, "max directory recursion depth")
|
||||
rootCmd.Flags().StringArrayVar(&ignorePatterns, "ignore", nil, "additional ignore patterns (glob)")
|
||||
@@ -99,7 +100,6 @@ func init() {
|
||||
rootCmd.Flags().BoolVar(&frontendOnly, "frontend-only", false, "include only frontend file types and README.md")
|
||||
rootCmd.Flags().BoolVar(&omitTest, "omit-test", false, "exclude test files (Go *_test.go, Java *Test.java) and strip #[cfg(test)] blocks from Rust")
|
||||
rootCmd.Flags().BoolVar(&omitMd, "omit-md", false, "omit all .md files except README.md")
|
||||
rootCmd.Flags().BoolVar(&tuiMode, "tui", false, "launch interactive TUI configuration wizard")
|
||||
}
|
||||
|
||||
func run(cmd *cobra.Command, args []string) error {
|
||||
|
||||
@@ -5,23 +5,29 @@ 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
|
||||
github.com/spf13/cobra v1.10.2
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/atotto/clipboard v0.1.4 // indirect
|
||||
github.com/catppuccin/go v0.2.0 // indirect
|
||||
github.com/charmbracelet/colorprofile v0.4.3 // indirect
|
||||
github.com/charmbracelet/ultraviolet v0.0.0-20260811164956-006e29f97886 // indirect
|
||||
github.com/charmbracelet/x/ansi v0.11.8 // indirect
|
||||
github.com/charmbracelet/x/exp/ordered v0.1.0 // indirect
|
||||
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect
|
||||
github.com/charmbracelet/x/term v0.2.2 // indirect
|
||||
github.com/charmbracelet/x/termios v0.1.1 // indirect
|
||||
github.com/charmbracelet/x/windows v0.2.2 // indirect
|
||||
github.com/clipperhouse/displaywidth v0.11.0 // indirect
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.4.1 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.27 // indirect
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
|
||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/spf13/pflag v1.0.9 // indirect
|
||||
|
||||
@@ -2,6 +2,8 @@ charm.land/bubbles/v2 v2.2.1 h1:Fq1+qm5hV6GkvzLQDhCBpXXE5tLgvh1PRriCLwSvIQU=
|
||||
charm.land/bubbles/v2 v2.2.1/go.mod h1:wdMgn+sje1KNXdwFizIWjbf328fIUBxqEmJ/vYPo8yc=
|
||||
charm.land/bubbletea/v2 v2.0.9 h1:DpJCMWKgzQK8SJv4zbKKFHAI10ymWy/evClPFk0k0f8=
|
||||
charm.land/bubbletea/v2 v2.0.9/go.mod h1:2SkdgoTXluXJHOUwAoRlRXF/28vklb1rFl6GcgV1/ss=
|
||||
charm.land/huh/v2 v2.0.3 h1:2cJsMqEPwSywGHvdlKsJyQKPtSJLVnFKyFbsYZTlLkU=
|
||||
charm.land/huh/v2 v2.0.3/go.mod h1:93eEveeeqn47MwiC3tf+2atZ2l7Is88rAtmZNZ8x9Wc=
|
||||
charm.land/lipgloss/v2 v2.0.6 h1:EaGKeuA8FvF+v2BT5VmZd2LoYLaMZJXA5n34th8nCIQ=
|
||||
charm.land/lipgloss/v2 v2.0.6/go.mod h1:ipDDJNSGa1hlwDtSfW1s2/xR8Vdhbut4PXh2zEKZd0Q=
|
||||
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
|
||||
@@ -10,31 +12,49 @@ github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z
|
||||
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||
github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o=
|
||||
github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w=
|
||||
github.com/catppuccin/go v0.2.0 h1:ktBeIrIP42b/8FGiScP9sgrWOss3lw0Z5SktRoithGA=
|
||||
github.com/catppuccin/go v0.2.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
|
||||
github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q=
|
||||
github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q=
|
||||
github.com/charmbracelet/ultraviolet v0.0.0-20260811164956-006e29f97886 h1:rdnVWKgJpTVXKuKuJyxDJ+NFJdUaUqGvyGy61OcvlbA=
|
||||
github.com/charmbracelet/ultraviolet v0.0.0-20260811164956-006e29f97886/go.mod h1:nAw0d9PhFp1qdzi2xhQU5YOu5sVpDIHWlaW2Uz/bCro=
|
||||
github.com/charmbracelet/x/ansi v0.11.8 h1:JMFwp0CgDC2+jcOB162HH5k7I3FVbgFSMMYg7dSPBQQ=
|
||||
github.com/charmbracelet/x/ansi v0.11.8/go.mod h1:ZNN+3mXny/516oTQPLMPIBeSINvNJJQ8uQXDgbeJxY0=
|
||||
github.com/charmbracelet/x/conpty v0.1.1 h1:s1bUxjoi7EpqiXysVtC+a8RrvPPNcNvAjfi4jxsAuEs=
|
||||
github.com/charmbracelet/x/conpty v0.1.1/go.mod h1:OmtR77VODEFbiTzGE9G1XiRJAga6011PIm4u5fTNZpk=
|
||||
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA=
|
||||
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0=
|
||||
github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA=
|
||||
github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I=
|
||||
github.com/charmbracelet/x/exp/ordered v0.1.0 h1:55/qLwjIh0gL0Vni+QAWk7T/qRVP6sBf+2agPBgnOFE=
|
||||
github.com/charmbracelet/x/exp/ordered v0.1.0/go.mod h1:5UHwmG+is5THxMyCJHNPCn2/ecI07aKNrW+LcResjJ8=
|
||||
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4=
|
||||
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ=
|
||||
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
|
||||
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
|
||||
github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
|
||||
github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=
|
||||
github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM=
|
||||
github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k=
|
||||
github.com/charmbracelet/x/xpty v0.1.3 h1:eGSitii4suhzrISYH50ZfufV3v085BXQwIytcOdFSsw=
|
||||
github.com/charmbracelet/x/xpty v0.1.3/go.mod h1:poPYpWuLDBFCKmKLDnhBp51ATa0ooD8FhypRwEFtH3Y=
|
||||
github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=
|
||||
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
||||
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/lucasb-eyer/go-colorful v1.4.1 h1:1EO+WB73+EH8EVbzlrG3KLAfEypQWVHIBqlTf+2hNss=
|
||||
github.com/lucasb-eyer/go-colorful v1.4.1/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mattn/go-runewidth v0.0.27 h1:Feg/Oou5zI/wnpgDF6omIU0OokC9GxLC/WRknhVlIR0=
|
||||
github.com/mattn/go-runewidth v0.0.27/go.mod h1:3qAiGCV4Koz/yuveO58qUefmUTRm8r0IGEXZ9jeHp/8=
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
|
||||
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
package tui
|
||||
|
||||
type Checkbox struct {
|
||||
Label string
|
||||
Checked bool
|
||||
}
|
||||
|
||||
func (c *Checkbox) Toggle() {
|
||||
c.Checked = !c.Checked
|
||||
}
|
||||
|
||||
func (c Checkbox) Render(focused bool) string {
|
||||
marker := "[ ]"
|
||||
if c.Checked {
|
||||
marker = "[x]"
|
||||
}
|
||||
|
||||
if c.Checked {
|
||||
marker = checkedStyle.Render(marker)
|
||||
} else {
|
||||
marker = uncheckedStyle.Render(marker)
|
||||
}
|
||||
|
||||
if focused {
|
||||
return focusedStyle.Render("▶ " + marker + " " + c.Label)
|
||||
}
|
||||
|
||||
return " " + marker + " " + labelStyle.Render(c.Label)
|
||||
}
|
||||
@@ -1,487 +0,0 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"charm.land/bubbles/v2/textarea"
|
||||
"charm.land/bubbles/v2/textinput"
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"charm.land/lipgloss/v2"
|
||||
|
||||
"dirmd/internal/config"
|
||||
)
|
||||
|
||||
const (
|
||||
fieldInputDir = iota
|
||||
fieldOutputDir
|
||||
fieldForce
|
||||
fieldVerticalSlices
|
||||
fieldSkipFrontend
|
||||
fieldFrontendOnly
|
||||
fieldOmitTest
|
||||
fieldOmitMd
|
||||
fieldProtonDrive
|
||||
fieldDrivePath
|
||||
fieldMaxDepth
|
||||
fieldMaxSize
|
||||
fieldIgnorePatterns
|
||||
fieldGenerateBtn
|
||||
fieldCancelBtn
|
||||
fieldCount
|
||||
)
|
||||
|
||||
type FormModel struct {
|
||||
focusIndex int
|
||||
inputDir textinput.Model
|
||||
outputDir textinput.Model
|
||||
drivePath textinput.Model
|
||||
maxDepth textinput.Model
|
||||
maxSize textinput.Model
|
||||
ignoreArea textarea.Model
|
||||
force Checkbox
|
||||
verticalSlices Checkbox
|
||||
skipFrontend Checkbox
|
||||
frontendOnly Checkbox
|
||||
omitTest Checkbox
|
||||
omitMd Checkbox
|
||||
protonDrive Checkbox
|
||||
isDark bool
|
||||
width int
|
||||
height int
|
||||
err string
|
||||
}
|
||||
|
||||
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 NewForm() FormModel {
|
||||
inputDir := textinput.New()
|
||||
inputDir.Placeholder = "."
|
||||
inputDir.SetValue(".")
|
||||
inputDir.SetWidth(60)
|
||||
|
||||
defOut := defaultOutputDir()
|
||||
outputDir := textinput.New()
|
||||
outputDir.Placeholder = defOut
|
||||
outputDir.SetValue(defOut)
|
||||
outputDir.SetWidth(60)
|
||||
|
||||
drivePath := textinput.New()
|
||||
drivePath.Placeholder = "/my-files/md/"
|
||||
drivePath.SetWidth(60)
|
||||
|
||||
maxDepth := textinput.New()
|
||||
maxDepth.SetValue("20")
|
||||
maxDepth.SetWidth(10)
|
||||
|
||||
maxSize := textinput.New()
|
||||
maxSize.SetValue("512")
|
||||
maxSize.SetWidth(10)
|
||||
|
||||
ignoreArea := textarea.New()
|
||||
ignoreArea.Placeholder = "One pattern per line..."
|
||||
ignoreArea.SetHeight(5)
|
||||
ignoreArea.ShowLineNumbers = false
|
||||
ignoreArea.CharLimit = 0
|
||||
|
||||
m := FormModel{
|
||||
inputDir: inputDir,
|
||||
outputDir: outputDir,
|
||||
drivePath: drivePath,
|
||||
maxDepth: maxDepth,
|
||||
maxSize: maxSize,
|
||||
ignoreArea: ignoreArea,
|
||||
force: Checkbox{Label: "Force overwrite"},
|
||||
verticalSlices: Checkbox{Label: "Vertical slices"},
|
||||
skipFrontend: Checkbox{Label: "Skip frontend files"},
|
||||
frontendOnly: Checkbox{Label: "Frontend only"},
|
||||
omitTest: Checkbox{Label: "Omit test files"},
|
||||
omitMd: Checkbox{Label: "Omit non-README markdown"},
|
||||
protonDrive: Checkbox{Label: "Proton Drive upload"},
|
||||
focusIndex: fieldInputDir,
|
||||
isDark: true,
|
||||
}
|
||||
|
||||
m.applyStyles()
|
||||
m.focusActiveField()
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *FormModel) SetDark(isDark bool) {
|
||||
if m.isDark == isDark {
|
||||
return
|
||||
}
|
||||
m.isDark = isDark
|
||||
m.applyStyles()
|
||||
}
|
||||
|
||||
func (m *FormModel) applyStyles() {
|
||||
tiStyles := getDefaultInputStyles(m.isDark)
|
||||
m.inputDir.SetStyles(tiStyles)
|
||||
m.outputDir.SetStyles(tiStyles)
|
||||
m.drivePath.SetStyles(tiStyles)
|
||||
m.maxDepth.SetStyles(tiStyles)
|
||||
m.maxSize.SetStyles(tiStyles)
|
||||
|
||||
taStyles := getDefaultTextareaStyles(m.isDark)
|
||||
m.ignoreArea.SetStyles(taStyles)
|
||||
}
|
||||
|
||||
func (m *FormModel) shouldSkipField(idx int) bool {
|
||||
return idx == fieldDrivePath && !m.protonDrive.Checked
|
||||
}
|
||||
|
||||
func isTextInputField(idx int) bool {
|
||||
switch idx {
|
||||
case fieldInputDir, fieldOutputDir, fieldDrivePath, fieldMaxDepth, fieldMaxSize, fieldIgnorePatterns:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *FormModel) focusActiveField() tea.Cmd {
|
||||
m.inputDir.Blur()
|
||||
m.outputDir.Blur()
|
||||
m.drivePath.Blur()
|
||||
m.maxDepth.Blur()
|
||||
m.maxSize.Blur()
|
||||
m.ignoreArea.Blur()
|
||||
|
||||
var cmd tea.Cmd
|
||||
|
||||
switch m.focusIndex {
|
||||
case fieldInputDir:
|
||||
cmd = m.inputDir.Focus()
|
||||
case fieldOutputDir:
|
||||
cmd = m.outputDir.Focus()
|
||||
case fieldDrivePath:
|
||||
cmd = m.drivePath.Focus()
|
||||
case fieldMaxDepth:
|
||||
cmd = m.maxDepth.Focus()
|
||||
case fieldMaxSize:
|
||||
cmd = m.maxSize.Focus()
|
||||
case fieldIgnorePatterns:
|
||||
cmd = m.ignoreArea.Focus()
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (m FormModel) Update(msg tea.Msg) (FormModel, tea.Cmd) {
|
||||
var cmds []tea.Cmd
|
||||
|
||||
if k, ok := msg.(tea.KeyPressMsg); ok {
|
||||
if isTextInputField(m.focusIndex) {
|
||||
if isNavigateDown(k) || isNavigateUp(k) {
|
||||
if isNavigateDown(k) {
|
||||
m.focusIndex++
|
||||
if m.focusIndex >= fieldCount {
|
||||
m.focusIndex = 0
|
||||
}
|
||||
for m.shouldSkipField(m.focusIndex) {
|
||||
m.focusIndex++
|
||||
if m.focusIndex >= fieldCount {
|
||||
m.focusIndex = 0
|
||||
}
|
||||
}
|
||||
} else {
|
||||
m.focusIndex--
|
||||
if m.focusIndex < 0 {
|
||||
m.focusIndex = fieldCount - 1
|
||||
}
|
||||
for m.shouldSkipField(m.focusIndex) {
|
||||
m.focusIndex--
|
||||
if m.focusIndex < 0 {
|
||||
m.focusIndex = fieldCount - 1
|
||||
}
|
||||
}
|
||||
}
|
||||
cmd := m.focusActiveField()
|
||||
if cmd != nil {
|
||||
cmds = append(cmds, cmd)
|
||||
}
|
||||
return m, tea.Batch(cmds...)
|
||||
}
|
||||
} else {
|
||||
switch {
|
||||
case isNavigateDown(k):
|
||||
m.focusIndex++
|
||||
if m.focusIndex >= fieldCount {
|
||||
m.focusIndex = 0
|
||||
}
|
||||
for m.shouldSkipField(m.focusIndex) {
|
||||
m.focusIndex++
|
||||
if m.focusIndex >= fieldCount {
|
||||
m.focusIndex = 0
|
||||
}
|
||||
}
|
||||
cmd := m.focusActiveField()
|
||||
if cmd != nil {
|
||||
cmds = append(cmds, cmd)
|
||||
}
|
||||
return m, tea.Batch(cmds...)
|
||||
case isNavigateUp(k):
|
||||
m.focusIndex--
|
||||
if m.focusIndex < 0 {
|
||||
m.focusIndex = fieldCount - 1
|
||||
}
|
||||
for m.shouldSkipField(m.focusIndex) {
|
||||
m.focusIndex--
|
||||
if m.focusIndex < 0 {
|
||||
m.focusIndex = fieldCount - 1
|
||||
}
|
||||
}
|
||||
cmd := m.focusActiveField()
|
||||
if cmd != nil {
|
||||
cmds = append(cmds, cmd)
|
||||
}
|
||||
return m, tea.Batch(cmds...)
|
||||
case isActivate(k):
|
||||
switch m.focusIndex {
|
||||
case fieldForce:
|
||||
m.force.Toggle()
|
||||
case fieldVerticalSlices:
|
||||
m.verticalSlices.Toggle()
|
||||
case fieldSkipFrontend:
|
||||
m.skipFrontend.Toggle()
|
||||
case fieldFrontendOnly:
|
||||
m.frontendOnly.Toggle()
|
||||
case fieldOmitTest:
|
||||
m.omitTest.Toggle()
|
||||
case fieldOmitMd:
|
||||
m.omitMd.Toggle()
|
||||
case fieldProtonDrive:
|
||||
m.protonDrive.Toggle()
|
||||
case fieldGenerateBtn:
|
||||
return m, nil
|
||||
case fieldCancelBtn:
|
||||
return m, tea.Quit
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch m.focusIndex {
|
||||
case fieldInputDir:
|
||||
var cmd tea.Cmd
|
||||
m.inputDir, cmd = m.inputDir.Update(msg)
|
||||
cmds = append(cmds, cmd)
|
||||
case fieldOutputDir:
|
||||
var cmd tea.Cmd
|
||||
m.outputDir, cmd = m.outputDir.Update(msg)
|
||||
cmds = append(cmds, cmd)
|
||||
case fieldDrivePath:
|
||||
var cmd tea.Cmd
|
||||
m.drivePath, cmd = m.drivePath.Update(msg)
|
||||
cmds = append(cmds, cmd)
|
||||
case fieldMaxDepth:
|
||||
var cmd tea.Cmd
|
||||
m.maxDepth, cmd = m.maxDepth.Update(msg)
|
||||
cmds = append(cmds, cmd)
|
||||
case fieldMaxSize:
|
||||
var cmd tea.Cmd
|
||||
m.maxSize, cmd = m.maxSize.Update(msg)
|
||||
cmds = append(cmds, cmd)
|
||||
case fieldIgnorePatterns:
|
||||
var cmd tea.Cmd
|
||||
m.ignoreArea, cmd = m.ignoreArea.Update(msg)
|
||||
cmds = append(cmds, cmd)
|
||||
}
|
||||
|
||||
return m, tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
func (m FormModel) View() string {
|
||||
usableWidth := m.width - 30
|
||||
if usableWidth < 50 {
|
||||
usableWidth = 50
|
||||
}
|
||||
if usableWidth > 80 {
|
||||
usableWidth = 80
|
||||
}
|
||||
m.inputDir.SetWidth(usableWidth)
|
||||
m.outputDir.SetWidth(usableWidth)
|
||||
m.drivePath.SetWidth(usableWidth)
|
||||
m.ignoreArea.SetWidth(usableWidth)
|
||||
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString(titleStyle.Render("dirmd"))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
b.WriteString(m.renderTextField("Input directory", m.inputDir, m.focusIndex == fieldInputDir))
|
||||
b.WriteString("\n")
|
||||
b.WriteString(m.renderTextField("Output directory", m.outputDir, m.focusIndex == fieldOutputDir))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
b.WriteString(m.force.Render(m.focusIndex == fieldForce))
|
||||
b.WriteString("\n")
|
||||
b.WriteString(m.verticalSlices.Render(m.focusIndex == fieldVerticalSlices))
|
||||
b.WriteString("\n")
|
||||
b.WriteString(m.skipFrontend.Render(m.focusIndex == fieldSkipFrontend))
|
||||
b.WriteString("\n")
|
||||
b.WriteString(m.frontendOnly.Render(m.focusIndex == fieldFrontendOnly))
|
||||
b.WriteString("\n")
|
||||
b.WriteString(m.omitTest.Render(m.focusIndex == fieldOmitTest))
|
||||
b.WriteString("\n")
|
||||
b.WriteString(m.omitMd.Render(m.focusIndex == fieldOmitMd))
|
||||
b.WriteString("\n")
|
||||
b.WriteString(m.protonDrive.Render(m.focusIndex == fieldProtonDrive))
|
||||
b.WriteString("\n")
|
||||
|
||||
if m.protonDrive.Checked {
|
||||
b.WriteString(m.renderTextField("Remote path", m.drivePath, m.focusIndex == fieldDrivePath))
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
b.WriteString("\n")
|
||||
b.WriteString(m.renderTextField("Max depth", m.maxDepth, m.focusIndex == fieldMaxDepth))
|
||||
b.WriteString("\n")
|
||||
b.WriteString(m.renderTextField("Max size (KB)", m.maxSize, m.focusIndex == fieldMaxSize))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
ignoreFocused := m.focusIndex == fieldIgnorePatterns
|
||||
if ignoreFocused {
|
||||
b.WriteString(focusedStyle.Render("▶ Ignore patterns:"))
|
||||
} else {
|
||||
b.WriteString(" " + labelStyle.Render("Ignore patterns:"))
|
||||
}
|
||||
b.WriteString("\n")
|
||||
b.WriteString(m.ignoreArea.View())
|
||||
b.WriteString("\n\n")
|
||||
|
||||
btnFocused := m.focusIndex == fieldGenerateBtn
|
||||
if btnFocused {
|
||||
b.WriteString(focusedStyle.Render("▶ [ Generate ]"))
|
||||
} else {
|
||||
b.WriteString(" " + labelStyle.Render("[ Generate ]"))
|
||||
}
|
||||
b.WriteString(" ")
|
||||
cancelFocused := m.focusIndex == fieldCancelBtn
|
||||
if cancelFocused {
|
||||
b.WriteString(focusedStyle.Render("▶ [ Cancel ]"))
|
||||
} else {
|
||||
b.WriteString(" " + labelStyle.Render("[ Cancel ]"))
|
||||
}
|
||||
|
||||
if m.err != "" {
|
||||
b.WriteString("\n\n")
|
||||
b.WriteString(errorStyle.Render(m.err))
|
||||
}
|
||||
|
||||
b.WriteString("\n\n")
|
||||
b.WriteString(footerStyle.Render("Tab/↑↓: navigate Space: toggle Enter: submit Esc: quit"))
|
||||
|
||||
content := appBorder.Render(b.String())
|
||||
|
||||
if m.width > 0 && m.height > 0 {
|
||||
return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, content)
|
||||
}
|
||||
return content
|
||||
}
|
||||
|
||||
func (m FormModel) renderTextField(label string, input textinput.Model, focused bool) string {
|
||||
if focused {
|
||||
return focusedStyle.Render("▶ "+label+":") + " " + input.View()
|
||||
}
|
||||
return " " + labelStyle.Render(label+":") + " " + input.View()
|
||||
}
|
||||
|
||||
func (m FormModel) ToConfig() (*config.Config, error) {
|
||||
inputDir := m.inputDir.Value()
|
||||
if inputDir == "" {
|
||||
inputDir = "."
|
||||
}
|
||||
|
||||
outputDir := m.outputDir.Value()
|
||||
if outputDir == "" {
|
||||
return nil, fmt.Errorf("output directory required")
|
||||
}
|
||||
|
||||
maxDepth, err := parseIntDefault(m.maxDepth.Value(), 20)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("max depth: %w", err)
|
||||
}
|
||||
|
||||
maxSizeKB, err := parseIntDefault(m.maxSize.Value(), 512)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("max size: %w", err)
|
||||
}
|
||||
|
||||
var ignores []string
|
||||
raw := strings.TrimSpace(m.ignoreArea.Value())
|
||||
if raw != "" {
|
||||
for _, line := range strings.Split(raw, "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if line != "" {
|
||||
ignores = append(ignores, line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if m.skipFrontend.Checked && m.frontendOnly.Checked {
|
||||
return nil, fmt.Errorf("--skip-frontend and --frontend-only are mutually exclusive")
|
||||
}
|
||||
|
||||
excludedExts := append([]string{}, config.DefaultExts...)
|
||||
if m.skipFrontend.Checked {
|
||||
excludedExts = append(excludedExts, config.FrontendExts...)
|
||||
}
|
||||
|
||||
allIgnores := append([]string{}, config.DefaultIgnores...)
|
||||
allIgnores = append(allIgnores, ignores...)
|
||||
|
||||
cfg := &config.Config{
|
||||
InputPath: inputDir,
|
||||
OutputPath: outputDir,
|
||||
Force: m.force.Checked,
|
||||
MaxSize: int64(maxSizeKB) * 1024,
|
||||
MaxDepth: maxDepth,
|
||||
Ignores: allIgnores,
|
||||
Extensions: excludedExts,
|
||||
ProtonDrive: m.protonDrive.Checked,
|
||||
DrivePath: m.drivePath.Value(),
|
||||
VerticalSlices: m.verticalSlices.Checked,
|
||||
SkipFrontend: m.skipFrontend.Checked,
|
||||
FrontendOnly: m.frontendOnly.Checked,
|
||||
OmitTest: m.omitTest.Checked,
|
||||
OmitMd: m.omitMd.Checked,
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func (m *FormModel) SetError(msg string) {
|
||||
m.err = msg
|
||||
}
|
||||
|
||||
func (m *FormModel) SetSize(w, h int) {
|
||||
m.width = w
|
||||
m.height = h
|
||||
}
|
||||
|
||||
func parseIntDefault(s string, def int) (int, error) {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
return def, nil
|
||||
}
|
||||
var n int
|
||||
_, err := fmt.Sscanf(s, "%d", &n)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if n <= 0 {
|
||||
return def, nil
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
tea "charm.land/bubbletea/v2"
|
||||
)
|
||||
|
||||
func isQuit(msg tea.KeyPressMsg) bool {
|
||||
s := msg.String()
|
||||
return s == "ctrl+c" || s == "esc"
|
||||
}
|
||||
|
||||
func isNavigateDown(msg tea.KeyPressMsg) bool {
|
||||
s := msg.String()
|
||||
return s == "down" || s == "tab"
|
||||
}
|
||||
|
||||
func isNavigateUp(msg tea.KeyPressMsg) bool {
|
||||
s := msg.String()
|
||||
return s == "up" || s == "shift+tab"
|
||||
}
|
||||
|
||||
func isActivate(msg tea.KeyPressMsg) bool {
|
||||
s := msg.String()
|
||||
return s == "enter" || s == "space"
|
||||
}
|
||||
|
||||
func isSubmit(msg tea.KeyPressMsg) bool {
|
||||
return msg.String() == "enter"
|
||||
}
|
||||
@@ -1,253 +0,0 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"charm.land/bubbles/v2/textinput"
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"charm.land/lipgloss/v2"
|
||||
|
||||
"dirmd/internal/config"
|
||||
)
|
||||
|
||||
type State int
|
||||
|
||||
const (
|
||||
StateForm State = iota
|
||||
StateGenerating
|
||||
StateDone
|
||||
StateError
|
||||
)
|
||||
|
||||
type genResultMsg struct {
|
||||
result GenerateResult
|
||||
}
|
||||
|
||||
type spinnerState struct {
|
||||
frame int
|
||||
}
|
||||
|
||||
func (s *spinnerState) View() string {
|
||||
frames := []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"}
|
||||
return frames[s.frame%len(frames)]
|
||||
}
|
||||
|
||||
type Model struct {
|
||||
state State
|
||||
form FormModel
|
||||
result GenerateResult
|
||||
spinner spinnerState
|
||||
isDark bool
|
||||
width int
|
||||
height int
|
||||
command string
|
||||
}
|
||||
|
||||
func New() Model {
|
||||
return Model{
|
||||
state: StateForm,
|
||||
form: NewForm(),
|
||||
isDark: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (m Model) Init() tea.Cmd {
|
||||
return textinput.Blink
|
||||
}
|
||||
|
||||
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.BackgroundColorMsg:
|
||||
m.isDark = msg.IsDark()
|
||||
m.form.SetDark(m.isDark)
|
||||
return m, nil
|
||||
|
||||
case tea.WindowSizeMsg:
|
||||
m.width = msg.Width
|
||||
m.height = msg.Height
|
||||
m.form.SetSize(msg.Width, msg.Height)
|
||||
return m, nil
|
||||
|
||||
case tea.KeyPressMsg:
|
||||
if isQuit(msg) && m.state != StateGenerating {
|
||||
return m, tea.Quit
|
||||
}
|
||||
|
||||
switch m.state {
|
||||
case StateForm:
|
||||
if isSubmit(msg) && m.form.focusIndex == fieldGenerateBtn {
|
||||
cfg, err := m.form.ToConfig()
|
||||
if err != nil {
|
||||
m.form.SetError(err.Error())
|
||||
return m, nil
|
||||
}
|
||||
m.command = buildCommand(cfg)
|
||||
m.state = StateGenerating
|
||||
return m, func() tea.Msg {
|
||||
return genResultMsg{result: Generate(cfg)}
|
||||
}
|
||||
}
|
||||
if isSubmit(msg) && m.form.focusIndex == fieldCancelBtn {
|
||||
return m, tea.Quit
|
||||
}
|
||||
var cmd tea.Cmd
|
||||
m.form, cmd = m.form.Update(msg)
|
||||
return m, cmd
|
||||
|
||||
case StateDone, StateError:
|
||||
return m, tea.Quit
|
||||
}
|
||||
|
||||
case genResultMsg:
|
||||
m.result = msg.result
|
||||
if m.result.Error != nil {
|
||||
m.state = StateError
|
||||
} else {
|
||||
m.state = StateDone
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
if m.state == StateForm {
|
||||
var cmd tea.Cmd
|
||||
m.form, cmd = m.form.Update(msg)
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
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", fmt.Sprintf("%d", cfg.MaxDepth))
|
||||
}
|
||||
if cfg.MaxSize != 512*1024 {
|
||||
parts = append(parts, "--max-size", fmt.Sprintf("%d", cfg.MaxSize))
|
||||
}
|
||||
|
||||
extraIgnores := extraIgnores(cfg)
|
||||
for _, ig := range extraIgnores {
|
||||
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
|
||||
}
|
||||
|
||||
func (m Model) View() tea.View {
|
||||
var content string
|
||||
|
||||
switch m.state {
|
||||
case StateForm:
|
||||
content = m.form.View()
|
||||
case StateGenerating:
|
||||
content = m.renderGenerating()
|
||||
case StateDone:
|
||||
content = m.renderDone()
|
||||
case StateError:
|
||||
content = m.renderError()
|
||||
}
|
||||
|
||||
v := tea.NewView(content)
|
||||
v.AltScreen = true
|
||||
return v
|
||||
}
|
||||
|
||||
func (m Model) renderGenerating() string {
|
||||
var b strings.Builder
|
||||
b.WriteString(titleStyle.Render("dirmd"))
|
||||
b.WriteString("\n\n")
|
||||
b.WriteString(fmt.Sprintf(" %s Generating...\n", m.spinner.View()))
|
||||
b.WriteString("\n")
|
||||
b.WriteString(footerStyle.Render("Please wait."))
|
||||
|
||||
content := appBorder.Render(b.String())
|
||||
if m.width > 0 && m.height > 0 {
|
||||
return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, content)
|
||||
}
|
||||
return content
|
||||
}
|
||||
|
||||
func (m Model) renderDone() string {
|
||||
var b strings.Builder
|
||||
b.WriteString(titleStyle.Render("dirmd"))
|
||||
b.WriteString("\n\n")
|
||||
b.WriteString(successStyle.Render("✓ Done"))
|
||||
b.WriteString("\n\n")
|
||||
for _, f := range m.result.Files {
|
||||
b.WriteString(fmt.Sprintf(" %s\n", f))
|
||||
}
|
||||
if m.result.Uploaded {
|
||||
b.WriteString(successStyle.Render("\n Uploaded to Proton Drive"))
|
||||
}
|
||||
b.WriteString("\n\n")
|
||||
b.WriteString(footerStyle.Render("Press any key to exit."))
|
||||
|
||||
content := appBorder.Render(b.String())
|
||||
if m.width > 0 && m.height > 0 {
|
||||
return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, content)
|
||||
}
|
||||
return content
|
||||
}
|
||||
|
||||
func (m Model) renderError() string {
|
||||
var b strings.Builder
|
||||
b.WriteString(titleStyle.Render("dirmd"))
|
||||
b.WriteString("\n\n")
|
||||
b.WriteString(errorStyle.Render("✗ Error"))
|
||||
b.WriteString("\n\n")
|
||||
b.WriteString(fmt.Sprintf(" %s\n", m.result.Error.Error()))
|
||||
b.WriteString("\n")
|
||||
b.WriteString(footerStyle.Render("Press any key to exit."))
|
||||
|
||||
content := appBorder.Render(b.String())
|
||||
if m.width > 0 && m.height > 0 {
|
||||
return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, content)
|
||||
}
|
||||
return content
|
||||
}
|
||||
+187
-12
@@ -3,27 +3,202 @@ package tui
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"charm.land/huh/v2"
|
||||
"charm.land/huh/v2/spinner"
|
||||
|
||||
"dirmd/internal/config"
|
||||
)
|
||||
|
||||
func Run() error {
|
||||
p := tea.NewProgram(New())
|
||||
m, err := p.Run()
|
||||
if err != nil {
|
||||
return fmt.Errorf("tui error: %w", err)
|
||||
var (
|
||||
inputDir string
|
||||
outputDir string
|
||||
force bool
|
||||
verticalSlices bool
|
||||
skipFrontend bool
|
||||
frontendOnly bool
|
||||
omitTest bool
|
||||
mitMd bool
|
||||
protonDrive bool
|
||||
drivePath string
|
||||
maxDepthStr string
|
||||
maxSizeStr string
|
||||
ignorePatterns string
|
||||
)
|
||||
|
||||
inputDir = "."
|
||||
maxDepthStr = "20"
|
||||
maxSizeStr = "512"
|
||||
defOut := defaultOutputDir()
|
||||
outputDir = defOut
|
||||
|
||||
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.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.NewGroup(
|
||||
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 {
|
||||
return fmt.Errorf("form: %w", err)
|
||||
}
|
||||
|
||||
if model, ok := m.(Model); ok {
|
||||
if model.command != "" {
|
||||
if model.result.Error != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed: %s\n", model.command)
|
||||
fmt.Fprintf(os.Stderr, "error: %v\n", model.result.Error)
|
||||
} else {
|
||||
fmt.Fprintf(os.Stdout, "%s\n", model.command)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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...)
|
||||
}
|
||||
|
||||
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: mitMd,
|
||||
}
|
||||
|
||||
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 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
|
||||
}
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"charm.land/bubbles/v2/textarea"
|
||||
"charm.land/bubbles/v2/textinput"
|
||||
"charm.land/lipgloss/v2"
|
||||
)
|
||||
|
||||
var (
|
||||
appBorder = lipgloss.NewStyle().
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderForeground(lipgloss.Color("62")).
|
||||
Padding(1, 2)
|
||||
|
||||
titleStyle = lipgloss.NewStyle().
|
||||
Bold(true).
|
||||
Foreground(lipgloss.Color("117")).
|
||||
MarginBottom(1)
|
||||
|
||||
labelStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("250"))
|
||||
|
||||
focusedStyle = lipgloss.NewStyle().
|
||||
Bold(true).
|
||||
Foreground(lipgloss.Color("117"))
|
||||
|
||||
checkedStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("82"))
|
||||
|
||||
uncheckedStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("250"))
|
||||
|
||||
footerStyle = lipgloss.NewStyle().
|
||||
MarginTop(1).
|
||||
Foreground(lipgloss.Color("248"))
|
||||
|
||||
errorStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("203")).
|
||||
Bold(true)
|
||||
|
||||
successStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("82")).
|
||||
Bold(true)
|
||||
)
|
||||
|
||||
func getDefaultInputStyles(isDark bool) textinput.Styles {
|
||||
s := textinput.DefaultStyles(isDark)
|
||||
s.Focused.Prompt = lipgloss.NewStyle().Foreground(lipgloss.Color("117")).Bold(true)
|
||||
s.Focused.Text = lipgloss.NewStyle().Foreground(lipgloss.Color("255"))
|
||||
s.Focused.Placeholder = lipgloss.NewStyle().Foreground(lipgloss.Color("243"))
|
||||
s.Blurred.Prompt = lipgloss.NewStyle().Foreground(lipgloss.Color("245"))
|
||||
s.Blurred.Text = lipgloss.NewStyle().Foreground(lipgloss.Color("245"))
|
||||
s.Blurred.Placeholder = lipgloss.NewStyle().Foreground(lipgloss.Color("240"))
|
||||
return s
|
||||
}
|
||||
|
||||
func getDefaultTextareaStyles(isDark bool) textarea.Styles {
|
||||
s := textarea.DefaultStyles(isDark)
|
||||
s.Focused.Prompt = lipgloss.NewStyle().Foreground(lipgloss.Color("117")).Bold(true)
|
||||
s.Focused.Text = lipgloss.NewStyle().Foreground(lipgloss.Color("255"))
|
||||
s.Focused.Placeholder = lipgloss.NewStyle().Foreground(lipgloss.Color("243"))
|
||||
s.Focused.Base = lipgloss.NewStyle().Foreground(lipgloss.Color("255"))
|
||||
s.Blurred.Prompt = lipgloss.NewStyle().Foreground(lipgloss.Color("245"))
|
||||
s.Blurred.Text = lipgloss.NewStyle().Foreground(lipgloss.Color("245"))
|
||||
s.Blurred.Placeholder = lipgloss.NewStyle().Foreground(lipgloss.Color("240"))
|
||||
s.Blurred.Base = lipgloss.NewStyle().Foreground(lipgloss.Color("245"))
|
||||
return s
|
||||
}
|
||||
Reference in New Issue
Block a user