diff --git a/internal/tui/checkbox.go b/internal/tui/checkbox.go index f72e2d3..a2e45a8 100644 --- a/internal/tui/checkbox.go +++ b/internal/tui/checkbox.go @@ -21,11 +21,9 @@ func (c Checkbox) Render(focused bool) string { marker = uncheckedStyle.Render(marker) } - label := labelStyle.Render(c.Label) if focused { - label = focusedStyle.Render("▶ " + marker + " " + c.Label) - return label + return focusedStyle.Render("▶ " + marker + " " + c.Label) } - return " " + marker + " " + label + return " " + marker + " " + labelStyle.Render(c.Label) } diff --git a/internal/tui/form.go b/internal/tui/form.go index 786c326..fc59a66 100644 --- a/internal/tui/form.go +++ b/internal/tui/form.go @@ -14,7 +14,7 @@ import ( const ( fieldInputDir = iota - fieldOutputFile + fieldOutputDir fieldAppend fieldForce fieldVerticalSlices @@ -35,7 +35,7 @@ const ( type FormModel struct { focusIndex int inputDir textinput.Model - outputFile textinput.Model + outputDir textinput.Model drivePath textinput.Model maxDepth textinput.Model maxSize textinput.Model @@ -60,10 +60,10 @@ func NewForm() FormModel { inputDir.SetValue(".") inputDir.SetWidth(60) - outputFile := textinput.New() - outputFile.Placeholder = "./output_dirmd/index.md" - outputFile.SetValue("./output_dirmd/index.md") - outputFile.SetWidth(60) + outputDir := textinput.New() + outputDir.Placeholder = "./output_dirmd" + outputDir.SetValue("./output_dirmd") + outputDir.SetWidth(60) drivePath := textinput.New() drivePath.Placeholder = "/my-files/md/" @@ -85,7 +85,7 @@ func NewForm() FormModel { m := FormModel{ inputDir: inputDir, - outputFile: outputFile, + outputDir: outputDir, drivePath: drivePath, maxDepth: maxDepth, maxSize: maxSize, @@ -118,7 +118,7 @@ func (m *FormModel) SetDark(isDark bool) { func (m *FormModel) applyStyles() { tiStyles := getDefaultInputStyles(m.isDark) m.inputDir.SetStyles(tiStyles) - m.outputFile.SetStyles(tiStyles) + m.outputDir.SetStyles(tiStyles) m.drivePath.SetStyles(tiStyles) m.maxDepth.SetStyles(tiStyles) m.maxSize.SetStyles(tiStyles) @@ -133,7 +133,7 @@ func (m *FormModel) shouldSkipField(idx int) bool { func isTextInputField(idx int) bool { switch idx { - case fieldInputDir, fieldOutputFile, fieldDrivePath, fieldMaxDepth, fieldMaxSize, fieldIgnorePatterns: + case fieldInputDir, fieldOutputDir, fieldDrivePath, fieldMaxDepth, fieldMaxSize, fieldIgnorePatterns: return true } return false @@ -141,7 +141,7 @@ func isTextInputField(idx int) bool { func (m *FormModel) focusActiveField() tea.Cmd { m.inputDir.Blur() - m.outputFile.Blur() + m.outputDir.Blur() m.drivePath.Blur() m.maxDepth.Blur() m.maxSize.Blur() @@ -152,8 +152,8 @@ func (m *FormModel) focusActiveField() tea.Cmd { switch m.focusIndex { case fieldInputDir: cmd = m.inputDir.Focus() - case fieldOutputFile: - cmd = m.outputFile.Focus() + case fieldOutputDir: + cmd = m.outputDir.Focus() case fieldDrivePath: cmd = m.drivePath.Focus() case fieldMaxDepth: @@ -269,9 +269,9 @@ func (m FormModel) Update(msg tea.Msg) (FormModel, tea.Cmd) { var cmd tea.Cmd m.inputDir, cmd = m.inputDir.Update(msg) cmds = append(cmds, cmd) - case fieldOutputFile: + case fieldOutputDir: var cmd tea.Cmd - m.outputFile, cmd = m.outputFile.Update(msg) + m.outputDir, cmd = m.outputDir.Update(msg) cmds = append(cmds, cmd) case fieldDrivePath: var cmd tea.Cmd @@ -303,7 +303,7 @@ func (m FormModel) View() string { usableWidth = 80 } m.inputDir.SetWidth(usableWidth) - m.outputFile.SetWidth(usableWidth) + m.outputDir.SetWidth(usableWidth) m.drivePath.SetWidth(usableWidth) m.ignoreArea.SetWidth(usableWidth) @@ -314,7 +314,7 @@ func (m FormModel) View() string { b.WriteString(m.renderTextField("Input directory", m.inputDir, m.focusIndex == fieldInputDir)) b.WriteString("\n") - b.WriteString(m.renderTextField("Output file", m.outputFile, m.focusIndex == fieldOutputFile)) + b.WriteString(m.renderTextField("Output directory", m.outputDir, m.focusIndex == fieldOutputDir)) b.WriteString("\n\n") b.WriteString(m.appendMode.Render(m.focusIndex == fieldAppend)) @@ -398,9 +398,9 @@ func (m FormModel) ToConfig() (*config.Config, error) { inputDir = "." } - outputFile := m.outputFile.Value() - if outputFile == "" { - return nil, fmt.Errorf("output file required") + outputDir := m.outputDir.Value() + if outputDir == "" { + return nil, fmt.Errorf("output directory required") } maxDepth, err := parseIntDefault(m.maxDepth.Value(), 20) @@ -442,7 +442,7 @@ func (m FormModel) ToConfig() (*config.Config, error) { cfg := &config.Config{ InputPath: inputDir, - OutputPath: outputFile, + OutputPath: outputDir, AppendMode: m.appendMode.Checked, Force: m.force.Checked, MaxSize: int64(maxSizeKB) * 1024, diff --git a/internal/tui/generate.go b/internal/tui/generate.go index 2a9ed4c..70fc76b 100644 --- a/internal/tui/generate.go +++ b/internal/tui/generate.go @@ -27,7 +27,7 @@ func Generate(cfg *config.Config) GenerateResult { } cfg.AbsRoot = abs - var outputBase string + var outputDir string if cfg.AppendMode || cfg.SingleFile { absOutput, err := filepath.Abs(cfg.OutputPath) @@ -49,18 +49,17 @@ func Generate(cfg *config.Config) GenerateResult { return result } - outputBase = absOutput + outputDir = absOutput } else { absOutput, err := filepath.Abs(cfg.OutputPath) if err != nil { result.Error = fmt.Errorf("resolve output path: %w", err) return result } - absOutput = filepath.Join(filepath.Dir(absOutput), filepath.Base(absOutput)+"_dirmd") - if !cfg.AppendMode { - if _, err := os.Stat(absOutput); err == nil && !cfg.Force { - result.Error = fmt.Errorf("output %s exists (enable force)", absOutput) + if _, err := os.Stat(absOutput); err == nil && !cfg.Force { + if info, err2 := os.Stat(absOutput); err2 == nil && !info.IsDir() { + result.Error = fmt.Errorf("output %s exists and is not a directory (enable force)", absOutput) return result } } @@ -71,7 +70,7 @@ func Generate(cfg *config.Config) GenerateResult { } cfg.OutputPath = absOutput - outputBase = absOutput + outputDir = absOutput } walkResult, err := walker.Run(cfg) @@ -96,6 +95,10 @@ func Generate(cfg *config.Config) GenerateResult { result.Files = generated } else { indexPath := cfg.OutputPath + if !cfg.AppendMode && !cfg.SingleFile { + indexPath = filepath.Join(outputDir, "index_"+repoName+".md") + } + if err := renderer.Write(walkResult, indexPath, cfg.AppendMode, cfg.SingleFile); err != nil { result.Error = err return result @@ -106,7 +109,7 @@ func Generate(cfg *config.Config) GenerateResult { if cfg.ProtonDrive { remoteDir := cfg.DrivePath if remoteDir == "" { - remoteDir = "/my-files/md/" + filepath.Base(outputBase) + remoteDir = "/my-files/md/" + filepath.Base(outputDir) } if err := drive.CheckAuth(); err != nil { @@ -121,6 +124,9 @@ func Generate(cfg *config.Config) GenerateResult { } } else { indexPath := cfg.OutputPath + if !cfg.AppendMode && !cfg.SingleFile { + indexPath = filepath.Join(outputDir, "index_"+repoName+".md") + } if err := drive.Upload(indexPath, remoteDir); err != nil { result.Error = fmt.Errorf("drive upload: %w", err) return result diff --git a/internal/tui/model.go b/internal/tui/model.go index 4b2e017..90f50c3 100644 --- a/internal/tui/model.go +++ b/internal/tui/model.go @@ -7,6 +7,8 @@ import ( "charm.land/bubbles/v2/textinput" tea "charm.land/bubbletea/v2" "charm.land/lipgloss/v2" + + "dirmd/internal/config" ) type State int @@ -39,6 +41,7 @@ type Model struct { isDark bool width int height int + command string } func New() Model { @@ -79,6 +82,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 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)} @@ -114,6 +118,70 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.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.AppendMode { + parts = append(parts, "-a") + } + 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 diff --git a/internal/tui/run.go b/internal/tui/run.go index 78992f9..eae2be9 100644 --- a/internal/tui/run.go +++ b/internal/tui/run.go @@ -2,15 +2,28 @@ package tui import ( "fmt" + "os" tea "charm.land/bubbletea/v2" ) func Run() error { p := tea.NewProgram(New()) - _, err := p.Run() + m, err := p.Run() if err != nil { return fmt.Errorf("tui error: %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) + } + } + } + return nil }