working on tui

This commit is contained in:
2026-08-27 15:49:18 +01:00
parent 3ea14b91c0
commit 71961f68da
5 changed files with 167 additions and 51 deletions
+102 -29
View File
@@ -13,10 +13,14 @@ import (
const ( const (
fieldInputDir = iota fieldInputDir = iota
fieldOutputDir fieldOutputFile
fieldAppend
fieldForce fieldForce
fieldVerticalSlices fieldVerticalSlices
fieldSkipFrontend fieldSkipFrontend
fieldFrontendOnly
fieldOmitTest
fieldOmitMd
fieldProtonDrive fieldProtonDrive
fieldDrivePath fieldDrivePath
fieldMaxDepth fieldMaxDepth
@@ -30,15 +34,20 @@ const (
type FormModel struct { type FormModel struct {
focusIndex int focusIndex int
inputDir textinput.Model inputDir textinput.Model
outputDir textinput.Model outputFile textinput.Model
drivePath textinput.Model drivePath textinput.Model
maxDepth textinput.Model maxDepth textinput.Model
maxSize textinput.Model maxSize textinput.Model
ignoreArea textarea.Model ignoreArea textarea.Model
appendMode Checkbox
force Checkbox force Checkbox
verticalSlices Checkbox verticalSlices Checkbox
skipFrontend Checkbox skipFrontend Checkbox
frontendOnly Checkbox
omitTest Checkbox
omitMd Checkbox
protonDrive Checkbox protonDrive Checkbox
isDark bool
width int width int
height int height int
err string err string
@@ -50,10 +59,10 @@ func NewForm() FormModel {
inputDir.SetValue(".") inputDir.SetValue(".")
inputDir.SetWidth(40) inputDir.SetWidth(40)
outputDir := textinput.New() outputFile := textinput.New()
outputDir.Placeholder = "./dirmd_output" outputFile.Placeholder = "./output.md"
outputDir.SetValue("./dirmd_output") outputFile.SetValue("./output.md")
outputDir.SetWidth(40) outputFile.SetWidth(40)
drivePath := textinput.New() drivePath := textinput.New()
drivePath.Placeholder = "/my-files/md/" drivePath.Placeholder = "/my-files/md/"
@@ -74,44 +83,74 @@ func NewForm() FormModel {
m := FormModel{ m := FormModel{
inputDir: inputDir, inputDir: inputDir,
outputDir: outputDir, outputFile: outputFile,
drivePath: drivePath, drivePath: drivePath,
maxDepth: maxDepth, maxDepth: maxDepth,
maxSize: maxSize, maxSize: maxSize,
ignoreArea: ignoreArea, ignoreArea: ignoreArea,
appendMode: Checkbox{Label: "Append to existing file"},
force: Checkbox{Label: "Force overwrite"}, force: Checkbox{Label: "Force overwrite"},
verticalSlices: Checkbox{Label: "Vertical slices"}, verticalSlices: Checkbox{Label: "Vertical slices"},
skipFrontend: Checkbox{Label: "Skip frontend files"}, 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"}, protonDrive: Checkbox{Label: "Proton Drive upload"},
focusIndex: fieldInputDir, focusIndex: fieldInputDir,
isDark: true,
} }
m.applyStyles()
m.focusActiveField() m.focusActiveField()
return m 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.inputDir.Blur()
m.outputDir.Blur() m.outputFile.Blur()
m.drivePath.Blur() m.drivePath.Blur()
m.maxDepth.Blur() m.maxDepth.Blur()
m.maxSize.Blur() m.maxSize.Blur()
m.ignoreArea.Blur() m.ignoreArea.Blur()
var cmd tea.Cmd
switch m.focusIndex { switch m.focusIndex {
case fieldInputDir: case fieldInputDir:
m.inputDir.Focus() cmd = m.inputDir.Focus()
case fieldOutputDir: case fieldOutputFile:
m.outputDir.Focus() cmd = m.outputFile.Focus()
case fieldDrivePath: case fieldDrivePath:
m.drivePath.Focus() cmd = m.drivePath.Focus()
case fieldMaxDepth: case fieldMaxDepth:
m.maxDepth.Focus() cmd = m.maxDepth.Focus()
case fieldMaxSize: case fieldMaxSize:
m.maxSize.Focus() cmd = m.maxSize.Focus()
case fieldIgnorePatterns: case fieldIgnorePatterns:
m.ignoreArea.Focus() cmd = m.ignoreArea.Focus()
} }
return cmd
} }
func (m FormModel) Update(msg tea.Msg) (FormModel, tea.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 m.focusIndex = 0
} }
if m.focusIndex == fieldDrivePath && !m.protonDrive.Checked { if m.focusIndex == fieldDrivePath && !m.protonDrive.Checked {
m.focusIndex = fieldMaxDepth m.focusIndex = fieldProtonDrive
} }
m.focusActiveField() cmd := m.focusActiveField()
return m, nil if cmd != nil {
cmds = append(cmds, cmd)
}
return m, tea.Batch(cmds...)
case isNavigateUp(k): case isNavigateUp(k):
m.focusIndex-- m.focusIndex--
if m.focusIndex < 0 { 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 { if m.focusIndex == fieldDrivePath && !m.protonDrive.Checked {
m.focusIndex = fieldProtonDrive m.focusIndex = fieldProtonDrive
} }
m.focusActiveField() cmd := m.focusActiveField()
return m, nil if cmd != nil {
cmds = append(cmds, cmd)
}
return m, tea.Batch(cmds...)
case isActivate(k): case isActivate(k):
switch m.focusIndex { switch m.focusIndex {
case fieldAppend:
m.appendMode.Toggle()
case fieldForce: case fieldForce:
m.force.Toggle() m.force.Toggle()
case fieldVerticalSlices: case fieldVerticalSlices:
m.verticalSlices.Toggle() m.verticalSlices.Toggle()
case fieldSkipFrontend: case fieldSkipFrontend:
m.skipFrontend.Toggle() m.skipFrontend.Toggle()
case fieldFrontendOnly:
m.frontendOnly.Toggle()
case fieldOmitTest:
m.omitTest.Toggle()
case fieldOmitMd:
m.omitMd.Toggle()
case fieldProtonDrive: case fieldProtonDrive:
m.protonDrive.Toggle() m.protonDrive.Toggle()
case fieldGenerateBtn: case fieldGenerateBtn:
@@ -163,9 +216,9 @@ func (m FormModel) Update(msg tea.Msg) (FormModel, tea.Cmd) {
var cmd tea.Cmd var cmd tea.Cmd
m.inputDir, cmd = m.inputDir.Update(msg) m.inputDir, cmd = m.inputDir.Update(msg)
cmds = append(cmds, cmd) cmds = append(cmds, cmd)
case fieldOutputDir: case fieldOutputFile:
var cmd tea.Cmd var cmd tea.Cmd
m.outputDir, cmd = m.outputDir.Update(msg) m.outputFile, cmd = m.outputFile.Update(msg)
cmds = append(cmds, cmd) cmds = append(cmds, cmd)
case fieldDrivePath: case fieldDrivePath:
var cmd tea.Cmd 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(m.renderTextField("Input directory", m.inputDir, m.focusIndex == fieldInputDir))
b.WriteString("\n") 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("\n\n")
b.WriteString(m.appendMode.Render(m.focusIndex == fieldAppend))
b.WriteString("\n")
b.WriteString(m.force.Render(m.focusIndex == fieldForce)) b.WriteString(m.force.Render(m.focusIndex == fieldForce))
b.WriteString("\n") b.WriteString("\n")
b.WriteString(m.verticalSlices.Render(m.focusIndex == fieldVerticalSlices)) b.WriteString(m.verticalSlices.Render(m.focusIndex == fieldVerticalSlices))
b.WriteString("\n") b.WriteString("\n")
b.WriteString(m.skipFrontend.Render(m.focusIndex == fieldSkipFrontend)) b.WriteString(m.skipFrontend.Render(m.focusIndex == fieldSkipFrontend))
b.WriteString("\n") 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(m.protonDrive.Render(m.focusIndex == fieldProtonDrive))
b.WriteString("\n") b.WriteString("\n")
@@ -275,9 +336,9 @@ func (m FormModel) ToConfig() (*config.Config, error) {
inputDir = "." inputDir = "."
} }
outputDir := m.outputDir.Value() outputFile := m.outputFile.Value()
if outputDir == "" { if outputFile == "" {
return nil, fmt.Errorf("output directory required") return nil, fmt.Errorf("output file required")
} }
maxDepth, err := parseIntDefault(m.maxDepth.Value(), 20) 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...) excludedExts := append([]string{}, config.DefaultExts...)
if m.skipFrontend.Checked { if m.skipFrontend.Checked {
excludedExts = append(excludedExts, config.FrontendExts...) excludedExts = append(excludedExts, config.FrontendExts...)
@@ -311,7 +380,8 @@ func (m FormModel) ToConfig() (*config.Config, error) {
cfg := &config.Config{ cfg := &config.Config{
InputPath: inputDir, InputPath: inputDir,
OutputPath: outputDir, OutputPath: outputFile,
AppendMode: m.appendMode.Checked,
Force: m.force.Checked, Force: m.force.Checked,
MaxSize: int64(maxSizeKB) * 1024, MaxSize: int64(maxSizeKB) * 1024,
MaxDepth: maxDepth, MaxDepth: maxDepth,
@@ -321,6 +391,9 @@ func (m FormModel) ToConfig() (*config.Config, error) {
DrivePath: m.drivePath.Value(), DrivePath: m.drivePath.Value(),
VerticalSlices: m.verticalSlices.Checked, VerticalSlices: m.verticalSlices.Checked,
SkipFrontend: m.skipFrontend.Checked, SkipFrontend: m.skipFrontend.Checked,
FrontendOnly: m.frontendOnly.Checked,
OmitTest: m.omitTest.Checked,
OmitMd: m.omitMd.Checked,
} }
return cfg, nil return cfg, nil
@@ -341,7 +414,7 @@ func (m *FormModel) SetSize(w, h int) {
usable = 60 usable = 60
} }
m.inputDir.SetWidth(usable) m.inputDir.SetWidth(usable)
m.outputDir.SetWidth(usable) m.outputFile.SetWidth(usable)
m.drivePath.SetWidth(usable) m.drivePath.SetWidth(usable)
} }
+55 -20
View File
@@ -27,23 +27,51 @@ func Generate(cfg *config.Config) GenerateResult {
} }
cfg.AbsRoot = abs cfg.AbsRoot = abs
absOutput, err := filepath.Abs(cfg.OutputPath) var outputBase string
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 cfg.AppendMode || cfg.SingleFile {
if _, err := os.Stat(absOutput); err == nil && !cfg.Force { absOutput, err := filepath.Abs(cfg.OutputPath)
result.Error = fmt.Errorf("output %s exists (enable force)", absOutput) if err != nil {
result.Error = fmt.Errorf("resolve output path: %w", err)
return result return result
} }
}
if err := os.MkdirAll(absOutput, 0755); err != nil { if !cfg.AppendMode {
result.Error = fmt.Errorf("create output dir: %w", err) if _, err := os.Stat(absOutput); err == nil && !cfg.Force {
return result 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) walkResult, err := walker.Run(cfg)
@@ -60,15 +88,14 @@ func Generate(cfg *config.Config) GenerateResult {
repoName := filepath.Base(cfg.AbsRoot) repoName := filepath.Base(cfg.AbsRoot)
if cfg.VerticalSlices { if cfg.VerticalSlices {
generated, err := renderer.WriteSlices(walkResult, absOutput, repoName) generated, err := renderer.WriteSlices(walkResult, cfg.OutputPath, repoName)
if err != nil { if err != nil {
result.Error = err result.Error = err
return result return result
} }
result.Files = generated result.Files = generated
} else { } else {
indexBasename := "index_" + repoName + ".md" indexPath := cfg.OutputPath
indexPath := filepath.Join(absOutput, indexBasename)
if err := renderer.Write(walkResult, indexPath, cfg.AppendMode, cfg.SingleFile); err != nil { if err := renderer.Write(walkResult, indexPath, cfg.AppendMode, cfg.SingleFile); err != nil {
result.Error = err result.Error = err
return result return result
@@ -79,7 +106,7 @@ func Generate(cfg *config.Config) GenerateResult {
if cfg.ProtonDrive { if cfg.ProtonDrive {
remoteDir := cfg.DrivePath remoteDir := cfg.DrivePath
if remoteDir == "" { if remoteDir == "" {
remoteDir = "/my-files/md/" + filepath.Base(absOutput) remoteDir = "/my-files/md/" + filepath.Base(outputBase)
} }
if err := drive.CheckAuth(); err != nil { if err := drive.CheckAuth(); err != nil {
@@ -87,9 +114,17 @@ func Generate(cfg *config.Config) GenerateResult {
return result return result
} }
if err := drive.UploadDir(absOutput, remoteDir); err != nil { if cfg.VerticalSlices {
result.Error = fmt.Errorf("drive upload: %w", err) if err := drive.UploadDir(cfg.OutputPath, remoteDir); err != nil {
return result 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 result.Uploaded = true
} }
+1 -1
View File
@@ -21,7 +21,7 @@ func isNavigateUp(msg tea.KeyPressMsg) bool {
func isActivate(msg tea.KeyPressMsg) bool { func isActivate(msg tea.KeyPressMsg) bool {
s := msg.String() s := msg.String()
return s == "enter" || s == " " return s == "enter" || s == "space"
} }
func isSubmit(msg tea.KeyPressMsg) bool { func isSubmit(msg tea.KeyPressMsg) bool {
+7
View File
@@ -35,6 +35,7 @@ type Model struct {
form FormModel form FormModel
result GenerateResult result GenerateResult
spinner spinnerState spinner spinnerState
isDark bool
width int width int
height int height int
} }
@@ -43,6 +44,7 @@ func New() Model {
return Model{ return Model{
state: StateForm, state: StateForm,
form: NewForm(), 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) { func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) { switch msg := msg.(type) {
case tea.BackgroundColorMsg:
m.isDark = msg.IsDark()
m.form.SetDark(m.isDark)
return m, nil
case tea.WindowSizeMsg: case tea.WindowSizeMsg:
m.width = msg.Width m.width = msg.Width
m.height = msg.Height m.height = msg.Height
+2 -1
View File
@@ -8,7 +8,8 @@ import (
func Run() error { func Run() error {
p := tea.NewProgram(New()) p := tea.NewProgram(New())
if _, err := p.Run(); err != nil { _, err := p.Run()
if err != nil {
return fmt.Errorf("tui error: %w", err) return fmt.Errorf("tui error: %w", err)
} }
return nil return nil