This commit is contained in:
2026-08-27 20:09:59 +01:00
parent 8bfd6b22c5
commit ab670b3ecd
9 changed files with 215 additions and 880 deletions
-29
View File
@@ -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)
}
-487
View File
@@ -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
}
-29
View File
@@ -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"
}
-253
View File
@@ -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
View File
@@ -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
}
-68
View File
@@ -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
}