WIP: stuff
This commit is contained in:
89
cli/cmd/ts-run/ts-run.go
Normal file
89
cli/cmd/ts-run/ts-run.go
Normal file
@ -0,0 +1,89 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/boyter/gocodewalker"
|
||||
)
|
||||
|
||||
type env struct {
|
||||
DBType string
|
||||
DBUser string
|
||||
DBPass string
|
||||
DBHost string
|
||||
DBPort string
|
||||
DBName string
|
||||
|
||||
RootDir string
|
||||
NodeDir string
|
||||
ProtoDir string
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Get pwd
|
||||
path, err := os.Getwd()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Printf("Current path: %s\n", path)
|
||||
|
||||
findApps(path)
|
||||
return
|
||||
|
||||
// c := make(chan apps.Msg, 10)
|
||||
|
||||
// // Create protobuf watcher
|
||||
// proto, err := apps.NewProto(env.ProtoDir, env.RootDir, c)
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
|
||||
// // Create node watcher
|
||||
// node := apps.NewNode(env.NodeDir, c)
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
|
||||
// apps := []*apps.App{
|
||||
// &proto.App,
|
||||
// &node.App,
|
||||
// }
|
||||
|
||||
// // Start tea
|
||||
// p := tea.NewProgram(
|
||||
// models.NewRunner(c, apps),
|
||||
// tea.WithAltScreen(),
|
||||
// tea.WithMouseCellMotion(),
|
||||
// )
|
||||
// if _, err := p.Run(); err != nil {
|
||||
// fmt.Printf("Alas, there's been an error: %v", err)
|
||||
// }
|
||||
|
||||
// // Cancel watchers
|
||||
// proto.Cancel()
|
||||
// proto.Wait()
|
||||
|
||||
// node.Cancel()
|
||||
// node.Wait()
|
||||
|
||||
// close(c)
|
||||
}
|
||||
|
||||
func findApps(path string) {
|
||||
fileListQueue := make(chan *gocodewalker.File, 100)
|
||||
fileWalker := gocodewalker.NewFileWalker(path, fileListQueue)
|
||||
|
||||
errorHandler := func(e error) bool {
|
||||
fmt.Println("ERR", e.Error())
|
||||
return true
|
||||
}
|
||||
fileWalker.SetErrorHandler(errorHandler)
|
||||
|
||||
go fileWalker.Start()
|
||||
|
||||
for f := range fileListQueue {
|
||||
fmt.Printf("%s, %s\n", f.Filename, f.Location)
|
||||
}
|
||||
}
|
31
cli/go.mod
Normal file
31
cli/go.mod
Normal file
@ -0,0 +1,31 @@
|
||||
module github.com/spotdemo4/trevstack/cli
|
||||
|
||||
go 1.23.6
|
||||
|
||||
require (
|
||||
github.com/charmbracelet/bubbles v0.20.0
|
||||
github.com/charmbracelet/bubbletea v1.3.4
|
||||
github.com/charmbracelet/lipgloss v1.0.0
|
||||
github.com/fsnotify/fsnotify v1.8.0
|
||||
github.com/joho/godotenv v1.5.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/boyter/gocodewalker v1.4.0 // indirect
|
||||
github.com/charmbracelet/x/ansi v0.8.0 // indirect
|
||||
github.com/charmbracelet/x/term v0.2.1 // indirect
|
||||
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 // indirect
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||
github.com/muesli/termenv v0.16.0 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
golang.org/x/sync v0.11.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/text v0.3.8 // indirect
|
||||
)
|
47
cli/go.sum
Normal file
47
cli/go.sum
Normal file
@ -0,0 +1,47 @@
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||
github.com/boyter/gocodewalker v1.4.0 h1:fVmFeQxKpj5tlpjPcyTtJ96btgaHYd9yn6m+T/66et4=
|
||||
github.com/boyter/gocodewalker v1.4.0/go.mod h1:hXG8xzR1uURS+99P5/3xh3uWHjaV2XfoMMmvPyhrCDg=
|
||||
github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE=
|
||||
github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU=
|
||||
github.com/charmbracelet/bubbletea v1.3.4 h1:kCg7B+jSCFPLYRA52SDZjr51kG/fMUEoPoZrkaDHyoI=
|
||||
github.com/charmbracelet/bubbletea v1.3.4/go.mod h1:dtcUCyCGEX3g9tosuYiut3MXgY/Jsv9nKVdibKKRRXo=
|
||||
github.com/charmbracelet/lipgloss v1.0.0 h1:O7VkGDvqEdGi93X+DeqsQ7PKHDgtQfF8j8/O2qFMQNg=
|
||||
github.com/charmbracelet/lipgloss v1.0.0/go.mod h1:U5fy9Z+C38obMs+T+tJqst9VGzlOYGj4ri9reL3qUlo=
|
||||
github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
|
||||
github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
|
||||
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
|
||||
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
|
||||
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 h1:y5HC9v93H5EPKqaS1UYVg1uYah5Xf51mBfIoWehClUQ=
|
||||
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9hchkHSWYkEqJwUGisez3G1QY8Ryz0sdWrLPMGjLk=
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
||||
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
|
||||
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
||||
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
|
||||
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
||||
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
|
||||
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
|
||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
19
cli/internal/apps/msg.go
Normal file
19
cli/internal/apps/msg.go
Normal file
@ -0,0 +1,19 @@
|
||||
package apps
|
||||
|
||||
import "time"
|
||||
|
||||
type Msg struct {
|
||||
Text string
|
||||
Time time.Time
|
||||
Key *string
|
||||
Loading *bool
|
||||
Success *bool
|
||||
|
||||
App *App
|
||||
}
|
||||
|
||||
type App struct {
|
||||
Name string
|
||||
Color string
|
||||
Loading *bool
|
||||
}
|
118
cli/internal/apps/node.go
Normal file
118
cli/internal/apps/node.go
Normal file
@ -0,0 +1,118 @@
|
||||
package apps
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/spotdemo4/trevstack/cli/internal/utils"
|
||||
)
|
||||
|
||||
type Node struct {
|
||||
App App
|
||||
c chan Msg
|
||||
ctx context.Context
|
||||
wg *sync.WaitGroup
|
||||
|
||||
dir string
|
||||
|
||||
Cancel context.CancelFunc
|
||||
Wait func()
|
||||
}
|
||||
|
||||
func NewNode(dir string, c chan Msg) *Node {
|
||||
|
||||
// Create new context
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
// Create wait group
|
||||
wg := sync.WaitGroup{}
|
||||
|
||||
node := Node{
|
||||
App: App{
|
||||
Name: "node",
|
||||
Color: "#fab387",
|
||||
},
|
||||
c: c,
|
||||
ctx: ctx,
|
||||
wg: &wg,
|
||||
|
||||
dir: dir,
|
||||
|
||||
Cancel: cancel,
|
||||
Wait: wg.Wait,
|
||||
}
|
||||
|
||||
// Start watching
|
||||
go node.dev()
|
||||
|
||||
return &node
|
||||
}
|
||||
|
||||
func (n *Node) msg(m Msg) {
|
||||
m.Time = time.Now()
|
||||
m.App = &n.App
|
||||
n.c <- m
|
||||
}
|
||||
|
||||
func (n *Node) dev() {
|
||||
n.wg.Add(1)
|
||||
defer n.wg.Done()
|
||||
|
||||
// Create cmd
|
||||
cmd := exec.Command("npm", "run", "dev")
|
||||
cmd.Dir = n.dir
|
||||
|
||||
// Stop cmd on exit
|
||||
n.wg.Add(1)
|
||||
go func() {
|
||||
defer n.wg.Done()
|
||||
<-n.ctx.Done()
|
||||
|
||||
if err := cmd.Process.Signal(os.Interrupt); err != nil {
|
||||
cmd.Process.Kill() // If the process is not responding to the interrupt signal, kill it
|
||||
}
|
||||
}()
|
||||
|
||||
// Start cmd
|
||||
out, err := utils.Run(cmd)
|
||||
if err != nil {
|
||||
n.msg(Msg{
|
||||
Text: err.Error(),
|
||||
Success: utils.BoolPointer(false),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Watch for output
|
||||
for line := range out {
|
||||
switch line := line.(type) {
|
||||
case utils.Stdout:
|
||||
n.msg(Msg{
|
||||
Text: string(line),
|
||||
})
|
||||
|
||||
case utils.Stderr:
|
||||
n.msg(Msg{
|
||||
Text: string(line),
|
||||
Success: utils.BoolPointer(false),
|
||||
})
|
||||
|
||||
case utils.ExitCode:
|
||||
if line == 0 {
|
||||
n.msg(Msg{
|
||||
Text: "Node stopped",
|
||||
Success: utils.BoolPointer(true),
|
||||
})
|
||||
} else {
|
||||
n.msg(Msg{
|
||||
Text: fmt.Sprintf("Node failed with exit code %d", out),
|
||||
Success: utils.BoolPointer(false),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
269
cli/internal/apps/proto.go
Normal file
269
cli/internal/apps/proto.go
Normal file
@ -0,0 +1,269 @@
|
||||
package apps
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"github.com/spotdemo4/trevstack/cli/internal/utils"
|
||||
)
|
||||
|
||||
type Proto struct {
|
||||
App App
|
||||
c chan Msg
|
||||
ctx context.Context
|
||||
wg *sync.WaitGroup
|
||||
watcher *fsnotify.Watcher
|
||||
|
||||
dir string
|
||||
rootDir string
|
||||
|
||||
Cancel context.CancelFunc
|
||||
Wait func()
|
||||
}
|
||||
|
||||
func NewProto(dir string, rootDir string, c chan Msg) (*Proto, error) {
|
||||
|
||||
// Create new watcher
|
||||
watcher, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Add directory to watcher
|
||||
err = filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if d.IsDir() {
|
||||
if slices.Contains(watcher.WatchList(), path) {
|
||||
return nil
|
||||
}
|
||||
|
||||
err := watcher.Add(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create new context
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
// Create wait group
|
||||
wg := sync.WaitGroup{}
|
||||
|
||||
proto := Proto{
|
||||
App: App{
|
||||
Name: "proto",
|
||||
Color: "#89dceb",
|
||||
},
|
||||
c: c,
|
||||
ctx: ctx,
|
||||
wg: &wg,
|
||||
watcher: watcher,
|
||||
|
||||
dir: dir,
|
||||
rootDir: rootDir,
|
||||
|
||||
Cancel: cancel,
|
||||
Wait: wg.Wait,
|
||||
}
|
||||
|
||||
// Start watching
|
||||
go proto.watch()
|
||||
|
||||
return &proto, nil
|
||||
}
|
||||
|
||||
func (p *Proto) msg(m Msg) {
|
||||
m.Time = time.Now()
|
||||
m.App = &p.App
|
||||
p.c <- m
|
||||
}
|
||||
|
||||
func (p *Proto) watch() {
|
||||
p.wg.Add(1)
|
||||
defer p.wg.Done()
|
||||
defer p.watcher.Close()
|
||||
|
||||
// Create new rate limit map
|
||||
rateLimit := make(map[string]time.Time)
|
||||
|
||||
p.lint()
|
||||
|
||||
p.msg(Msg{
|
||||
Text: "Watching for proto changes...",
|
||||
})
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-p.ctx.Done():
|
||||
return
|
||||
|
||||
case event, ok := <-p.watcher.Events:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// Rate limit
|
||||
rl, ok := rateLimit[event.Name]
|
||||
if ok && time.Since(rl) < 1*time.Second {
|
||||
continue
|
||||
}
|
||||
rateLimit[event.Name] = time.Now()
|
||||
|
||||
p.msg(Msg{
|
||||
Text: "File changed: " + strings.TrimPrefix(event.Name, p.dir),
|
||||
})
|
||||
|
||||
ok, _ = p.lint()
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
p.generate()
|
||||
|
||||
case err, ok := <-p.watcher.Errors:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
p.msg(Msg{
|
||||
Text: err.Error(),
|
||||
Success: utils.BoolPointer(false),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Proto) lint() (bool, error) {
|
||||
p.msg(Msg{
|
||||
Text: "Linting",
|
||||
Loading: utils.BoolPointer(true),
|
||||
Key: utils.StringPointer("lint"),
|
||||
})
|
||||
|
||||
// Run buf lint
|
||||
cmd := exec.Command("buf", "lint")
|
||||
cmd.Dir = p.rootDir
|
||||
out, err := utils.Run(cmd)
|
||||
if err != nil {
|
||||
p.msg(Msg{
|
||||
Text: err.Error(),
|
||||
Success: utils.BoolPointer(false),
|
||||
})
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Watch for output
|
||||
for line := range out {
|
||||
switch line := line.(type) {
|
||||
case utils.Stdout:
|
||||
p.msg(Msg{
|
||||
Text: string(line),
|
||||
})
|
||||
|
||||
case utils.Stderr:
|
||||
p.msg(Msg{
|
||||
Text: string(line),
|
||||
Success: utils.BoolPointer(false),
|
||||
})
|
||||
|
||||
case utils.ExitCode:
|
||||
if line == 0 {
|
||||
p.msg(Msg{
|
||||
Text: "Buf lint successful",
|
||||
Success: utils.BoolPointer(true),
|
||||
Loading: utils.BoolPointer(false),
|
||||
Key: utils.StringPointer("lint"),
|
||||
})
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
p.msg(Msg{
|
||||
Text: fmt.Sprintf("Buf lint failed with exit code %d", out),
|
||||
Success: utils.BoolPointer(false),
|
||||
Loading: utils.BoolPointer(false),
|
||||
Key: utils.StringPointer("lint"),
|
||||
})
|
||||
|
||||
return false, fmt.Errorf("buf lint failed with exit code %d", line)
|
||||
}
|
||||
}
|
||||
|
||||
return false, fmt.Errorf("buf lint failed")
|
||||
}
|
||||
|
||||
func (p *Proto) generate() error {
|
||||
p.msg(Msg{
|
||||
Text: "Generating proto files",
|
||||
Loading: utils.BoolPointer(true),
|
||||
Key: utils.StringPointer("generate"),
|
||||
})
|
||||
|
||||
// Run buf gen
|
||||
cmd := exec.Command("buf", "generate")
|
||||
cmd.Dir = p.rootDir
|
||||
out, err := utils.Run(cmd)
|
||||
if err != nil {
|
||||
p.msg(Msg{
|
||||
Text: err.Error(),
|
||||
Success: utils.BoolPointer(false),
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// Watch for output
|
||||
for line := range out {
|
||||
switch line := line.(type) {
|
||||
case utils.Stdout:
|
||||
p.msg(Msg{
|
||||
Text: string(line),
|
||||
})
|
||||
|
||||
case utils.Stderr:
|
||||
p.msg(Msg{
|
||||
Text: string(line),
|
||||
Success: utils.BoolPointer(false),
|
||||
})
|
||||
|
||||
case utils.ExitCode:
|
||||
if line == 0 {
|
||||
p.msg(Msg{
|
||||
Text: "Buf generate successful",
|
||||
Success: utils.BoolPointer(true),
|
||||
Loading: utils.BoolPointer(false),
|
||||
Key: utils.StringPointer("generate"),
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
p.msg(Msg{
|
||||
Text: fmt.Sprintf("Buf generate failed with exit code %d", out),
|
||||
Success: utils.BoolPointer(false),
|
||||
Loading: utils.BoolPointer(false),
|
||||
Key: utils.StringPointer("generate"),
|
||||
})
|
||||
|
||||
return fmt.Errorf("generate failed with exit code %d", line)
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("generate failed")
|
||||
}
|
82
cli/internal/models/cbox.go
Normal file
82
cli/internal/models/cbox.go
Normal file
@ -0,0 +1,82 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/bubbles/viewport"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
type Cbox struct {
|
||||
style lipgloss.Style
|
||||
Viewport *viewport.Model
|
||||
maxPrefixLen int
|
||||
}
|
||||
|
||||
func NewCbox(maxPrefixLen int) *Cbox {
|
||||
s := lipgloss.NewStyle().
|
||||
BorderStyle(lipgloss.NormalBorder()).
|
||||
BorderForeground(lipgloss.Color("#45475a")).
|
||||
BorderTop(true).
|
||||
BorderBottom(true).
|
||||
Margin(1, 0, 0)
|
||||
|
||||
return &Cbox{
|
||||
style: s,
|
||||
Viewport: nil,
|
||||
maxPrefixLen: maxPrefixLen,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cbox) Gen(text string, width int, height int, mtop int, mbottom int) string {
|
||||
if c.Viewport == nil {
|
||||
vp := viewport.New(width, height-(mtop+mbottom))
|
||||
c.Viewport = &vp
|
||||
c.Viewport.YPosition = mtop
|
||||
c.Viewport.Style = c.style
|
||||
} else {
|
||||
c.Viewport.Width = width
|
||||
c.Viewport.Height = height - (mtop + mbottom)
|
||||
c.Viewport.YPosition = mtop
|
||||
}
|
||||
|
||||
atBottom := c.Viewport.AtBottom()
|
||||
|
||||
// Need to add extra lines because of https://github.com/charmbracelet/bubbles/pull/731
|
||||
c.Viewport.SetContent(text + "\n\n\n")
|
||||
|
||||
if atBottom {
|
||||
c.Viewport.GotoBottom()
|
||||
}
|
||||
|
||||
return c.Viewport.View()
|
||||
}
|
||||
|
||||
func (c *Cbox) GenItem(ti time.Time, prefix string, text string, color string, width int) string {
|
||||
t := lipgloss.NewStyle().
|
||||
Padding(0, 1, 0, 1).
|
||||
Foreground(lipgloss.Color("#a6adc8")).
|
||||
Render(ti.Format(time.Kitchen))
|
||||
|
||||
p := lipgloss.NewStyle().
|
||||
Padding(0, 1, 0, 0).
|
||||
Width(c.maxPrefixLen).
|
||||
Foreground(lipgloss.Color(color)).
|
||||
BorderStyle(lipgloss.NormalBorder()).
|
||||
BorderRight(true).
|
||||
BorderForeground(lipgloss.Color(color))
|
||||
|
||||
m := lipgloss.NewStyle().
|
||||
Padding(0, 1, 0, 1).
|
||||
Foreground(lipgloss.Color("#cdd6f4")).
|
||||
Width(width - lipgloss.Width(t) - lipgloss.Width(p.Render(prefix))).
|
||||
Render(text)
|
||||
|
||||
p = p.Height(lipgloss.Height(m))
|
||||
|
||||
combine := lipgloss.JoinHorizontal(lipgloss.Top, t, p.Render(prefix), m)
|
||||
|
||||
return lipgloss.NewStyle().
|
||||
Width(width).
|
||||
Render(combine)
|
||||
}
|
33
cli/internal/models/header.go
Normal file
33
cli/internal/models/header.go
Normal file
@ -0,0 +1,33 @@
|
||||
package models
|
||||
|
||||
import "github.com/charmbracelet/lipgloss"
|
||||
|
||||
type Header struct {
|
||||
style lipgloss.Style
|
||||
}
|
||||
|
||||
func NewHeader() *Header {
|
||||
s := lipgloss.NewStyle().
|
||||
AlignHorizontal(lipgloss.Center).
|
||||
AlignVertical(lipgloss.Bottom).
|
||||
MarginTop(1)
|
||||
|
||||
return &Header{
|
||||
style: s,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Header) Gen(width int, items ...string) string {
|
||||
s := h.style.Width(width)
|
||||
|
||||
pp := lipgloss.JoinHorizontal(lipgloss.Center, items...)
|
||||
|
||||
return s.Render(pp)
|
||||
}
|
||||
|
||||
func (h *Header) GenItem(text string) string {
|
||||
return lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#cdd6f4")).
|
||||
Margin(0, 1).
|
||||
Render(text)
|
||||
}
|
84
cli/internal/models/help.go
Normal file
84
cli/internal/models/help.go
Normal file
@ -0,0 +1,84 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"github.com/charmbracelet/bubbles/help"
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
// keyMap defines a set of keybindings. To work for help it must satisfy
|
||||
// key.Map. It could also very easily be a map[string]key.Binding.
|
||||
type keyMap struct {
|
||||
Up key.Binding
|
||||
Down key.Binding
|
||||
Left key.Binding
|
||||
Right key.Binding
|
||||
Help key.Binding
|
||||
Quit key.Binding
|
||||
}
|
||||
|
||||
// ShortHelp returns keybindings to be shown in the mini help view. It's part
|
||||
// of the key.Map interface.
|
||||
func (k keyMap) ShortHelp() []key.Binding {
|
||||
return []key.Binding{k.Help, k.Quit}
|
||||
}
|
||||
|
||||
// FullHelp returns keybindings for the expanded help view. It's part of the
|
||||
// key.Map interface.
|
||||
func (k keyMap) FullHelp() [][]key.Binding {
|
||||
return [][]key.Binding{
|
||||
{k.Up, k.Down, k.Left, k.Right}, // first column
|
||||
{k.Help, k.Quit}, // second column
|
||||
}
|
||||
}
|
||||
|
||||
type Help struct {
|
||||
keys keyMap
|
||||
style lipgloss.Style
|
||||
help help.Model
|
||||
}
|
||||
|
||||
func NewHelp() *Help {
|
||||
keys := keyMap{
|
||||
Up: key.NewBinding(
|
||||
key.WithKeys("up", "k"),
|
||||
key.WithHelp("↑/k", "move up"),
|
||||
),
|
||||
Down: key.NewBinding(
|
||||
key.WithKeys("down", "j"),
|
||||
key.WithHelp("↓/j", "move down"),
|
||||
),
|
||||
Left: key.NewBinding(
|
||||
key.WithKeys("left", "h"),
|
||||
key.WithHelp("←/h", "move left"),
|
||||
),
|
||||
Right: key.NewBinding(
|
||||
key.WithKeys("right", "l"),
|
||||
key.WithHelp("→/l", "move right"),
|
||||
),
|
||||
Help: key.NewBinding(
|
||||
key.WithKeys("?"),
|
||||
key.WithHelp("?", "toggle help"),
|
||||
),
|
||||
Quit: key.NewBinding(
|
||||
key.WithKeys("q", "esc", "ctrl+c"),
|
||||
key.WithHelp("q", "quit"),
|
||||
),
|
||||
}
|
||||
|
||||
return &Help{
|
||||
keys: keys,
|
||||
style: lipgloss.NewStyle().Padding(1, 2),
|
||||
help: help.New(),
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Help) Gen(width int) string {
|
||||
h.help.Width = width
|
||||
render := h.help.View(h.keys)
|
||||
return h.style.Render(render)
|
||||
}
|
||||
|
||||
func (h *Help) Toggle() {
|
||||
h.help.ShowAll = !h.help.ShowAll
|
||||
}
|
220
cli/internal/models/runner.go
Normal file
220
cli/internal/models/runner.go
Normal file
@ -0,0 +1,220 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
"github.com/charmbracelet/bubbles/spinner"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/spotdemo4/trevstack/cli/internal/apps"
|
||||
"github.com/spotdemo4/trevstack/cli/internal/utils"
|
||||
)
|
||||
|
||||
type runner struct {
|
||||
width *int
|
||||
height *int
|
||||
|
||||
prefix lipgloss.Style
|
||||
checkmark string
|
||||
xmark string
|
||||
|
||||
header *Header
|
||||
cbox *Cbox
|
||||
help *Help
|
||||
spinner spinner.Model
|
||||
|
||||
msgChan chan apps.Msg
|
||||
msgs []apps.Msg
|
||||
apps []*apps.App
|
||||
}
|
||||
|
||||
func NewRunner(msgChan chan apps.Msg, applications []*apps.App) *runner {
|
||||
|
||||
prefix := lipgloss.NewStyle().
|
||||
Padding(0, 1, 0, 1).
|
||||
Margin(0, 1, 0, 1).
|
||||
Background(lipgloss.Color("#89dceb")).
|
||||
Foreground(lipgloss.Color("#11111b"))
|
||||
|
||||
checkmark := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#a6e3a1")).
|
||||
Bold(true).
|
||||
Render("✓")
|
||||
|
||||
xmark := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#f38ba8")).
|
||||
Bold(true).
|
||||
Render("✕")
|
||||
|
||||
mpl := 0
|
||||
for _, app := range applications {
|
||||
if len(app.Name) > mpl {
|
||||
mpl = len(app.Name)
|
||||
}
|
||||
}
|
||||
|
||||
return &runner{
|
||||
width: nil,
|
||||
height: nil,
|
||||
|
||||
prefix: prefix,
|
||||
checkmark: checkmark,
|
||||
xmark: xmark,
|
||||
|
||||
header: NewHeader(),
|
||||
cbox: NewCbox(mpl + 1),
|
||||
help: NewHelp(),
|
||||
spinner: spinner.New(spinner.WithSpinner(spinner.MiniDot), spinner.WithStyle(lipgloss.NewStyle().Foreground(lipgloss.Color("#a6adc8")))),
|
||||
|
||||
msgChan: msgChan,
|
||||
msgs: []apps.Msg{},
|
||||
apps: applications,
|
||||
}
|
||||
}
|
||||
|
||||
func (m runner) Init() tea.Cmd {
|
||||
return tea.Batch(
|
||||
m.spinner.Tick,
|
||||
func() tea.Msg {
|
||||
return apps.Msg(<-m.msgChan)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func (m runner) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
var cmd tea.Cmd
|
||||
var cmds []tea.Cmd
|
||||
|
||||
switch msg := msg.(type) {
|
||||
|
||||
case apps.Msg:
|
||||
// Remove old message with the same key
|
||||
if msg.Key != nil && msg.Loading != nil && !*msg.Loading {
|
||||
for i, prev := range m.msgs {
|
||||
if prev.Key != nil && prev.Loading != nil && *prev.Key == *msg.Key && *prev.Loading {
|
||||
m.msgs = append(m.msgs[:i], m.msgs[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set current state
|
||||
if msg.Loading != nil {
|
||||
if *msg.Loading {
|
||||
msg.App.Loading = nil
|
||||
} else {
|
||||
msg.App.Loading = msg.Success
|
||||
}
|
||||
}
|
||||
|
||||
// Append new message
|
||||
m.msgs = append(m.msgs, msg)
|
||||
|
||||
return m, func() tea.Msg {
|
||||
return apps.Msg(<-m.msgChan)
|
||||
}
|
||||
|
||||
case spinner.TickMsg:
|
||||
m.spinner, cmd = m.spinner.Update(msg)
|
||||
return m, cmd
|
||||
|
||||
case tea.WindowSizeMsg:
|
||||
m.width = utils.IntPointer(msg.Width)
|
||||
m.height = utils.IntPointer(msg.Height)
|
||||
|
||||
case tea.KeyMsg:
|
||||
switch {
|
||||
case key.Matches(msg, m.help.keys.Help):
|
||||
m.help.Toggle()
|
||||
|
||||
case key.Matches(msg, m.help.keys.Quit):
|
||||
return m, tea.Quit
|
||||
}
|
||||
|
||||
case tea.MouseMsg:
|
||||
switch msg.Button {
|
||||
case tea.MouseButtonWheelDown:
|
||||
m.cbox.Viewport.LineDown(1)
|
||||
|
||||
case tea.MouseButtonWheelUp:
|
||||
m.cbox.Viewport.LineUp(1)
|
||||
}
|
||||
}
|
||||
|
||||
return m, tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
func (m runner) term() (rows []string) {
|
||||
if m.width == nil {
|
||||
return rows
|
||||
}
|
||||
|
||||
for _, msg := range m.msgs {
|
||||
item := []string{}
|
||||
|
||||
if msg.Loading != nil && *msg.Loading {
|
||||
item = append(item, m.spinner.View())
|
||||
}
|
||||
|
||||
if msg.Success != nil {
|
||||
if *msg.Success {
|
||||
item = append(item, m.checkmark)
|
||||
} else {
|
||||
item = append(item, m.xmark)
|
||||
}
|
||||
}
|
||||
|
||||
item = append(item, msg.Text)
|
||||
itemStr := strings.Join(item, " ")
|
||||
|
||||
// Render the row
|
||||
rows = append(rows, m.cbox.GenItem(msg.Time, msg.App.Name, itemStr, msg.App.Color, *m.width))
|
||||
}
|
||||
|
||||
return rows
|
||||
}
|
||||
|
||||
func (m runner) head() (items []string) {
|
||||
for _, app := range m.apps {
|
||||
item := []string{}
|
||||
|
||||
if app.Loading == nil {
|
||||
item = append(item, m.spinner.View())
|
||||
} else if *app.Loading {
|
||||
item = append(item, m.checkmark)
|
||||
} else {
|
||||
item = append(item, m.xmark)
|
||||
}
|
||||
|
||||
item = append(item, app.Name)
|
||||
items = append(items, m.header.GenItem(strings.Join(item, " ")))
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
func (m runner) View() string {
|
||||
if m.width == nil || m.height == nil {
|
||||
return fmt.Sprintf("\n %s Loading...", m.spinner.View())
|
||||
}
|
||||
|
||||
// Generate the UI
|
||||
header := m.header.Gen(*m.width, m.head()...)
|
||||
footer := m.help.Gen(*m.width)
|
||||
main := m.cbox.Gen(
|
||||
strings.Join(m.term(), "\n"),
|
||||
*m.width,
|
||||
*m.height,
|
||||
lipgloss.Height(header),
|
||||
lipgloss.Height(footer),
|
||||
)
|
||||
|
||||
s := header
|
||||
s += main
|
||||
s += footer
|
||||
|
||||
// Send the UI for rendering
|
||||
return s
|
||||
}
|
13
cli/internal/utils/pointers.go
Normal file
13
cli/internal/utils/pointers.go
Normal file
@ -0,0 +1,13 @@
|
||||
package utils
|
||||
|
||||
func BoolPointer(b bool) *bool {
|
||||
return &b
|
||||
}
|
||||
|
||||
func StringPointer(s string) *string {
|
||||
return &s
|
||||
}
|
||||
|
||||
func IntPointer(i int) *int {
|
||||
return &i
|
||||
}
|
55
cli/internal/utils/run.go
Normal file
55
cli/internal/utils/run.go
Normal file
@ -0,0 +1,55 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
type Stdout string
|
||||
type Stderr string
|
||||
type ExitCode int
|
||||
|
||||
func Run(cmd *exec.Cmd) (chan interface{}, error) {
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stderr, err := cmd.StderrPipe()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := cmd.Start(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c := make(chan interface{}, 10)
|
||||
|
||||
go func() {
|
||||
scan := bufio.NewScanner(stdout)
|
||||
for scan.Scan() {
|
||||
c <- Stdout(scan.Text())
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
scan := bufio.NewScanner(stderr)
|
||||
for scan.Scan() {
|
||||
c <- Stderr(scan.Text())
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer close(c)
|
||||
|
||||
if err := cmd.Wait(); err != nil {
|
||||
if exitError, ok := err.(*exec.ExitError); ok {
|
||||
c <- ExitCode(exitError.ExitCode())
|
||||
} else {
|
||||
c <- ExitCode(1)
|
||||
}
|
||||
} else {
|
||||
c <- ExitCode(0)
|
||||
}
|
||||
}()
|
||||
|
||||
return c, nil
|
||||
}
|
Reference in New Issue
Block a user