add tui
This commit is contained in:
+15
-3
@@ -10,6 +10,7 @@ import (
|
|||||||
"dirmd/internal/config"
|
"dirmd/internal/config"
|
||||||
"dirmd/internal/drive"
|
"dirmd/internal/drive"
|
||||||
"dirmd/internal/renderer"
|
"dirmd/internal/renderer"
|
||||||
|
"dirmd/internal/tui"
|
||||||
"dirmd/internal/walker"
|
"dirmd/internal/walker"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -51,7 +52,10 @@ Examples:
|
|||||||
dirmd -o ./docs ~/repos/myproject --proton-drive
|
dirmd -o ./docs ~/repos/myproject --proton-drive
|
||||||
|
|
||||||
# Generate and upload to a specific remote path
|
# Generate and upload to a specific remote path
|
||||||
dirmd -o ./docs ~/repos/myproject --proton-drive --drive-path /my-files/docs/api`,
|
dirmd -o ./docs ~/repos/myproject --proton-drive --drive-path /my-files/docs/api
|
||||||
|
|
||||||
|
# Interactive TUI mode
|
||||||
|
dirmd --tui`,
|
||||||
RunE: run,
|
RunE: run,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,6 +74,7 @@ Examples:
|
|||||||
frontendOnly bool
|
frontendOnly bool
|
||||||
omitTest bool
|
omitTest bool
|
||||||
omitMd bool
|
omitMd bool
|
||||||
|
tuiMode bool
|
||||||
)
|
)
|
||||||
|
|
||||||
func Execute() {
|
func Execute() {
|
||||||
@@ -94,11 +99,18 @@ func init() {
|
|||||||
rootCmd.Flags().BoolVar(&frontendOnly, "frontend-only", false, "include only frontend file types and README.md")
|
rootCmd.Flags().BoolVar(&frontendOnly, "frontend-only", false, "include only frontend file types and README.md")
|
||||||
rootCmd.Flags().BoolVar(&omitTest, "omit-test", false, "exclude test files (Go *_test.go, Java *Test.java) and strip #[cfg(test)] blocks from Rust")
|
rootCmd.Flags().BoolVar(&omitTest, "omit-test", false, "exclude test files (Go *_test.go, Java *Test.java) and strip #[cfg(test)] blocks from Rust")
|
||||||
rootCmd.Flags().BoolVar(&omitMd, "omit-md", false, "omit all .md files except README.md")
|
rootCmd.Flags().BoolVar(&omitMd, "omit-md", false, "omit all .md files except README.md")
|
||||||
|
rootCmd.Flags().BoolVar(&tuiMode, "tui", false, "launch interactive TUI configuration wizard")
|
||||||
rootCmd.MarkPersistentFlagRequired("output")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func run(cmd *cobra.Command, args []string) error {
|
func run(cmd *cobra.Command, args []string) error {
|
||||||
|
if tuiMode {
|
||||||
|
return tui.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
if outputPath == "" {
|
||||||
|
return fmt.Errorf("-o is required (or use --tui for interactive mode)")
|
||||||
|
}
|
||||||
|
|
||||||
if drivePath != "" && !protonDrive {
|
if drivePath != "" && !protonDrive {
|
||||||
return fmt.Errorf("--drive-path requires --proton-drive")
|
return fmt.Errorf("--drive-path requires --proton-drive")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,30 @@ module dirmd
|
|||||||
|
|
||||||
go 1.26.4
|
go 1.26.4
|
||||||
|
|
||||||
require github.com/spf13/cobra v1.10.2
|
require (
|
||||||
|
charm.land/bubbles/v2 v2.2.1
|
||||||
|
charm.land/bubbletea/v2 v2.0.9
|
||||||
|
charm.land/lipgloss/v2 v2.0.6
|
||||||
|
github.com/spf13/cobra v1.10.2
|
||||||
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/atotto/clipboard v0.1.4 // indirect
|
||||||
|
github.com/charmbracelet/colorprofile v0.4.3 // indirect
|
||||||
|
github.com/charmbracelet/ultraviolet v0.0.0-20260811164956-006e29f97886 // indirect
|
||||||
|
github.com/charmbracelet/x/ansi v0.11.8 // indirect
|
||||||
|
github.com/charmbracelet/x/term v0.2.2 // indirect
|
||||||
|
github.com/charmbracelet/x/termios v0.1.1 // indirect
|
||||||
|
github.com/charmbracelet/x/windows v0.2.2 // indirect
|
||||||
|
github.com/clipperhouse/displaywidth v0.11.0 // indirect
|
||||||
|
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.4.1 // indirect
|
||||||
|
github.com/mattn/go-runewidth v0.0.27 // indirect
|
||||||
|
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||||
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
github.com/spf13/pflag v1.0.9 // indirect
|
github.com/spf13/pflag v1.0.9 // indirect
|
||||||
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
|
golang.org/x/sync v0.22.0 // indirect
|
||||||
|
golang.org/x/sys v0.47.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,10 +1,56 @@
|
|||||||
|
charm.land/bubbles/v2 v2.2.1 h1:Fq1+qm5hV6GkvzLQDhCBpXXE5tLgvh1PRriCLwSvIQU=
|
||||||
|
charm.land/bubbles/v2 v2.2.1/go.mod h1:wdMgn+sje1KNXdwFizIWjbf328fIUBxqEmJ/vYPo8yc=
|
||||||
|
charm.land/bubbletea/v2 v2.0.9 h1:DpJCMWKgzQK8SJv4zbKKFHAI10ymWy/evClPFk0k0f8=
|
||||||
|
charm.land/bubbletea/v2 v2.0.9/go.mod h1:2SkdgoTXluXJHOUwAoRlRXF/28vklb1rFl6GcgV1/ss=
|
||||||
|
charm.land/lipgloss/v2 v2.0.6 h1:EaGKeuA8FvF+v2BT5VmZd2LoYLaMZJXA5n34th8nCIQ=
|
||||||
|
charm.land/lipgloss/v2 v2.0.6/go.mod h1:ipDDJNSGa1hlwDtSfW1s2/xR8Vdhbut4PXh2zEKZd0Q=
|
||||||
|
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
|
||||||
|
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
|
||||||
|
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||||
|
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||||
|
github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o=
|
||||||
|
github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w=
|
||||||
|
github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q=
|
||||||
|
github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q=
|
||||||
|
github.com/charmbracelet/ultraviolet v0.0.0-20260811164956-006e29f97886 h1:rdnVWKgJpTVXKuKuJyxDJ+NFJdUaUqGvyGy61OcvlbA=
|
||||||
|
github.com/charmbracelet/ultraviolet v0.0.0-20260811164956-006e29f97886/go.mod h1:nAw0d9PhFp1qdzi2xhQU5YOu5sVpDIHWlaW2Uz/bCro=
|
||||||
|
github.com/charmbracelet/x/ansi v0.11.8 h1:JMFwp0CgDC2+jcOB162HH5k7I3FVbgFSMMYg7dSPBQQ=
|
||||||
|
github.com/charmbracelet/x/ansi v0.11.8/go.mod h1:ZNN+3mXny/516oTQPLMPIBeSINvNJJQ8uQXDgbeJxY0=
|
||||||
|
github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA=
|
||||||
|
github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I=
|
||||||
|
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
|
||||||
|
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
|
||||||
|
github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
|
||||||
|
github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=
|
||||||
|
github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM=
|
||||||
|
github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k=
|
||||||
|
github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=
|
||||||
|
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
|
||||||
|
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
|
||||||
|
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.4.1 h1:1EO+WB73+EH8EVbzlrG3KLAfEypQWVHIBqlTf+2hNss=
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.4.1/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
|
github.com/mattn/go-runewidth v0.0.27 h1:Feg/Oou5zI/wnpgDF6omIU0OokC9GxLC/WRknhVlIR0=
|
||||||
|
github.com/mattn/go-runewidth v0.0.27/go.mod h1:3qAiGCV4Koz/yuveO58qUefmUTRm8r0IGEXZ9jeHp/8=
|
||||||
|
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||||
|
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||||
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||||
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||||
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
|
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
|
||||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||||
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
|
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
||||||
|
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
||||||
|
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
|
||||||
|
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||||
|
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||||
|
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package tui
|
||||||
|
|
||||||
|
type Checkbox struct {
|
||||||
|
Label string
|
||||||
|
Checked bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Checkbox) Toggle() {
|
||||||
|
c.Checked = !c.Checked
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Checkbox) Render(focused bool) string {
|
||||||
|
marker := checkBoxUnchecked
|
||||||
|
if c.Checked {
|
||||||
|
marker = checkBoxChecked
|
||||||
|
}
|
||||||
|
|
||||||
|
style := labelStyle
|
||||||
|
if focused {
|
||||||
|
style = focusedStyle.Bold(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
return style.Render(marker + " " + c.Label)
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
package tui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"dirmd/internal/config"
|
||||||
|
"dirmd/internal/drive"
|
||||||
|
"dirmd/internal/renderer"
|
||||||
|
"dirmd/internal/walker"
|
||||||
|
)
|
||||||
|
|
||||||
|
type GenerateResult struct {
|
||||||
|
Files []string
|
||||||
|
Uploaded bool
|
||||||
|
Error error
|
||||||
|
}
|
||||||
|
|
||||||
|
func Generate(cfg *config.Config) GenerateResult {
|
||||||
|
result := GenerateResult{}
|
||||||
|
|
||||||
|
abs, err := filepath.Abs(cfg.InputPath)
|
||||||
|
if err != nil {
|
||||||
|
result.Error = fmt.Errorf("resolve input path: %w", err)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
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")
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
walkResult, err := walker.Run(cfg)
|
||||||
|
if err != nil {
|
||||||
|
result.Error = err
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(walkResult.Entries) == 0 {
|
||||||
|
result.Error = fmt.Errorf("no readable files in %s", cfg.AbsRoot)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
repoName := filepath.Base(cfg.AbsRoot)
|
||||||
|
|
||||||
|
if cfg.VerticalSlices {
|
||||||
|
generated, err := renderer.WriteSlices(walkResult, absOutput, repoName)
|
||||||
|
if err != nil {
|
||||||
|
result.Error = err
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
result.Files = generated
|
||||||
|
} else {
|
||||||
|
indexBasename := "index_" + repoName + ".md"
|
||||||
|
indexPath := filepath.Join(absOutput, indexBasename)
|
||||||
|
if err := renderer.Write(walkResult, indexPath, cfg.AppendMode, cfg.SingleFile); err != nil {
|
||||||
|
result.Error = err
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
result.Files = []string{indexPath}
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.ProtonDrive {
|
||||||
|
remoteDir := cfg.DrivePath
|
||||||
|
if remoteDir == "" {
|
||||||
|
remoteDir = "/my-files/md/" + filepath.Base(absOutput)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := drive.CheckAuth(); err != nil {
|
||||||
|
result.Error = fmt.Errorf("drive auth: %w", err)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := drive.UploadDir(absOutput, remoteDir); err != nil {
|
||||||
|
result.Error = fmt.Errorf("drive upload: %w", err)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
result.Uploaded = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package tui
|
||||||
|
|
||||||
|
import (
|
||||||
|
tea "charm.land/bubbletea/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func isQuit(msg tea.KeyPressMsg) bool {
|
||||||
|
s := msg.String()
|
||||||
|
return s == "q" || s == "ctrl+c" || s == "esc"
|
||||||
|
}
|
||||||
|
|
||||||
|
func isNavigateDown(msg tea.KeyPressMsg) bool {
|
||||||
|
s := msg.String()
|
||||||
|
return s == "down" || s == "j" || s == "tab"
|
||||||
|
}
|
||||||
|
|
||||||
|
func isNavigateUp(msg tea.KeyPressMsg) bool {
|
||||||
|
s := msg.String()
|
||||||
|
return s == "up" || s == "k" || s == "shift+tab"
|
||||||
|
}
|
||||||
|
|
||||||
|
func isActivate(msg tea.KeyPressMsg) bool {
|
||||||
|
s := msg.String()
|
||||||
|
return s == "enter" || s == " "
|
||||||
|
}
|
||||||
|
|
||||||
|
func isSubmit(msg tea.KeyPressMsg) bool {
|
||||||
|
return msg.String() == "enter"
|
||||||
|
}
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
package tui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"charm.land/bubbles/v2/textinput"
|
||||||
|
tea "charm.land/bubbletea/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type State int
|
||||||
|
|
||||||
|
const (
|
||||||
|
StateForm State = iota
|
||||||
|
StateGenerating
|
||||||
|
StateDone
|
||||||
|
StateError
|
||||||
|
)
|
||||||
|
|
||||||
|
type genResultMsg struct {
|
||||||
|
result GenerateResult
|
||||||
|
}
|
||||||
|
|
||||||
|
type spinnerState struct {
|
||||||
|
frame int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *spinnerState) View() string {
|
||||||
|
frames := []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"}
|
||||||
|
return frames[s.frame%len(frames)]
|
||||||
|
}
|
||||||
|
|
||||||
|
type Model struct {
|
||||||
|
state State
|
||||||
|
form FormModel
|
||||||
|
result GenerateResult
|
||||||
|
spinner spinnerState
|
||||||
|
width int
|
||||||
|
height int
|
||||||
|
}
|
||||||
|
|
||||||
|
func New() Model {
|
||||||
|
return Model{
|
||||||
|
state: StateForm,
|
||||||
|
form: NewForm(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Model) Init() tea.Cmd {
|
||||||
|
return textinput.Blink
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
switch msg := msg.(type) {
|
||||||
|
case tea.WindowSizeMsg:
|
||||||
|
m.width = msg.Width
|
||||||
|
m.height = msg.Height
|
||||||
|
m.form.SetSize(msg.Width, msg.Height)
|
||||||
|
return m, nil
|
||||||
|
|
||||||
|
case tea.KeyPressMsg:
|
||||||
|
if isQuit(msg) && m.state != StateGenerating {
|
||||||
|
return m, tea.Quit
|
||||||
|
}
|
||||||
|
|
||||||
|
switch m.state {
|
||||||
|
case StateForm:
|
||||||
|
if isSubmit(msg) && m.form.focusIndex == fieldGenerateBtn {
|
||||||
|
cfg, err := m.form.ToConfig()
|
||||||
|
if err != nil {
|
||||||
|
m.form.SetError(err.Error())
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
m.state = StateGenerating
|
||||||
|
return m, func() tea.Msg {
|
||||||
|
return genResultMsg{result: Generate(cfg)}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if isSubmit(msg) && m.form.focusIndex == fieldCancelBtn {
|
||||||
|
return m, tea.Quit
|
||||||
|
}
|
||||||
|
var cmd tea.Cmd
|
||||||
|
m.form, cmd = m.form.Update(msg)
|
||||||
|
return m, cmd
|
||||||
|
|
||||||
|
case StateDone, StateError:
|
||||||
|
return m, tea.Quit
|
||||||
|
}
|
||||||
|
|
||||||
|
case genResultMsg:
|
||||||
|
m.result = msg.result
|
||||||
|
if m.result.Error != nil {
|
||||||
|
m.state = StateError
|
||||||
|
} else {
|
||||||
|
m.state = StateDone
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.state == StateForm {
|
||||||
|
var cmd tea.Cmd
|
||||||
|
m.form, cmd = m.form.Update(msg)
|
||||||
|
return m, cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Model) View() tea.View {
|
||||||
|
var content string
|
||||||
|
|
||||||
|
switch m.state {
|
||||||
|
case StateForm:
|
||||||
|
content = m.form.View()
|
||||||
|
case StateGenerating:
|
||||||
|
content = m.renderGenerating()
|
||||||
|
case StateDone:
|
||||||
|
content = m.renderDone()
|
||||||
|
case StateError:
|
||||||
|
content = m.renderError()
|
||||||
|
}
|
||||||
|
|
||||||
|
v := tea.NewView(content)
|
||||||
|
v.AltScreen = true
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Model) renderGenerating() string {
|
||||||
|
var b strings.Builder
|
||||||
|
b.WriteString(titleStyle.Render("dirmd"))
|
||||||
|
b.WriteString("\n\n")
|
||||||
|
b.WriteString(fmt.Sprintf(" %s Generating...\n", m.spinner.View()))
|
||||||
|
b.WriteString("\n")
|
||||||
|
b.WriteString(footerStyle.Render("Please wait."))
|
||||||
|
return appBorder.Render(b.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Model) renderDone() string {
|
||||||
|
var b strings.Builder
|
||||||
|
b.WriteString(titleStyle.Render("dirmd"))
|
||||||
|
b.WriteString("\n\n")
|
||||||
|
b.WriteString(successStyle.Render("✓ Done"))
|
||||||
|
b.WriteString("\n\n")
|
||||||
|
for _, f := range m.result.Files {
|
||||||
|
b.WriteString(fmt.Sprintf(" %s\n", f))
|
||||||
|
}
|
||||||
|
if m.result.Uploaded {
|
||||||
|
b.WriteString(successStyle.Render("\n Uploaded to Proton Drive"))
|
||||||
|
}
|
||||||
|
b.WriteString("\n\n")
|
||||||
|
b.WriteString(footerStyle.Render("Press any key to exit."))
|
||||||
|
return appBorder.Render(b.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Model) renderError() string {
|
||||||
|
var b strings.Builder
|
||||||
|
b.WriteString(titleStyle.Render("dirmd"))
|
||||||
|
b.WriteString("\n\n")
|
||||||
|
b.WriteString(errorStyle.Render("✗ Error"))
|
||||||
|
b.WriteString("\n\n")
|
||||||
|
b.WriteString(fmt.Sprintf(" %s\n", m.result.Error.Error()))
|
||||||
|
b.WriteString("\n")
|
||||||
|
b.WriteString(footerStyle.Render("Press any key to exit."))
|
||||||
|
return appBorder.Render(b.String())
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package tui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
tea "charm.land/bubbletea/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Run() error {
|
||||||
|
p := tea.NewProgram(New())
|
||||||
|
if _, err := p.Run(); err != nil {
|
||||||
|
return fmt.Errorf("tui error: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package tui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"charm.land/lipgloss/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
appBorder = lipgloss.NewStyle().
|
||||||
|
Border(lipgloss.RoundedBorder()).
|
||||||
|
Padding(1, 2)
|
||||||
|
|
||||||
|
titleStyle = lipgloss.NewStyle().
|
||||||
|
Bold(true).
|
||||||
|
MarginBottom(1)
|
||||||
|
|
||||||
|
labelStyle = lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.Color("244")).
|
||||||
|
Width(20)
|
||||||
|
|
||||||
|
focusedStyle = lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.Color("39"))
|
||||||
|
|
||||||
|
checkBoxChecked = "[x]"
|
||||||
|
checkBoxUnchecked = "[ ]"
|
||||||
|
|
||||||
|
footerStyle = lipgloss.NewStyle().
|
||||||
|
MarginTop(1).
|
||||||
|
Foreground(lipgloss.Color("241"))
|
||||||
|
|
||||||
|
errorStyle = lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.Color("196")).
|
||||||
|
Bold(true)
|
||||||
|
|
||||||
|
successStyle = lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.Color("46"))
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user