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 (
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)
}
+55 -20
View File
@@ -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
}
+1 -1
View File
@@ -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 {
+7
View File
@@ -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
+2 -1
View File
@@ -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