diff --git a/internal/tui/form.go b/internal/tui/form.go index 3635b1e..0fd7177 100644 --- a/internal/tui/form.go +++ b/internal/tui/form.go @@ -13,10 +13,14 @@ import ( const ( fieldInputDir = iota - fieldOutputDir + fieldOutputFile + fieldAppend fieldForce fieldVerticalSlices fieldSkipFrontend + fieldFrontendOnly + fieldOmitTest + fieldOmitMd fieldProtonDrive fieldDrivePath fieldMaxDepth @@ -30,15 +34,20 @@ const ( type FormModel struct { focusIndex int inputDir textinput.Model - outputDir textinput.Model + outputFile textinput.Model drivePath textinput.Model maxDepth textinput.Model maxSize textinput.Model ignoreArea textarea.Model + appendMode Checkbox force Checkbox verticalSlices Checkbox skipFrontend Checkbox + frontendOnly Checkbox + omitTest Checkbox + omitMd Checkbox protonDrive Checkbox + isDark bool width int height int err string @@ -50,10 +59,10 @@ func NewForm() FormModel { inputDir.SetValue(".") inputDir.SetWidth(40) - outputDir := textinput.New() - outputDir.Placeholder = "./dirmd_output" - outputDir.SetValue("./dirmd_output") - outputDir.SetWidth(40) + outputFile := textinput.New() + outputFile.Placeholder = "./output.md" + outputFile.SetValue("./output.md") + outputFile.SetWidth(40) drivePath := textinput.New() drivePath.Placeholder = "/my-files/md/" @@ -74,44 +83,74 @@ func NewForm() FormModel { m := FormModel{ inputDir: inputDir, - outputDir: outputDir, + outputFile: outputFile, drivePath: drivePath, maxDepth: maxDepth, maxSize: maxSize, ignoreArea: ignoreArea, + appendMode: Checkbox{Label: "Append to existing file"}, 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) focusActiveField() { +func (m *FormModel) SetDark(isDark bool) { + if m.isDark == isDark { + return + } + m.isDark = isDark + m.applyStyles() +} + +func (m *FormModel) applyStyles() { + tiStyles := textinput.DefaultStyles(m.isDark) + m.inputDir.SetStyles(tiStyles) + m.outputFile.SetStyles(tiStyles) + m.drivePath.SetStyles(tiStyles) + m.maxDepth.SetStyles(tiStyles) + m.maxSize.SetStyles(tiStyles) + + taStyles := textarea.DefaultStyles(m.isDark) + m.ignoreArea.SetStyles(taStyles) +} + +func (m *FormModel) focusActiveField() tea.Cmd { m.inputDir.Blur() - m.outputDir.Blur() + m.outputFile.Blur() m.drivePath.Blur() m.maxDepth.Blur() m.maxSize.Blur() m.ignoreArea.Blur() + var cmd tea.Cmd + switch m.focusIndex { case fieldInputDir: - m.inputDir.Focus() - case fieldOutputDir: - m.outputDir.Focus() + cmd = m.inputDir.Focus() + case fieldOutputFile: + cmd = m.outputFile.Focus() case fieldDrivePath: - m.drivePath.Focus() + cmd = m.drivePath.Focus() case fieldMaxDepth: - m.maxDepth.Focus() + cmd = m.maxDepth.Focus() case fieldMaxSize: - m.maxSize.Focus() + cmd = m.maxSize.Focus() case fieldIgnorePatterns: - m.ignoreArea.Focus() + cmd = m.ignoreArea.Focus() } + + return cmd } func (m FormModel) Update(msg tea.Msg) (FormModel, tea.Cmd) { @@ -125,10 +164,13 @@ func (m FormModel) Update(msg tea.Msg) (FormModel, tea.Cmd) { m.focusIndex = 0 } if m.focusIndex == fieldDrivePath && !m.protonDrive.Checked { - m.focusIndex = fieldMaxDepth + m.focusIndex = fieldProtonDrive } - m.focusActiveField() - return m, nil + cmd := m.focusActiveField() + if cmd != nil { + cmds = append(cmds, cmd) + } + return m, tea.Batch(cmds...) case isNavigateUp(k): m.focusIndex-- if m.focusIndex < 0 { @@ -137,16 +179,27 @@ func (m FormModel) Update(msg tea.Msg) (FormModel, tea.Cmd) { if m.focusIndex == fieldDrivePath && !m.protonDrive.Checked { m.focusIndex = fieldProtonDrive } - m.focusActiveField() - return m, nil + cmd := m.focusActiveField() + if cmd != nil { + cmds = append(cmds, cmd) + } + return m, tea.Batch(cmds...) case isActivate(k): switch m.focusIndex { + case fieldAppend: + m.appendMode.Toggle() 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: @@ -163,9 +216,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 fieldOutputDir: + case fieldOutputFile: var cmd tea.Cmd - m.outputDir, cmd = m.outputDir.Update(msg) + m.outputFile, cmd = m.outputFile.Update(msg) cmds = append(cmds, cmd) case fieldDrivePath: var cmd tea.Cmd @@ -196,15 +249,23 @@ func (m FormModel) View() string { 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(m.renderTextField("Output file", m.outputFile, m.focusIndex == fieldOutputFile)) b.WriteString("\n\n") + b.WriteString(m.appendMode.Render(m.focusIndex == fieldAppend)) + b.WriteString("\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") @@ -275,9 +336,9 @@ func (m FormModel) ToConfig() (*config.Config, error) { inputDir = "." } - outputDir := m.outputDir.Value() - if outputDir == "" { - return nil, fmt.Errorf("output directory required") + outputFile := m.outputFile.Value() + if outputFile == "" { + return nil, fmt.Errorf("output file required") } maxDepth, err := parseIntDefault(m.maxDepth.Value(), 20) @@ -301,6 +362,14 @@ func (m FormModel) ToConfig() (*config.Config, error) { } } + if m.skipFrontend.Checked && m.frontendOnly.Checked { + return nil, fmt.Errorf("--skip-frontend and --frontend-only are mutually exclusive") + } + + if m.appendMode.Checked && m.verticalSlices.Checked { + return nil, fmt.Errorf("--append and --vertical-slices are mutually exclusive") + } + excludedExts := append([]string{}, config.DefaultExts...) if m.skipFrontend.Checked { excludedExts = append(excludedExts, config.FrontendExts...) @@ -311,7 +380,8 @@ func (m FormModel) ToConfig() (*config.Config, error) { cfg := &config.Config{ InputPath: inputDir, - OutputPath: outputDir, + OutputPath: outputFile, + AppendMode: m.appendMode.Checked, Force: m.force.Checked, MaxSize: int64(maxSizeKB) * 1024, MaxDepth: maxDepth, @@ -321,6 +391,9 @@ func (m FormModel) ToConfig() (*config.Config, error) { 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 @@ -341,7 +414,7 @@ func (m *FormModel) SetSize(w, h int) { usable = 60 } m.inputDir.SetWidth(usable) - m.outputDir.SetWidth(usable) + m.outputFile.SetWidth(usable) m.drivePath.SetWidth(usable) } diff --git a/internal/tui/generate.go b/internal/tui/generate.go index faab09a..2a9ed4c 100644 --- a/internal/tui/generate.go +++ b/internal/tui/generate.go @@ -27,23 +27,51 @@ func Generate(cfg *config.Config) GenerateResult { } cfg.AbsRoot = abs - 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") + var outputBase string - if !cfg.AppendMode { - if _, err := os.Stat(absOutput); err == nil && !cfg.Force { - result.Error = fmt.Errorf("output %s exists (enable force)", absOutput) + if cfg.AppendMode || cfg.SingleFile { + absOutput, err := filepath.Abs(cfg.OutputPath) + if err != nil { + result.Error = fmt.Errorf("resolve output path: %w", err) return result } - } - if err := os.MkdirAll(absOutput, 0755); err != nil { - result.Error = fmt.Errorf("create output dir: %w", err) - return result + if !cfg.AppendMode { + if _, err := os.Stat(absOutput); err == nil && !cfg.Force { + result.Error = fmt.Errorf("output %s exists (enable force or append)", absOutput) + return result + } + } + + dir := filepath.Dir(absOutput) + if err := os.MkdirAll(dir, 0755); err != nil { + result.Error = fmt.Errorf("create output dir: %w", err) + return result + } + + outputBase = 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) + return result + } + } + + if err := os.MkdirAll(absOutput, 0755); err != nil { + result.Error = fmt.Errorf("create output dir: %w", err) + return result + } + + cfg.OutputPath = absOutput + outputBase = absOutput } walkResult, err := walker.Run(cfg) @@ -60,15 +88,14 @@ func Generate(cfg *config.Config) GenerateResult { repoName := filepath.Base(cfg.AbsRoot) if cfg.VerticalSlices { - generated, err := renderer.WriteSlices(walkResult, absOutput, repoName) + generated, err := renderer.WriteSlices(walkResult, cfg.OutputPath, repoName) if err != nil { result.Error = err return result } result.Files = generated } else { - indexBasename := "index_" + repoName + ".md" - indexPath := filepath.Join(absOutput, indexBasename) + indexPath := cfg.OutputPath if err := renderer.Write(walkResult, indexPath, cfg.AppendMode, cfg.SingleFile); err != nil { result.Error = err return result @@ -79,7 +106,7 @@ func Generate(cfg *config.Config) GenerateResult { if cfg.ProtonDrive { remoteDir := cfg.DrivePath if remoteDir == "" { - remoteDir = "/my-files/md/" + filepath.Base(absOutput) + remoteDir = "/my-files/md/" + filepath.Base(outputBase) } if err := drive.CheckAuth(); err != nil { @@ -87,9 +114,17 @@ func Generate(cfg *config.Config) GenerateResult { return result } - if err := drive.UploadDir(absOutput, remoteDir); err != nil { - result.Error = fmt.Errorf("drive upload: %w", err) - return result + if cfg.VerticalSlices { + if err := drive.UploadDir(cfg.OutputPath, remoteDir); err != nil { + result.Error = fmt.Errorf("drive upload: %w", err) + return result + } + } else { + indexPath := cfg.OutputPath + if err := drive.Upload(indexPath, remoteDir); err != nil { + result.Error = fmt.Errorf("drive upload: %w", err) + return result + } } result.Uploaded = true } diff --git a/internal/tui/keys.go b/internal/tui/keys.go index 4c45f75..59e00e1 100644 --- a/internal/tui/keys.go +++ b/internal/tui/keys.go @@ -21,7 +21,7 @@ func isNavigateUp(msg tea.KeyPressMsg) bool { func isActivate(msg tea.KeyPressMsg) bool { s := msg.String() - return s == "enter" || s == " " + return s == "enter" || s == "space" } func isSubmit(msg tea.KeyPressMsg) bool { diff --git a/internal/tui/model.go b/internal/tui/model.go index 6ac9f52..91eed28 100644 --- a/internal/tui/model.go +++ b/internal/tui/model.go @@ -35,6 +35,7 @@ type Model struct { form FormModel result GenerateResult spinner spinnerState + isDark bool width int height int } @@ -43,6 +44,7 @@ func New() Model { return Model{ state: StateForm, form: NewForm(), + isDark: true, } } @@ -52,6 +54,11 @@ func (m Model) Init() tea.Cmd { 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 diff --git a/internal/tui/run.go b/internal/tui/run.go index 7df772c..78992f9 100644 --- a/internal/tui/run.go +++ b/internal/tui/run.go @@ -8,7 +8,8 @@ import ( func Run() error { p := tea.NewProgram(New()) - if _, err := p.Run(); err != nil { + _, err := p.Run() + if err != nil { return fmt.Errorf("tui error: %w", err) } return nil