diff --git a/cli/debounce.go b/cli/debounce.go new file mode 100644 index 0000000..67c4759 --- /dev/null +++ b/cli/debounce.go @@ -0,0 +1,34 @@ +package main + +import ( + "sync" + "time" +) + +// Debouncer is a struct that holds the debounce logic +type Debouncer struct { + delay time.Duration + timer *time.Timer + mu sync.Mutex +} + +// NewDebouncer creates a new Debouncer with the specified delay +func NewDebouncer(delay time.Duration) *Debouncer { + return &Debouncer{ + delay: delay, + } +} + +// Do calls the provided function after the delay, resetting the delay if called again +func (d *Debouncer) Do(f func()) { + d.mu.Lock() + defer d.mu.Unlock() + + // If there's an existing timer, stop it + if d.timer != nil { + d.timer.Stop() + } + + // Create a new timer + d.timer = time.AfterFunc(d.delay, f) +} diff --git a/cli/go.mod b/cli/go.mod new file mode 100644 index 0000000..5c3677c --- /dev/null +++ b/cli/go.mod @@ -0,0 +1,13 @@ +module github.com/maddalax/htmgo/cli + +go 1.23.0 + +require ( + github.com/dave/jennifer v1.7.1 + github.com/fsnotify/fsnotify v1.7.0 + golang.org/x/mod v0.21.0 + golang.org/x/net v0.29.0 + golang.org/x/tools v0.25.0 +) + +require golang.org/x/sys v0.25.0 // indirect diff --git a/framework/tooling/htmgo/Taskfile.yml b/cli/htmgo/Taskfile.yml similarity index 100% rename from framework/tooling/htmgo/Taskfile.yml rename to cli/htmgo/Taskfile.yml diff --git a/framework/tooling/htmltogo/entry.go b/cli/htmltogo/entry.go similarity index 100% rename from framework/tooling/htmltogo/entry.go rename to cli/htmltogo/entry.go diff --git a/cli/runner.go b/cli/runner.go new file mode 100644 index 0000000..7d10f42 --- /dev/null +++ b/cli/runner.go @@ -0,0 +1,83 @@ +package main + +import ( + "flag" + "fmt" + "github.com/maddalax/htmgo/cli/tasks/astgen" + "github.com/maddalax/htmgo/cli/tasks/copyassets" + "github.com/maddalax/htmgo/cli/tasks/css" + "github.com/maddalax/htmgo/cli/tasks/downloadtemplate" + "github.com/maddalax/htmgo/cli/tasks/process" + "github.com/maddalax/htmgo/cli/tasks/reloader" + "github.com/maddalax/htmgo/cli/tasks/run" + "os" + "strings" +) + +func main() { + done := RegisterSignals() + + commandMap := make(map[string]*flag.FlagSet) + commands := []string{"template", "run", "watch", "build", "setup", "css"} + + for _, command := range commands { + commandMap[command] = flag.NewFlagSet(command, flag.ExitOnError) + } + + if len(os.Args) < 2 { + fmt.Println(fmt.Sprintf("Usage: htmgo [%s]", strings.Join(commands, " | "))) + os.Exit(1) + } + + c := commandMap[os.Args[1]] + + if c == nil { + fmt.Println(fmt.Sprintf("Usage: htmgo [%s]", strings.Join(commands, " | "))) + os.Exit(1) + return + } + + err := c.Parse(os.Args[2:]) + + if err != nil { + fmt.Println(err.Error()) + os.Exit(1) + return + } + + taskName := os.Args[1] + + if taskName == "watch" { + astgen.GenAst(true) + css.GenerateCss(true) + go func() { + _ = run.Server(true) + }() + startWatcher(reloader.OnFileChange) + } else { + if taskName == "setup" { + process.RunOrExit("go mod download") + process.RunOrExit("go mod tidy") + copyassets.CopyAssets() + _ = astgen.GenAst(true) + _ = css.GenerateCss(true) + } + if taskName == "css" { + _ = css.GenerateCss(true) + } + if taskName == "ast" { + _ = astgen.GenAst(true) + } + if taskName == "run" { + _ = astgen.GenAst(true) + _ = css.GenerateCss(true) + _ = run.Server(true) + } + if taskName == "template" { + downloadtemplate.DownloadTemplate("./my-app") + } + } + + <-done + fmt.Println("Cleanup complete. Exiting.") +} diff --git a/cli/signals.go b/cli/signals.go new file mode 100644 index 0000000..2ec875f --- /dev/null +++ b/cli/signals.go @@ -0,0 +1,31 @@ +package main + +import ( + "fmt" + "github.com/maddalax/htmgo/cli/tasks/process" + "os" + "os/signal" + "syscall" +) + +func RegisterSignals() chan bool { + // Create a channel to receive OS signals + sigs := make(chan os.Signal, 1) + // Register the channel to receive interrupt and terminate signals + signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) + + done := make(chan bool, 1) + // Run a goroutine to handle signals + go func() { + // Block until a signal is received + sig := <-sigs + fmt.Println() + fmt.Println("Received signal:", sig) + // Perform cleanup + process.KillAll() + // Signal that cleanup is done + done <- true + }() + + return done +} diff --git a/framework/tooling/astgen/ast.go b/cli/tasks/astgen/ast.go similarity index 99% rename from framework/tooling/astgen/ast.go rename to cli/tasks/astgen/ast.go index 7c77a80..4a10b4f 100644 --- a/framework/tooling/astgen/ast.go +++ b/cli/tasks/astgen/ast.go @@ -1,4 +1,4 @@ -package main +package astgen import ( "bytes" diff --git a/framework/tooling/astgen/codebuilder.go b/cli/tasks/astgen/codebuilder.go similarity index 99% rename from framework/tooling/astgen/codebuilder.go rename to cli/tasks/astgen/codebuilder.go index 3743144..2c4b8fa 100644 --- a/framework/tooling/astgen/codebuilder.go +++ b/cli/tasks/astgen/codebuilder.go @@ -1,4 +1,4 @@ -package main +package astgen import ( "bytes" diff --git a/framework/tooling/astgen/entry.go b/cli/tasks/astgen/entry.go similarity index 96% rename from framework/tooling/astgen/entry.go rename to cli/tasks/astgen/entry.go index 3a5eacb..4f55430 100644 --- a/framework/tooling/astgen/entry.go +++ b/cli/tasks/astgen/entry.go @@ -1,4 +1,4 @@ -package main +package astgen import ( "fmt" @@ -243,6 +243,10 @@ func writePartialsFile() { return } + if len(partials) == 0 { + return + } + builder := NewCodeBuilder(nil) builder.AppendLine(`// Package partials THIS FILE IS GENERATED. DO NOT EDIT.`) builder.AppendLine("package load") @@ -284,6 +288,7 @@ func formatRoute(path string) string { } func writePagesFile() { + builder := NewCodeBuilder(nil) builder.AppendLine(`// Package pages THIS FILE IS GENERATED. DO NOT EDIT.`) builder.AppendLine("package pages") @@ -292,6 +297,10 @@ func writePagesFile() { pages, _ := findPublicFuncsReturningHPage("pages") + if len(pages) == 0 { + return + } + for _, page := range pages { if page.Import != "" && page.Package != "pages" { builder.AddImport(page.Import) @@ -336,13 +345,20 @@ func GetModuleName() string { goModBytes, err := os.ReadFile(modPath) if err != nil { fmt.Fprintf(os.Stderr, "error reading go.mod: %v\n", err) - os.Exit(1) + return "" } modName := modfile.ModulePath(goModBytes) return modName } -func main() { +func GenAst(exitOnError bool) error { + if GetModuleName() == "" { + if exitOnError { + os.Exit(1) + } + return fmt.Errorf("error getting module name") + } writePartialsFile() writePagesFile() + return nil } diff --git a/framework/tooling/astgen/map.go b/cli/tasks/astgen/map.go similarity index 99% rename from framework/tooling/astgen/map.go rename to cli/tasks/astgen/map.go index e212093..201ccea 100644 --- a/framework/tooling/astgen/map.go +++ b/cli/tasks/astgen/map.go @@ -1,4 +1,4 @@ -package main +package astgen // OrderedMap is a generic data structure that maintains the order of keys. type OrderedMap[K comparable, V any] struct { diff --git a/framework/tooling/astgen/util.go b/cli/tasks/astgen/util.go similarity index 96% rename from framework/tooling/astgen/util.go rename to cli/tasks/astgen/util.go index 204d389..e55c5a9 100644 --- a/framework/tooling/astgen/util.go +++ b/cli/tasks/astgen/util.go @@ -1,4 +1,4 @@ -package main +package astgen import ( "fmt" diff --git a/framework/tooling/astgen/writer.go b/cli/tasks/astgen/writer.go similarity index 98% rename from framework/tooling/astgen/writer.go rename to cli/tasks/astgen/writer.go index b20e076..a600354 100644 --- a/framework/tooling/astgen/writer.go +++ b/cli/tasks/astgen/writer.go @@ -1,4 +1,4 @@ -package main +package astgen import ( "go/ast" diff --git a/framework/tooling/copyassets/bundle.go b/cli/tasks/copyassets/bundle.go similarity index 98% rename from framework/tooling/copyassets/bundle.go rename to cli/tasks/copyassets/bundle.go index 6c4c39b..60fadb5 100644 --- a/framework/tooling/copyassets/bundle.go +++ b/cli/tasks/copyassets/bundle.go @@ -1,4 +1,4 @@ -package main +package copyassets import ( "fmt" @@ -83,7 +83,7 @@ func copyDir(srcDir, dstDir string) error { }) } -func main() { +func CopyAssets() { modulePath := "github.com/maddalax/htmgo/framework" version, err := getModuleVersion(modulePath) if err != nil { diff --git a/cli/tasks/css/css.go b/cli/tasks/css/css.go new file mode 100644 index 0000000..50eae20 --- /dev/null +++ b/cli/tasks/css/css.go @@ -0,0 +1,10 @@ +package css + +import "github.com/maddalax/htmgo/cli/tasks/process" + +func GenerateCss(exitOnError bool) error { + return process.RunMany([]string{ + "chmod +x ./assets/css/tailwindcss", + "./assets/css/tailwindcss -i ./assets/css/input.css -o ./assets/dist/main.css -c ./assets/css/tailwind.config.js", + }, exitOnError) +} diff --git a/framework/tooling/downloadtemplate/main.go b/cli/tasks/downloadtemplate/main.go similarity index 77% rename from framework/tooling/downloadtemplate/main.go rename to cli/tasks/downloadtemplate/main.go index 3ce7b24..10a995b 100644 --- a/framework/tooling/downloadtemplate/main.go +++ b/cli/tasks/downloadtemplate/main.go @@ -1,4 +1,4 @@ -package main +package downloadtemplate import ( "flag" @@ -34,25 +34,23 @@ func deleteAllExceptTemplate(outPath string, excludeDir string) { } } -func main() { +func DownloadTemplate(outPath string) { cwd, _ := os.Getwd() - outPath := flag.String("out", "", "Specify the output path for the new app") - flag.Parse() - *outPath = strings.ReplaceAll(*outPath, "\n", "") - *outPath = strings.ReplaceAll(*outPath, " ", "-") - *outPath = strings.ToLower(*outPath) + outPath = strings.ReplaceAll(outPath, "\n", "") + outPath = strings.ReplaceAll(outPath, " ", "-") + outPath = strings.ToLower(outPath) - if *outPath == "" { + if outPath == "" { fmt.Println("Please provide a name for your app.") return } excludeDir := "starter-template" - install := exec.Command("git", "clone", "https://github.com/maddalax/htmgo", "--depth=1", *outPath) + install := exec.Command("git", "clone", "https://github.com/maddalax/htmgo", "--depth=1", outPath) install.Stdout = os.Stdout install.Stderr = os.Stderr err := install.Run() @@ -62,9 +60,9 @@ func main() { return } - deleteAllExceptTemplate(*outPath, excludeDir) + deleteAllExceptTemplate(outPath, excludeDir) - newDir := filepath.Join(cwd, *outPath) + newDir := filepath.Join(cwd, outPath) commands := [][]string{ {"cp", "-vaR", fmt.Sprintf("%s/.", excludeDir), "."}, @@ -88,8 +86,8 @@ func main() { fmt.Println("Template downloaded successfully.") fmt.Println("To start the development server, run the following commands:") - fmt.Printf("cd %s && htmgo run\n", *outPath) + fmt.Printf("cd %s && htmgo run\n", outPath) fmt.Printf("To build the project, run the following command:\n") - fmt.Printf("cd %s && htmgo build\n", *outPath) + fmt.Printf("cd %s && htmgo build\n", outPath) } diff --git a/cli/tasks/process/process.go b/cli/tasks/process/process.go new file mode 100644 index 0000000..6c87ac7 --- /dev/null +++ b/cli/tasks/process/process.go @@ -0,0 +1,145 @@ +package process + +import ( + "errors" + "fmt" + "log" + "os" + "os/exec" + "strings" + "syscall" + "time" +) + +var commands = make([]*exec.Cmd, 0) + +func AppendRunning(cmd *exec.Cmd) { + commands = append(commands, cmd) +} + +func KillAll() { + + tries := 0 + for { + tries++ + allFinished := true + for _, cmd := range commands { + if cmd.Process == nil { + allFinished = false + + if tries > 50 { + args := strings.Join(cmd.Args, " ") + log.Printf("process %v is not running after 50 tries, breaking.\n", args) + allFinished = true + break + } else { + time.Sleep(time.Millisecond * 50) + continue + } + } + } + if allFinished { + break + } + } + + for _, command := range commands { + pid := command.Process.Pid + err := syscall.Kill(-pid, syscall.SIGKILL) + if err != nil { + continue + } + } + + for { + finished := true + for _, c := range commands { + if c.Process == nil { + continue + } + exists, err := PidExists(int32(c.Process.Pid)) + if err != nil { + finished = false + } + if exists { + syscall.Kill(-c.Process.Pid, syscall.SIGKILL) + finished = false + } + } + + if finished { + break + } else { + fmt.Printf("waiting for all processes to exit\n") + time.Sleep(time.Millisecond * 5) + } + } + + commands = make([]*exec.Cmd, 0) +} + +func PidExists(pid int32) (bool, error) { + if pid <= 0 { + return false, fmt.Errorf("invalid pid %v", pid) + } + proc, err := os.FindProcess(int(pid)) + if err != nil { + return false, err + } + err = proc.Signal(syscall.Signal(0)) + if err == nil { + return true, nil + } + if err.Error() == "os: process already finished" { + return false, nil + } + var errno syscall.Errno + ok := errors.As(err, &errno) + if !ok { + return false, err + } + switch errno { + case syscall.ESRCH: + return false, nil + case syscall.EPERM: + return true, nil + } + return false, err +} + +func RunOrExit(command string) { + _ = Run(command, true) +} + +func RunMany(commands []string, exitOnError bool) error { + for _, command := range commands { + err := Run(command, false) + if err != nil { + if exitOnError { + os.Exit(1) + } + return err + } + } + return nil +} + +func Run(command string, exitOnError bool) error { + cmd := exec.Command("bash", "-c", command) + cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + AppendRunning(cmd) + err := cmd.Run() + if err != nil { + if strings.Contains(err.Error(), "signal: killed") { + return nil + } + if exitOnError { + log.Println(fmt.Sprintf("error: %v", err)) + os.Exit(1) + } + return err + } + return nil +} diff --git a/cli/tasks/reloader/reloader.go b/cli/tasks/reloader/reloader.go new file mode 100644 index 0000000..8405463 --- /dev/null +++ b/cli/tasks/reloader/reloader.go @@ -0,0 +1,115 @@ +package reloader + +import ( + "fmt" + "github.com/fsnotify/fsnotify" + "github.com/maddalax/htmgo/cli/tasks/astgen" + "github.com/maddalax/htmgo/cli/tasks/css" + "github.com/maddalax/htmgo/cli/tasks/process" + "github.com/maddalax/htmgo/cli/tasks/run" + "strings" + "sync" +) + +type Change struct { + name string +} + +func NewChange(name string) *Change { + return &Change{name: name} +} + +func (c *Change) Name() string { + return c.name +} + +func (c *Change) HasAnyPrefix(prefix ...string) bool { + for _, s := range prefix { + if strings.HasPrefix(c.name, s) { + return true + } + } + return false +} + +func (c *Change) HasAnySuffix(suffix ...string) bool { + for _, s := range suffix { + if strings.HasSuffix(c.name, s) { + return true + } + } + return false +} + +func (c *Change) IsGenerated() bool { + return c.HasAnySuffix("generated.go") +} + +func (c *Change) IsGo() bool { + return c.HasAnySuffix(".go") +} + +type Tasks struct { + AstGen bool + Css bool + Run bool +} + +func OnFileChange(events []*fsnotify.Event) { + tasks := Tasks{} + + for _, event := range events { + c := NewChange(event.Name) + + if c.IsGenerated() { + continue + } + + if c.IsGo() && c.HasAnyPrefix("pages/", "partials/") { + tasks.AstGen = true + } + + if c.IsGo() { + tasks.Css = true + tasks.Run = true + } + + if c.HasAnySuffix("tailwind.config.js", ".css") { + tasks.Css = true + } + } + + deps := make([]func() any, 0) + + if tasks.AstGen { + deps = append(deps, func() any { + return astgen.GenAst(false) + }) + } + + if tasks.Css { + deps = append(deps, func() any { + return css.GenerateCss(false) + }) + } + + wg := sync.WaitGroup{} + + for _, dep := range deps { + wg.Add(1) + go func(dep func() any) { + defer wg.Done() + err := dep() + if err != nil { + fmt.Println(err) + } + }(dep) + } + + wg.Wait() + + if tasks.Run { + process.KillAll() + _ = run.Server(false) + } +} diff --git a/cli/tasks/run/runserver.go b/cli/tasks/run/runserver.go new file mode 100644 index 0000000..00b646f --- /dev/null +++ b/cli/tasks/run/runserver.go @@ -0,0 +1,7 @@ +package run + +import "github.com/maddalax/htmgo/cli/tasks/process" + +func Server(exitOnError bool) error { + return process.Run("go run .", exitOnError) +} diff --git a/cli/watcher.go b/cli/watcher.go new file mode 100644 index 0000000..d67216e --- /dev/null +++ b/cli/watcher.go @@ -0,0 +1,73 @@ +package main + +import ( + "fmt" + "github.com/fsnotify/fsnotify" + "log" + "os" + "path/filepath" +) + +func startWatcher(cb func(file []*fsnotify.Event)) { + //debouncer := NewDebouncer(time.Millisecond * 100) + events := make([]*fsnotify.Event, 0) + + defer func() { + if r := recover(); r != nil { + fmt.Println("Recovered from fatal error:", r) + // You can log the error here or take other corrective actions + } + }() + // Create new watcher. + watcher, err := fsnotify.NewWatcher() + if err != nil { + panic(err) + } + defer watcher.Close() + // Start listening for events. + go func() { + for { + select { + case event, ok := <-watcher.Events: + if !ok { + return + } + if event.Has(fsnotify.Write) { + events = append(events, &event) + go cb(events) + events = make([]*fsnotify.Event, 0) + } + case err, ok := <-watcher.Errors: + if !ok { + return + } + log.Println("error:", err) + } + } + }() + + rootDir := "." + // Walk through the root directory and add all subdirectories to the watcher + err = filepath.Walk(rootDir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + // Only watch directories + if info.IsDir() { + err = watcher.Add(path) + if err != nil { + log.Println("Error adding directory to watcher:", err) + } else { + log.Println("Watching directory:", path) + } + } + return nil + }) + if err != nil { + log.Fatal(err) + } + + done := RegisterSignals() + <-done + println("process exited") +} diff --git a/framework/h/events.go b/framework/h/events.go new file mode 100644 index 0000000..198aa82 --- /dev/null +++ b/framework/h/events.go @@ -0,0 +1,15 @@ +package h + +type HxEvent = string + +var ( + HxBeforeRequest HxEvent = "hx-on::before-request" + HxAfterRequest HxEvent = "hx-on::after-request" + HxOnMutationError HxEvent = "hx-on::mutation-error" + HxOnLoad HxEvent = "hx-on::load" + HxOnLoadError HxEvent = "hx-on::load-error" + HxRequestTimeout HxEvent = "hx-on::request-timeout" + HxTrigger HxEvent = "hx-on::trigger" + HxRequestStart HxEvent = "hx-on::xhr:loadstart" + HxRequestProgress HxEvent = "hx-on::xhr:progress" +) diff --git a/framework/h/lifecycle.go b/framework/h/lifecycle.go index 9fe9552..8839896 100644 --- a/framework/h/lifecycle.go +++ b/framework/h/lifecycle.go @@ -4,58 +4,72 @@ import ( "fmt" ) -var HxBeforeRequest = "hx-on::before-request" -var HxAfterRequest = "hx-on::after-request" -var HxOnMutationError = "hx-on::mutation-error" - type LifeCycle struct { - beforeRequest []JsCommand - afterRequest []JsCommand - onMutationError []JsCommand + handlers map[HxEvent][]JsCommand } func NewLifeCycle() *LifeCycle { return &LifeCycle{ - beforeRequest: []JsCommand{}, - afterRequest: []JsCommand{}, - onMutationError: []JsCommand{}, + handlers: make(map[HxEvent][]JsCommand), } } -func (l *LifeCycle) BeforeRequest(cmd ...JsCommand) *LifeCycle { - l.beforeRequest = append(l.beforeRequest, cmd...) +func (l *LifeCycle) OnEvent(event HxEvent, cmd ...JsCommand) *LifeCycle { + if l.handlers[event] == nil { + l.handlers[event] = []JsCommand{} + } + l.handlers[event] = append(l.handlers[event], cmd...) return l } +func (l *LifeCycle) BeforeRequest(cmd ...JsCommand) *LifeCycle { + l.OnEvent(HxBeforeRequest, cmd...) + return l +} + +func OnEvent(event HxEvent, cmd ...JsCommand) *LifeCycle { + return NewLifeCycle().OnEvent(event, cmd...) +} + +func BeforeRequest(cmd ...JsCommand) *LifeCycle { + return NewLifeCycle().BeforeRequest(cmd...) +} + +func AfterRequest(cmd ...JsCommand) *LifeCycle { + return NewLifeCycle().AfterRequest(cmd...) +} + +func OnMutationError(cmd ...JsCommand) *LifeCycle { + return NewLifeCycle().OnMutationError(cmd...) +} + func (l *LifeCycle) AfterRequest(cmd ...JsCommand) *LifeCycle { - l.afterRequest = append(l.afterRequest, cmd...) + l.OnEvent(HxAfterRequest, cmd...) return l } func (l *LifeCycle) OnMutationError(cmd ...JsCommand) *LifeCycle { - l.onMutationError = append(l.onMutationError, cmd...) + l.OnEvent(HxOnMutationError, cmd...) return l } func (l *LifeCycle) Render() *Node { - beforeRequest := "" - afterReqest := "" - onMutationError := "" - for _, command := range l.beforeRequest { - beforeRequest += fmt.Sprintf("%s;", command.Command) - } - for _, command := range l.afterRequest { - afterReqest += fmt.Sprintf("%s;", command.Command) - } - for _, command := range l.onMutationError { - onMutationError += fmt.Sprintf("%s;", command.Command) + m := make(map[string]string) + + for event, commands := range l.handlers { + m[event] = "" + for _, command := range commands { + m[event] += fmt.Sprintf("%s;", command.Command) + } } - return Children( - If(beforeRequest != "", Attribute(HxBeforeRequest, beforeRequest)), - If(afterReqest != "", Attribute(HxAfterRequest, afterReqest)), - If(onMutationError != "", Attribute(HxOnMutationError, onMutationError)), - ).Render() + children := make([]Renderable, 0) + + for event, js := range m { + children = append(children, Attribute(event, js)) + } + + return Children(children...).Render() } type JsCommand struct { @@ -66,10 +80,30 @@ func SetText(text string) JsCommand { return JsCommand{Command: fmt.Sprintf("this.innerText = '%s'", text)} } +func Increment(amount int) JsCommand { + return JsCommand{Command: fmt.Sprintf("this.innerText = parseInt(this.innerText) + %d", amount)} +} + +func SetInnerHtml(r Renderable) JsCommand { + return JsCommand{Command: fmt.Sprintf("this.innerHTML = `%s`", Render(r.Render()))} +} + +func SetOuterHtml(r Renderable) JsCommand { + return JsCommand{Command: fmt.Sprintf("this.outerHTML = `%s`", Render(r.Render()))} +} + func AddAttribute(name, value string) JsCommand { return JsCommand{Command: fmt.Sprintf("this.setAttribute('%s', '%s')", name, value)} } +func SetDisabled(disabled bool) JsCommand { + if disabled { + return AddAttribute("disabled", "true") + } else { + return RemoveAttribute("disabled") + } +} + func RemoveAttribute(name string) JsCommand { return JsCommand{Command: fmt.Sprintf("this.removeAttribute('%s')", name)} } diff --git a/framework/pages/generated.go b/framework/pages/generated.go deleted file mode 100644 index ad8c21e..0000000 --- a/framework/pages/generated.go +++ /dev/null @@ -1,8 +0,0 @@ -// Package pages THIS FILE IS GENERATED. DO NOT EDIT. -package pages - -import "github.com/gofiber/fiber/v2" -import "github.com/maddalax/htmgo/framework/h" - -func RegisterPages(f *fiber.App) { -} diff --git a/framework/tooling/astgen/go.mod b/framework/tooling/astgen/go.mod deleted file mode 100644 index 00800eb..0000000 --- a/framework/tooling/astgen/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module github.com/maddalax/htmgo/framework/tooling/astgen - -go 1.23.0 \ No newline at end of file diff --git a/framework/tooling/copyassets/go.mod b/framework/tooling/copyassets/go.mod deleted file mode 100644 index a09dfd7..0000000 --- a/framework/tooling/copyassets/go.mod +++ /dev/null @@ -1,5 +0,0 @@ -module github.com/maddalax/htmgo/framework/tooling/copyassets - -go 1.23.0 - -require golang.org/x/mod v0.21.0 diff --git a/framework/tooling/downloadtemplate/go.mod b/framework/tooling/downloadtemplate/go.mod deleted file mode 100644 index 3d626c1..0000000 --- a/framework/tooling/downloadtemplate/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module github.com/maddalax/htmgo/framework/tooling/downloadtemplate - -go 1.23.0 \ No newline at end of file diff --git a/framework/tooling/htmgo/go.mod b/framework/tooling/htmgo/go.mod deleted file mode 100644 index f376e8a..0000000 --- a/framework/tooling/htmgo/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module github.com/maddalax/htmgo/framework/tooling/htmgo - -go 1.23.0 \ No newline at end of file diff --git a/framework/tooling/htmgo/runner.go b/framework/tooling/htmgo/runner.go deleted file mode 100644 index 4f58aa4..0000000 --- a/framework/tooling/htmgo/runner.go +++ /dev/null @@ -1,94 +0,0 @@ -package main - -import ( - _ "embed" - "flag" - "fmt" - "os" - "os/exec" - "strings" - "sync" -) - -//go:embed Taskfile.yml -var taskFile string - -func main() { - commandMap := make(map[string]*flag.FlagSet) - commands := []string{"template", "run", "watch", "build", "setup", "css", "css-watch", "ast-watch", "go-watch"} - - for _, command := range commands { - commandMap[command] = flag.NewFlagSet(command, flag.ExitOnError) - } - - if len(os.Args) < 2 { - fmt.Println(fmt.Sprintf("Usage: htmgo [%s]", strings.Join(commands, " | "))) - os.Exit(1) - } - - c := commandMap[os.Args[1]] - - if c == nil { - fmt.Println(fmt.Sprintf("Usage: htmgo [%s]", strings.Join(commands, " | "))) - os.Exit(1) - return - } - - err := c.Parse(os.Args[2:]) - if err != nil { - fmt.Println(err.Error()) - os.Exit(1) - return - } - - // Install the latest version of Task - install := exec.Command("go", "install", "github.com/go-task/task/v3/cmd/task@latest") - - err = install.Run() - if err != nil { - fmt.Printf("Error installing task: %v\n", err) - return - } - - temp, err := os.CreateTemp("", "Taskfile.yml") - - if err != nil { - fmt.Printf("Error creating temporary file: %v\n", err) - return - } - - os.WriteFile(temp.Name(), []byte(taskFile), 0644) - - taskName := os.Args[1] - - if taskName == "watch" { - tasks := []string{"css-watch", "ast-watch", "go-watch"} - wg := sync.WaitGroup{} - for _, task := range tasks { - wg.Add(1) - go func() { - cmd := exec.Command("task", "-t", temp.Name(), task) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - err := cmd.Run() - if err != nil { - fmt.Printf("Error running task command: %v\n", err) - } - wg.Done() - }() - } - wg.Wait() - } else { - // Define the command and arguments - cmd := exec.Command("task", "-t", temp.Name(), os.Args[1]) - // Set the standard output and error to be the same as the Go program - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - // Run the command - err = cmd.Run() - if err != nil { - fmt.Printf("Error running task command: %v\n", err) - return - } - } -} diff --git a/framework/tooling/watch.go b/framework/tooling/watch.go deleted file mode 100644 index 5712934..0000000 --- a/framework/tooling/watch.go +++ /dev/null @@ -1,117 +0,0 @@ -package main - -import ( - "bytes" - "fmt" - "github.com/fsnotify/fsnotify" - "log" - "os" - "os/exec" - "path/filepath" - "strings" -) - -func main() { - once := false - if len(os.Args) > 1 { - once = os.Args[1] == "--once" - } - - command := "" - for i, arg := range os.Args { - if arg == "--command" { - command = os.Args[i+1] - } - } - - if command == "" { - panic("command is required") - } - - if once { - runCommand(command) - return - } - - defer func() { - if r := recover(); r != nil { - fmt.Println("Recovered from fatal error:", r) - // You can log the error here or take other corrective actions - } - }() - - runCommand(command) - // Create new watcher. - watcher, err := fsnotify.NewWatcher() - if err != nil { - panic(err) - } - defer watcher.Close() - // Start listening for events. - go func() { - for { - select { - case event, ok := <-watcher.Events: - if !ok { - return - } - - if strings.HasSuffix(event.Name, "generated.go") { - continue - } - - if event.Has(fsnotify.Write) { - success := runCommand(command) - if success { - log.Println(fmt.Sprintf("file changed. code generation successful")) - } else { - log.Println(fmt.Sprintf("file changed. code generation failed")) - } - } - case err, ok := <-watcher.Errors: - if !ok { - return - } - log.Println("error:", err) - } - } - }() - - cwd, _ := os.Getwd() - pagesDir := filepath.Join(cwd, "pages") - partialsDir := filepath.Join(cwd, "partials") - - toWatch := []string{pagesDir, partialsDir} - - for _, watch := range toWatch { - err = watcher.Add(watch) - if err != nil { - panic(err) - } - } - - // Block main goroutine forever. - <-make(chan struct{}) -} - -func runCommand(command string) bool { - // Create a new command - cmd := exec.Command("bash", "-c", command) - - // Capture stdout and stderr in buffers - var out bytes.Buffer - var stderr bytes.Buffer - cmd.Stdout = &out - cmd.Stderr = &stderr - - // Run the command - err := cmd.Run() - if err != nil { - log.Println(fmt.Sprintf("error: %v", err)) - println(stderr.String()) - return false - } else { - println(out.String()) - return true - } -} diff --git a/starter-template/main.go b/starter-template/main.go index 505ab53..c04124d 100644 --- a/starter-template/main.go +++ b/starter-template/main.go @@ -21,9 +21,9 @@ func main() { now := time.Now() err := ctx.Next() duration := time.Since(now) - ctx.Set("X-Response-Time", duration.String()) + ctx.Set("X-Response-Times", duration.String()) // Log or print the request method, URL, and duration - log.Printf("Request: %s %s took %dms", ctx.Method(), ctx.OriginalURL(), duration.Milliseconds()) + log.Printf("Requests: %s %s took %dms", ctx.Method(), ctx.OriginalURL(), duration.Milliseconds()) return err }) diff --git a/starter-template/pages/index.go b/starter-template/pages/index.go index 2ad53fe..a53ac6b 100644 --- a/starter-template/pages/index.go +++ b/starter-template/pages/index.go @@ -8,30 +8,38 @@ import ( func IndexPage(c *fiber.Ctx) *h.Page { return h.NewPage(h.Html( - h.Class("bg-slate-400 flex flex-col items-center h-full w-full"), + h.Class("bg-blue-400 flex flex-col items-center h-full w-full"), h.Head( h.Link("/public/main.css", "stylesheet"), h.Script("/public/htmgo.js"), ), h.Body( - h.Class("flex flex-col gap-3"), + h.Class("flex flex-col gap-4"), h.Div( h.Class("flex flex-col items-center justify-center gap-6 p-12 text-center"), h.H1( h.Class("text-4xl sm:text-5xl font-bold max-w-3xl"), - h.Text("Welcome to htmgo"), + h.Text("Welcome to my fast!!"), ), h.P( - h.Class("text-lg sm:text-xl max-w-2xl"), - h.Text("Combine the simplicity of Go with the power of HTMX for dynamic, JavaScript-light web development."), + h.Class("text-lg sm:text-xl max-w-1xl"), ), h.Div( - h.Button(h.Class("btn bg-blue-500 p-4 rounded text-white"), - h.Text("Click here to load a partial"), - h.GetPartial(partials.SamplePartial), - ), + Button(), ), ), ), )) } + +func Button() h.Renderable { + return h.Button(h.Class("btn bg-slate-500 p-4 rounded text-white"), + h.Text("Click here use my ytes"), + h.AfterRequest( + h.SetDisabled(true), + h.RemoveClass("bg-red-600"), + h.AddClass("bg-gray-500"), + ), + h.GetPartial(partials.SamplePartial), + ) +} diff --git a/starter-template/partials/index.go b/starter-template/partials/index.go index 61a7407..11e7107 100644 --- a/starter-template/partials/index.go +++ b/starter-template/partials/index.go @@ -6,9 +6,13 @@ import ( ) func SamplePartial(ctx *fiber.Ctx) *h.Partial { - return h.NewPartial(h.Div(h.P(h.Text("This is a sample partials.")))) + return h.NewPartial(h.Div(h.P(h.Text(" asdas")))) } func NewPartial(ctx *fiber.Ctx) *h.Partial { - return h.NewPartial(h.Div(h.P(h.Text("This is a new pardtiasl.")))) + return h.NewPartial(h.Div(h.P(h.Text("This sadsl.")))) +} + +func NewPartial2(ctx *fiber.Ctx) *h.Partial { + return h.NewPartial(h.Div(h.P(h.Text("This sasdsadasdwl.")))) } diff --git a/starter-template/partials/load/generated.go b/starter-template/partials/load/generated.go index 880f802..f5e3b8b 100644 --- a/starter-template/partials/load/generated.go +++ b/starter-template/partials/load/generated.go @@ -13,6 +13,9 @@ func GetPartialFromContext(ctx *fiber.Ctx) *h.Partial { if path == "NewPartial" || path == "/starter-template/partials.NewPartial" { return partials.NewPartial(ctx) } + if path == "NewPartial2" || path == "/starter-template/partials.NewPartial2" { + return partials.NewPartial2(ctx) + } return nil }