add tui
This commit is contained in:
@@ -0,0 +1,362 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"charm.land/bubbles/v2/textarea"
|
||||
"charm.land/bubbles/v2/textinput"
|
||||
tea "charm.land/bubbletea/v2"
|
||||
|
||||
"dirmd/internal/config"
|
||||
)
|
||||
|
||||
const (
|
||||
fieldInputDir = iota
|
||||
fieldOutputDir
|
||||
fieldForce
|
||||
fieldVerticalSlices
|
||||
fieldSkipFrontend
|
||||
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
|
||||
protonDrive Checkbox
|
||||
width int
|
||||
height int
|
||||
err string
|
||||
}
|
||||
|
||||
func NewForm() FormModel {
|
||||
inputDir := textinput.New()
|
||||
inputDir.Placeholder = "."
|
||||
inputDir.SetValue(".")
|
||||
inputDir.SetWidth(40)
|
||||
|
||||
outputDir := textinput.New()
|
||||
outputDir.Placeholder = "./dirmd_output"
|
||||
outputDir.SetValue("./dirmd_output")
|
||||
outputDir.SetWidth(40)
|
||||
|
||||
drivePath := textinput.New()
|
||||
drivePath.Placeholder = "/my-files/md/"
|
||||
drivePath.SetValue("")
|
||||
drivePath.SetWidth(40)
|
||||
|
||||
maxDepth := textinput.New()
|
||||
maxDepth.SetValue("20")
|
||||
maxDepth.SetWidth(5)
|
||||
|
||||
maxSize := textinput.New()
|
||||
maxSize.SetValue("512")
|
||||
maxSize.SetWidth(5)
|
||||
|
||||
ignoreArea := textarea.New()
|
||||
ignoreArea.Placeholder = "node_modules\n.venv"
|
||||
ignoreArea.SetHeight(3)
|
||||
|
||||
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"},
|
||||
protonDrive: Checkbox{Label: "Proton Drive upload"},
|
||||
focusIndex: fieldInputDir,
|
||||
}
|
||||
|
||||
m.focusActiveField()
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *FormModel) focusActiveField() {
|
||||
m.inputDir.Blur()
|
||||
m.outputDir.Blur()
|
||||
m.drivePath.Blur()
|
||||
m.maxDepth.Blur()
|
||||
m.maxSize.Blur()
|
||||
m.ignoreArea.Blur()
|
||||
|
||||
switch m.focusIndex {
|
||||
case fieldInputDir:
|
||||
m.inputDir.Focus()
|
||||
case fieldOutputDir:
|
||||
m.outputDir.Focus()
|
||||
case fieldDrivePath:
|
||||
m.drivePath.Focus()
|
||||
case fieldMaxDepth:
|
||||
m.maxDepth.Focus()
|
||||
case fieldMaxSize:
|
||||
m.maxSize.Focus()
|
||||
case fieldIgnorePatterns:
|
||||
m.ignoreArea.Focus()
|
||||
}
|
||||
}
|
||||
|
||||
func (m FormModel) Update(msg tea.Msg) (FormModel, tea.Cmd) {
|
||||
var cmds []tea.Cmd
|
||||
|
||||
if k, ok := msg.(tea.KeyPressMsg); ok {
|
||||
switch {
|
||||
case isNavigateDown(k):
|
||||
m.focusIndex++
|
||||
if m.focusIndex >= fieldCount {
|
||||
m.focusIndex = 0
|
||||
}
|
||||
if m.focusIndex == fieldDrivePath && !m.protonDrive.Checked {
|
||||
m.focusIndex = fieldMaxDepth
|
||||
}
|
||||
m.focusActiveField()
|
||||
return m, nil
|
||||
case isNavigateUp(k):
|
||||
m.focusIndex--
|
||||
if m.focusIndex < 0 {
|
||||
m.focusIndex = fieldCount - 1
|
||||
}
|
||||
if m.focusIndex == fieldDrivePath && !m.protonDrive.Checked {
|
||||
m.focusIndex = fieldProtonDrive
|
||||
}
|
||||
m.focusActiveField()
|
||||
return m, nil
|
||||
case isActivate(k):
|
||||
switch m.focusIndex {
|
||||
case fieldForce:
|
||||
m.force.Toggle()
|
||||
case fieldVerticalSlices:
|
||||
m.verticalSlices.Toggle()
|
||||
case fieldSkipFrontend:
|
||||
m.skipFrontend.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 {
|
||||
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.protonDrive.Render(m.focusIndex == fieldProtonDrive))
|
||||
b.WriteString("\n")
|
||||
|
||||
if m.protonDrive.Checked {
|
||||
b.WriteString(" ")
|
||||
b.WriteString(m.renderTextField("Remote path", m.drivePath, m.focusIndex == fieldDrivePath))
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
b.WriteString("\n")
|
||||
depthLabel := labelStyle.Render("Max depth:")
|
||||
if m.focusIndex == fieldMaxDepth {
|
||||
depthLabel = focusedStyle.Bold(true).Render("Max depth:")
|
||||
}
|
||||
sizeLabel := labelStyle.Render("Max size (KB):")
|
||||
if m.focusIndex == fieldMaxSize {
|
||||
sizeLabel = focusedStyle.Bold(true).Render("Max size (KB):")
|
||||
}
|
||||
b.WriteString(fmt.Sprintf("%s %s %s %s",
|
||||
depthLabel, m.maxDepth.View(),
|
||||
sizeLabel, m.maxSize.View(),
|
||||
))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
ignoreLabel := labelStyle.Render("Ignore patterns:")
|
||||
if m.focusIndex == fieldIgnorePatterns {
|
||||
ignoreLabel = focusedStyle.Bold(true).Render("Ignore patterns:")
|
||||
}
|
||||
b.WriteString(ignoreLabel)
|
||||
b.WriteString("\n")
|
||||
b.WriteString(m.ignoreArea.View())
|
||||
b.WriteString("\n\n")
|
||||
|
||||
btnStyle := labelStyle
|
||||
if m.focusIndex == fieldGenerateBtn {
|
||||
btnStyle = focusedStyle.Bold(true)
|
||||
}
|
||||
b.WriteString(btnStyle.Render("[ Generate ]"))
|
||||
b.WriteString(" ")
|
||||
cancelStyle := labelStyle
|
||||
if m.focusIndex == fieldCancelBtn {
|
||||
cancelStyle = focusedStyle.Bold(true)
|
||||
}
|
||||
b.WriteString(cancelStyle.Render("[ Cancel ]"))
|
||||
|
||||
if m.err != "" {
|
||||
b.WriteString("\n\n")
|
||||
b.WriteString(errorStyle.Render(m.err))
|
||||
}
|
||||
|
||||
b.WriteString("\n")
|
||||
b.WriteString(footerStyle.Render("Tab/j/k: navigate Space: toggle Enter: submit Esc: quit"))
|
||||
|
||||
return appBorder.Render(b.String())
|
||||
}
|
||||
|
||||
func (m FormModel) renderTextField(label string, input textinput.Model, focused bool) string {
|
||||
style := labelStyle
|
||||
if focused {
|
||||
style = focusedStyle
|
||||
}
|
||||
return fmt.Sprintf("%s %s", style.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
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
|
||||
usable := w - 30
|
||||
if usable < 20 {
|
||||
usable = 20
|
||||
}
|
||||
if usable > 60 {
|
||||
usable = 60
|
||||
}
|
||||
m.inputDir.SetWidth(usable)
|
||||
m.outputDir.SetWidth(usable)
|
||||
m.drivePath.SetWidth(usable)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user