commit d7e1c5c59bee1a67030b892134edd4fecf14c66e Author: Bryson Steck Date: Sat Jun 28 17:14:54 2025 -0600 init basic working program, need to fix command running diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..8ab7850 --- /dev/null +++ b/go.mod @@ -0,0 +1,9 @@ +module forge.steck.dev/bryson/listen + +go 1.24 + +require github.com/jessevdk/go-flags v1.6.1 + +require github.com/fsnotify/fsnotify v1.9.0 + +require golang.org/x/sys v0.21.0 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..15bb6c9 --- /dev/null +++ b/go.sum @@ -0,0 +1,6 @@ +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/jessevdk/go-flags v1.6.1 h1:Cvu5U8UGrLay1rZfv/zP7iLpSHGUZ/Ou68T0iX1bBK4= +github.com/jessevdk/go-flags v1.6.1/go.mod h1:Mk8T1hIAWpOiJiHa9rJASDK2UGWji0EuPGBnNLMooyc= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/main.go b/main.go new file mode 100644 index 0000000..22f6fa2 --- /dev/null +++ b/main.go @@ -0,0 +1,162 @@ +package main + +import ( + "fmt" + "io/fs" + "log" + "maps" + "os" + "os/exec" + "os/signal" + "path/filepath" + "time" + + "github.com/fsnotify/fsnotify" + flags "github.com/jessevdk/go-flags" +) + +const VERSION string = "v0.1.0" + +var opts struct { + Version bool `short:"v" long:"version" description:"Displays version info and exits"` + Files []string `short:"f" long:"file" description:"File(s) to listen to (watch)" value-name:"FILE"` + Condition string `short:"w" long:"when" description:"If multiple files are specified, choose if any file or all files specified are needed to trigger COMMAND" default:"any" choice:"any" choice:"all"` + Checksum bool `short:"c" long:"checksum" description:"Use checksum to determine when file(s) are changed instead of modification time (only used when -i is specified)"` + Interval string `short:"i" long:"interval" description:"Use this time interval (ex. 5m30s, 1s) between filesystem checks instead of using kernel events"` +} + +func validateFiles(files []string) error { + if len(files) <= 0 { + fmt.Println("listen: at least one file (-f) is required") + return flags.ErrCommandRequired + } + + for _, file := range files { + info, err := os.Stat(file) + if err != nil { + fmt.Printf("listen: %s\n", err) + return err + } + + if info.IsDir() { + fmt.Printf("listen: %s: not a file\n", file) + return fs.ErrInvalid + } + + _, err = os.Open(file) + if err != nil { + fmt.Printf("listen: %s\n", err) + return err + } + } + + return nil +} + +func listen(files map[string]bool, condition string, cksum bool, interval string, command []string) { + // catch ^C + quit := make(chan bool) + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt) + + // main loop + if interval == "" { + watcher, err := fsnotify.NewWatcher() + if err != nil { + log.Fatal(err) + } + + for key := range files { + watcher.Add(key) + } + + go func() { + for { + trigger := false + select { + case event, ok := <-watcher.Events: + if !ok { + return + } + fmt.Println(event) + + for key := range files { + if event.Has(fsnotify.Write) && event.Name == key { + if condition == "any" { + trigger = true + break + } + files[key] = true + } + } + + if condition == "all" { + trigger = true + for value := range maps.Values(files) { + if !value { + trigger = false + break + } + } + } + + case _, ok := <-watcher.Errors: + if !ok { + return + } + } + + if trigger { + if len(command) >= 1 { + exec.Command(command[0], command[1:]...) + } else { + quit <- true + } + } + } + }() + + } else { + go func() { + for { + fmt.Printf("running") + time.Sleep(1 * time.Second) + } + + }() + + } + + select { + case <-c: + case <-quit: + } + +} + +func main() { + parser := flags.NewParser(&opts, flags.Default) + parser.Usage = "[OPTIONS] -- [COMMAND]" + remaining, err := parser.Parse() + + if err == nil { + if opts.Version { + fmt.Printf("listen %s\n", VERSION) + } else if err := validateFiles(opts.Files); err == nil { + var filesMap map[string]bool = make(map[string]bool) + success := true + for _, file := range opts.Files { + abs, err := filepath.Abs(file) + if err == nil { + filesMap[abs] = false + } else { + success = false + } + } + + if success { + listen(filesMap, opts.Condition, opts.Checksum, opts.Interval, remaining) + } + } + } +}