1 | package main
|
2 |
|
3 | import (
|
4 | "encoding/json"
|
5 | "fmt"
|
6 | "github.com/fatih/color"
|
7 | "io/ioutil"
|
8 | "time"
|
9 |
|
10 | "github.com/kevinschoon/pomo/libnotify"
|
11 | )
|
12 |
|
13 | // RefreshInterval is the frequency at which
|
14 | // the display is updated.
|
15 | const RefreshInterval = 800 * time.Millisecond
|
16 |
|
17 | // Message is used internally for updating
|
18 | // the display.
|
19 | type Message struct {
|
20 | Start time.Time
|
21 | Duration time.Duration
|
22 | Pomodoros int
|
23 | CurrentPomodoro int
|
24 | Wheel *Wheel
|
25 | }
|
26 |
|
27 | // Wheel keeps track of an ASCII spinner
|
28 | type Wheel struct {
|
29 | state int
|
30 | }
|
31 |
|
32 | func (w *Wheel) String() string {
|
33 | switch w.state {
|
34 | case 0:
|
35 | w.state++
|
36 | return "|"
|
37 | case 1:
|
38 | w.state++
|
39 | return "/"
|
40 | case 2:
|
41 | w.state++
|
42 | return "-"
|
43 | case 3:
|
44 | w.state = 0
|
45 | return "\\"
|
46 | }
|
47 | return ""
|
48 | }
|
49 |
|
50 | // Config represents user preferences
|
51 | type Config struct {
|
52 | Colors map[string]*color.Color
|
53 | }
|
54 |
|
55 | var colorMap = map[string]*color.Color{
|
56 | "red": color.New(color.FgRed),
|
57 | "blue": color.New(color.FgBlue),
|
58 | "green": color.New(color.FgGreen),
|
59 | "white": color.New(color.FgWhite),
|
60 | }
|
61 |
|
62 | func (c *Config) UnmarshalJSON(raw []byte) error {
|
63 | config := &struct {
|
64 | Colors map[string]string `json:"colors"`
|
65 | }{}
|
66 | err := json.Unmarshal(raw, config)
|
67 | if err != nil {
|
68 | return err
|
69 | }
|
70 | for key, name := range config.Colors {
|
71 | if color, ok := colorMap[name]; ok {
|
72 | c.Colors[key] = color
|
73 | } else {
|
74 | return fmt.Errorf("bad color choice: %s", name)
|
75 | }
|
76 | }
|
77 | return nil
|
78 | }
|
79 |
|
80 | func NewConfig(path string) (*Config, error) {
|
81 | raw, err := ioutil.ReadFile(path)
|
82 | if err != nil {
|
83 | return nil, err
|
84 | }
|
85 | config := &Config{
|
86 | Colors: map[string]*color.Color{},
|
87 | }
|
88 | return config, json.Unmarshal(raw, config)
|
89 | }
|
90 |
|
91 | // Task describes some activity
|
92 | type Task struct {
|
93 | ID int `json:"id"`
|
94 | Message string `json:"message"`
|
95 | // Array of completed pomodoros
|
96 | Pomodoros []*Pomodoro `json:"pomodoros"`
|
97 | // Free-form tags associated with this task
|
98 | Tags []string `json:"tags"`
|
99 | // Number of pomodoros for this task
|
100 | NPomodoros int `json:"n_pomodoros"`
|
101 | // Duration of each pomodoro
|
102 | Duration time.Duration `json:"duration"`
|
103 | }
|
104 |
|
105 | // ByID is a sortable array of tasks
|
106 | type ByID []*Task
|
107 |
|
108 | func (b ByID) Len() int { return len(b) }
|
109 | func (b ByID) Swap(i, j int) { b[i], b[j] = b[j], b[i] }
|
110 | func (b ByID) Less(i, j int) bool { return b[i].ID < b[j].ID }
|
111 |
|
112 | // Pomodoro is a unit of time to spend working
|
113 | // on a single task.
|
114 | type Pomodoro struct {
|
115 | Start time.Time `json:"start"`
|
116 | End time.Time `json:"end"`
|
117 | }
|
118 |
|
119 | // Notifier implements a system specific
|
120 | // notification. On Linux this libnotify.
|
121 | // TODO: OSX, Windows(?)
|
122 | type Notifier interface {
|
123 | Begin(int, Task) error
|
124 | Break(Task) error
|
125 | Finish(Task) error
|
126 | }
|
127 |
|
128 | // LibNotifier implements a Linux
|
129 | // notifier client.
|
130 | type LibNotifier struct {
|
131 | client *libnotify.Client
|
132 | iconPath string
|
133 | }
|
134 |
|
135 | func NewLibNotifier() Notifier {
|
136 | ln := &LibNotifier{
|
137 | client: libnotify.NewClient(),
|
138 | }
|
139 | // Write the tomato icon to a temp path
|
140 | raw := MustAsset("tomato-icon.png")
|
141 | fp, _ := ioutil.TempFile("", "pomo")
|
142 | fp.Write(raw)
|
143 | ln.iconPath = fp.Name()
|
144 | fp.Close()
|
145 | return ln
|
146 | }
|
147 |
|
148 | func (ln LibNotifier) Begin(count int, t Task) error {
|
149 | return ln.client.Notify(libnotify.Notification{
|
150 | Title: t.Message,
|
151 | Body: fmt.Sprintf("Task is starting (%d/%d pomodoros)", count, t.NPomodoros),
|
152 | Icon: ln.iconPath,
|
153 | })
|
154 | }
|
155 |
|
156 | func (ln LibNotifier) Break(t Task) error {
|
157 | return ln.client.Notify(libnotify.Notification{
|
158 | Title: t.Message,
|
159 | Urgency: "critical",
|
160 | Body: fmt.Sprintf("Time to take a break!\nPress enter at the console to initiate the break."),
|
161 | Icon: ln.iconPath,
|
162 | })
|
163 | }
|
164 |
|
165 | func (ln LibNotifier) Finish(t Task) error {
|
166 | return ln.client.Notify(libnotify.Notification{
|
167 | Title: t.Message,
|
168 | Urgency: "critical",
|
169 | Body: fmt.Sprintf("This task session is complete!"),
|
170 | Icon: ln.iconPath,
|
171 | })
|
172 | }
|