├── .gitignore ├── .goreleaser.yaml ├── Makefile ├── README.md ├── cmd └── drexler │ └── main.go ├── go.mod ├── go.sum └── internal ├── cli ├── commands.go └── handlers.go ├── genfile ├── cmd.go ├── gitignore.go ├── main.go ├── makefile.go ├── mod.go ├── model.go ├── msg.go ├── readme.go └── view.go ├── style └── style.go └── tui └── bubbles.go /.gitignore: -------------------------------------------------------------------------------- 1 | **/.DS_Store 2 | .idea 3 | dist/ 4 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | # This is an example .goreleaser.yml file with some sensible defaults. 2 | # Make sure to check the documentation at https://goreleaser.com 3 | before: 4 | hooks: 5 | # You may remove this if you don't use go modules. 6 | - go mod tidy 7 | # you may remove this if you don't need go generate 8 | - go generate ./... 9 | builds: 10 | - main: ./cmd/drexler/main.go 11 | env: 12 | - CGO_ENABLED=0 13 | goos: 14 | - linux 15 | - windows 16 | - darwin 17 | 18 | archives: 19 | - format: tar.gz 20 | # this name template makes the OS and Arch compatible with the results of uname. 21 | name_template: >- 22 | {{ .ProjectName }}_ 23 | {{- title .Os }}_ 24 | {{- if eq .Arch "amd64" }}x86_64 25 | {{- else if eq .Arch "386" }}i386 26 | {{- else }}{{ .Arch }}{{ end }} 27 | {{- if .Arm }}v{{ .Arm }}{{ end }} 28 | # use zip for windows archives 29 | format_overrides: 30 | - goos: windows 31 | format: zip 32 | checksum: 33 | name_template: 'checksums.txt' 34 | snapshot: 35 | name_template: "{{ incpatch .Version }}-next" 36 | changelog: 37 | sort: asc 38 | filters: 39 | exclude: 40 | - '^docs:' 41 | - '^test:' 42 | 43 | # The lines beneath this are called `modelines`. See `:help modeline` 44 | # Feel free to remove those if you don't want/use them. 45 | # yaml-language-server: $schema=https://goreleaser.com/static/schema.json 46 | # vim: set ts=2 sw=2 tw=0 fo=cnqoj 47 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BINARY_NAME=drexler 2 | 3 | build: 4 | go build -o ${BINARY_NAME} cmd/drexler/main.go 5 | 6 | deps: 7 | go mod download 8 | 9 | tidy: 10 | go mod tidy -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # drexler 2 | 3 | ### A Bubble Tea TUI project generator 4 | 5 | ![drexler](https://user-images.githubusercontent.com/11764379/222506345-d309c288-ab9a-47f9-bfa3-317446b125bf.gif) 6 | 7 | #### Getting Started 8 | 9 | ``` 10 | drexler init [appName] 11 | cd [appName] 12 | make run 13 | ``` 14 | 15 | #### Supported Commands 16 | 17 | ``` 18 | drexler help 19 | drexler init [appName] 20 | ``` 21 | 22 | #### Generated File Structure 23 | 24 | ``` 25 | [appName]/ 26 | ├─ cmd/ 27 | │ ├─ [appName]/ 28 | │ │ ├─ main.go 29 | ├─ internal/ 30 | │ ├─ services/ 31 | │ ├─ tui/ 32 | │ │ ├─ cmd.go 33 | │ │ ├─ model.go 34 | │ │ ├─ msg.go 35 | │ │ ├─ view.go 36 | ├─ pkg/ 37 | ├─ .gitignore 38 | ├─ go.mod 39 | ├─ go.sum 40 | ├─ Makefile 41 | ├─ README.md 42 | ``` 43 | 44 | #### WIP 45 | 46 | ``` 47 | drexler bubble [bubbleName] 48 | drexler cmd [cmdName] 49 | drexler msg [msgName] 50 | drexler publish [homebrew, etc] 51 | ``` 52 | -------------------------------------------------------------------------------- /cmd/drexler/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "github.com/gzipchrist/drexler/internal/cli" 6 | "github.com/gzipchrist/drexler/internal/style" 7 | "log" 8 | "os" 9 | "time" 10 | ) 11 | 12 | func main() { 13 | t := time.Now() 14 | 15 | style.ShowHeader() 16 | 17 | flag.Parse() 18 | args := flag.Args() 19 | 20 | err := cli.HandleArgs(args) 21 | if err != nil { 22 | log.Fatalln(err) 23 | } 24 | 25 | style.ShowComplete(t) 26 | 27 | os.Exit(0) 28 | } 29 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/gzipchrist/drexler 2 | 3 | go 1.17 4 | 5 | require github.com/charmbracelet/lipgloss v0.6.0 6 | 7 | require ( 8 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 9 | github.com/mattn/go-isatty v0.0.14 // indirect 10 | github.com/mattn/go-runewidth v0.0.13 // indirect 11 | github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68 // indirect 12 | github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0 // indirect 13 | github.com/rivo/uniseg v0.2.0 // indirect 14 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect 15 | ) 16 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/charmbracelet/lipgloss v0.6.0 h1:1StyZB9vBSOyuZxQUcUwGr17JmojPNm87inij9N3wJY= 2 | github.com/charmbracelet/lipgloss v0.6.0/go.mod h1:tHh2wr34xcHjC2HCXIlGSG1jaDF0S0atAUvBMP6Ppuk= 3 | github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= 4 | github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 5 | github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= 6 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 7 | github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= 8 | github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= 9 | github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 10 | github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68 h1:y1p/ycavWjGT9FnmSjdbWUlLGvcxrY0Rw3ATltrxOhk= 11 | github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ= 12 | github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0 h1:STjmj0uFfRryL9fzRA/OupNppeAID6QJYPMavTL7jtY= 13 | github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs= 14 | github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 15 | github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= 16 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 17 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I= 18 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 19 | -------------------------------------------------------------------------------- /internal/cli/commands.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import "errors" 4 | 5 | type Command string 6 | 7 | const ( 8 | Help = "help" 9 | Init = "init" 10 | Bubble = "bubble" 11 | ) 12 | 13 | var m = map[Command]bool{ 14 | Help: true, 15 | Init: true, 16 | Bubble: true, 17 | } 18 | 19 | func (c Command) Validate() error { 20 | _, ok := m[c] 21 | if !ok { 22 | return errors.New("invalid command") 23 | } 24 | 25 | return nil 26 | } 27 | 28 | func (c Command) String() string { 29 | _, ok := m[c] 30 | if !ok { 31 | return "" 32 | } 33 | 34 | return string(c) 35 | } 36 | -------------------------------------------------------------------------------- /internal/cli/handlers.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "fmt" 5 | "github.com/gzipchrist/drexler/internal/genfile" 6 | "github.com/gzipchrist/drexler/internal/style" 7 | "github.com/gzipchrist/drexler/internal/tui" 8 | "log" 9 | "os" 10 | "os/exec" 11 | "strings" 12 | ) 13 | 14 | func Usage() { 15 | help := ` 16 | Usage: drexler [command] [subcommand] 17 | 18 | Examples: 19 | drexler init myProject 20 | 21 | Commands: 22 | help 23 | init [myProject] 24 | bubble [textinput, timer, etc] 25 | cmd [myCmd] 26 | msg [myMsg] 27 | deploy [platform] 28 | 29 | ` 30 | 31 | fmt.Print(help) 32 | } 33 | 34 | func HandleArgs(args []string) error { 35 | switch { 36 | case len(args) <= 1: 37 | Usage() 38 | os.Exit(0) 39 | 40 | case len(args) >= 2: 41 | command := Command(args[0]) 42 | 43 | err := command.Validate() 44 | if err != nil { 45 | return err 46 | } 47 | 48 | switch command { 49 | case Help: 50 | Usage() 51 | 52 | case Init: 53 | HandleInit(args[1:]) 54 | 55 | case Bubble: 56 | err := HandleBubble(args[1:]) 57 | if err != nil { 58 | return err 59 | } 60 | } 61 | } 62 | 63 | return nil 64 | } 65 | 66 | func HandleInit(args []string) { 67 | // TODO: Dynamically generate this, possible with runtime pkg. 68 | goVersion := "go 1.17" 69 | 70 | root := strings.Join(args, "-") 71 | 72 | if root == Help { 73 | fmt.Printf("Usage: drexler init [root] This scaffolds out a project.\n\n") 74 | os.Exit(0) 75 | } 76 | 77 | fmt.Printf("\n %s Initialized project %s\n", style.Check, root) 78 | 79 | dir, err := os.Getwd() 80 | if err != nil { 81 | log.Fatalln(err) 82 | } 83 | 84 | path := fmt.Sprintf("%s/%s", dir, root) 85 | 86 | dirs := []string{ 87 | "/cmd", 88 | "/internal", 89 | "/pkg", 90 | } 91 | 92 | for _, dir := range dirs { 93 | fmt.Printf(" %s Created directory %s\n", style.Check, dir) 94 | } 95 | 96 | subDirs := []string{ 97 | fmt.Sprintf("/cmd/%s", root), 98 | "/internal/tui", 99 | "/internal/services", 100 | "/pkg", 101 | } 102 | 103 | for _, dir := range subDirs { 104 | err := os.MkdirAll(path+dir, os.FileMode(0755)) 105 | if err != nil { 106 | _ = fmt.Errorf(" %s error making subdir: %w", style.X, err) 107 | os.Exit(1) 108 | } 109 | fmt.Printf(" %s Created subdirectory %s\n", style.Check, dir) 110 | } 111 | 112 | files := make(map[string]string) 113 | files[fmt.Sprintf("cmd/%s/main.go", root)] = genfile.Main(root) 114 | files["internal/tui/cmd.go"] = genfile.Cmd() 115 | files["internal/tui/model.go"] = genfile.Model(root) 116 | files["internal/tui/msg.go"] = genfile.Msg() 117 | files["internal/tui/view.go"] = genfile.View 118 | files[".gitignore"] = genfile.Gitignore 119 | files["README.md"] = genfile.ReadMe(root) 120 | files["go.mod"] = genfile.Mod(root, goVersion) 121 | files["Makefile"] = genfile.Make(root) 122 | 123 | for name, content := range files { 124 | file, err := os.Create(root + "/" + name) 125 | if err != nil { 126 | _ = fmt.Errorf(" %s error creating file %w", style.X, err) 127 | os.Exit(1) 128 | } 129 | 130 | fmt.Printf(" %s Created file %s\n", style.Check, name) 131 | 132 | _, err = file.WriteString(content) 133 | if err != nil { 134 | _ = fmt.Errorf(" %s error writing to file %w", style.X, err) 135 | os.Exit(1) 136 | } 137 | 138 | fmt.Printf(" %s Generated boilerplate for %s\n", style.Check, name) 139 | } 140 | 141 | err = os.Chdir(path) 142 | if err != nil { 143 | log.Fatalf(" %s error changing dir: %v\n", style.X, err) 144 | } 145 | 146 | b, err := exec.Command("make", "tidy").CombinedOutput() 147 | if err != nil { 148 | fmt.Printf(" %s error running make deps: %s\n", style.X, string(b)) 149 | } 150 | 151 | fmt.Printf(" %s Installed charmbracelet dependencies\n", style.Check) 152 | } 153 | 154 | func HandleBubble(args []string) error { 155 | b := tui.Bubble(args[1]) 156 | err := b.Validate() 157 | if err != nil { 158 | return err 159 | } 160 | 161 | // TODO: Add codegen for all bubbles. 162 | switch b { 163 | case tui.TextInput: 164 | 165 | } 166 | 167 | return nil 168 | } 169 | -------------------------------------------------------------------------------- /internal/genfile/cmd.go: -------------------------------------------------------------------------------- 1 | package genfile 2 | 3 | func Cmd() string { 4 | return `package tui 5 | 6 | // Put tea.Cmds here. 7 | ` 8 | } 9 | -------------------------------------------------------------------------------- /internal/genfile/gitignore.go: -------------------------------------------------------------------------------- 1 | package genfile 2 | 3 | const Gitignore = "# Mac OS X Files\n.DS_Store\n" 4 | -------------------------------------------------------------------------------- /internal/genfile/main.go: -------------------------------------------------------------------------------- 1 | package genfile 2 | 3 | import "fmt" 4 | 5 | func Main(s string) string { 6 | return fmt.Sprintf(`package main 7 | 8 | import ( 9 | tea "github.com/charmbracelet/bubbletea" 10 | "log" 11 | tui "%s/internal/tui" 12 | ) 13 | 14 | func main() { 15 | m := tui.NewModel() 16 | p := tea.NewProgram(m) 17 | 18 | _, err := p.Run() 19 | if err != nil { 20 | log.Fatal(err) 21 | } 22 | } 23 | 24 | `, s) 25 | } 26 | -------------------------------------------------------------------------------- /internal/genfile/makefile.go: -------------------------------------------------------------------------------- 1 | package genfile 2 | 3 | import "fmt" 4 | 5 | func Make(s string) string { 6 | return fmt.Sprintf(`BINARY_NAME=%s 7 | 8 | build: 9 | go build -o ${BINARY_NAME} cmd/%s/main.go 10 | 11 | run: 12 | go run cmd/%s/main.go 13 | 14 | tidy: 15 | go mod tidy 16 | 17 | `, s, s, s) 18 | } 19 | -------------------------------------------------------------------------------- /internal/genfile/mod.go: -------------------------------------------------------------------------------- 1 | package genfile 2 | 3 | import "fmt" 4 | 5 | func Mod(s string, v string) string { 6 | return fmt.Sprintf("module %s\n\n%s\n\nrequire (\n\tgithub.com/charmbracelet/bubbles v0.14.0\n\tgithub.com/charmbracelet/bubbletea v0.23.1\n\tgithub.com/charmbracelet/lipgloss v0.6.0\n)\n", s, v) 7 | } 8 | -------------------------------------------------------------------------------- /internal/genfile/model.go: -------------------------------------------------------------------------------- 1 | package genfile 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | func Model(s string) string { 8 | return fmt.Sprintf(`package tui 9 | 10 | import ( 11 | "github.com/charmbracelet/bubbles/spinner" 12 | tea "github.com/charmbracelet/bubbletea" 13 | ) 14 | 15 | // MainModel is the root state of the app. 16 | type MainModel struct { 17 | appName string 18 | spinner spinner.Model 19 | err error 20 | } 21 | 22 | // NewModel configures the initial model at runtime. 23 | func NewModel() MainModel { 24 | s := spinner.New() 25 | s.Spinner = spinner.Globe 26 | 27 | return MainModel{ 28 | appName: "%s", 29 | spinner: s, 30 | } 31 | } 32 | 33 | // Init returns any number of tea.Cmds at runtime. 34 | func (m MainModel) Init() tea.Cmd { 35 | return m.spinner.Tick 36 | } 37 | 38 | // Update handles all tea.Msgs in the Bubble Tea event loop. 39 | func (m MainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 40 | var cmd tea.Cmd 41 | 42 | switch msg := msg.(type) { 43 | 44 | // Handle keypress messages. 45 | case tea.KeyMsg: 46 | switch msg.Type { 47 | case tea.KeyCtrlC: 48 | return m, tea.Quit 49 | } 50 | 51 | case ErrMsg: 52 | m.err = msg 53 | return m, nil 54 | 55 | } 56 | 57 | m.spinner, cmd = m.spinner.Update(msg) 58 | return m, cmd 59 | } 60 | 61 | // View renders a string representation of the MainModel. 62 | func (m MainModel) View() string { 63 | return titleView(m.appName) + 64 | descView() + 65 | m.spinner.View() + 66 | footerView() 67 | } 68 | 69 | `, s) 70 | } 71 | -------------------------------------------------------------------------------- /internal/genfile/msg.go: -------------------------------------------------------------------------------- 1 | package genfile 2 | 3 | func Msg() string { 4 | return `package tui 5 | 6 | type ErrMsg error 7 | 8 | ` 9 | } 10 | -------------------------------------------------------------------------------- /internal/genfile/readme.go: -------------------------------------------------------------------------------- 1 | package genfile 2 | 3 | import "fmt" 4 | 5 | func ReadMe(s string) string { 6 | return fmt.Sprintf(`# %s 7 | 8 | ### This project was generated using the [drexler](https://github.com/gzipChrist/drexler) TUI project generator 9 | 10 | `, s) 11 | } 12 | -------------------------------------------------------------------------------- /internal/genfile/view.go: -------------------------------------------------------------------------------- 1 | package genfile 2 | 3 | const View = `package tui 4 | 5 | import ( 6 | "fmt" 7 | 8 | "github.com/charmbracelet/lipgloss" 9 | ) 10 | 11 | 12 | var keyParts = []string{ 13 | " _______", 14 | "|\\ /", 15 | "| +---+ ", 16 | "| | | ", 17 | "| |%s | ", 18 | "| +---+ ", 19 | "|/_____\\", 20 | } 21 | 22 | func keySwitchArt(s string) string { 23 | art := "\n" 24 | for i, line := range keyParts { 25 | for _, c := range s { 26 | if i == 4 { 27 | art += fmt.Sprintf(line, string(c)) 28 | } else { 29 | art += line 30 | } 31 | } 32 | 33 | if i == 0 { 34 | art += "\n" 35 | } else { 36 | art += "|\n" 37 | } 38 | 39 | } 40 | 41 | art += "\n" 42 | 43 | return art 44 | } 45 | 46 | func titleView(appName string) string { 47 | return keySwitchArt(appName) 48 | } 49 | 50 | func descView() string { 51 | return "Hello, it's your World " 52 | } 53 | 54 | func footerView() string { 55 | return lipgloss.NewStyle().Faint(true).SetString("\n\n\n\nGenerated by drexler, with love ❤️\n[ctrl+c to quit]\n").String() 56 | } 57 | 58 | ` 59 | -------------------------------------------------------------------------------- /internal/style/style.go: -------------------------------------------------------------------------------- 1 | package style 2 | 3 | import ( 4 | "fmt" 5 | "github.com/charmbracelet/lipgloss" 6 | "time" 7 | ) 8 | 9 | var header = ` 10 | 11 | '|| '|| 12 | .. || ... .. .... ... ... || .... ... .. 13 | .' '|| ||' '' .|...|| '|..' || .|...|| ||' '' 14 | |. || || || .|. || || || 15 | '|..'||. .||. '|...' .| ||. .||. '|...' .||. 16 | ` 17 | 18 | var Check = lipgloss.NewStyle().SetString("✓").Foreground(lipgloss.Color("#b7fbd7")) 19 | var X = lipgloss.NewStyle().SetString("𐄂").Foreground(lipgloss.Color("#ff0040")) 20 | 21 | func ShowHeader() { 22 | h := lipgloss.NewStyle().SetString(header).Foreground(lipgloss.Color("#85cbf6")) 23 | fmt.Printf("%s\n The TUI Project Generator\n", h) 24 | } 25 | 26 | func ShowComplete(t time.Time) { 27 | fmt.Printf(" %s All done! Generated your TUI project in %v\n", Check, time.Since(t)) 28 | } 29 | -------------------------------------------------------------------------------- /internal/tui/bubbles.go: -------------------------------------------------------------------------------- 1 | package tui 2 | 3 | import "errors" 4 | 5 | type Bubble string 6 | 7 | const ( 8 | TextInput = "textinput" 9 | ) 10 | 11 | var m = map[Bubble]bool{ 12 | TextInput: true, 13 | } 14 | 15 | func (b Bubble) Validate() error { 16 | _, ok := m[b] 17 | if !ok { 18 | return errors.New("invalid bubble") 19 | } 20 | 21 | return nil 22 | } 23 | 24 | func (b Bubble) String() string { 25 | _, ok := m[b] 26 | if !ok { 27 | return "" 28 | } 29 | 30 | return string(b) 31 | } 32 | --------------------------------------------------------------------------------