├── README.md ├── flippo_test.go └── flippo.go /README.md: -------------------------------------------------------------------------------- 1 | # flippo 2 | 3 | A simple utility that let's you know when it's time to take a break from the computer. Made for OS X. Must run in foreground (otherwise you won't get notified). 4 | 5 | By default a notification comes after 40 minutes of not being idle (system-wide) and bugs you every minute until you become idle. You are considered idle after 10 seconds of no activity. A break must be at least 10 minutes. 6 | 7 | The defaults are configurable via flags. 8 | 9 | ``` 10 | Usage of flippo: 11 | -break int 12 | break alert interval (seconds) (default 2400) 13 | -debug 14 | verbose display 15 | -freq int 16 | notification frequency (seconds) (default 60) 17 | -idle-after int 18 | time after which user is considered idle (seconds) (default 10) 19 | -min-break int 20 | break length (seconds) (default 600) 21 | -sound string 22 | sound name (from ~/Library/Sounds or /System/Library/Sounds) (default "Hero") 23 | -sound-after string 24 | sound name after break (from ~/Library/Sounds or /System/Library/Sounds) (default "Purr") 25 | ``` 26 | -------------------------------------------------------------------------------- /flippo_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func withIdle(d time.Duration) { 9 | idleDuration = func() time.Duration { 10 | return d 11 | } 12 | } 13 | 14 | var ( 15 | origIdleDuration = idleDuration 16 | origNotify = notify 17 | origBreakLength = *breakLength 18 | origBreakAlert = *breakAlert 19 | origNotifyEvery = *notifyEvery 20 | origIdleAfter = *idleAfter 21 | ) 22 | 23 | func teardown() { 24 | idleDuration = origIdleDuration 25 | notify = origNotify 26 | *breakLength = origBreakLength 27 | *breakAlert = origBreakAlert 28 | *notifyEvery = origNotifyEvery 29 | *idleAfter = origIdleAfter 30 | } 31 | 32 | func TestIdle(t *testing.T) { 33 | t.Run("not idle", func(t *testing.T) { 34 | defer teardown() 35 | *idleAfter = 2 36 | tt := newTimeTracker() 37 | withIdle(time.Second) 38 | tt.check(time.Now()) 39 | if tt.isIdle != false { 40 | t.Fatal("should not be idle") 41 | } 42 | }) 43 | 44 | t.Run("idle", func(t *testing.T) { 45 | defer teardown() 46 | *idleAfter = 2 47 | tt := newTimeTracker() 48 | withIdle(3 * time.Second) 49 | tt.check(time.Now()) 50 | if tt.isIdle != true { 51 | t.Fatal("should be idle") 52 | } 53 | }) 54 | } 55 | 56 | func TestBreak(t *testing.T) { 57 | defer teardown() 58 | *breakLength = 5 59 | tt := newTimeTracker() 60 | startBreak := tt.lastBreak 61 | 62 | withIdle(2 * time.Second) 63 | tt.check(time.Now()) 64 | if !tt.lastBreak.Equal(startBreak) { 65 | t.Fatal("should not have changed break") 66 | } 67 | 68 | var notified bool 69 | notify = func(_, _, _ string) { 70 | notified = true 71 | } 72 | withIdle(6 * time.Second) 73 | tt.check(time.Now()) 74 | if !tt.lastBreak.After(startBreak) || !notified { 75 | t.Fatal("should have changed break") 76 | } 77 | 78 | notified = false 79 | notify = func(_, _, _ string) { 80 | notified = true 81 | } 82 | withIdle(25 * time.Second) 83 | tt.check(time.Now()) 84 | if notified { 85 | t.Fatal("should not have notified again") 86 | } 87 | } 88 | 89 | func TestNotify(t *testing.T) { 90 | t.Run("notify", func(t *testing.T) { 91 | defer teardown() 92 | tt := newTimeTracker() 93 | *idleAfter = 3 94 | *breakAlert = 6 95 | *notifyEvery = 2 96 | withIdle(0) 97 | notified := false 98 | notify = func(_, _, _ string) { 99 | notified = true 100 | } 101 | m := time.Now().Add(10 * time.Second) 102 | 103 | tt.check(m) 104 | if !tt.notified.Equal(m) { 105 | t.Fatal("expected notification") 106 | } 107 | }) 108 | 109 | t.Run("notify interval", func(t *testing.T) { 110 | defer teardown() 111 | tt := newTimeTracker() 112 | *idleAfter = 3 113 | *breakAlert = 5 114 | *notifyEvery = 2 115 | withIdle(0) 116 | notified := false 117 | notify = func(_, _, _ string) { 118 | notified = true 119 | } 120 | m := time.Now().Add(6 * time.Second) 121 | // after 6 seconds, first notification 122 | tt.check(m) 123 | if !tt.notified.Equal(m) || !notified { 124 | t.Fatal("expected notification") 125 | } 126 | // after one more second, no notification 127 | m = m.Add(time.Second) 128 | notified = false 129 | tt.check(m) 130 | if tt.notified.Equal(m) || notified { 131 | t.Fatal("notifed too early") 132 | } 133 | // after two more seconds, second notification 134 | m = m.Add(2 * time.Second) 135 | notified = false 136 | tt.check(m) 137 | if !tt.notified.Equal(m) || !notified { 138 | t.Fatal("expected notification after interval") 139 | } 140 | }) 141 | } 142 | -------------------------------------------------------------------------------- /flippo.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "os/exec" 9 | "strconv" 10 | "time" 11 | 12 | _ "net/http/pprof" 13 | ) 14 | 15 | const ( 16 | title = "Take a break" 17 | body = "52 minutes passed since your last." 18 | titleBreak = "You took a break" 19 | bodyBreak = "Good!" 20 | ) 21 | 22 | var ( 23 | sound = flag.String("sound", "Hero", "sound name (from ~/Library/Sounds or /System/Library/Sounds)") 24 | soundBreak = flag.String("sound-after", "Purr", "sound name after break (from ~/Library/Sounds or /System/Library/Sounds)") 25 | breakLength = flag.Int64("min-break", 1020, "break length (seconds)") 26 | breakAlert = flag.Int64("break", 3120, "break alert interval (seconds)") 27 | notifyEvery = flag.Int64("freq", 60, "notification frequency (seconds)") 28 | idleAfter = flag.Int64("idle-after", 10, "time after which user is considered idle (seconds)") 29 | debug = flag.Bool("debug", false, "verbose display") 30 | ) 31 | 32 | // idleDuration returns system idle time 33 | var idleDuration = func() time.Duration { 34 | awk := exec.Command("/usr/bin/awk", "/HIDIdleTime/ {printf int($NF/1000000000); exit}") 35 | stdin, err := awk.StdinPipe() 36 | if err != nil { 37 | log.Println(err) 38 | } 39 | ioreg, err := exec.Command("/usr/sbin/ioreg", "-c", "IOHIDSystem").Output() 40 | if err != nil { 41 | log.Println(err) 42 | } 43 | go func() { 44 | defer stdin.Close() 45 | _, err := stdin.Write(ioreg) 46 | if err != nil { 47 | log.Println(err) 48 | } 49 | }() 50 | out, err := awk.Output() 51 | if err != nil { 52 | log.Println(err) 53 | } 54 | sec, err := strconv.ParseInt(string(out), 10, 64) 55 | if err != nil { 56 | log.Println(err) 57 | } 58 | return time.Duration(sec) * time.Second 59 | } 60 | 61 | // notify notifies the using the given title, body and system sound 62 | var notify = func(title, body, sound string) { 63 | script := fmt.Sprintf(`display notification "%s" with title "%s" sound name "%s"`, title, body, sound) 64 | cmd := exec.Command("osascript", "-e", script) 65 | 66 | if err := cmd.Run(); err != nil { 67 | log.Fatal("%+v", err) 68 | } 69 | } 70 | 71 | type timeTracker struct { 72 | // lastBreak is the time when the most recent break was interrupted, 73 | // as in the user became active again 74 | lastBreak time.Time 75 | // notified is the last time the user was sent a break notification 76 | notified time.Time 77 | // isIdle is true if the user is idle 78 | isIdle bool 79 | // inBreak will be true if the user is in a break 80 | inBreak bool 81 | } 82 | 83 | // newTimeTracker creates a new tracker to track the users activity 84 | func newTimeTracker() *timeTracker { 85 | return &timeTracker{ 86 | lastBreak: time.Now(), 87 | notified: time.Now(), 88 | } 89 | } 90 | 91 | // check checks the users activity status at time t. 92 | func (tt *timeTracker) check(t time.Time) { 93 | sinceLast := t.Sub(tt.lastBreak) 94 | lastNotified := t.Sub(tt.notified) 95 | idle := idleDuration() 96 | secs := func(s *int64) time.Duration { 97 | return time.Duration(*s) * time.Second 98 | } 99 | if *debug { 100 | log.Printf("Idle: %ds", idle) 101 | } 102 | inBreak := idle > secs(breakLength) 103 | if inBreak { 104 | if !tt.inBreak { 105 | notify(titleBreak, bodyBreak, *soundBreak) 106 | if *debug { 107 | log.Println("Completed break.") 108 | } 109 | } 110 | if *debug { 111 | log.Println("In break.") 112 | } 113 | tt.lastBreak = t 114 | } 115 | tt.inBreak = inBreak 116 | tt.isIdle = idle > secs(idleAfter) 117 | if !tt.isIdle && sinceLast > secs(breakAlert) && lastNotified > secs(notifyEvery) { 118 | if *debug { 119 | log.Println("Notified to take break.") 120 | } 121 | notify(title, body, *sound) 122 | tt.notified = t 123 | } 124 | } 125 | 126 | func main() { 127 | flag.Parse() 128 | tracker := newTimeTracker() 129 | go func() { 130 | http.ListenAndServe(":6060", nil) 131 | }() 132 | for { 133 | time.Sleep(time.Second) 134 | tracker.check(time.Now()) 135 | } 136 | } 137 | --------------------------------------------------------------------------------