├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── gat ├── print.go └── run.go ├── go.mod ├── go.sum ├── input.go ├── input_test.go ├── looper.go ├── looper.png ├── looper.sublime-project ├── print.go └── watch.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | looper 24 | 25 | .DS_Store 26 | 27 | # editors 28 | *.sublime-workspace 29 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Looper Changelog 2 | 3 | Roadmap & voting at the public [Trello board](https://trello.com/b/VvblYiSE). 4 | 5 | ## v0.3.3 / 2015-08-23 6 | 7 | * Skip vendor folder on full test run when GO15VENDOREXPERIMENT is set. 8 | * Fix typo in NewRecursiveWatcher (thanks @corrupt). 9 | 10 | ## v0.3.2 / 2014-11-13 11 | 12 | * Don't watch directories starting with an underscore (like the go tool). 13 | 14 | ## v0.3.0 / 2014-11-12 15 | 16 | * Add Godep support `godeps go test` (thanks @sudhirj) 17 | * Switch tests from launchpad to gopkg.in/check.v1 (thanks @aibou) 18 | 19 | ## v0.2.3 / 2014-06-12 20 | 21 | * Update to new gophertown/fsnotify API (v0.11.0). 22 | * Ignore metadata changes when detecting modifications. 23 | 24 | ## v0.2.2 / 2014-05-23 25 | 26 | * Use gophertown/fsnotify (experimenting with the API there for now) 27 | 28 | ## v0.2.1 / 2013-07-06 29 | 30 | * Add --debug flag to help track down [#6] Tests run twice 31 | 32 | ## v0.2.0 / 2013-05-16 33 | 34 | * Rename to Looper 35 | * Packages are the unit of compilation in Go. Use a package-level granularity for testing. 36 | * Don't log Stat errors (can be caused by atomic saves in editors) 37 | 38 | ## v0.1.1 / 2013-04-21 39 | 40 | * Fixes "named files must all be in one directory" error [#2] 41 | * Pass through for -tags command line argument. Thanks @jtacoma. 42 | 43 | ## v0.1.0 / 2013-02-24 44 | 45 | * Recursively watches the file system, adding subfolders when created. 46 | * Readline interaction to run all tests or exit. 47 | * ANSI colors to add some flare. 48 | * Focused testing of a single file for a quick TDD loop (subject to change) 49 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | To hack on this project: 4 | 5 | 1. Install as usual (`go get -u github.com/...`) 6 | 2. Create your feature branch (`git checkout -b my-new-feature`) 7 | 3. Ensure everything works and the tests pass (`go test`) 8 | 4. Commit your changes (`git commit -am 'Add some feature'`) 9 | 10 | Contribute upstream: 11 | 12 | 1. Fork it on GitHub 13 | 2. Add your remote (`git remote add fork git@github.com:mycompany/repo.git`) 14 | 3. Push to the branch (`git push fork my-new-feature`) 15 | 4. Create a new Pull Request on GitHub 16 | 17 | For other team members: 18 | 19 | 1. Install as usual (`go get -u github.com/...`) 20 | 2. Add your remote (`git remote add fork git@github.com:mycompany/repo.git`) 21 | 3. Pull your revisions (`git fetch fork; git checkout -b my-new-feature fork/my-new-feature`) 22 | 23 | Notice: Always use the original import path by installing with `go get`. 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013, Nathan Youngman 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 14 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 17 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 18 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 19 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 20 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 21 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 22 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Looper 2 | 3 | Looper is a development tool for the [Go Programming Language][go]. It automatically runs your tests and (will eventually) hot compile your code when it detects file system changes. 4 | 5 | ![Looper screenshot](https://raw.githubusercontent.com/nathany/looper/master/looper.png) 6 | 7 | ## Status 8 | 9 | [![Stories in Ready](https://badge.waffle.io/nathany/looper.svg?label=ready&title=Ready)](http://waffle.io/nathany/looper) [![Build Status](https://drone.io/github.com/nathany/looper/status.png)](https://drone.io/github.com/nathany/looper/latest) [![Coverage](http://gocover.io/_badge/github.com/nathany/looper)](http://gocover.io/github.com/nathany/looper) [![GoDoc](https://godoc.org/github.com/nathany/looper?status.svg)](http://godoc.org/github.com/nathany/looper) 10 | 11 | [![Throughput Graph](https://graphs.waffle.io/nathany/looper/throughput.svg)](https://waffle.io/nathany/looper/metrics) 12 | 13 | This is an *early alpha*. There is still quite a lot to do (Hot Compiles, Growl notifications, and interactions for profiling, benchmarking, etc.). 14 | 15 | ## Get Going 16 | 17 | If you are on OS X, you need to first install GNU Readline via [Homebrew](http://mxcl.github.com/homebrew/): 18 | 19 | ``` console 20 | $ brew install readline 21 | ``` 22 | 23 | If you are on Linux, you'll need the readline development headers: 24 | 25 | Debian/Ubuntu: 26 | 27 | ```console 28 | sudo apt-get install libreadline-dev 29 | ``` 30 | 31 | Red Hat-based systems: 32 | 33 | ```console 34 | sudo yum install readline-devel 35 | ``` 36 | 37 | To install Looper, or to update your installation, run: 38 | 39 | ``` console 40 | $ go get -u github.com/nathany/looper 41 | ``` 42 | 43 | Then run `looper` in your project folder: 44 | 45 | ``` console 46 | $ looper 47 | Looper 0.3.3 is watching your files 48 | Type help for help. 49 | 50 | Watching path ./ 51 | ``` 52 | 53 | Note: There is [a known issue](https://github.com/nathany/looper/issues/6) where tests may run multiple times on OS X. Until this is resolved, please add your development folder to Spotlight Privacy in System Preferences. 54 | 55 | ## Gat (Go Autotest) 56 | 57 | Packages are the unit of compilation in Go. By convention, each package has a separate folder, though a single folder may also have a `*_test` package for black box testing. 58 | 59 | When Looper detects a change to a `*.go file`, it will build & run the tests for that directory. You can also run all tests against all packages at once. 60 | 61 | To setup a Suite definition ([Gocheck][], [PrettyTest][pat]), additional Checkers, or other test helpers, use any test file you like in the same folder (eg. `suite_test.go`). 62 | 63 | Gat is inspired by Andrea Fazzi's [PrettyAutoTest][pat]. 64 | 65 | ## Blunderbuss (Hot Compile) 66 | 67 | ...to be determined... 68 | 69 | Blunderbuss is inspired by [shotgun][], both in name and purpose. 70 | 71 | ## Interactions 72 | 73 | * `a`, `all`, `↩`: Run all tests. 74 | * `h`, `help`: Show help. 75 | * `e`, `exit`: Quit Looper 76 | 77 | ## Related Projects 78 | 79 | ### General purpose 80 | 81 | * [Reflex](https://github.com/cespare/reflex) by Caleb Spare 82 | * [rerun](https://github.com/skelterjohn/rerun) by John Asmuth to autobuild and kill/relaunch 83 | * [Watch](https://github.com/eaburns/Watch) by Ethan Burns includes acme integration 84 | * [watcher](https://github.com/tmc/watcher) by Travis Cline 85 | 86 | ### Testing 87 | 88 | * [PrettyAutoTest][pat] by Andrea Fazzi 89 | * [Glitch](https://github.com/levicook/glitch) by Levi Cook 90 | 91 | ### Web development 92 | 93 | * [App Engine devserver](https://developers.google.com/appengine/docs/go/tools/devserver) 94 | * [devweb](http://code.google.com/p/rsc/source/browse/devweb/) by Russ Cox 95 | * [gin](https://github.com/codegangsta/gin) by Jeremy Saenz 96 | * [rego](https://github.com/sqs/rego) by Quinn Slack 97 | * [shotgun-go](https://github.com/danielheath/shotgun-go) by Daniel Heath 98 | * [Revel](http://revel.github.io/) by Rob Figueiredo does Hot Code Reloading 99 | 100 | ### Comprehensive 101 | 102 | * [golab](https://github.com/mb0/lab) Linux IDE by Martin Schnabel 103 | * [GoTray](http://gotray.extremedev.org/) for OS X 104 | 105 | ## Thanks 106 | 107 | Special thanks to Chris Howey for the [fsnotify][] package. 108 | 109 | [go]: http://golang.org/ 110 | [fsnotify]: http://fsnotify.org/ 111 | [pat]: https://github.com/remogatto/prettytest 112 | [shotgun]: https://rubygems.org/gems/shotgun 113 | [Gocheck]: http://labix.org/gocheck 114 | 115 | -------------------------------------------------------------------------------- /gat/print.go: -------------------------------------------------------------------------------- 1 | package gat 2 | 3 | import ( 4 | "fmt" 5 | "github.com/koyachi/go-term-ansicolor/ansicolor" 6 | "strings" 7 | "time" 8 | ) 9 | 10 | func PrintCommand(args []string) { 11 | ClearPrompt() 12 | fmt.Println(ansicolor.Yellow(strings.Join(args, " "))) 13 | } 14 | 15 | func PrintCommandOutput(out []byte) { 16 | fmt.Print(string(out)) 17 | } 18 | 19 | func RedGreen(pass bool) { 20 | if pass { 21 | fmt.Print(ansicolor.Green("PASS")) 22 | } else { 23 | fmt.Print(ansicolor.Red("FAIL")) 24 | } 25 | } 26 | 27 | func ShowDuration(dur time.Duration) { 28 | fmt.Printf(" (%.2f seconds)\n", dur.Seconds()) 29 | } 30 | 31 | const CSI = "\x1b[" 32 | 33 | // remove from the screen anything that's been typed 34 | // from github.com/kierdavis/ansi 35 | func ClearPrompt() { 36 | fmt.Printf("%s2K", CSI) // clear line 37 | fmt.Printf("%s%dG", CSI, 0) // go to column 0 38 | } 39 | -------------------------------------------------------------------------------- /gat/run.go: -------------------------------------------------------------------------------- 1 | package gat 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "os/exec" 7 | "path/filepath" 8 | "strings" 9 | ) 10 | 11 | // IgnoreVendor if using Go 1.5 vendor experiment 12 | var IgnoreVendor = (os.Getenv("GO15VENDOREXPERIMENT") == "1") 13 | 14 | type Run struct { 15 | Tags string 16 | } 17 | 18 | func (run Run) RunAll() { 19 | if IgnoreVendor { 20 | pkgs := goList() 21 | run.goTest(pkgs...) 22 | } else { 23 | run.goTest("./...") 24 | } 25 | } 26 | 27 | func (run Run) RunOnChange(file string) { 28 | if isGoFile(file) { 29 | // TODO: optimization, skip if no test files exist 30 | packageDir := "./" + filepath.Dir(file) // watchDir = ./ 31 | run.goTest(packageDir) 32 | } 33 | } 34 | 35 | func (run Run) goTest(pkgs ...string) { 36 | args := []string{"test"} 37 | if len(run.Tags) > 0 { 38 | args = append(args, []string{"-tags", run.Tags}...) 39 | } 40 | args = append(args, pkgs...) 41 | 42 | command := "go" 43 | 44 | if _, err := os.Stat("Godeps/Godeps.json"); err == nil { 45 | args = append([]string{"go"}, args...) 46 | command = "godep" 47 | } 48 | 49 | cmd := exec.Command(command, args...) 50 | // cmd.Dir watchDir = ./ 51 | 52 | PrintCommand(cmd.Args) // includes "go" 53 | 54 | out, err := cmd.CombinedOutput() 55 | if err != nil { 56 | log.Println(err) 57 | } 58 | PrintCommandOutput(out) 59 | 60 | RedGreen(cmd.ProcessState.Success()) 61 | ShowDuration(cmd.ProcessState.UserTime()) 62 | } 63 | 64 | func goList() []string { 65 | cmd := exec.Command("go", "list", "./...") 66 | out, err := cmd.Output() 67 | if err != nil { 68 | log.Println(err) 69 | } 70 | allPkgs := strings.Split(string(out), "\n") 71 | 72 | pkgs := []string{} 73 | // remove packages that contain /vendor/ or are blank (last newline) 74 | for _, pkg := range allPkgs { 75 | if len(pkg) != 0 && !strings.Contains(pkg, "/vendor/") { 76 | pkgs = append(pkgs, pkg) 77 | } 78 | } 79 | return pkgs 80 | } 81 | 82 | func isGoFile(file string) bool { 83 | return filepath.Ext(file) == ".go" 84 | } 85 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/nathany/looper 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/bobappleyard/readline v0.0.0-20150707195538-7e300e02d38e 7 | github.com/fsnotify/fsnotify v1.4.7 8 | github.com/koyachi/go-term-ansicolor v0.0.0-20130114081603-6f81280f9360 9 | golang.org/x/sys v0.0.0-20191002091554-b397fe3ad8ed // indirect 10 | ) 11 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/bobappleyard/readline v0.0.0-20150707195538-7e300e02d38e h1:4G8AYOOwZdDWOiJR6D6JXaFmj5BDS7c5D5PyqsG/+Hg= 2 | github.com/bobappleyard/readline v0.0.0-20150707195538-7e300e02d38e/go.mod h1:fmqtV+Wqx0uFYLN1F4VhjZdtT56Dr8c3yA7nALFsw/Q= 3 | github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= 4 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 5 | github.com/koyachi/go-term-ansicolor v0.0.0-20130114081603-6f81280f9360 h1:RAav20p/CzLXPs8DxPry4eM4YuTooJ1d6ctCmXjpaA8= 6 | github.com/koyachi/go-term-ansicolor v0.0.0-20130114081603-6f81280f9360/go.mod h1:zUllqwUpvIS0pyapVJDiHwwEkmaV9qeCR9+sh5MPdRs= 7 | golang.org/x/sys v0.0.0-20191002091554-b397fe3ad8ed h1:5TJcLJn2a55mJjzYk0yOoqN8X1OdvBDUnaZaKKyQtkY= 8 | golang.org/x/sys v0.0.0-20191002091554-b397fe3ad8ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 9 | -------------------------------------------------------------------------------- /input.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "strings" 7 | 8 | "github.com/bobappleyard/readline" 9 | ) 10 | 11 | type Command int 12 | 13 | const ( 14 | Unknown Command = iota 15 | Help 16 | Exit 17 | RunAll 18 | ) 19 | 20 | func CommandParser() <-chan Command { 21 | commands := make(chan Command, 1) 22 | 23 | go func() { 24 | for { 25 | in, err := readline.String("") 26 | if err == io.EOF { // Ctrl+D 27 | commands <- Exit 28 | break 29 | } else if err != nil { 30 | log.Fatal(err) 31 | } 32 | 33 | commands <- NormalizeCommand(in) 34 | readline.AddHistory(in) 35 | } 36 | }() 37 | 38 | return commands 39 | } 40 | 41 | func NormalizeCommand(in string) (c Command) { 42 | command := strings.ToLower(strings.TrimSpace(in)) 43 | switch command { 44 | case "exit", "e", "x", "quit", "q": 45 | c = Exit 46 | case "all", "a", "": 47 | c = RunAll 48 | case "help", "h", "?": 49 | c = Help 50 | default: 51 | UnknownCommand(command) 52 | c = Unknown 53 | } 54 | return c 55 | } 56 | -------------------------------------------------------------------------------- /input_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestNormalizeCommand(t *testing.T) { 8 | commands := []struct { 9 | input string 10 | cmd Command 11 | }{ 12 | {" Exit", Exit}, 13 | {"sudo", Unknown}, 14 | } 15 | 16 | for _, c := range commands { 17 | actual := NormalizeCommand(c.input) 18 | if actual != c.cmd { 19 | t.Errorf("Expected '%s' to result in %v, but got %v", c.input, c.cmd, actual) 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /looper.go: -------------------------------------------------------------------------------- 1 | // Autotesting tool with readline support. 2 | package main 3 | 4 | import ( 5 | "flag" 6 | "log" 7 | 8 | "github.com/nathany/looper/gat" 9 | ) 10 | 11 | type Runner interface { 12 | RunOnChange(file string) 13 | RunAll() 14 | } 15 | 16 | func EventLoop(runner Runner, debug bool) { 17 | commands := CommandParser() 18 | watcher, err := NewRecursiveWatcher("./") 19 | if err != nil { 20 | log.Fatal(err) 21 | } 22 | watcher.Run(debug) 23 | defer watcher.Close() 24 | 25 | out: 26 | for { 27 | select { 28 | case file := <-watcher.Files: 29 | runner.RunOnChange(file) 30 | case folder := <-watcher.Folders: 31 | PrintWatching(folder) 32 | case command := <-commands: 33 | switch command { 34 | case Exit: 35 | break out 36 | case RunAll: 37 | runner.RunAll() 38 | case Help: 39 | DisplayHelp() 40 | } 41 | } 42 | } 43 | } 44 | 45 | func main() { 46 | var tags string 47 | var debug bool 48 | flag.StringVar(&tags, "tags", "", "a list of build tags for testing.") 49 | flag.BoolVar(&debug, "debug", false, "adds additional logging") 50 | flag.Parse() 51 | 52 | runner := gat.Run{Tags: tags} 53 | 54 | Header() 55 | if debug { 56 | DebugEnabled() 57 | } 58 | EventLoop(runner, debug) 59 | } 60 | -------------------------------------------------------------------------------- /looper.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nathany/looper/0da277402eb3910fb8bbfc94c8f3133ee2fe50e1/looper.png -------------------------------------------------------------------------------- /looper.sublime-project: -------------------------------------------------------------------------------- 1 | { 2 | "folders": 3 | [ 4 | { 5 | "path": ".", 6 | "file_exclude_patterns": ["looper"] 7 | } 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /print.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/koyachi/go-term-ansicolor/ansicolor" 7 | ) 8 | 9 | func Header() { 10 | fmt.Println(ansicolor.Cyan("Looper 0.3.3 is watching your files")) 11 | fmt.Println("Type " + ansicolor.Magenta("help") + " for help.\n") 12 | } 13 | 14 | func DebugEnabled() { 15 | DebugMessage("Debug mode enabled.\n") 16 | } 17 | 18 | func DebugMessage(format string, a ...interface{}) { 19 | msg := fmt.Sprintf(format, a...) 20 | fmt.Println(ansicolor.IntenseBlack(msg)) 21 | } 22 | 23 | func DebugError(msg error) { 24 | fmt.Println(ansicolor.IntenseBlack(msg.Error())) 25 | } 26 | 27 | func DisplayHelp() { 28 | fmt.Println(ansicolor.Magenta("\nInteractions:\n")) 29 | fmt.Println(" * a, all Run all tests") 30 | fmt.Println(" * h, help You found it") 31 | fmt.Println(" * e, exit Leave Looper") 32 | } 33 | 34 | func PrintWatching(folder string) { 35 | ClearPrompt() 36 | fmt.Println(ansicolor.Yellow("Watching path"), ansicolor.Yellow(folder)) 37 | } 38 | 39 | func UnknownCommand(command string) { 40 | fmt.Println(ansicolor.Red("ERROR:")+" Unknown command", ansicolor.Magenta(command)) 41 | } 42 | 43 | const CSI = "\x1b[" 44 | 45 | // remove from the screen anything that's been typed 46 | // from github.com/kierdavis/ansi 47 | func ClearPrompt() { 48 | fmt.Printf("%s2K", CSI) // clear line 49 | fmt.Printf("%s%dG", CSI, 0) // go to column 0 50 | } 51 | -------------------------------------------------------------------------------- /watch.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "log" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | 10 | "github.com/fsnotify/fsnotify" 11 | ) 12 | 13 | type RecursiveWatcher struct { 14 | *fsnotify.Watcher 15 | Files chan string 16 | Folders chan string 17 | } 18 | 19 | func NewRecursiveWatcher(path string) (*RecursiveWatcher, error) { 20 | folders := Subfolders(path) 21 | if len(folders) == 0 { 22 | return nil, errors.New("No folders to watch.") 23 | } 24 | 25 | watcher, err := fsnotify.NewWatcher() 26 | if err != nil { 27 | return nil, err 28 | } 29 | rw := &RecursiveWatcher{Watcher: watcher} 30 | 31 | rw.Files = make(chan string, 10) 32 | rw.Folders = make(chan string, len(folders)) 33 | 34 | for _, folder := range folders { 35 | rw.AddFolder(folder) 36 | } 37 | return rw, nil 38 | } 39 | 40 | func (watcher *RecursiveWatcher) AddFolder(folder string) { 41 | err := watcher.Add(folder) 42 | if err != nil { 43 | log.Println("Error watching: ", folder, err) 44 | } 45 | watcher.Folders <- folder 46 | } 47 | 48 | func (watcher *RecursiveWatcher) Run(debug bool) { 49 | go func() { 50 | for { 51 | select { 52 | case event := <-watcher.Events: 53 | // create a file/directory 54 | if event.Op&fsnotify.Create == fsnotify.Create { 55 | fi, err := os.Stat(event.Name) 56 | if err != nil { 57 | // eg. stat .subl513.tmp : no such file or directory 58 | if debug { 59 | DebugError(err) 60 | } 61 | } else if fi.IsDir() { 62 | if debug { 63 | DebugMessage("Detected new directory %s", event.Name) 64 | } 65 | if !shouldIgnoreFile(filepath.Base(event.Name)) { 66 | watcher.AddFolder(event.Name) 67 | } 68 | } else { 69 | if debug { 70 | DebugMessage("Detected new file %s", event.Name) 71 | } 72 | watcher.Files <- event.Name // created a file 73 | } 74 | } 75 | 76 | if event.Op&fsnotify.Write == fsnotify.Write { 77 | // modified a file, assuming that you don't modify folders 78 | if debug { 79 | DebugMessage("Detected file modification %s", event.Name) 80 | } 81 | watcher.Files <- event.Name 82 | } 83 | 84 | case err := <-watcher.Errors: 85 | log.Println("error", err) 86 | } 87 | } 88 | }() 89 | } 90 | 91 | // Subfolders returns a slice of subfolders (recursive), including the folder provided. 92 | func Subfolders(path string) (paths []string) { 93 | filepath.Walk(path, func(newPath string, info os.FileInfo, err error) error { 94 | if err != nil { 95 | return err 96 | } 97 | 98 | if info.IsDir() { 99 | name := info.Name() 100 | // skip folders that begin with a dot 101 | if shouldIgnoreFile(name) && name != "." && name != ".." { 102 | return filepath.SkipDir 103 | } 104 | paths = append(paths, newPath) 105 | } 106 | return nil 107 | }) 108 | return paths 109 | } 110 | 111 | // shouldIgnoreFile determines if a file should be ignored. 112 | // File names that begin with "." or "_" are ignored by the go tool. 113 | func shouldIgnoreFile(name string) bool { 114 | return strings.HasPrefix(name, ".") || strings.HasPrefix(name, "_") 115 | } 116 | --------------------------------------------------------------------------------