Author: Kevin Schoon [kevinschoon@gmail.com]
Hash: 34eab4e98b8153aee3366b791f9d0b8cdc24782c
Timestamp: Tue, 08 Sep 2020 16:35:47 +0000 (4 years ago)

+1447 -1552 +/-26 browse
restructure project into modules
1diff --git a/Gopkg.lock b/Gopkg.lock
2deleted file mode 100644
3index 23d71b1..0000000
4--- a/Gopkg.lock
5+++ /dev/null
6 @@ -1,90 +0,0 @@
7- # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
8-
9-
10- [[projects]]
11- branch = "master"
12- name = "github.com/0xAX/notificator"
13- packages = ["."]
14- revision = "d81462e38c2145023f9ecf5414fc84d45d5bfe82"
15-
16- [[projects]]
17- name = "github.com/fatih/color"
18- packages = ["."]
19- revision = "5b77d2a35fb0ede96d138fc9a99f5c9b6aef11b4"
20- version = "v1.7.0"
21-
22- [[projects]]
23- name = "github.com/gizak/termui"
24- packages = ["."]
25- revision = "bf53c5cbea3f372f745e1d8e4660e29f73d41517"
26- version = "v2.3.0"
27-
28- [[projects]]
29- name = "github.com/jawher/mow.cli"
30- packages = [
31- ".",
32- "internal/container",
33- "internal/flow",
34- "internal/fsm",
35- "internal/lexer",
36- "internal/matcher",
37- "internal/parser",
38- "internal/values"
39- ]
40- revision = "2f22195f169da29d54624afd9eb83ada5c9e4ee9"
41- version = "v1.0.4"
42-
43- [[projects]]
44- branch = "master"
45- name = "github.com/maruel/panicparse"
46- packages = ["stack"]
47- revision = "f20d4c4d746f810c9110e21928d4135e1f2a3efa"
48-
49- [[projects]]
50- name = "github.com/mattn/go-colorable"
51- packages = ["."]
52- revision = "167de6bfdfba052fa6b2d3664c8f5272e23c9072"
53- version = "v0.0.9"
54-
55- [[projects]]
56- name = "github.com/mattn/go-isatty"
57- packages = ["."]
58- revision = "6ca4dbf54d38eea1a992b3c722a76a5d1c4cb25c"
59- version = "v0.0.4"
60-
61- [[projects]]
62- name = "github.com/mattn/go-runewidth"
63- packages = ["."]
64- revision = "3ee7d812e62a0804a7d0a324e0249ca2db3476d3"
65- version = "v0.0.4"
66-
67- [[projects]]
68- name = "github.com/mattn/go-sqlite3"
69- packages = ["."]
70- revision = "c7c4067b79cc51e6dfdcef5c702e74b1e0fa7c75"
71- version = "v1.10.0"
72-
73- [[projects]]
74- name = "github.com/mitchellh/go-wordwrap"
75- packages = ["."]
76- revision = "9e67c67572bc5dd02aef930e2b0ae3c02a4b5a5c"
77- version = "v1.0.0"
78-
79- [[projects]]
80- branch = "master"
81- name = "github.com/nsf/termbox-go"
82- packages = ["."]
83- revision = "0938b5187e61bb8c4dcac2b0a9cf4047d83784fc"
84-
85- [[projects]]
86- branch = "master"
87- name = "golang.org/x/sys"
88- packages = ["unix"]
89- revision = "11f53e03133963fb11ae0588e08b5e0b85be8be5"
90-
91- [solve-meta]
92- analyzer-name = "dep"
93- analyzer-version = 1
94- inputs-digest = "5913a16a0927350ebbd5117158473bb0252181f6aa8777cb5a095a18dee8bd40"
95- solver-name = "gps-cdcl"
96- solver-version = 1
97 diff --git a/Gopkg.toml b/Gopkg.toml
98deleted file mode 100644
99index 329330b..0000000
100--- a/Gopkg.toml
101+++ /dev/null
102 @@ -1,33 +0,0 @@
103- # Gopkg.toml example
104- #
105- # Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md
106- # for detailed Gopkg.toml documentation.
107- #
108- # required = ["github.com/user/thing/cmd/thing"]
109- # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
110- #
111- # [[constraint]]
112- # name = "github.com/user/project"
113- # version = "1.0.0"
114- #
115- # [[constraint]]
116- # name = "github.com/user/project2"
117- # branch = "dev"
118- # source = "github.com/myfork/project2"
119- #
120- # [[override]]
121- # name = "github.com/x/y"
122- # version = "2.4.0"
123-
124-
125- [[constraint]]
126- name = "github.com/fatih/color"
127- version = "1.5.0"
128-
129- [[constraint]]
130- name = "github.com/jawher/mow.cli"
131- version = "1.0.3"
132-
133- [[constraint]]
134- name = "github.com/mattn/go-sqlite3"
135- version = "1.6.0"
136 diff --git a/Makefile b/Makefile
137index 9505092..eb7a2e5 100644
138--- a/Makefile
139+++ b/Makefile
140 @@ -1,10 +1,14 @@
141 DOCKER_CMD=docker run --rm -ti -w /build/pomo -v $$PWD:/build/pomo
142 DOCKER_IMAGE=pomo-build
143+
144 VERSION ?= $(shell git describe --tags 2>/dev/null)
145 ifeq "$(VERSION)" ""
146 VERSION := UNKNOWN
147 endif
148
149+ LDFLAGS=\
150+ -X github.com/kevinschoon/pomo/pkg/internal/version.Version=$(VERSION)
151+
152 .PHONY: \
153 test \
154 docs \
155 @@ -14,11 +18,16 @@ endif
156 release-linux \
157 release-darwin
158
159+ default:
160+ cd cmd/pomo && \
161+ go install -ldflags '${LDFLAGS}'
162+
163 bin/pomo: test
164- go build -o $@
165+ cd cmd/pomo && \
166+ go build -ldflags '${LDFLAGS}' -o ../../$@
167
168- bindata.go: tomato-icon.png
169- go-bindata -pkg main -o $@ $^
170+ #bindata.go: tomato-icon.png
171+ # go-bindata -pkg main -o $@ $^
172
173 test:
174 go test ./...
175 @@ -31,16 +40,16 @@ bin/pomo-linux: bin/pomo-$(VERSION)-linux-amd64
176
177 bin/pomo-darwin: bin/pomo-$(VERSION)-darwin-amd64
178
179- bin/pomo-$(VERSION)-linux-amd64: bin bindata.go
180- $(DOCKER_CMD) --env GOOS=linux --env GOARCH=amd64 $(DOCKER_IMAGE) go build -ldflags "-X main.Version=$(VERSION)" -o $@
181+ bin/pomo-$(VERSION)-linux-amd64: bin
182+ $(DOCKER_CMD) --env GOOS=linux --env GOARCH=amd64 $(DOCKER_IMAGE) go build -ldflags "${LDFLAGS}" -o $@
183
184 bin/pomo-$(VERSION)-linux-amd64.md5:
185 md5sum bin/pomo-$(VERSION)-linux-amd64 | sed -e 's/bin\///' > $@
186
187- bin/pomo-$(VERSION)-darwin-amd64: bin bindata.go
188+ bin/pomo-$(VERSION)-darwin-amd64: bin
189 # This is used to cross-compile a Darwin compatible Mach-O executable
190 # on Linux for OSX, you need to install https://github.com/tpoechtrager/osxcross
191- $(DOCKER_CMD) --env GOOS=darwin --env GOARCH=amd64 --env CC=x86_64-apple-darwin15-cc --env CGO_ENABLED=1 $(DOCKER_IMAGE) go build -ldflags "-X main.Version=$(VERSION)" -o $@
192+ $(DOCKER_CMD) --env GOOS=darwin --env GOARCH=amd64 --env CC=x86_64-apple-darwin15-cc --env CGO_ENABLED=1 $(DOCKER_IMAGE) go build -ldflags "${LDFLAGS}" -o $@
193
194
195 bin/pomo-$(VERSION)-darwin-amd64.md5:
196 diff --git a/bindata.go b/bindata.go
197deleted file mode 100644
198index 8f4bcd9..0000000
199--- a/bindata.go
200+++ /dev/null
201 @@ -1,235 +0,0 @@
202- // Code generated by go-bindata.
203- // sources:
204- // tomato-icon.png
205- // DO NOT EDIT!
206-
207- package main
208-
209- import (
210- "bytes"
211- "compress/gzip"
212- "fmt"
213- "io"
214- "io/ioutil"
215- "os"
216- "path/filepath"
217- "strings"
218- "time"
219- )
220-
221- func bindataRead(data []byte, name string) ([]byte, error) {
222- gz, err := gzip.NewReader(bytes.NewBuffer(data))
223- if err != nil {
224- return nil, fmt.Errorf("Read %q: %v", name, err)
225- }
226-
227- var buf bytes.Buffer
228- _, err = io.Copy(&buf, gz)
229- clErr := gz.Close()
230-
231- if err != nil {
232- return nil, fmt.Errorf("Read %q: %v", name, err)
233- }
234- if clErr != nil {
235- return nil, err
236- }
237-
238- return buf.Bytes(), nil
239- }
240-
241- type asset struct {
242- bytes []byte
243- info os.FileInfo
244- }
245-
246- type bindataFileInfo struct {
247- name string
248- size int64
249- mode os.FileMode
250- modTime time.Time
251- }
252-
253- func (fi bindataFileInfo) Name() string {
254- return fi.name
255- }
256- func (fi bindataFileInfo) Size() int64 {
257- return fi.size
258- }
259- func (fi bindataFileInfo) Mode() os.FileMode {
260- return fi.mode
261- }
262- func (fi bindataFileInfo) ModTime() time.Time {
263- return fi.modTime
264- }
265- func (fi bindataFileInfo) IsDir() bool {
266- return false
267- }
268- func (fi bindataFileInfo) Sys() interface{} {
269- return nil
270- }
271-
272- var _tomatoIconPng = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x00\x5c\x0f\xa3\xf0\x89\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\x00\x00\x30\x00\x00\x00\x2b\x08\x06\x00\x00\x00\x3e\x13\x0b\xdf\x00\x00\x00\x06\x62\x4b\x47\x44\x00\xff\x00\xff\x00\xff\xa0\xbd\xa7\x93\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x21\x38\x00\x00\x21\x38\x01\x45\x96\x31\x60\x00\x00\x00\x07\x74\x49\x4d\x45\x07\xe2\x01\x15\x08\x10\x11\xe3\x9e\xfd\x2f\x00\x00\x0e\xe9\x49\x44\x41\x54\x68\xde\xbd\x99\x7b\xb0\x5f\x55\x75\xc7\x3f\x6b\xef\xf3\xf8\x3d\xee\xcd\xcd\xbd\x24\x04\x12\xc2\x0d\x24\x24\x40\x54\x10\x82\x48\x40\x8c\x3c\x1a\xc1\x57\xcb\x8c\xd8\x8e\x56\xb1\x6a\x47\xc4\x57\xa1\x0e\x95\x69\xad\x45\x6a\xd5\xce\x50\xc5\x0a\x4c\x61\x46\x45\xab\xad\x45\xda\x29\x1d\x6d\xb4\xf1\x0f\xd4\x0a\x02\xa6\x3c\x42\x0c\x90\xc7\x0d\x49\x48\xb8\x79\xdd\xdc\xdf\xeb\x3c\xf6\x5e\xfd\x63\x9f\xdf\xbd\x37\x0f\x2c\x81\xb6\x67\xe6\xcc\x39\x73\xce\xde\xfb\xac\xef\x7a\x7c\xd7\x5a\xfb\xc0\xcb\x38\xde\xf9\xe1\xf3\xb8\xfe\xf3\x97\xcb\xe1\xcf\xef\xbe\xff\xa3\xe7\xf2\xff\x7c\x98\x97\x33\xe9\xf4\xe5\xc7\x9f\x76\xeb\x4d\x3f\xd6\x99\xcf\xee\xfc\x97\x3f\x34\xe0\xdf\x75\xf8\xd8\xb3\x2e\x58\xf4\x92\xd6\xfc\xe2\x5d\x57\xfd\xdf\x03\xb8\xe5\x8e\x77\xbc\xfa\x73\x77\xbe\xfd\xc2\xa7\x37\xec\xd9\xf4\x27\xb7\xbe\xf9\xd0\x97\x4e\x4f\x2c\x8b\xe2\x75\x33\x1f\x5d\x7c\xe5\x19\x3c\xf6\x8b\xad\x9c\x7f\xc9\xd2\x5b\x56\xbd\x75\xf9\xd6\x37\xbe\xfd\xcc\xe3\x01\x3e\xfd\x57\x57\x02\xf0\xf9\xdb\x7f\xc7\xdc\x72\xfb\x6f\x37\xbe\xf8\x77\x57\x99\x1b\x3f\x74\xdf\x51\xbf\x79\xdd\x9f\xad\xb2\xbf\x49\x26\x79\xa9\xc2\x7f\xe5\xdb\xef\xe2\xc0\xfe\xee\x95\xdb\x77\x1c\x58\x70\xd7\x17\x1e\xb8\xeb\x9a\x1b\x56\x32\x7f\xeb\x04\x9f\xff\xfe\x7a\x00\xbe\xfc\xed\xf7\xcd\x01\xc6\xbd\xf3\x0b\xaf\x7f\xdf\xb7\xb6\xf7\xe7\x5d\xf4\xe6\xd3\x3f\x10\x27\xe6\xee\xa4\x66\x37\xad\xf9\xde\x13\x4b\xfa\xcf\x6f\xfa\xc2\x95\x75\x1b\x9b\xd1\xcf\xdd\xf0\x6f\xbf\x3e\xda\xf7\x3e\x70\xc3\x1b\x68\x34\x92\x18\xc1\x7c\xf5\xe6\xb5\xd9\x2b\x06\xd0\x3f\x6e\xf8\xdc\xe5\xef\x9f\x68\xf5\xce\xb9\xfb\x8b\x3f\xfd\x18\xc0\x37\x41\x46\x41\x57\x01\xb7\x7c\xed\xea\xcd\xde\xf9\x9d\x9f\xf9\xf8\xbd\x17\x01\x5c\x79\xd5\xab\x86\x34\x36\x9b\xe3\xd4\x8e\x44\xc2\x79\xf7\x7d\x73\xdd\x23\x00\x9f\xf8\xf4\xa5\x57\xa7\xa9\xdd\xf3\xa5\xcf\xfe\xe8\x27\x3b\x41\x9e\x04\x3d\x01\x78\x4d\xf5\x8d\x3b\x2e\x3d\x95\x6b\xd7\x6e\xe6\x83\x9f\xba\x78\x71\x6c\xec\x58\x5c\x33\xe5\x6d\x7f\xb1\xf6\xe5\x03\x78\x16\x58\x02\xdc\xfe\xee\x73\xf9\xc8\xdf\x3f\xca\x87\x6f\x5c\xf5\xd5\xdc\x95\xe7\x0d\xb5\xf3\x2b\x6f\xbd\xe3\x97\xfb\x5f\x58\x7e\x72\x74\xfc\xfa\x6d\xee\xcf\xff\xf4\x8a\xd7\xb7\x2d\x3f\xf7\x5e\x9f\x59\xb0\xa3\x75\xfe\x83\xae\x7d\x95\x8f\xcc\xdd\xd6\xc8\xee\x8b\xc6\xf3\xa5\x1f\xff\xe1\xee\xce\x27\x7e\x7f\xf1\xf9\x79\x2d\xba\xf6\x8e\xbf\x7d\xe0\xbd\xe3\xe7\x2e\x4b\xf0\x28\xbe\x54\x2d\x32\xcf\xf8\x7e\x9d\x9c\x2c\x74\x49\x2f\xf7\xef\xbd\x6e\xe5\x1c\x63\xcd\xaa\x6f\xdc\xf6\xb3\x7b\x5f\x91\x0b\x3d\x0d\x2c\x05\x9e\x02\x99\x9d\x20\x66\xe9\x22\x99\xf7\x64\xce\x87\xaf\x3b\xe5\x33\x45\x5d\xae\x1f\x2c\xf4\xfa\x2f\x7f\x65\xc3\x77\x38\x7f\xc4\x72\xa0\xe7\x3f\xf0\xc6\xd1\x3b\x4a\x2f\xef\x51\xef\x77\x76\x6d\x9e\x02\xc7\x59\xe5\xf1\x9b\x9f\x9e\xbc\xec\x9e\x39\x03\xaf\xdf\xd4\x8c\xef\x3b\x2d\x2f\xdf\x72\xf3\xd8\xbe\x87\x11\x51\xac\xf7\xa8\x78\xd4\x78\x9e\xee\x79\xd9\xb3\xbd\xf7\xbb\xef\x7d\xdd\xb5\x02\xbb\xbe\x7b\xcf\x2f\xff\xd9\x01\x7b\x80\x79\xc7\x0a\x60\x0c\x58\x87\xe1\xf5\x78\xca\xe3\x1a\x26\x9e\x3b\x62\xa2\xfa\x40\x34\x62\x0b\xf3\x5c\xcf\xcc\x6e\x77\x38\xf5\xde\xd3\x9a\x5f\xda\xbc\xa4\x7e\x41\x33\x36\x0f\x5d\xf2\xd0\x44\xf7\xe4\xdd\x45\xbd\x1d\x61\x6f\x5b\x3e\xb0\xc2\xc5\x10\xcd\x16\x50\x18\xee\xfa\xbd\xef\x79\xb8\xb3\xee\x2b\x4b\xea\x17\x78\xd1\xe6\x35\x5b\x32\xd2\xd2\x33\x52\x82\xa8\xd2\xa9\xd9\xf1\x9d\x0d\xf3\xf8\xd8\x70\xf4\xd8\x84\x70\xe2\x85\xe3\xdd\x4f\xbe\x75\xdd\x96\x3d\xcf\xbf\xe6\x24\x6b\x77\xee\x76\xcf\xef\xe9\xf8\xb3\x81\xfd\xc0\xf0\xb1\x58\x60\x1b\x48\xed\xec\x53\x2c\x3e\x8e\x53\xa3\xf1\xbe\x82\x39\xbb\x7a\xe6\x26\x2f\xbc\x33\x86\xba\x15\xa2\x71\x8b\xdc\x73\x46\x9d\xc6\x69\x03\x0c\xf4\x3c\xbf\xf5\xab\x83\xfc\xa2\x9b\xf3\xc8\x29\x4d\x9a\x23\x36\x00\xe8\x79\x0e\x3e\x97\xf3\x42\x04\x83\x85\x52\x02\xef\x9e\x74\x14\x0b\xea\xac\x9b\x93\xb0\x6f\xbc\xc7\x3b\xc6\x32\x16\x64\x0a\x8e\xd2\x29\x7b\x53\xab\x1f\x5b\x31\xe2\x7e\x30\xde\xb5\x4e\x7d\xa7\x48\x9e\xdc\xee\x87\x41\x5f\x92\x05\x36\x01\xb5\xb9\x75\x13\x9f\x30\x3f\x9a\xbb\x70\x48\x1e\xdf\xd4\x3e\xbb\x5d\x72\x3d\xc2\xd5\xb1\x80\xb5\x50\xa0\x6c\x3a\xb1\xce\x9e\x0b\x47\x71\x93\x05\x3f\xd8\xb0\x03\xaf\x8a\x4b\x84\x46\xe6\x29\x13\x43\x63\x24\x22\xaa\x1b\xbc\x42\xb6\xcf\x91\xd6\x2c\xc9\x40\x00\x35\x99\x7b\x16\xef\xca\x58\xb5\x3b\x63\x45\xe6\x29\x10\x72\x85\x42\x21\xf7\x42\xee\xc1\x2b\x0f\xa7\x46\x6f\x5c\xb1\x32\x7b\xe0\x85\x47\x0d\xf1\xfa\x2d\x7e\x04\xf4\x39\x60\xe1\x8b\x01\xd8\x04\xd2\x9c\x9d\x4a\xf4\x9a\xb3\x63\xe6\xcf\x1f\xda\xbc\x61\xec\x8f\xcb\x4e\xf7\x53\x89\x78\x62\x03\xa9\x35\x48\x22\xfc\xe4\xcc\x59\x8c\xc4\x09\xe7\x3f\x33\xc1\x3f\xfa\x92\x35\x0d\xa1\x4f\xd8\x3a\xe3\x3a\x70\x42\x42\x3a\x68\xa9\xa5\x09\x07\x77\x75\x69\x8f\x67\x24\xc0\x15\x07\x4a\xce\x29\x94\x93\x44\xb0\x0a\x8a\xe2\x14\x4a\x85\xc2\x43\xee\xa1\xe7\x85\xdc\x81\x08\x37\x9e\xd6\x74\x5f\xc3\xb9\x2c\x5b\xbf\xc5\x2d\x9c\x61\x89\x29\x00\x7b\x80\x39\xc0\xce\xe1\xc4\xd4\xce\x59\x91\x74\x47\x97\x2c\xd8\xf1\xeb\xb1\xef\x49\xb7\x7d\x4e\xdc\x6b\x93\x88\x52\x4b\x0c\x71\x2d\xc6\x0d\x24\x34\xa2\x88\x24\xb6\x6c\x68\x75\x69\x95\x9e\x8e\x01\xa7\x50\xa8\x67\x20\x89\x88\x54\x69\xe7\x05\x59\x59\x82\x15\x4c\xcf\x31\x0b\x38\x4e\x84\x05\x6a\x49\xa3\x88\xae\x31\xf4\xbc\xc7\xbb\x92\x86\x73\x44\x4e\xf1\xaa\x53\x20\x32\x0f\x5d\x17\x80\x78\xe5\x6f\x2e\xde\xbc\xf1\xfa\x17\xce\x9c\x6f\xf7\x3f\xb5\xd3\x0d\x02\xf3\x81\x08\xe0\xd1\x4a\xf8\xf1\x54\x24\x7a\xd5\xab\xa3\xf6\xc9\x4b\x16\xee\x1a\xdb\xfd\xa3\xb4\x16\x2d\xb2\xed\x82\xd8\x28\xb5\x48\x88\x1b\x31\xf1\xec\x26\x76\x78\x00\x33\x50\x47\x93\x88\xe1\x76\x97\x7a\x5e\x52\x38\x8f\xa8\xb2\x70\x78\x90\xba\x2a\xae\xd5\x41\xf7\xb7\xd1\x89\x36\xda\xcd\xd0\x52\x41\x04\x92\x04\x33\x38\x08\x8d\x06\xcd\x24\xa1\xe9\x3c\xf4\xba\xb8\xc9\x49\xb4\xdb\xc1\x38\x87\xf1\x60\x6c\x28\x13\xa4\xb2\x65\xcf\xc9\x1f\xfd\x6c\xf1\xb2\x2d\x26\xd2\xbb\x66\xcd\xad\xeb\x83\xe3\x3d\x0f\x1a\x00\x9c\x0b\x6c\x04\xf1\xcb\x4e\xb5\x43\x97\x5c\xd2\x7c\xf6\xe7\x8f\xff\xb0\xd6\xac\x2f\xb2\xe3\xbb\x88\xcb\x8c\xc4\x28\x49\x64\x89\x6a\x31\x76\x56\x03\x3b\x32\x80\x0c\x35\x21\x8d\x39\x29\x1a\xe1\x60\xa7\x47\xb7\x93\xd3\xa8\x27\x34\x6a\x31\xda\x2b\x90\xd8\x42\xe9\xf1\xdd\x1c\xcd\x0a\x54\x14\xac\xc5\xd4\xeb\xc8\xf0\x30\x32\x34\x8c\x24\x09\x78\x87\xb6\x5b\x18\x55\x7c\x51\x80\xf7\x58\xf1\xa0\x20\x36\x38\x89\x56\xee\xd8\x75\xfc\x75\xcf\xf9\xef\xd6\x8e\x9f\x3b\xb1\x60\x7c\x9b\x02\x6a\x00\x7e\x88\x30\x30\xb7\x26\xb5\xa5\x67\xd4\xd6\x3d\xf8\xd4\x5f\xd6\x46\x86\x17\x47\x79\x46\xdc\x69\x91\xe2\x49\x8c\x62\xc4\x23\xde\x43\x51\xa0\x79\x81\xf6\x72\xc8\xc3\xfd\x60\x1c\x33\x54\x4f\x68\xc6\x16\xcd\x0a\xc8\x8a\xf0\xae\x70\xe0\x3d\x68\xe5\xb2\xaa\xe0\x3d\x5a\x3a\x70\x05\x94\x05\x94\x25\x5a\x3a\x54\x3d\x33\x49\xc6\x08\x58\x20\xb6\x90\x1a\x48\x8d\x12\x0b\xe9\xa6\xae\xfd\xce\x71\xcb\x22\x3f\xda\x40\x66\xf7\x2d\xf4\x04\xc8\x82\xb3\x17\x47\xf9\x05\x97\x8e\x3e\xbf\x73\xef\x33\x8d\x81\x3a\x66\xf3\x33\x24\x93\xfb\x89\x45\xb1\x22\x88\x15\x24\x89\x91\x66\x0d\x33\x58\x47\x1a\x29\x92\x44\x10\x9b\x60\x6f\xd1\x10\x04\xa5\x43\xb3\x12\xdf\xe9\xa1\xad\x2e\xda\xca\xd0\xbc\x0c\x40\x44\x90\x28\x42\x1a\x4d\xa4\xd1\x40\xe2\x18\xf5\x1e\xcd\x72\xb4\xdb\x41\x7b\x3d\x70\x65\x00\x5a\x81\x76\x0a\xb9\x0a\x5d\x07\xad\x12\xba\x4e\x7c\x62\x58\xb1\x28\xe9\x3d\xd1\x79\x6a\xac\x8c\x6e\x03\x86\x07\x90\xe1\x0f\xbd\x8b\x47\xee\x7d\xe4\xeb\x03\x27\x2f\xc0\xec\xdf\x43\xd4\x6d\x13\x09\x58\xa9\x8c\xe8\x09\x9a\x57\x8f\x66\x05\x12\x9b\xe0\x26\x91\x0d\xea\x12\xc0\x87\xe8\xd3\xa2\x44\x0b\x0f\x45\x19\xac\xa0\x0a\x62\x40\x40\xbd\x42\xa7\x83\xe6\x19\x12\xc5\x60\x4d\xa5\x78\x45\x22\x0b\x28\xea\xfa\xd6\x50\x0c\x1a\x2c\x61\xc2\x99\x2b\xa6\xf0\xac\xaa\xc5\xc9\xfa\x6d\x31\x12\x5d\x0a\xd8\x79\xf3\xcc\x53\xf7\xff\xea\x55\x76\x70\x60\x65\x64\x0d\x32\x71\x80\xc8\x15\xd8\x99\x34\xa5\x01\x04\x85\x47\x5d\x06\x85\x81\x58\x82\x8d\x8d\x09\x1f\xec\x03\x28\xc3\x89\x56\x0b\x88\x41\x4c\x7f\x5c\xdf\x3f\x22\x88\x63\x48\x12\xc4\x58\xc4\x3b\x34\xcf\x03\xb0\xbc\x40\xbd\x03\xaf\x88\x2a\x56\x15\xeb\x95\xb8\x72\x2b\x27\x5c\x36\x6b\x56\xfe\xd5\xfa\xec\x21\x89\xa2\x08\x89\x06\x87\x4d\xe6\xe5\xa2\x74\x68\xd0\x48\xd6\xc3\x76\x5a\x18\x14\x11\x3d\x32\xcb\xa9\x0f\x52\x59\x50\x6b\x90\x58\x90\x48\x40\x25\x08\xee\x05\xdc\x34\x4b\x8b\x11\x88\x62\x24\x4e\x20\x8a\x10\x63\x02\x1b\x45\x31\x34\xea\x98\x7a\x13\x92\x38\xb8\x5e\xb7\x8d\xb6\x5a\x68\xaf\x87\x14\x39\xea\x1c\x38\x1f\x98\x49\x3d\x46\x35\x18\xac\x64\x05\xc3\xc2\x70\x14\x49\x64\x63\x2b\x73\xce\x38\xc5\x3c\xb7\xdf\xbe\x31\xaa\xd5\x90\xf1\xe7\xb1\x65\x5e\x79\x85\x4c\x07\x96\x4c\x47\x97\x44\xc1\x9e\xa6\x6e\x91\x7a\x15\x07\x0a\xe4\x0e\x28\x50\x27\x88\x17\x54\x4d\x10\x34\xad\x05\xbf\x4f\x93\x90\xc6\xc5\x20\x49\x82\x0c\xce\x0a\x94\x9a\xa4\x50\x16\xf8\xc9\x49\x34\x9d\xc0\xb7\x26\x21\xcf\x2a\xc2\xc8\x91\x3c\xc7\x6a\x81\xf5\x0e\x83\x82\x70\x3c\x13\xc6\x26\x43\x49\x19\xc5\xd6\x08\xe7\x9e\x25\xfc\xe4\xa9\x73\x8d\x11\xa4\xd3\x01\xe7\x2a\xf2\x92\x43\xab\x0e\x21\x04\x73\x64\x20\xb5\x48\x33\x42\x9a\x31\x92\x58\xd4\x29\x74\xa5\x4a\xa5\x8a\x3a\x10\x0c\x92\xa4\x48\xb3\x89\x99\x35\x04\xf5\x06\x12\x47\x60\x0c\x52\xab\x23\x43\x43\x98\xa1\xd9\x48\x92\xa2\x65\x89\x1c\x9c\xc0\xa7\x29\xd2\xa8\xa3\xdd\x2e\xe4\x19\xda\xe9\xe0\xda\xed\x40\xab\x4e\x11\xe3\x31\xa5\xb2\x7e\x57\x7c\xd2\xe2\x86\xdf\x1a\xd5\xe6\x0c\xc0\xbc\x13\x2c\xb2\x61\x91\x38\x07\x59\x2f\xb8\xcf\x4c\xed\x4f\x61\x90\xe0\xbf\x91\x41\x52\x8b\xd4\x63\xa4\x19\x43\x62\x90\x22\x14\x2f\xf4\x4a\xd4\x84\x71\x82\x85\x24\x46\x1a\x0d\x18\x1c\xc4\x0c\x0c\x42\x1c\x57\x4c\xd4\x40\x66\x0f\x07\x00\x69\x0d\x2d\x0b\x7c\x9a\x62\xa2\x08\xad\xd5\xd0\x6e\x17\xed\x75\x21\x4e\x30\x08\xb6\xf4\x98\xdc\x05\xb7\x16\xa5\xe7\x19\xb6\xea\xc7\xa2\x7a\xa3\x2e\x3b\xfe\xe3\x91\xe3\xc5\x5a\x28\x4b\x28\xf2\xa9\xe0\x3b\x02\x82\x04\x10\x81\x52\x6d\x00\x51\xb3\x15\x95\x0a\xf4\x1c\xbe\x02\x29\xc6\xa0\x62\x31\x71\x8c\xa4\x35\x4c\xbd\x01\x8d\x3a\x92\xd4\x30\x49\x02\x8d\x66\x10\x7e\xd6\x10\x52\xaf\x23\x79\x0e\x22\x78\xf5\x61\xad\x34\x85\x76\x1c\x34\x5f\xe4\xd0\xed\x81\xcd\x10\x09\xc5\x91\xaa\x46\x46\x3d\x51\x1a\x45\x3a\xb1\xf7\xe0\x5c\x13\xa7\x68\x59\x4c\xb9\xcf\x51\x85\xef\x5f\x6d\xc5\xaf\x91\x40\x6c\x90\xc8\x86\xd8\xb6\x21\x68\xa7\xe6\x19\x83\x58\x8b\xc4\x31\x12\xc7\x10\xa7\x21\x0e\xd2\x5a\x10\xba\x56\x0b\x96\x48\x6b\x68\x14\x21\x45\x81\xa9\xd5\xf0\x79\x11\x58\xcf\x79\xc8\xf3\x30\x2f\x9a\xc1\x62\x80\x35\xb4\xbd\x58\x35\xd6\x1a\xf1\xae\xac\x85\xac\x51\x65\xce\x17\x2b\x55\x65\xe6\x29\xd3\x8c\x22\x87\x0f\x96\xca\xe3\x24\xf0\xbf\x39\xda\x69\x43\x40\x57\x6b\x48\xf5\x5c\x4d\x3f\xaf\x54\x56\xad\xd6\xf7\x48\xc8\x6f\xd5\xda\x23\x91\xdf\xdd\x2d\x55\x0c\x06\x35\x51\xd4\x53\xf5\xa8\x57\x54\xa7\xf5\x7e\x04\x89\xf6\x8b\x12\x1f\x92\x96\x7a\x5f\x5d\xab\x1c\xa0\x87\xce\xd4\x2a\xa3\xaa\xf7\xa8\x2a\xd2\x1f\xe7\x5d\xc8\xb8\x45\x28\x25\x70\x25\x5a\x84\xd2\x42\x8a\x02\xf2\xbc\xca\x09\x39\x5a\xe4\x68\x59\x82\x73\xa8\x06\xf9\xbc\xc2\xe8\x68\xb1\x3f\xcb\x32\x8d\x26\x5b\x6d\xe6\x2e\x3c\x61\xfb\xf6\xb1\xdd\x68\x62\x0f\x91\xd1\x1c\x92\xc8\x2a\xdd\x6a\xc8\xa6\x52\x7a\xb4\xf0\x48\xee\x10\x05\x5f\x56\x0a\xf0\xd3\xdd\x80\x54\xc2\x8b\x73\xa1\xe6\x29\xf2\xe0\x62\x55\x66\xf6\x69\x82\x89\x93\x00\x20\x2f\xd0\x76\x0b\xdf\x9a\x44\x5b\x93\x68\xbb\x0d\xdd\x0e\xda\x6e\xa3\xdd\x2e\x3e\x2f\x70\xce\xe1\x54\x91\xd0\xd3\xd0\x9d\xc8\x88\x5a\x7b\xbb\x9c\xf4\x96\x0b\xf6\x6d\xbb\xed\xde\xcc\xf9\x34\x15\x31\x95\x32\x15\x3d\xdc\x87\x7c\xf0\x4b\x29\x3c\x9a\x39\xe8\x96\x78\x2b\x48\xaa\x50\x7a\x28\x3c\xe2\x74\xda\x92\xce\x05\x2d\x67\x59\xa0\x45\x6b\x43\x01\x67\x2d\x9a\xe7\x18\x25\x58\x27\x4d\x21\x2f\xf0\x13\x07\xd0\xbd\x7b\xf1\x13\x07\xf0\x9d\x36\x74\xbb\x68\xbb\x8d\x6b\xb5\x70\x59\x86\x2f\x1d\x2e\x78\xf8\x63\x4c\x2a\xde\xab\x46\x9d\x89\x96\xf2\xc4\x13\x1e\xe7\x1f\xf5\xce\xaf\x74\x18\x4a\x95\x6a\xcb\x4e\x31\x32\x03\x85\x0a\xea\x41\x4a\x45\x72\x8f\x74\xca\xc0\xac\x55\xff\xe7\x7b\x65\xc8\xc6\xce\x87\x2a\x0c\x87\xe6\x19\xbe\xd3\x09\x81\xec\x1c\x12\x45\x28\x60\xe2\x18\xdf\xeb\xa2\xdd\x4e\x95\x07\x0a\x74\xe2\x00\x7e\xdf\x5e\x74\x62\x02\xed\x74\xf0\x79\x86\xf6\x32\x7c\x2f\xa3\xcc\x73\x4a\xe7\xf1\xde\x63\xe0\xa7\x93\xad\xc4\xec\x9e\x98\x28\xa3\x7d\x05\xda\x5e\xfb\xa0\xa2\x83\x3f\x2a\xb2\x6c\xa5\x8a\xc5\x68\x70\x1f\x45\xa6\xdd\xa8\x9f\xd7\x9c\x22\xa5\x43\x7a\xa1\xc8\x13\x07\x3e\xae\x8a\xaf\xdc\xa1\x3d\x87\x96\x5a\xd5\x42\x82\x98\x1c\x6d\xb7\x43\x9c\xf7\x7a\xa1\x26\xc2\xe3\x4d\x84\x69\xb5\x60\xb2\x85\x24\x31\x14\x21\x13\xfb\x89\x03\x68\x6b\x32\x58\xad\x28\x70\xa5\xc3\x15\x25\x65\xe1\x29\x4a\xa5\x54\x7c\x04\x0f\x75\xf3\xd2\x0d\x17\x68\x34\x0f\x74\x72\xeb\x33\x3e\x1a\x7d\xed\xda\x7c\xb2\xfd\x59\x05\xf0\x02\xa2\x81\xde\x75\x06\x83\x1a\x05\x27\x53\x5d\x92\x78\x17\xac\x61\x2b\x7a\x2b\xb5\xaa\x42\x83\x05\x14\x8f\x64\x05\xe2\x3b\x48\xe9\x42\x2d\xd4\xb7\xa8\x18\x24\x9d\xc4\xd4\x0e\x06\x1a\x76\x2e\x24\xaf\x4e\x3b\x94\xd5\x45\x81\xf7\x0e\xe7\x94\xdc\x29\x59\xa9\x14\x1e\xbc\x67\xcf\xeb\x8e\x73\x0f\xbd\xb0\x7d\x97\xdf\x0e\xc8\xfd\xc0\x59\x20\x0b\x55\xe5\xe7\xaf\x3d\xff\x09\xab\xee\xcc\xb8\xdb\x21\x15\x37\x95\x9f\xa6\x58\xb2\x4f\x9f\x80\xa9\x72\x81\x58\x99\xe6\xe7\x2a\x46\x70\x15\xdb\x54\x33\xc5\x9a\xd0\x8d\x19\x13\xe2\x4a\x41\x4c\xe8\x0d\x88\xe2\x90\xf4\x34\x34\x4b\xfd\xfa\x27\x30\x17\x94\x5e\xe9\x39\xe8\x39\xa5\x1b\x94\xf7\x7b\x2b\x17\xb4\xfe\x69\xec\xa7\x3b\x74\x11\xf8\xe8\x6d\xc0\x66\x40\x2f\x5c\x2e\x43\x83\xc7\xbd\x7d\xdf\xae\x7d\xcf\x7a\xaf\xa8\x11\x1c\x60\xd1\x29\x10\x7d\x2b\x08\x02\x1a\xdc\x47\x44\xc1\xf8\x69\x86\xd2\x50\x7a\x8b\x07\x24\xb8\x51\x18\xeb\xa7\xb5\x4f\xe8\x8f\x45\x0a\x30\x59\xb8\x57\x42\x7b\xe9\xdd\x94\xf0\xde\x2b\x85\x42\xcf\x43\xe6\x05\x07\xeb\xce\x1b\x2a\xff\x75\xf7\xd6\x8c\x6b\x02\x51\x22\x5b\x80\x53\x80\xed\x35\x64\xf6\xf2\x65\xd1\xc3\xfb\xe5\x56\x54\x3f\x1a\x87\x92\x07\x2b\x15\x9d\x4a\x00\x32\x6d\x88\xca\x95\x4c\xbf\x6a\x0d\x31\xa1\x3a\xdd\x88\x33\xc3\xfd\x10\x39\x2c\x39\x56\xc5\xa1\xc8\x94\x55\x50\x0d\xec\xe7\x15\xad\xba\xb1\xb0\xc5\x12\xf6\x8c\x2c\x5c\xb1\x74\x96\x5f\xdb\xfe\xaf\x67\xca\x53\x2b\x62\x97\xfe\x46\x56\x01\xcc\x1e\xb0\x66\xde\xaa\x45\xac\x7d\x32\x7a\xce\x08\xf3\xfb\xc2\xf7\x2d\x30\xe5\x4e\x87\xb9\xd5\x21\xd7\xbe\x82\x5f\x6c\xf7\xec\x45\xf6\x02\x75\x1a\x73\xc8\x75\x15\x00\xe7\xa1\x0c\xd3\x3e\x79\xf1\x65\x93\xb7\xbd\xf0\xf5\xe7\x65\x7d\xa1\x7e\x3e\x70\xfa\xe1\xcb\x1d\x00\x86\x46\x90\xff\x1c\x5e\x36\xdc\x29\x59\x2f\x86\x13\xcc\x74\x37\x18\xa8\x55\x42\x60\x73\x58\x05\x71\x48\x9c\x4c\x2b\xfc\x48\xb9\x95\x23\x32\xbd\xce\x00\xd0\x6f\x26\xfb\x89\xdd\x85\x41\xdf\x7a\xd3\x3c\xff\x07\x3b\xb6\xed\xf5\xed\x9d\xfb\xfc\xb2\x17\xd3\xc7\x58\xb5\x95\x7e\x16\xc8\x63\xa3\x4b\xe6\x3a\xb1\xcf\x09\x24\x1c\x5a\x02\xcd\x2c\x4c\x8f\xa8\xf3\x8e\x26\x7c\xff\x9d\xfe\x06\xad\xeb\x8c\xce\x55\xa7\x3d\x0a\xe0\x1b\x97\x6d\xdd\xf8\xfe\x5d\x16\x53\x73\xa1\x52\x1b\x79\xa9\x9b\xbb\x3f\x3e\x79\xe9\x5c\x2f\xb2\x16\x78\xf5\x4c\xa1\x84\xa3\xbb\x88\x1c\xe3\x6f\x13\x7d\x11\x6b\xcc\x48\x3b\xd7\x5d\xbe\x75\xe3\xed\xbf\x8c\x22\x29\xcb\x52\x57\xbe\xdc\x1f\x1c\x6b\x46\x97\xdd\x0d\x5c\x53\x15\xd2\x47\xcc\xfc\x9f\x16\x39\x9a\xf6\x8f\x1a\x03\xd3\x37\x5b\x54\x79\xdb\xea\x6d\x1b\xd7\xf7\x1f\x3f\x00\x5c\x7c\xac\x3f\xf9\xd6\x8c\x2e\x63\xcd\xe8\x32\x56\x8f\x6d\xfc\x20\xb0\x1c\x78\x76\xa6\x79\xfb\xa7\x3f\xca\x79\xf8\xfb\xdf\x34\xf6\x90\xf1\xf0\x6e\x0f\x67\xae\xde\xb6\x71\xfd\xbf\xbf\xed\xfd\xfc\xb8\xff\xc3\xf0\x95\xfc\x23\xab\x40\xf4\xef\xaf\x03\x3e\x02\x9c\xf9\xbf\xf8\xbb\x77\x03\xf0\x0f\xab\xc7\x36\xde\x7c\xf8\xf7\x78\x09\xd6\x3d\xe6\x63\xcd\xe8\x32\x53\xfd\x36\xbb\x13\x78\xd3\x2b\x10\xfc\xfb\xc0\x4d\xc0\xa6\xd5\x63\x1b\xdd\xb1\x0a\x4f\x7f\x77\xfa\x65\x1c\x7e\xf5\xd8\xc6\xa7\x81\x4b\xd6\x8c\x2e\x9b\x05\xbc\x01\x78\x6d\x45\xcd\x27\x03\x73\x81\x05\x40\x13\xd8\x05\x8c\x03\xbb\x81\x2d\x95\xb6\xd7\x01\xbf\x58\x3d\xb6\xb1\xe8\x0b\xdd\x3f\x8e\x45\x78\x80\xff\x06\x26\x17\xf9\x29\x2d\x26\x91\x99\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\x01\x00\x00\xff\xff\x8e\x30\x0d\x01\x5c\x0f\x00\x00")
273-
274- func tomatoIconPngBytes() ([]byte, error) {
275- return bindataRead(
276- _tomatoIconPng,
277- "tomato-icon.png",
278- )
279- }
280-
281- func tomatoIconPng() (*asset, error) {
282- bytes, err := tomatoIconPngBytes()
283- if err != nil {
284- return nil, err
285- }
286-
287- info := bindataFileInfo{name: "tomato-icon.png", size: 3932, mode: os.FileMode(420), modTime: time.Unix(1516522577, 0)}
288- a := &asset{bytes: bytes, info: info}
289- return a, nil
290- }
291-
292- // Asset loads and returns the asset for the given name.
293- // It returns an error if the asset could not be found or
294- // could not be loaded.
295- func Asset(name string) ([]byte, error) {
296- cannonicalName := strings.Replace(name, "\\", "/", -1)
297- if f, ok := _bindata[cannonicalName]; ok {
298- a, err := f()
299- if err != nil {
300- return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err)
301- }
302- return a.bytes, nil
303- }
304- return nil, fmt.Errorf("Asset %s not found", name)
305- }
306-
307- // MustAsset is like Asset but panics when Asset would return an error.
308- // It simplifies safe initialization of global variables.
309- func MustAsset(name string) []byte {
310- a, err := Asset(name)
311- if err != nil {
312- panic("asset: Asset(" + name + "): " + err.Error())
313- }
314-
315- return a
316- }
317-
318- // AssetInfo loads and returns the asset info for the given name.
319- // It returns an error if the asset could not be found or
320- // could not be loaded.
321- func AssetInfo(name string) (os.FileInfo, error) {
322- cannonicalName := strings.Replace(name, "\\", "/", -1)
323- if f, ok := _bindata[cannonicalName]; ok {
324- a, err := f()
325- if err != nil {
326- return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err)
327- }
328- return a.info, nil
329- }
330- return nil, fmt.Errorf("AssetInfo %s not found", name)
331- }
332-
333- // AssetNames returns the names of the assets.
334- func AssetNames() []string {
335- names := make([]string, 0, len(_bindata))
336- for name := range _bindata {
337- names = append(names, name)
338- }
339- return names
340- }
341-
342- // _bindata is a table, holding each asset generator, mapped to its name.
343- var _bindata = map[string]func() (*asset, error){
344- "tomato-icon.png": tomatoIconPng,
345- }
346-
347- // AssetDir returns the file names below a certain
348- // directory embedded in the file by go-bindata.
349- // For example if you run go-bindata on data/... and data contains the
350- // following hierarchy:
351- // data/
352- // foo.txt
353- // img/
354- // a.png
355- // b.png
356- // then AssetDir("data") would return []string{"foo.txt", "img"}
357- // AssetDir("data/img") would return []string{"a.png", "b.png"}
358- // AssetDir("foo.txt") and AssetDir("notexist") would return an error
359- // AssetDir("") will return []string{"data"}.
360- func AssetDir(name string) ([]string, error) {
361- node := _bintree
362- if len(name) != 0 {
363- cannonicalName := strings.Replace(name, "\\", "/", -1)
364- pathList := strings.Split(cannonicalName, "/")
365- for _, p := range pathList {
366- node = node.Children[p]
367- if node == nil {
368- return nil, fmt.Errorf("Asset %s not found", name)
369- }
370- }
371- }
372- if node.Func != nil {
373- return nil, fmt.Errorf("Asset %s not found", name)
374- }
375- rv := make([]string, 0, len(node.Children))
376- for childName := range node.Children {
377- rv = append(rv, childName)
378- }
379- return rv, nil
380- }
381-
382- type bintree struct {
383- Func func() (*asset, error)
384- Children map[string]*bintree
385- }
386- var _bintree = &bintree{nil, map[string]*bintree{
387- "tomato-icon.png": &bintree{tomatoIconPng, map[string]*bintree{}},
388- }}
389-
390- // RestoreAsset restores an asset under the given directory
391- func RestoreAsset(dir, name string) error {
392- data, err := Asset(name)
393- if err != nil {
394- return err
395- }
396- info, err := AssetInfo(name)
397- if err != nil {
398- return err
399- }
400- err = os.MkdirAll(_filePath(dir, filepath.Dir(name)), os.FileMode(0755))
401- if err != nil {
402- return err
403- }
404- err = ioutil.WriteFile(_filePath(dir, name), data, info.Mode())
405- if err != nil {
406- return err
407- }
408- err = os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime())
409- if err != nil {
410- return err
411- }
412- return nil
413- }
414-
415- // RestoreAssets restores an asset under the given directory recursively
416- func RestoreAssets(dir, name string) error {
417- children, err := AssetDir(name)
418- // File
419- if err != nil {
420- return RestoreAsset(dir, name)
421- }
422- // Dir
423- for _, child := range children {
424- err = RestoreAssets(dir, filepath.Join(name, child))
425- if err != nil {
426- return err
427- }
428- }
429- return nil
430- }
431-
432- func _filePath(dir, name string) string {
433- cannonicalName := strings.Replace(name, "\\", "/", -1)
434- return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...)
435- }
436-
437 diff --git a/cmd/pomo/main.go b/cmd/pomo/main.go
438new file mode 100644
439index 0000000..d8aa4d2
440--- /dev/null
441+++ b/cmd/pomo/main.go
442 @@ -0,0 +1,7 @@
443+ package main
444+
445+ import "github.com/kevinschoon/pomo/pkg/cmd"
446+
447+ func main() {
448+ cmd.Run()
449+ }
450 diff --git a/config.go b/config.go
451deleted file mode 100644
452index ec68006..0000000
453--- a/config.go
454+++ /dev/null
455 @@ -1,114 +0,0 @@
456- package main
457-
458- import (
459- "encoding/json"
460- "io/ioutil"
461- "os"
462- "path"
463-
464- "github.com/fatih/color"
465- )
466-
467- const (
468- defaultDateTimeFmt = "2006-01-02 15:04"
469- )
470-
471- // Config represents user preferences
472- type Config struct {
473- Colors *ColorMap `json:"colors"`
474- DateTimeFmt string `json:"dateTimeFmt"`
475- BasePath string `json:"basePath"`
476- DBPath string `json:"dbPath"`
477- SocketPath string `json:"socketPath"`
478- IconPath string `json:"iconPath"`
479- }
480-
481- type ColorMap struct {
482- colors map[string]*color.Color
483- tags map[string]string
484- }
485-
486- func (c *ColorMap) Get(name string) *color.Color {
487- if color, ok := c.colors[name]; ok {
488- return color
489- }
490- return nil
491- }
492-
493- func (c *ColorMap) MarshalJSON() ([]byte, error) {
494- return json.Marshal(c.tags)
495- }
496-
497- func (c *ColorMap) UnmarshalJSON(raw []byte) error {
498- lookup := map[string]*color.Color{
499- "black": color.New(color.FgBlack),
500- "hiblack": color.New(color.FgHiBlack),
501- "blue": color.New(color.FgBlue),
502- "hiblue": color.New(color.FgHiBlue),
503- "cyan": color.New(color.FgCyan),
504- "hicyan": color.New(color.FgHiCyan),
505- "green": color.New(color.FgGreen),
506- "higreen": color.New(color.FgHiGreen),
507- "magenta": color.New(color.FgMagenta),
508- "himagenta": color.New(color.FgHiMagenta),
509- "red": color.New(color.FgRed),
510- "hired": color.New(color.FgHiRed),
511- "white": color.New(color.FgWhite),
512- "hiwrite": color.New(color.FgHiWhite),
513- "yellow": color.New(color.FgYellow),
514- "hiyellow": color.New(color.FgHiYellow),
515- }
516- cm := &ColorMap{
517- colors: map[string]*color.Color{},
518- tags: map[string]string{},
519- }
520- err := json.Unmarshal(raw, &cm.tags)
521- if err != nil {
522- return err
523- }
524- for tag, colorName := range cm.tags {
525- if color, ok := lookup[colorName]; ok {
526- cm.colors[tag] = color
527- }
528- }
529- *c = *cm
530- return nil
531- }
532-
533- func LoadConfig(configPath string, config *Config) error {
534- raw, err := ioutil.ReadFile(configPath)
535- if err != nil {
536- os.MkdirAll(path.Dir(configPath), 0755)
537- // Create an empty config file
538- // if it does not already exist.
539- if os.IsNotExist(err) {
540- raw, _ := json.Marshal(map[string]string{})
541- err := ioutil.WriteFile(configPath, raw, 0644)
542- if err != nil {
543- return err
544- }
545- return LoadConfig(configPath, config)
546- }
547- return err
548- }
549- err = json.Unmarshal(raw, config)
550- if err != nil {
551- return err
552- }
553- if config.DateTimeFmt == "" {
554- config.DateTimeFmt = defaultDateTimeFmt
555- }
556- if config.BasePath == "" {
557- config.BasePath = path.Dir(configPath)
558- }
559- if config.DBPath == "" {
560- config.DBPath = path.Join(config.BasePath, "/pomo.db")
561- }
562- if config.SocketPath == "" {
563- config.SocketPath = path.Join(config.BasePath, "/pomo.sock")
564- }
565- if config.IconPath == "" {
566- config.IconPath = path.Join(config.BasePath, "/icon.png")
567- }
568- return nil
569- }
570 diff --git a/main.go b/main.go
571deleted file mode 100644
572index e8675d4..0000000
573--- a/main.go
574+++ /dev/null
575 @@ -1,239 +0,0 @@
576- package main
577-
578- import (
579- "database/sql"
580- "encoding/json"
581- "fmt"
582- "os"
583- "sort"
584- "time"
585-
586- cli "github.com/jawher/mow.cli"
587- )
588-
589- func start(config *Config) func(*cli.Cmd) {
590- return func(cmd *cli.Cmd) {
591- cmd.Spec = "[OPTIONS] MESSAGE"
592- var (
593- duration = cmd.StringOpt("d duration", "25m", "duration of each stent")
594- pomodoros = cmd.IntOpt("p pomodoros", 4, "number of pomodoros")
595- message = cmd.StringArg("MESSAGE", "", "descriptive name of the given task")
596- tags = cmd.StringsOpt("t tag", []string{}, "tags associated with this task")
597- )
598- cmd.Action = func() {
599- parsed, err := time.ParseDuration(*duration)
600- maybe(err)
601- db, err := NewStore(config.DBPath)
602- maybe(err)
603- defer db.Close()
604- task := &Task{
605- Message: *message,
606- Tags: *tags,
607- NPomodoros: *pomodoros,
608- Duration: parsed,
609- }
610- maybe(db.With(func(tx *sql.Tx) error {
611- id, err := db.CreateTask(tx, *task)
612- if err != nil {
613- return err
614- }
615- task.ID = id
616- return nil
617- }))
618- runner, err := NewTaskRunner(task, config)
619- maybe(err)
620- server, err := NewServer(runner, config)
621- maybe(err)
622- server.Start()
623- defer server.Stop()
624- runner.Start()
625- startUI(runner)
626- }
627- }
628- }
629-
630- func create(config *Config) func(*cli.Cmd) {
631- return func(cmd *cli.Cmd) {
632- cmd.Spec = "[OPTIONS] MESSAGE"
633- var (
634- duration = cmd.StringOpt("d duration", "25m", "duration of each stent")
635- pomodoros = cmd.IntOpt("p pomodoros", 4, "number of pomodoros")
636- message = cmd.StringArg("MESSAGE", "", "descriptive name of the given task")
637- tags = cmd.StringsOpt("t tag", []string{}, "tags associated with this task")
638- )
639- cmd.Action = func() {
640- parsed, err := time.ParseDuration(*duration)
641- maybe(err)
642- db, err := NewStore(config.DBPath)
643- maybe(err)
644- defer db.Close()
645- task := &Task{
646- Message: *message,
647- Tags: *tags,
648- NPomodoros: *pomodoros,
649- Duration: parsed,
650- }
651- maybe(db.With(func(tx *sql.Tx) error {
652- taskId, err := db.CreateTask(tx, *task)
653- if err != nil {
654- return err
655- }
656- fmt.Println(taskId)
657- return nil
658- }))
659- }
660- }
661- }
662-
663- func begin(config *Config) func(*cli.Cmd) {
664- return func(cmd *cli.Cmd) {
665- cmd.Spec = "[OPTIONS] TASK_ID"
666- var (
667- taskId = cmd.IntArg("TASK_ID", -1, "ID of Pomodoro to begin")
668- )
669-
670- cmd.Action = func() {
671- db, err := NewStore(config.DBPath)
672- maybe(err)
673- defer db.Close()
674- var task *Task
675- maybe(db.With(func(tx *sql.Tx) error {
676- read, err := db.ReadTask(tx, *taskId)
677- if err != nil {
678- return err
679- }
680- task = read
681- err = db.DeletePomodoros(tx, *taskId)
682- if err != nil {
683- return err
684- }
685- task.Pomodoros = []*Pomodoro{}
686- return nil
687- }))
688- runner, err := NewTaskRunner(task, config)
689- maybe(err)
690- server, err := NewServer(runner, config)
691- maybe(err)
692- server.Start()
693- defer server.Stop()
694- runner.Start()
695- startUI(runner)
696- }
697- }
698- }
699-
700- func initialize(config *Config) func(*cli.Cmd) {
701- return func(cmd *cli.Cmd) {
702- cmd.Spec = "[OPTIONS]"
703- cmd.Action = func() {
704- db, err := NewStore(config.DBPath)
705- maybe(err)
706- defer db.Close()
707- maybe(initDB(db))
708- }
709- }
710- }
711-
712- func list(config *Config) func(*cli.Cmd) {
713- return func(cmd *cli.Cmd) {
714- cmd.Spec = "[OPTIONS]"
715- var (
716- asJSON = cmd.BoolOpt("json", false, "output task history as JSON")
717- assend = cmd.BoolOpt("assend", false, "sort tasks assending in age")
718- all = cmd.BoolOpt("a all", true, "output all tasks")
719- limit = cmd.IntOpt("n limit", 0, "limit the number of results by n")
720- duration = cmd.StringOpt("d duration", "24h", "show tasks within this duration")
721- )
722- cmd.Action = func() {
723- duration, err := time.ParseDuration(*duration)
724- maybe(err)
725- db, err := NewStore(config.DBPath)
726- maybe(err)
727- defer db.Close()
728- maybe(db.With(func(tx *sql.Tx) error {
729- tasks, err := db.ReadTasks(tx)
730- maybe(err)
731- if *assend {
732- sort.Sort(sort.Reverse(ByID(tasks)))
733- }
734- if !*all {
735- tasks = After(time.Now().Add(-duration), tasks)
736- }
737- if *limit > 0 && (len(tasks) > *limit) {
738- tasks = tasks[0:*limit]
739- }
740- if *asJSON {
741- maybe(json.NewEncoder(os.Stdout).Encode(tasks))
742- return nil
743- }
744- maybe(err)
745- summerizeTasks(config, tasks)
746- return nil
747- }))
748- }
749- }
750- }
751-
752- func _delete(config *Config) func(*cli.Cmd) {
753- return func(cmd *cli.Cmd) {
754- cmd.Spec = "[OPTIONS] TASK_ID"
755- var taskID = cmd.IntArg("TASK_ID", -1, "task to delete")
756- cmd.Action = func() {
757- db, err := NewStore(config.DBPath)
758- maybe(err)
759- defer db.Close()
760- maybe(db.With(func(tx *sql.Tx) error {
761- return db.DeleteTask(tx, *taskID)
762- }))
763- }
764- }
765- }
766-
767- func _status(config *Config) func(*cli.Cmd) {
768- return func(cmd *cli.Cmd) {
769- cmd.Spec = "[OPTIONS]"
770- cmd.Action = func() {
771- client, err := NewClient(config.SocketPath)
772- if err != nil {
773- outputStatus(Status{})
774- return
775- }
776- defer client.Close()
777- status, err := client.Status()
778- maybe(err)
779- outputStatus(*status)
780- }
781- }
782- }
783-
784- func _config(config *Config) func(*cli.Cmd) {
785- return func(cmd *cli.Cmd) {
786- cmd.Spec = "[OPTIONS]"
787- cmd.Action = func() {
788- maybe(json.NewEncoder(os.Stdout).Encode(config))
789- }
790- }
791- }
792-
793- func main() {
794- app := cli.App("pomo", "Pomodoro CLI")
795- app.LongDesc = "Pomo helps you track what you did, how long it took you to do it, and how much effort you expect it to take."
796- app.Spec = "[OPTIONS]"
797- var (
798- config = &Config{}
799- path = app.StringOpt("p path", defaultConfigPath(), "path to the pomo config directory")
800- )
801- app.Before = func() {
802- maybe(LoadConfig(*path, config))
803- }
804- app.Version("v version", Version)
805- app.Command("start s", "start a new task", start(config))
806- app.Command("init", "initialize the sqlite database", initialize(config))
807- app.Command("config cf", "display the current configuration", _config(config))
808- app.Command("create c", "create a new task without starting", create(config))
809- app.Command("begin b", "begin requested pomodoro", begin(config))
810- app.Command("list l", "list historical tasks", list(config))
811- app.Command("delete d", "delete a stored task", _delete(config))
812- app.Command("status st", "output the current status", _status(config))
813- app.Run(os.Args)
814- }
815 diff --git a/pkg/cmd/cmd.go b/pkg/cmd/cmd.go
816new file mode 100644
817index 0000000..292e428
818--- /dev/null
819+++ b/pkg/cmd/cmd.go
820 @@ -0,0 +1,256 @@
821+ package cmd
822+
823+ import (
824+ "database/sql"
825+ "encoding/json"
826+ "fmt"
827+ "os"
828+ "os/user"
829+ "path"
830+ "sort"
831+ "time"
832+
833+ cli "github.com/jawher/mow.cli"
834+
835+ pomo "github.com/kevinschoon/pomo/pkg/internal"
836+ )
837+
838+ func maybe(err error) {
839+ if err != nil {
840+ fmt.Printf("Error:\n%s\n", err)
841+ os.Exit(1)
842+ }
843+ }
844+
845+ func defaultConfigPath() string {
846+ u, err := user.Current()
847+ maybe(err)
848+ return path.Join(u.HomeDir, "/.pomo/config.json")
849+ }
850+
851+ func start(config *pomo.Config) func(*cli.Cmd) {
852+ return func(cmd *cli.Cmd) {
853+ cmd.Spec = "[OPTIONS] MESSAGE"
854+ var (
855+ duration = cmd.StringOpt("d duration", "25m", "duration of each stent")
856+ pomodoros = cmd.IntOpt("p pomodoros", 4, "number of pomodoros")
857+ message = cmd.StringArg("MESSAGE", "", "descriptive name of the given task")
858+ tags = cmd.StringsOpt("t tag", []string{}, "tags associated with this task")
859+ )
860+ cmd.Action = func() {
861+ parsed, err := time.ParseDuration(*duration)
862+ maybe(err)
863+ db, err := pomo.NewStore(config.DBPath)
864+ maybe(err)
865+ defer db.Close()
866+ task := &pomo.Task{
867+ Message: *message,
868+ Tags: *tags,
869+ NPomodoros: *pomodoros,
870+ Duration: parsed,
871+ }
872+ maybe(db.With(func(tx *sql.Tx) error {
873+ id, err := db.CreateTask(tx, *task)
874+ if err != nil {
875+ return err
876+ }
877+ task.ID = id
878+ return nil
879+ }))
880+ runner, err := pomo.NewTaskRunner(task, config)
881+ maybe(err)
882+ server, err := pomo.NewServer(runner, config)
883+ maybe(err)
884+ server.Start()
885+ defer server.Stop()
886+ runner.Start()
887+ pomo.StartUI(runner)
888+ }
889+ }
890+ }
891+
892+ func create(config *pomo.Config) func(*cli.Cmd) {
893+ return func(cmd *cli.Cmd) {
894+ cmd.Spec = "[OPTIONS] MESSAGE"
895+ var (
896+ duration = cmd.StringOpt("d duration", "25m", "duration of each stent")
897+ pomodoros = cmd.IntOpt("p pomodoros", 4, "number of pomodoros")
898+ message = cmd.StringArg("MESSAGE", "", "descriptive name of the given task")
899+ tags = cmd.StringsOpt("t tag", []string{}, "tags associated with this task")
900+ )
901+ cmd.Action = func() {
902+ parsed, err := time.ParseDuration(*duration)
903+ maybe(err)
904+ db, err := pomo.NewStore(config.DBPath)
905+ maybe(err)
906+ defer db.Close()
907+ task := &pomo.Task{
908+ Message: *message,
909+ Tags: *tags,
910+ NPomodoros: *pomodoros,
911+ Duration: parsed,
912+ }
913+ maybe(db.With(func(tx *sql.Tx) error {
914+ taskId, err := db.CreateTask(tx, *task)
915+ if err != nil {
916+ return err
917+ }
918+ fmt.Println(taskId)
919+ return nil
920+ }))
921+ }
922+ }
923+ }
924+
925+ func begin(config *pomo.Config) func(*cli.Cmd) {
926+ return func(cmd *cli.Cmd) {
927+ cmd.Spec = "[OPTIONS] TASK_ID"
928+ var (
929+ taskId = cmd.IntArg("TASK_ID", -1, "ID of Pomodoro to begin")
930+ )
931+
932+ cmd.Action = func() {
933+ db, err := pomo.NewStore(config.DBPath)
934+ maybe(err)
935+ defer db.Close()
936+ var task *pomo.Task
937+ maybe(db.With(func(tx *sql.Tx) error {
938+ read, err := db.ReadTask(tx, *taskId)
939+ if err != nil {
940+ return err
941+ }
942+ task = read
943+ err = db.DeletePomodoros(tx, *taskId)
944+ if err != nil {
945+ return err
946+ }
947+ task.Pomodoros = []*pomo.Pomodoro{}
948+ return nil
949+ }))
950+ runner, err := pomo.NewTaskRunner(task, config)
951+ maybe(err)
952+ server, err := pomo.NewServer(runner, config)
953+ maybe(err)
954+ server.Start()
955+ defer server.Stop()
956+ runner.Start()
957+ pomo.StartUI(runner)
958+ }
959+ }
960+ }
961+
962+ func initialize(config *pomo.Config) func(*cli.Cmd) {
963+ return func(cmd *cli.Cmd) {
964+ cmd.Spec = "[OPTIONS]"
965+ cmd.Action = func() {
966+ db, err := pomo.NewStore(config.DBPath)
967+ maybe(err)
968+ defer db.Close()
969+ maybe(pomo.InitDB(db))
970+ }
971+ }
972+ }
973+
974+ func list(config *pomo.Config) func(*cli.Cmd) {
975+ return func(cmd *cli.Cmd) {
976+ cmd.Spec = "[OPTIONS]"
977+ var (
978+ asJSON = cmd.BoolOpt("json", false, "output task history as JSON")
979+ assend = cmd.BoolOpt("assend", false, "sort tasks assending in age")
980+ all = cmd.BoolOpt("a all", true, "output all tasks")
981+ limit = cmd.IntOpt("n limit", 0, "limit the number of results by n")
982+ duration = cmd.StringOpt("d duration", "24h", "show tasks within this duration")
983+ )
984+ cmd.Action = func() {
985+ duration, err := time.ParseDuration(*duration)
986+ maybe(err)
987+ db, err := pomo.NewStore(config.DBPath)
988+ maybe(err)
989+ defer db.Close()
990+ maybe(db.With(func(tx *sql.Tx) error {
991+ tasks, err := db.ReadTasks(tx)
992+ maybe(err)
993+ if *assend {
994+ sort.Sort(sort.Reverse(pomo.ByID(tasks)))
995+ }
996+ if !*all {
997+ tasks = pomo.After(time.Now().Add(-duration), tasks)
998+ }
999+ if *limit > 0 && (len(tasks) > *limit) {
1000+ tasks = tasks[0:*limit]
1001+ }
1002+ if *asJSON {
1003+ maybe(json.NewEncoder(os.Stdout).Encode(tasks))
1004+ return nil
1005+ }
1006+ maybe(err)
1007+ pomo.SummerizeTasks(config, tasks)
1008+ return nil
1009+ }))
1010+ }
1011+ }
1012+ }
1013+
1014+ func _delete(config *pomo.Config) func(*cli.Cmd) {
1015+ return func(cmd *cli.Cmd) {
1016+ cmd.Spec = "[OPTIONS] TASK_ID"
1017+ var taskID = cmd.IntArg("TASK_ID", -1, "task to delete")
1018+ cmd.Action = func() {
1019+ db, err := pomo.NewStore(config.DBPath)
1020+ maybe(err)
1021+ defer db.Close()
1022+ maybe(db.With(func(tx *sql.Tx) error {
1023+ return db.DeleteTask(tx, *taskID)
1024+ }))
1025+ }
1026+ }
1027+ }
1028+
1029+ func _status(config *pomo.Config) func(*cli.Cmd) {
1030+ return func(cmd *cli.Cmd) {
1031+ cmd.Spec = "[OPTIONS]"
1032+ cmd.Action = func() {
1033+ client, err := pomo.NewClient(config.SocketPath)
1034+ if err != nil {
1035+ pomo.OutputStatus(pomo.Status{})
1036+ return
1037+ }
1038+ defer client.Close()
1039+ status, err := client.Status()
1040+ maybe(err)
1041+ pomo.OutputStatus(*status)
1042+ }
1043+ }
1044+ }
1045+
1046+ func _config(config *pomo.Config) func(*cli.Cmd) {
1047+ return func(cmd *cli.Cmd) {
1048+ cmd.Spec = "[OPTIONS]"
1049+ cmd.Action = func() {
1050+ maybe(json.NewEncoder(os.Stdout).Encode(config))
1051+ }
1052+ }
1053+ }
1054+
1055+ func Run() {
1056+ app := cli.App("pomo", "Pomodoro CLI")
1057+ app.LongDesc = "Pomo helps you track what you did, how long it took you to do it, and how much effort you expect it to take."
1058+ app.Spec = "[OPTIONS]"
1059+ var (
1060+ config = &pomo.Config{}
1061+ path = app.StringOpt("p path", defaultConfigPath(), "path to the pomo config directory")
1062+ )
1063+ app.Before = func() {
1064+ maybe(pomo.LoadConfig(*path, config))
1065+ }
1066+ app.Version("v version", pomo.Version)
1067+ app.Command("start s", "start a new task", start(config))
1068+ app.Command("init", "initialize the sqlite database", initialize(config))
1069+ app.Command("config cf", "display the current configuration", _config(config))
1070+ app.Command("create c", "create a new task without starting", create(config))
1071+ app.Command("begin b", "begin requested pomodoro", begin(config))
1072+ app.Command("list l", "list historical tasks", list(config))
1073+ app.Command("delete d", "delete a stored task", _delete(config))
1074+ app.Command("status st", "output the current status", _status(config))
1075+ app.Run(os.Args)
1076+ }
1077 diff --git a/pkg/internal/bindata.go b/pkg/internal/bindata.go
1078new file mode 100644
1079index 0000000..48aeb4b
1080--- /dev/null
1081+++ b/pkg/internal/bindata.go
1082 @@ -0,0 +1,235 @@
1083+ // Code generated by go-bindata.
1084+ // sources:
1085+ // tomato-icon.png
1086+ // DO NOT EDIT!
1087+
1088+ package pomo
1089+
1090+ import (
1091+ "bytes"
1092+ "compress/gzip"
1093+ "fmt"
1094+ "io"
1095+ "io/ioutil"
1096+ "os"
1097+ "path/filepath"
1098+ "strings"
1099+ "time"
1100+ )
1101+
1102+ func bindataRead(data []byte, name string) ([]byte, error) {
1103+ gz, err := gzip.NewReader(bytes.NewBuffer(data))
1104+ if err != nil {
1105+ return nil, fmt.Errorf("Read %q: %v", name, err)
1106+ }
1107+
1108+ var buf bytes.Buffer
1109+ _, err = io.Copy(&buf, gz)
1110+ clErr := gz.Close()
1111+
1112+ if err != nil {
1113+ return nil, fmt.Errorf("Read %q: %v", name, err)
1114+ }
1115+ if clErr != nil {
1116+ return nil, err
1117+ }
1118+
1119+ return buf.Bytes(), nil
1120+ }
1121+
1122+ type asset struct {
1123+ bytes []byte
1124+ info os.FileInfo
1125+ }
1126+
1127+ type bindataFileInfo struct {
1128+ name string
1129+ size int64
1130+ mode os.FileMode
1131+ modTime time.Time
1132+ }
1133+
1134+ func (fi bindataFileInfo) Name() string {
1135+ return fi.name
1136+ }
1137+ func (fi bindataFileInfo) Size() int64 {
1138+ return fi.size
1139+ }
1140+ func (fi bindataFileInfo) Mode() os.FileMode {
1141+ return fi.mode
1142+ }
1143+ func (fi bindataFileInfo) ModTime() time.Time {
1144+ return fi.modTime
1145+ }
1146+ func (fi bindataFileInfo) IsDir() bool {
1147+ return false
1148+ }
1149+ func (fi bindataFileInfo) Sys() interface{} {
1150+ return nil
1151+ }
1152+
1153+ var _tomatoIconPng = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x00\x5c\x0f\xa3\xf0\x89\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\x00\x00\x30\x00\x00\x00\x2b\x08\x06\x00\x00\x00\x3e\x13\x0b\xdf\x00\x00\x00\x06\x62\x4b\x47\x44\x00\xff\x00\xff\x00\xff\xa0\xbd\xa7\x93\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x21\x38\x00\x00\x21\x38\x01\x45\x96\x31\x60\x00\x00\x00\x07\x74\x49\x4d\x45\x07\xe2\x01\x15\x08\x10\x11\xe3\x9e\xfd\x2f\x00\x00\x0e\xe9\x49\x44\x41\x54\x68\xde\xbd\x99\x7b\xb0\x5f\x55\x75\xc7\x3f\x6b\xef\xf3\xf8\x3d\xee\xcd\xcd\xbd\x24\x04\x12\xc2\x0d\x24\x24\x40\x54\x10\x82\x48\x40\x8c\x3c\x1a\xc1\x57\xcb\x8c\xd8\x8e\x56\xb1\x6a\x47\xc4\x57\xa1\x0e\x95\x69\xad\x45\x6a\xd5\xce\x50\xc5\x0a\x4c\x61\x46\x45\xab\xad\x45\xda\x29\x1d\x6d\xb4\xf1\x0f\xd4\x0a\x02\xa6\x3c\x42\x0c\x90\xc7\x0d\x49\x48\xb8\x79\xdd\xdc\xdf\xeb\x3c\xf6\x5e\xfd\x63\x9f\xdf\xbd\x37\x0f\x2c\x81\xb6\x67\xe6\xcc\x39\x73\xce\xde\xfb\xac\xef\x7a\x7c\xd7\x5a\xfb\xc0\xcb\x38\xde\xf9\xe1\xf3\xb8\xfe\xf3\x97\xcb\xe1\xcf\xef\xbe\xff\xa3\xe7\xf2\xff\x7c\x98\x97\x33\xe9\xf4\xe5\xc7\x9f\x76\xeb\x4d\x3f\xd6\x99\xcf\xee\xfc\x97\x3f\x34\xe0\xdf\x75\xf8\xd8\xb3\x2e\x58\xf4\x92\xd6\xfc\xe2\x5d\x57\xfd\xdf\x03\xb8\xe5\x8e\x77\xbc\xfa\x73\x77\xbe\xfd\xc2\xa7\x37\xec\xd9\xf4\x27\xb7\xbe\xf9\xd0\x97\x4e\x4f\x2c\x8b\xe2\x75\x33\x1f\x5d\x7c\xe5\x19\x3c\xf6\x8b\xad\x9c\x7f\xc9\xd2\x5b\x56\xbd\x75\xf9\xd6\x37\xbe\xfd\xcc\xe3\x01\x3e\xfd\x57\x57\x02\xf0\xf9\xdb\x7f\xc7\xdc\x72\xfb\x6f\x37\xbe\xf8\x77\x57\x99\x1b\x3f\x74\xdf\x51\xbf\x79\xdd\x9f\xad\xb2\xbf\x49\x26\x79\xa9\xc2\x7f\xe5\xdb\xef\xe2\xc0\xfe\xee\x95\xdb\x77\x1c\x58\x70\xd7\x17\x1e\xb8\xeb\x9a\x1b\x56\x32\x7f\xeb\x04\x9f\xff\xfe\x7a\x00\xbe\xfc\xed\xf7\xcd\x01\xc6\xbd\xf3\x0b\xaf\x7f\xdf\xb7\xb6\xf7\xe7\x5d\xf4\xe6\xd3\x3f\x10\x27\xe6\xee\xa4\x66\x37\xad\xf9\xde\x13\x4b\xfa\xcf\x6f\xfa\xc2\x95\x75\x1b\x9b\xd1\xcf\xdd\xf0\x6f\xbf\x3e\xda\xf7\x3e\x70\xc3\x1b\x68\x34\x92\x18\xc1\x7c\xf5\xe6\xb5\xd9\x2b\x06\xd0\x3f\x6e\xf8\xdc\xe5\xef\x9f\x68\xf5\xce\xb9\xfb\x8b\x3f\xfd\x18\xc0\x37\x41\x46\x41\x57\x01\xb7\x7c\xed\xea\xcd\xde\xf9\x9d\x9f\xf9\xf8\xbd\x17\x01\x5c\x79\xd5\xab\x86\x34\x36\x9b\xe3\xd4\x8e\x44\xc2\x79\xf7\x7d\x73\xdd\x23\x00\x9f\xf8\xf4\xa5\x57\xa7\xa9\xdd\xf3\xa5\xcf\xfe\xe8\x27\x3b\x41\x9e\x04\x3d\x01\x78\x4d\xf5\x8d\x3b\x2e\x3d\x95\x6b\xd7\x6e\xe6\x83\x9f\xba\x78\x71\x6c\xec\x58\x5c\x33\xe5\x6d\x7f\xb1\xf6\xe5\x03\x78\x16\x58\x02\xdc\xfe\xee\x73\xf9\xc8\xdf\x3f\xca\x87\x6f\x5c\xf5\xd5\xdc\x95\xe7\x0d\xb5\xf3\x2b\x6f\xbd\xe3\x97\xfb\x5f\x58\x7e\x72\x74\xfc\xfa\x6d\xee\xcf\xff\xf4\x8a\xd7\xb7\x2d\x3f\xf7\x5e\x9f\x59\xb0\xa3\x75\xfe\x83\xae\x7d\x95\x8f\xcc\xdd\xd6\xc8\xee\x8b\xc6\xf3\xa5\x1f\xff\xe1\xee\xce\x27\x7e\x7f\xf1\xf9\x79\x2d\xba\xf6\x8e\xbf\x7d\xe0\xbd\xe3\xe7\x2e\x4b\xf0\x28\xbe\x54\x2d\x32\xcf\xf8\x7e\x9d\x9c\x2c\x74\x49\x2f\xf7\xef\xbd\x6e\xe5\x1c\x63\xcd\xaa\x6f\xdc\xf6\xb3\x7b\x5f\x91\x0b\x3d\x0d\x2c\x05\x9e\x02\x99\x9d\x20\x66\xe9\x22\x99\xf7\x64\xce\x87\xaf\x3b\xe5\x33\x45\x5d\xae\x1f\x2c\xf4\xfa\x2f\x7f\x65\xc3\x77\x38\x7f\xc4\x72\xa0\xe7\x3f\xf0\xc6\xd1\x3b\x4a\x2f\xef\x51\xef\x77\x76\x6d\x9e\x02\xc7\x59\xe5\xf1\x9b\x9f\x9e\xbc\xec\x9e\x39\x03\xaf\xdf\xd4\x8c\xef\x3b\x2d\x2f\xdf\x72\xf3\xd8\xbe\x87\x11\x51\xac\xf7\xa8\x78\xd4\x78\x9e\xee\x79\xd9\xb3\xbd\xf7\xbb\xef\x7d\xdd\xb5\x02\xbb\xbe\x7b\xcf\x2f\xff\xd9\x01\x7b\x80\x79\xc7\x0a\x60\x0c\x58\x87\xe1\xf5\x78\xca\xe3\x1a\x26\x9e\x3b\x62\xa2\xfa\x40\x34\x62\x0b\xf3\x5c\xcf\xcc\x6e\x77\x38\xf5\xde\xd3\x9a\x5f\xda\xbc\xa4\x7e\x41\x33\x36\x0f\x5d\xf2\xd0\x44\xf7\xe4\xdd\x45\xbd\x1d\x61\x6f\x5b\x3e\xb0\xc2\xc5\x10\xcd\x16\x50\x18\xee\xfa\xbd\xef\x79\xb8\xb3\xee\x2b\x4b\xea\x17\x78\xd1\xe6\x35\x5b\x32\xd2\xd2\x33\x52\x82\xa8\xd2\xa9\xd9\xf1\x9d\x0d\xf3\xf8\xd8\x70\xf4\xd8\x84\x70\xe2\x85\xe3\xdd\x4f\xbe\x75\xdd\x96\x3d\xcf\xbf\xe6\x24\x6b\x77\xee\x76\xcf\xef\xe9\xf8\xb3\x81\xfd\xc0\xf0\xb1\x58\x60\x1b\x48\xed\xec\x53\x2c\x3e\x8e\x53\xa3\xf1\xbe\x82\x39\xbb\x7a\xe6\x26\x2f\xbc\x33\x86\xba\x15\xa2\x71\x8b\xdc\x73\x46\x9d\xc6\x69\x03\x0c\xf4\x3c\xbf\xf5\xab\x83\xfc\xa2\x9b\xf3\xc8\x29\x4d\x9a\x23\x36\x00\xe8\x79\x0e\x3e\x97\xf3\x42\x04\x83\x85\x52\x02\xef\x9e\x74\x14\x0b\xea\xac\x9b\x93\xb0\x6f\xbc\xc7\x3b\xc6\x32\x16\x64\x0a\x8e\xd2\x29\x7b\x53\xab\x1f\x5b\x31\xe2\x7e\x30\xde\xb5\x4e\x7d\xa7\x48\x9e\xdc\xee\x87\x41\x5f\x92\x05\x36\x01\xb5\xb9\x75\x13\x9f\x30\x3f\x9a\xbb\x70\x48\x1e\xdf\xd4\x3e\xbb\x5d\x72\x3d\xc2\xd5\xb1\x80\xb5\x50\xa0\x6c\x3a\xb1\xce\x9e\x0b\x47\x71\x93\x05\x3f\xd8\xb0\x03\xaf\x8a\x4b\x84\x46\xe6\x29\x13\x43\x63\x24\x22\xaa\x1b\xbc\x42\xb6\xcf\x91\xd6\x2c\xc9\x40\x00\x35\x99\x7b\x16\xef\xca\x58\xb5\x3b\x63\x45\xe6\x29\x10\x72\x85\x42\x21\xf7\x42\xee\xc1\x2b\x0f\xa7\x46\x6f\x5c\xb1\x32\x7b\xe0\x85\x47\x0d\xf1\xfa\x2d\x7e\x04\xf4\x39\x60\xe1\x8b\x01\xd8\x04\xd2\x9c\x9d\x4a\xf4\x9a\xb3\x63\xe6\xcf\x1f\xda\xbc\x61\xec\x8f\xcb\x4e\xf7\x53\x89\x78\x62\x03\xa9\x35\x48\x22\xfc\xe4\xcc\x59\x8c\xc4\x09\xe7\x3f\x33\xc1\x3f\xfa\x92\x35\x0d\xa1\x4f\xd8\x3a\xe3\x3a\x70\x42\x42\x3a\x68\xa9\xa5\x09\x07\x77\x75\x69\x8f\x67\x24\xc0\x15\x07\x4a\xce\x29\x94\x93\x44\xb0\x0a\x8a\xe2\x14\x4a\x85\xc2\x43\xee\xa1\xe7\x85\xdc\x81\x08\x37\x9e\xd6\x74\x5f\xc3\xb9\x2c\x5b\xbf\xc5\x2d\x9c\x61\x89\x29\x00\x7b\x80\x39\xc0\xce\xe1\xc4\xd4\xce\x59\x91\x74\x47\x97\x2c\xd8\xf1\xeb\xb1\xef\x49\xb7\x7d\x4e\xdc\x6b\x93\x88\x52\x4b\x0c\x71\x2d\xc6\x0d\x24\x34\xa2\x88\x24\xb6\x6c\x68\x75\x69\x95\x9e\x8e\x01\xa7\x50\xa8\x67\x20\x89\x88\x54\x69\xe7\x05\x59\x59\x82\x15\x4c\xcf\x31\x0b\x38\x4e\x84\x05\x6a\x49\xa3\x88\xae\x31\xf4\xbc\xc7\xbb\x92\x86\x73\x44\x4e\xf1\xaa\x53\x20\x32\x0f\x5d\x17\x80\x78\xe5\x6f\x2e\xde\xbc\xf1\xfa\x17\xce\x9c\x6f\xf7\x3f\xb5\xd3\x0d\x02\xf3\x81\x08\xe0\xd1\x4a\xf8\xf1\x54\x24\x7a\xd5\xab\xa3\xf6\xc9\x4b\x16\xee\x1a\xdb\xfd\xa3\xb4\x16\x2d\xb2\xed\x82\xd8\x28\xb5\x48\x88\x1b\x31\xf1\xec\x26\x76\x78\x00\x33\x50\x47\x93\x88\xe1\x76\x97\x7a\x5e\x52\x38\x8f\xa8\xb2\x70\x78\x90\xba\x2a\xae\xd5\x41\xf7\xb7\xd1\x89\x36\xda\xcd\xd0\x52\x41\x04\x92\x04\x33\x38\x08\x8d\x06\xcd\x24\xa1\xe9\x3c\xf4\xba\xb8\xc9\x49\xb4\xdb\xc1\x38\x87\xf1\x60\x6c\x28\x13\xa4\xb2\x65\xcf\xc9\x1f\xfd\x6c\xf1\xb2\x2d\x26\xd2\xbb\x66\xcd\xad\xeb\x83\xe3\x3d\x0f\x1a\x00\x9c\x0b\x6c\x04\xf1\xcb\x4e\xb5\x43\x97\x5c\xd2\x7c\xf6\xe7\x8f\xff\xb0\xd6\xac\x2f\xb2\xe3\xbb\x88\xcb\x8c\xc4\x28\x49\x64\x89\x6a\x31\x76\x56\x03\x3b\x32\x80\x0c\x35\x21\x8d\x39\x29\x1a\xe1\x60\xa7\x47\xb7\x93\xd3\xa8\x27\x34\x6a\x31\xda\x2b\x90\xd8\x42\xe9\xf1\xdd\x1c\xcd\x0a\x54\x14\xac\xc5\xd4\xeb\xc8\xf0\x30\x32\x34\x8c\x24\x09\x78\x87\xb6\x5b\x18\x55\x7c\x51\x80\xf7\x58\xf1\xa0\x20\x36\x38\x89\x56\xee\xd8\x75\xfc\x75\xcf\xf9\xef\xd6\x8e\x9f\x3b\xb1\x60\x7c\x9b\x02\x6a\x00\x7e\x88\x30\x30\xb7\x26\xb5\xa5\x67\xd4\xd6\x3d\xf8\xd4\x5f\xd6\x46\x86\x17\x47\x79\x46\xdc\x69\x91\xe2\x49\x8c\x62\xc4\x23\xde\x43\x51\xa0\x79\x81\xf6\x72\xc8\xc3\xfd\x60\x1c\x33\x54\x4f\x68\xc6\x16\xcd\x0a\xc8\x8a\xf0\xae\x70\xe0\x3d\x68\xe5\xb2\xaa\xe0\x3d\x5a\x3a\x70\x05\x94\x05\x94\x25\x5a\x3a\x54\x3d\x33\x49\xc6\x08\x58\x20\xb6\x90\x1a\x48\x8d\x12\x0b\xe9\xa6\xae\xfd\xce\x71\xcb\x22\x3f\xda\x40\x66\xf7\x2d\xf4\x04\xc8\x82\xb3\x17\x47\xf9\x05\x97\x8e\x3e\xbf\x73\xef\x33\x8d\x81\x3a\x66\xf3\x33\x24\x93\xfb\x89\x45\xb1\x22\x88\x15\x24\x89\x91\x66\x0d\x33\x58\x47\x1a\x29\x92\x44\x10\x9b\x60\x6f\xd1\x10\x04\xa5\x43\xb3\x12\xdf\xe9\xa1\xad\x2e\xda\xca\xd0\xbc\x0c\x40\x44\x90\x28\x42\x1a\x4d\xa4\xd1\x40\xe2\x18\xf5\x1e\xcd\x72\xb4\xdb\x41\x7b\x3d\x70\x65\x00\x5a\x81\x76\x0a\xb9\x0a\x5d\x07\xad\x12\xba\x4e\x7c\x62\x58\xb1\x28\xe9\x3d\xd1\x79\x6a\xac\x8c\x6e\x03\x86\x07\x90\xe1\x0f\xbd\x8b\x47\xee\x7d\xe4\xeb\x03\x27\x2f\xc0\xec\xdf\x43\xd4\x6d\x13\x09\x58\xa9\x8c\xe8\x09\x9a\x57\x8f\x66\x05\x12\x9b\xe0\x26\x91\x0d\xea\x12\xc0\x87\xe8\xd3\xa2\x44\x0b\x0f\x45\x19\xac\xa0\x0a\x62\x40\x40\xbd\x42\xa7\x83\xe6\x19\x12\xc5\x60\x4d\xa5\x78\x45\x22\x0b\x28\xea\xfa\xd6\x50\x0c\x1a\x2c\x61\xc2\x99\x2b\xa6\xf0\xac\xaa\xc5\xc9\xfa\x6d\x31\x12\x5d\x0a\xd8\x79\xf3\xcc\x53\xf7\xff\xea\x55\x76\x70\x60\x65\x64\x0d\x32\x71\x80\xc8\x15\xd8\x99\x34\xa5\x01\x04\x85\x47\x5d\x06\x85\x81\x58\x82\x8d\x8d\x09\x1f\xec\x03\x28\xc3\x89\x56\x0b\x88\x41\x4c\x7f\x5c\xdf\x3f\x22\x88\x63\x48\x12\xc4\x58\xc4\x3b\x34\xcf\x03\xb0\xbc\x40\xbd\x03\xaf\x88\x2a\x56\x15\xeb\x95\xb8\x72\x2b\x27\x5c\x36\x6b\x56\xfe\xd5\xfa\xec\x21\x89\xa2\x08\x89\x06\x87\x4d\xe6\xe5\xa2\x74\x68\xd0\x48\xd6\xc3\x76\x5a\x18\x14\x11\x3d\x32\xcb\xa9\x0f\x52\x59\x50\x6b\x90\x58\x90\x48\x40\x25\x08\xee\x05\xdc\x34\x4b\x8b\x11\x88\x62\x24\x4e\x20\x8a\x10\x63\x02\x1b\x45\x31\x34\xea\x98\x7a\x13\x92\x38\xb8\x5e\xb7\x8d\xb6\x5a\x68\xaf\x87\x14\x39\xea\x1c\x38\x1f\x98\x49\x3d\x46\x35\x18\xac\x64\x05\xc3\xc2\x70\x14\x49\x64\x63\x2b\x73\xce\x38\xc5\x3c\xb7\xdf\xbe\x31\xaa\xd5\x90\xf1\xe7\xb1\x65\x5e\x79\x85\x4c\x07\x96\x4c\x47\x97\x44\xc1\x9e\xa6\x6e\x91\x7a\x15\x07\x0a\xe4\x0e\x28\x50\x27\x88\x17\x54\x4d\x10\x34\xad\x05\xbf\x4f\x93\x90\xc6\xc5\x20\x49\x82\x0c\xce\x0a\x94\x9a\xa4\x50\x16\xf8\xc9\x49\x34\x9d\xc0\xb7\x26\x21\xcf\x2a\xc2\xc8\x91\x3c\xc7\x6a\x81\xf5\x0e\x83\x82\x70\x3c\x13\xc6\x26\x43\x49\x19\xc5\xd6\x08\xe7\x9e\x25\xfc\xe4\xa9\x73\x8d\x11\xa4\xd3\x01\xe7\x2a\xf2\x92\x43\xab\x0e\x21\x04\x73\x64\x20\xb5\x48\x33\x42\x9a\x31\x92\x58\xd4\x29\x74\xa5\x4a\xa5\x8a\x3a\x10\x0c\x92\xa4\x48\xb3\x89\x99\x35\x04\xf5\x06\x12\x47\x60\x0c\x52\xab\x23\x43\x43\x98\xa1\xd9\x48\x92\xa2\x65\x89\x1c\x9c\xc0\xa7\x29\xd2\xa8\xa3\xdd\x2e\xe4\x19\xda\xe9\xe0\xda\xed\x40\xab\x4e\x11\xe3\x31\xa5\xb2\x7e\x57\x7c\xd2\xe2\x86\xdf\x1a\xd5\xe6\x0c\xc0\xbc\x13\x2c\xb2\x61\x91\x38\x07\x59\x2f\xb8\xcf\x4c\xed\x4f\x61\x90\xe0\xbf\x91\x41\x52\x8b\xd4\x63\xa4\x19\x43\x62\x90\x22\x14\x2f\xf4\x4a\xd4\x84\x71\x82\x85\x24\x46\x1a\x0d\x18\x1c\xc4\x0c\x0c\x42\x1c\x57\x4c\xd4\x40\x66\x0f\x07\x00\x69\x0d\x2d\x0b\x7c\x9a\x62\xa2\x08\xad\xd5\xd0\x6e\x17\xed\x75\x21\x4e\x30\x08\xb6\xf4\x98\xdc\x05\xb7\x16\xa5\xe7\x19\xb6\xea\xc7\xa2\x7a\xa3\x2e\x3b\xfe\xe3\x91\xe3\xc5\x5a\x28\x4b\x28\xf2\xa9\xe0\x3b\x02\x82\x04\x10\x81\x52\x6d\x00\x51\xb3\x15\x95\x0a\xf4\x1c\xbe\x02\x29\xc6\xa0\x62\x31\x71\x8c\xa4\x35\x4c\xbd\x01\x8d\x3a\x92\xd4\x30\x49\x02\x8d\x66\x10\x7e\xd6\x10\x52\xaf\x23\x79\x0e\x22\x78\xf5\x61\xad\x34\x85\x76\x1c\x34\x5f\xe4\xd0\xed\x81\xcd\x10\x09\xc5\x91\xaa\x46\x46\x3d\x51\x1a\x45\x3a\xb1\xf7\xe0\x5c\x13\xa7\x68\x59\x4c\xb9\xcf\x51\x85\xef\x5f\x6d\xc5\xaf\x91\x40\x6c\x90\xc8\x86\xd8\xb6\x21\x68\xa7\xe6\x19\x83\x58\x8b\xc4\x31\x12\xc7\x10\xa7\x21\x0e\xd2\x5a\x10\xba\x56\x0b\x96\x48\x6b\x68\x14\x21\x45\x81\xa9\xd5\xf0\x79\x11\x58\xcf\x79\xc8\xf3\x30\x2f\x9a\xc1\x62\x80\x35\xb4\xbd\x58\x35\xd6\x1a\xf1\xae\xac\x85\xac\x51\x65\xce\x17\x2b\x55\x65\xe6\x29\xd3\x8c\x22\x87\x0f\x96\xca\xe3\x24\xf0\xbf\x39\xda\x69\x43\x40\x57\x6b\x48\xf5\x5c\x4d\x3f\xaf\x54\x56\xad\xd6\xf7\x48\xc8\x6f\xd5\xda\x23\x91\xdf\xdd\x2d\x55\x0c\x06\x35\x51\xd4\x53\xf5\xa8\x57\x54\xa7\xf5\x7e\x04\x89\xf6\x8b\x12\x1f\x92\x96\x7a\x5f\x5d\xab\x1c\xa0\x87\xce\xd4\x2a\xa3\xaa\xf7\xa8\x2a\xd2\x1f\xe7\x5d\xc8\xb8\x45\x28\x25\x70\x25\x5a\x84\xd2\x42\x8a\x02\xf2\xbc\xca\x09\x39\x5a\xe4\x68\x59\x82\x73\xa8\x06\xf9\xbc\xc2\xe8\x68\xb1\x3f\xcb\x32\x8d\x26\x5b\x6d\xe6\x2e\x3c\x61\xfb\xf6\xb1\xdd\x68\x62\x0f\x91\xd1\x1c\x92\xc8\x2a\xdd\x6a\xc8\xa6\x52\x7a\xb4\xf0\x48\xee\x10\x05\x5f\x56\x0a\xf0\xd3\xdd\x80\x54\xc2\x8b\x73\xa1\xe6\x29\xf2\xe0\x62\x55\x66\xf6\x69\x82\x89\x93\x00\x20\x2f\xd0\x76\x0b\xdf\x9a\x44\x5b\x93\x68\xbb\x0d\xdd\x0e\xda\x6e\xa3\xdd\x2e\x3e\x2f\x70\xce\xe1\x54\x91\xd0\xd3\xd0\x9d\xc8\x88\x5a\x7b\xbb\x9c\xf4\x96\x0b\xf6\x6d\xbb\xed\xde\xcc\xf9\x34\x15\x31\x95\x32\x15\x3d\xdc\x87\x7c\xf0\x4b\x29\x3c\x9a\x39\xe8\x96\x78\x2b\x48\xaa\x50\x7a\x28\x3c\xe2\x74\xda\x92\xce\x05\x2d\x67\x59\xa0\x45\x6b\x43\x01\x67\x2d\x9a\xe7\x18\x25\x58\x27\x4d\x21\x2f\xf0\x13\x07\xd0\xbd\x7b\xf1\x13\x07\xf0\x9d\x36\x74\xbb\x68\xbb\x8d\x6b\xb5\x70\x59\x86\x2f\x1d\x2e\x78\xf8\x63\x4c\x2a\xde\xab\x46\x9d\x89\x96\xf2\xc4\x13\x1e\xe7\x1f\xf5\xce\xaf\x74\x18\x4a\x95\x6a\xcb\x4e\x31\x32\x03\x85\x0a\xea\x41\x4a\x45\x72\x8f\x74\xca\xc0\xac\x55\xff\xe7\x7b\x65\xc8\xc6\xce\x87\x2a\x0c\x87\xe6\x19\xbe\xd3\x09\x81\xec\x1c\x12\x45\x28\x60\xe2\x18\xdf\xeb\xa2\xdd\x4e\x95\x07\x0a\x74\xe2\x00\x7e\xdf\x5e\x74\x62\x02\xed\x74\xf0\x79\x86\xf6\x32\x7c\x2f\xa3\xcc\x73\x4a\xe7\xf1\xde\x63\xe0\xa7\x93\xad\xc4\xec\x9e\x98\x28\xa3\x7d\x05\xda\x5e\xfb\xa0\xa2\x83\x3f\x2a\xb2\x6c\xa5\x8a\xc5\x68\x70\x1f\x45\xa6\xdd\xa8\x9f\xd7\x9c\x22\xa5\x43\x7a\xa1\xc8\x13\x07\x3e\xae\x8a\xaf\xdc\xa1\x3d\x87\x96\x5a\xd5\x42\x82\x98\x1c\x6d\xb7\x43\x9c\xf7\x7a\xa1\x26\xc2\xe3\x4d\x84\x69\xb5\x60\xb2\x85\x24\x31\x14\x21\x13\xfb\x89\x03\x68\x6b\x32\x58\xad\x28\x70\xa5\xc3\x15\x25\x65\xe1\x29\x4a\xa5\x54\x7c\x04\x0f\x75\xf3\xd2\x0d\x17\x68\x34\x0f\x74\x72\xeb\x33\x3e\x1a\x7d\xed\xda\x7c\xb2\xfd\x59\x05\xf0\x02\xa2\x81\xde\x75\x06\x83\x1a\x05\x27\x53\x5d\x92\x78\x17\xac\x61\x2b\x7a\x2b\xb5\xaa\x42\x83\x05\x14\x8f\x64\x05\xe2\x3b\x48\xe9\x42\x2d\xd4\xb7\xa8\x18\x24\x9d\xc4\xd4\x0e\x06\x1a\x76\x2e\x24\xaf\x4e\x3b\x94\xd5\x45\x81\xf7\x0e\xe7\x94\xdc\x29\x59\xa9\x14\x1e\xbc\x67\xcf\xeb\x8e\x73\x0f\xbd\xb0\x7d\x97\xdf\x0e\xc8\xfd\xc0\x59\x20\x0b\x55\xe5\xe7\xaf\x3d\xff\x09\xab\xee\xcc\xb8\xdb\x21\x15\x37\x95\x9f\xa6\x58\xb2\x4f\x9f\x80\xa9\x72\x81\x58\x99\xe6\xe7\x2a\x46\x70\x15\xdb\x54\x33\xc5\x9a\xd0\x8d\x19\x13\xe2\x4a\x41\x4c\xe8\x0d\x88\xe2\x90\xf4\x34\x34\x4b\xfd\xfa\x27\x30\x17\x94\x5e\xe9\x39\xe8\x39\xa5\x1b\x94\xf7\x7b\x2b\x17\xb4\xfe\x69\xec\xa7\x3b\x74\x11\xf8\xe8\x6d\xc0\x66\x40\x2f\x5c\x2e\x43\x83\xc7\xbd\x7d\xdf\xae\x7d\xcf\x7a\xaf\xa8\x11\x1c\x60\xd1\x29\x10\x7d\x2b\x08\x02\x1a\xdc\x47\x44\xc1\xf8\x69\x86\xd2\x50\x7a\x8b\x07\x24\xb8\x51\x18\xeb\xa7\xb5\x4f\xe8\x8f\x45\x0a\x30\x59\xb8\x57\x42\x7b\xe9\xdd\x94\xf0\xde\x2b\x85\x42\xcf\x43\xe6\x05\x07\xeb\xce\x1b\x2a\xff\x75\xf7\xd6\x8c\x6b\x02\x51\x22\x5b\x80\x53\x80\xed\x35\x64\xf6\xf2\x65\xd1\xc3\xfb\xe5\x56\x54\x3f\x1a\x87\x92\x07\x2b\x15\x9d\x4a\x00\x32\x6d\x88\xca\x95\x4c\xbf\x6a\x0d\x31\xa1\x3a\xdd\x88\x33\xc3\xfd\x10\x39\x2c\x39\x56\xc5\xa1\xc8\x94\x55\x50\x0d\xec\xe7\x15\xad\xba\xb1\xb0\xc5\x12\xf6\x8c\x2c\x5c\xb1\x74\x96\x5f\xdb\xfe\xaf\x67\xca\x53\x2b\x62\x97\xfe\x46\x56\x01\xcc\x1e\xb0\x66\xde\xaa\x45\xac\x7d\x32\x7a\xce\x08\xf3\xfb\xc2\xf7\x2d\x30\xe5\x4e\x87\xb9\xd5\x21\xd7\xbe\x82\x5f\x6c\xf7\xec\x45\xf6\x02\x75\x1a\x73\xc8\x75\x15\x00\xe7\xa1\x0c\xd3\x3e\x79\xf1\x65\x93\xb7\xbd\xf0\xf5\xe7\x65\x7d\xa1\x7e\x3e\x70\xfa\xe1\xcb\x1d\x00\x86\x46\x90\xff\x1c\x5e\x36\xdc\x29\x59\x2f\x86\x13\xcc\x74\x37\x18\xa8\x55\x42\x60\x73\x58\x05\x71\x48\x9c\x4c\x2b\xfc\x48\xb9\x95\x23\x32\xbd\xce\x00\xd0\x6f\x26\xfb\x89\xdd\x85\x41\xdf\x7a\xd3\x3c\xff\x07\x3b\xb6\xed\xf5\xed\x9d\xfb\xfc\xb2\x17\xd3\xc7\x58\xb5\x95\x7e\x16\xc8\x63\xa3\x4b\xe6\x3a\xb1\xcf\x09\x24\x1c\x5a\x02\xcd\x2c\x4c\x8f\xa8\xf3\x8e\x26\x7c\xff\x9d\xfe\x06\xad\xeb\x8c\xce\x55\xa7\x3d\x0a\xe0\x1b\x97\x6d\xdd\xf8\xfe\x5d\x16\x53\x73\xa1\x52\x1b\x79\xa9\x9b\xbb\x3f\x3e\x79\xe9\x5c\x2f\xb2\x16\x78\xf5\x4c\xa1\x84\xa3\xbb\x88\x1c\xe3\x6f\x13\x7d\x11\x6b\xcc\x48\x3b\xd7\x5d\xbe\x75\xe3\xed\xbf\x8c\x22\x29\xcb\x52\x57\xbe\xdc\x1f\x1c\x6b\x46\x97\xdd\x0d\x5c\x53\x15\xd2\x47\xcc\xfc\x9f\x16\x39\x9a\xf6\x8f\x1a\x03\xd3\x37\x5b\x54\x79\xdb\xea\x6d\x1b\xd7\xf7\x1f\x3f\x00\x5c\x7c\xac\x3f\xf9\xd6\x8c\x2e\x63\xcd\xe8\x32\x56\x8f\x6d\xfc\x20\xb0\x1c\x78\x76\xa6\x79\xfb\xa7\x3f\xca\x79\xf8\xfb\xdf\x34\xf6\x90\xf1\xf0\x6e\x0f\x67\xae\xde\xb6\x71\xfd\xbf\xbf\xed\xfd\xfc\xb8\xff\xc3\xf0\x95\xfc\x23\xab\x40\xf4\xef\xaf\x03\x3e\x02\x9c\xf9\xbf\xf8\xbb\x77\x03\xf0\x0f\xab\xc7\x36\xde\x7c\xf8\xf7\x78\x09\xd6\x3d\xe6\x63\xcd\xe8\x32\x53\xfd\x36\xbb\x13\x78\xd3\x2b\x10\xfc\xfb\xc0\x4d\xc0\xa6\xd5\x63\x1b\xdd\xb1\x0a\x4f\x7f\x77\xfa\x65\x1c\x7e\xf5\xd8\xc6\xa7\x81\x4b\xd6\x8c\x2e\x9b\x05\xbc\x01\x78\x6d\x45\xcd\x27\x03\x73\x81\x05\x40\x13\xd8\x05\x8c\x03\xbb\x81\x2d\x95\xb6\xd7\x01\xbf\x58\x3d\xb6\xb1\xe8\x0b\xdd\x3f\x8e\x45\x78\x80\xff\x06\x26\x17\xf9\x29\x2d\x26\x91\x99\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\x01\x00\x00\xff\xff\x8e\x30\x0d\x01\x5c\x0f\x00\x00")
1154+
1155+ func tomatoIconPngBytes() ([]byte, error) {
1156+ return bindataRead(
1157+ _tomatoIconPng,
1158+ "tomato-icon.png",
1159+ )
1160+ }
1161+
1162+ func tomatoIconPng() (*asset, error) {
1163+ bytes, err := tomatoIconPngBytes()
1164+ if err != nil {
1165+ return nil, err
1166+ }
1167+
1168+ info := bindataFileInfo{name: "tomato-icon.png", size: 3932, mode: os.FileMode(420), modTime: time.Unix(1516522577, 0)}
1169+ a := &asset{bytes: bytes, info: info}
1170+ return a, nil
1171+ }
1172+
1173+ // Asset loads and returns the asset for the given name.
1174+ // It returns an error if the asset could not be found or
1175+ // could not be loaded.
1176+ func Asset(name string) ([]byte, error) {
1177+ cannonicalName := strings.Replace(name, "\\", "/", -1)
1178+ if f, ok := _bindata[cannonicalName]; ok {
1179+ a, err := f()
1180+ if err != nil {
1181+ return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err)
1182+ }
1183+ return a.bytes, nil
1184+ }
1185+ return nil, fmt.Errorf("Asset %s not found", name)
1186+ }
1187+
1188+ // MustAsset is like Asset but panics when Asset would return an error.
1189+ // It simplifies safe initialization of global variables.
1190+ func MustAsset(name string) []byte {
1191+ a, err := Asset(name)
1192+ if err != nil {
1193+ panic("asset: Asset(" + name + "): " + err.Error())
1194+ }
1195+
1196+ return a
1197+ }
1198+
1199+ // AssetInfo loads and returns the asset info for the given name.
1200+ // It returns an error if the asset could not be found or
1201+ // could not be loaded.
1202+ func AssetInfo(name string) (os.FileInfo, error) {
1203+ cannonicalName := strings.Replace(name, "\\", "/", -1)
1204+ if f, ok := _bindata[cannonicalName]; ok {
1205+ a, err := f()
1206+ if err != nil {
1207+ return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err)
1208+ }
1209+ return a.info, nil
1210+ }
1211+ return nil, fmt.Errorf("AssetInfo %s not found", name)
1212+ }
1213+
1214+ // AssetNames returns the names of the assets.
1215+ func AssetNames() []string {
1216+ names := make([]string, 0, len(_bindata))
1217+ for name := range _bindata {
1218+ names = append(names, name)
1219+ }
1220+ return names
1221+ }
1222+
1223+ // _bindata is a table, holding each asset generator, mapped to its name.
1224+ var _bindata = map[string]func() (*asset, error){
1225+ "tomato-icon.png": tomatoIconPng,
1226+ }
1227+
1228+ // AssetDir returns the file names below a certain
1229+ // directory embedded in the file by go-bindata.
1230+ // For example if you run go-bindata on data/... and data contains the
1231+ // following hierarchy:
1232+ // data/
1233+ // foo.txt
1234+ // img/
1235+ // a.png
1236+ // b.png
1237+ // then AssetDir("data") would return []string{"foo.txt", "img"}
1238+ // AssetDir("data/img") would return []string{"a.png", "b.png"}
1239+ // AssetDir("foo.txt") and AssetDir("notexist") would return an error
1240+ // AssetDir("") will return []string{"data"}.
1241+ func AssetDir(name string) ([]string, error) {
1242+ node := _bintree
1243+ if len(name) != 0 {
1244+ cannonicalName := strings.Replace(name, "\\", "/", -1)
1245+ pathList := strings.Split(cannonicalName, "/")
1246+ for _, p := range pathList {
1247+ node = node.Children[p]
1248+ if node == nil {
1249+ return nil, fmt.Errorf("Asset %s not found", name)
1250+ }
1251+ }
1252+ }
1253+ if node.Func != nil {
1254+ return nil, fmt.Errorf("Asset %s not found", name)
1255+ }
1256+ rv := make([]string, 0, len(node.Children))
1257+ for childName := range node.Children {
1258+ rv = append(rv, childName)
1259+ }
1260+ return rv, nil
1261+ }
1262+
1263+ type bintree struct {
1264+ Func func() (*asset, error)
1265+ Children map[string]*bintree
1266+ }
1267+ var _bintree = &bintree{nil, map[string]*bintree{
1268+ "tomato-icon.png": &bintree{tomatoIconPng, map[string]*bintree{}},
1269+ }}
1270+
1271+ // RestoreAsset restores an asset under the given directory
1272+ func RestoreAsset(dir, name string) error {
1273+ data, err := Asset(name)
1274+ if err != nil {
1275+ return err
1276+ }
1277+ info, err := AssetInfo(name)
1278+ if err != nil {
1279+ return err
1280+ }
1281+ err = os.MkdirAll(_filePath(dir, filepath.Dir(name)), os.FileMode(0755))
1282+ if err != nil {
1283+ return err
1284+ }
1285+ err = ioutil.WriteFile(_filePath(dir, name), data, info.Mode())
1286+ if err != nil {
1287+ return err
1288+ }
1289+ err = os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime())
1290+ if err != nil {
1291+ return err
1292+ }
1293+ return nil
1294+ }
1295+
1296+ // RestoreAssets restores an asset under the given directory recursively
1297+ func RestoreAssets(dir, name string) error {
1298+ children, err := AssetDir(name)
1299+ // File
1300+ if err != nil {
1301+ return RestoreAsset(dir, name)
1302+ }
1303+ // Dir
1304+ for _, child := range children {
1305+ err = RestoreAssets(dir, filepath.Join(name, child))
1306+ if err != nil {
1307+ return err
1308+ }
1309+ }
1310+ return nil
1311+ }
1312+
1313+ func _filePath(dir, name string) string {
1314+ cannonicalName := strings.Replace(name, "\\", "/", -1)
1315+ return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...)
1316+ }
1317+
1318 diff --git a/pkg/internal/config.go b/pkg/internal/config.go
1319new file mode 100644
1320index 0000000..e5090f1
1321--- /dev/null
1322+++ b/pkg/internal/config.go
1323 @@ -0,0 +1,114 @@
1324+ package pomo
1325+
1326+ import (
1327+ "encoding/json"
1328+ "io/ioutil"
1329+ "os"
1330+ "path"
1331+
1332+ "github.com/fatih/color"
1333+ )
1334+
1335+ const (
1336+ defaultDateTimeFmt = "2006-01-02 15:04"
1337+ )
1338+
1339+ // Config represents user preferences
1340+ type Config struct {
1341+ Colors *ColorMap `json:"colors"`
1342+ DateTimeFmt string `json:"dateTimeFmt"`
1343+ BasePath string `json:"basePath"`
1344+ DBPath string `json:"dbPath"`
1345+ SocketPath string `json:"socketPath"`
1346+ IconPath string `json:"iconPath"`
1347+ }
1348+
1349+ type ColorMap struct {
1350+ colors map[string]*color.Color
1351+ tags map[string]string
1352+ }
1353+
1354+ func (c *ColorMap) Get(name string) *color.Color {
1355+ if color, ok := c.colors[name]; ok {
1356+ return color
1357+ }
1358+ return nil
1359+ }
1360+
1361+ func (c *ColorMap) MarshalJSON() ([]byte, error) {
1362+ return json.Marshal(c.tags)
1363+ }
1364+
1365+ func (c *ColorMap) UnmarshalJSON(raw []byte) error {
1366+ lookup := map[string]*color.Color{
1367+ "black": color.New(color.FgBlack),
1368+ "hiblack": color.New(color.FgHiBlack),
1369+ "blue": color.New(color.FgBlue),
1370+ "hiblue": color.New(color.FgHiBlue),
1371+ "cyan": color.New(color.FgCyan),
1372+ "hicyan": color.New(color.FgHiCyan),
1373+ "green": color.New(color.FgGreen),
1374+ "higreen": color.New(color.FgHiGreen),
1375+ "magenta": color.New(color.FgMagenta),
1376+ "himagenta": color.New(color.FgHiMagenta),
1377+ "red": color.New(color.FgRed),
1378+ "hired": color.New(color.FgHiRed),
1379+ "white": color.New(color.FgWhite),
1380+ "hiwrite": color.New(color.FgHiWhite),
1381+ "yellow": color.New(color.FgYellow),
1382+ "hiyellow": color.New(color.FgHiYellow),
1383+ }
1384+ cm := &ColorMap{
1385+ colors: map[string]*color.Color{},
1386+ tags: map[string]string{},
1387+ }
1388+ err := json.Unmarshal(raw, &cm.tags)
1389+ if err != nil {
1390+ return err
1391+ }
1392+ for tag, colorName := range cm.tags {
1393+ if color, ok := lookup[colorName]; ok {
1394+ cm.colors[tag] = color
1395+ }
1396+ }
1397+ *c = *cm
1398+ return nil
1399+ }
1400+
1401+ func LoadConfig(configPath string, config *Config) error {
1402+ raw, err := ioutil.ReadFile(configPath)
1403+ if err != nil {
1404+ os.MkdirAll(path.Dir(configPath), 0755)
1405+ // Create an empty config file
1406+ // if it does not already exist.
1407+ if os.IsNotExist(err) {
1408+ raw, _ := json.Marshal(map[string]string{})
1409+ err := ioutil.WriteFile(configPath, raw, 0644)
1410+ if err != nil {
1411+ return err
1412+ }
1413+ return LoadConfig(configPath, config)
1414+ }
1415+ return err
1416+ }
1417+ err = json.Unmarshal(raw, config)
1418+ if err != nil {
1419+ return err
1420+ }
1421+ if config.DateTimeFmt == "" {
1422+ config.DateTimeFmt = defaultDateTimeFmt
1423+ }
1424+ if config.BasePath == "" {
1425+ config.BasePath = path.Dir(configPath)
1426+ }
1427+ if config.DBPath == "" {
1428+ config.DBPath = path.Join(config.BasePath, "/pomo.db")
1429+ }
1430+ if config.SocketPath == "" {
1431+ config.SocketPath = path.Join(config.BasePath, "/pomo.sock")
1432+ }
1433+ if config.IconPath == "" {
1434+ config.IconPath = path.Join(config.BasePath, "/icon.png")
1435+ }
1436+ return nil
1437+ }
1438 diff --git a/pkg/internal/runner.go b/pkg/internal/runner.go
1439new file mode 100644
1440index 0000000..9325c74
1441--- /dev/null
1442+++ b/pkg/internal/runner.go
1443 @@ -0,0 +1,151 @@
1444+ package pomo
1445+
1446+ import (
1447+ "database/sql"
1448+ "time"
1449+ )
1450+
1451+ type TaskRunner struct {
1452+ count int
1453+ taskID int
1454+ taskMessage string
1455+ nPomodoros int
1456+ origDuration time.Duration
1457+ state State
1458+ store *Store
1459+ started time.Time
1460+ pause chan bool
1461+ toggle chan bool
1462+ notifier Notifier
1463+ duration time.Duration
1464+ }
1465+
1466+ func NewMockedTaskRunner(task *Task, store *Store, notifier Notifier) (*TaskRunner, error) {
1467+ tr := &TaskRunner{
1468+ taskID: task.ID,
1469+ taskMessage: task.Message,
1470+ nPomodoros: task.NPomodoros,
1471+ origDuration: task.Duration,
1472+ store: store,
1473+ state: State(0),
1474+ pause: make(chan bool),
1475+ toggle: make(chan bool),
1476+ notifier: notifier,
1477+ duration: task.Duration,
1478+ }
1479+ return tr, nil
1480+ }
1481+ func NewTaskRunner(task *Task, config *Config) (*TaskRunner, error) {
1482+ store, err := NewStore(config.DBPath)
1483+ if err != nil {
1484+ return nil, err
1485+ }
1486+ tr := &TaskRunner{
1487+ taskID: task.ID,
1488+ taskMessage: task.Message,
1489+ nPomodoros: task.NPomodoros,
1490+ origDuration: task.Duration,
1491+ store: store,
1492+ state: State(0),
1493+ pause: make(chan bool),
1494+ toggle: make(chan bool),
1495+ notifier: NewXnotifier(config.IconPath),
1496+ duration: task.Duration,
1497+ }
1498+ return tr, nil
1499+ }
1500+
1501+ func (t *TaskRunner) Start() {
1502+ go t.run()
1503+ }
1504+
1505+ func (t *TaskRunner) TimeRemaining() time.Duration {
1506+ return (t.duration - time.Since(t.started)).Truncate(time.Second)
1507+ }
1508+
1509+ func (t *TaskRunner) SetState(state State) {
1510+ t.state = state
1511+ }
1512+
1513+ func (t *TaskRunner) run() error {
1514+ for t.count < t.nPomodoros {
1515+ // Create a new pomodoro where we
1516+ // track the start / end time of
1517+ // of this session.
1518+ pomodoro := &Pomodoro{}
1519+ // Start this pomodoro
1520+ pomodoro.Start = time.Now()
1521+ // Set state to RUNNIN
1522+ t.SetState(RUNNING)
1523+ // Create a new timer
1524+ timer := time.NewTimer(t.duration)
1525+ // Record our started time
1526+ t.started = pomodoro.Start
1527+ loop:
1528+ select {
1529+ case <-timer.C:
1530+ t.SetState(BREAKING)
1531+ t.count++
1532+ case <-t.toggle:
1533+ // Catch any toggles when we
1534+ // are not expecting them
1535+ goto loop
1536+ case <-t.pause:
1537+ timer.Stop()
1538+ // Record the remaining time of the current pomodoro
1539+ remaining := t.TimeRemaining()
1540+ // Change state to PAUSED
1541+ t.SetState(PAUSED)
1542+ // Wait for the user to press [p]
1543+ <-t.pause
1544+ // Resume the timer with previous
1545+ // remaining time
1546+ timer.Reset(remaining)
1547+ // Change duration
1548+ t.started = time.Now()
1549+ t.duration = remaining
1550+ // Restore state to RUNNING
1551+ t.SetState(RUNNING)
1552+ goto loop
1553+ }
1554+ pomodoro.End = time.Now()
1555+ err := t.store.With(func(tx *sql.Tx) error {
1556+ return t.store.CreatePomodoro(tx, t.taskID, *pomodoro)
1557+ })
1558+ if err != nil {
1559+ return err
1560+ }
1561+ // All pomodoros completed
1562+ if t.count == t.nPomodoros {
1563+ break
1564+ }
1565+
1566+ t.notifier.Notify("Pomo", "It is time to take a break!")
1567+ // Reset the duration incase it
1568+ // was paused.
1569+ t.duration = t.origDuration
1570+ // User concludes the break
1571+ <-t.toggle
1572+
1573+ }
1574+ t.notifier.Notify("Pomo", "Pomo session has completed!")
1575+ t.SetState(COMPLETE)
1576+ return nil
1577+ }
1578+
1579+ func (t *TaskRunner) Toggle() {
1580+ t.toggle <- true
1581+ }
1582+
1583+ func (t *TaskRunner) Pause() {
1584+ t.pause <- true
1585+ }
1586+
1587+ func (t *TaskRunner) Status() *Status {
1588+ return &Status{
1589+ State: t.state,
1590+ Count: t.count,
1591+ NPomodoros: t.nPomodoros,
1592+ Remaining: t.TimeRemaining(),
1593+ }
1594+ }
1595 diff --git a/pkg/internal/runner_test.go b/pkg/internal/runner_test.go
1596new file mode 100644
1597index 0000000..54a316e
1598--- /dev/null
1599+++ b/pkg/internal/runner_test.go
1600 @@ -0,0 +1,37 @@
1601+ package pomo
1602+
1603+ import (
1604+ "fmt"
1605+ "io/ioutil"
1606+ "path"
1607+ "testing"
1608+ "time"
1609+ )
1610+
1611+ func TestTaskRunner(t *testing.T) {
1612+ baseDir, _ := ioutil.TempDir("/tmp", "")
1613+ store, err := NewStore(path.Join(baseDir, "pomo.db"))
1614+ if err != nil {
1615+ t.Error(err)
1616+ }
1617+ err = InitDB(store)
1618+ if err != nil {
1619+ t.Error(err)
1620+ }
1621+ runner, err := NewMockedTaskRunner(&Task{
1622+ Duration: time.Second * 2,
1623+ NPomodoros: 2,
1624+ Message: fmt.Sprint("Test Task"),
1625+ }, store, NoopNotifier{})
1626+ if err != nil {
1627+ t.Error(err)
1628+ }
1629+
1630+ runner.Start()
1631+
1632+ runner.Toggle()
1633+ runner.Toggle()
1634+
1635+ runner.Toggle()
1636+ runner.Toggle()
1637+ }
1638 diff --git a/pkg/internal/server.go b/pkg/internal/server.go
1639new file mode 100644
1640index 0000000..38357c7
1641--- /dev/null
1642+++ b/pkg/internal/server.go
1643 @@ -0,0 +1,93 @@
1644+ package pomo
1645+
1646+ import (
1647+ "encoding/json"
1648+ "errors"
1649+ "fmt"
1650+ "net"
1651+ "os"
1652+ )
1653+
1654+ // Server listens on a Unix domain socket
1655+ // for Pomo status requests
1656+ type Server struct {
1657+ listener net.Listener
1658+ runner *TaskRunner
1659+ running bool
1660+ }
1661+
1662+ func (s *Server) listen() {
1663+ for s.running {
1664+ conn, err := s.listener.Accept()
1665+ if err != nil {
1666+ break
1667+ }
1668+ buf := make([]byte, 512)
1669+ // Ignore any content
1670+ conn.Read(buf)
1671+ raw, _ := json.Marshal(s.runner.Status())
1672+ conn.Write(raw)
1673+ conn.Close()
1674+ }
1675+ }
1676+
1677+ func (s *Server) Start() {
1678+ s.running = true
1679+ go s.listen()
1680+ }
1681+
1682+ func (s *Server) Stop() {
1683+ s.running = false
1684+ s.listener.Close()
1685+ }
1686+
1687+ func NewServer(runner *TaskRunner, config *Config) (*Server, error) {
1688+ //check if socket file exists
1689+ if _, err := os.Stat(config.SocketPath); err == nil {
1690+ _, err := net.Dial("unix", config.SocketPath)
1691+ //if error then sock file was saved after crash
1692+ if err != nil {
1693+ os.Remove(config.SocketPath)
1694+ } else {
1695+ // another instance of pomo is running
1696+ return nil, errors.New(fmt.Sprintf("Socket %s is already in use", config.SocketPath))
1697+ }
1698+ }
1699+ listener, err := net.Listen("unix", config.SocketPath)
1700+ if err != nil {
1701+ return nil, err
1702+ }
1703+ return &Server{listener: listener, runner: runner}, nil
1704+ }
1705+
1706+ // Client makes requests to a listening
1707+ // pomo server to check the status of
1708+ // any currently running task session.
1709+ type Client struct {
1710+ conn net.Conn
1711+ }
1712+
1713+ func (c Client) read(statusCh chan *Status) {
1714+ buf := make([]byte, 512)
1715+ n, _ := c.conn.Read(buf)
1716+ status := &Status{}
1717+ json.Unmarshal(buf[0:n], status)
1718+ statusCh <- status
1719+ }
1720+
1721+ func (c Client) Status() (*Status, error) {
1722+ statusCh := make(chan *Status)
1723+ c.conn.Write([]byte("status"))
1724+ go c.read(statusCh)
1725+ return <-statusCh, nil
1726+ }
1727+
1728+ func (c Client) Close() error { return c.conn.Close() }
1729+
1730+ func NewClient(path string) (*Client, error) {
1731+ conn, err := net.Dial("unix", path)
1732+ if err != nil {
1733+ return nil, err
1734+ }
1735+ return &Client{conn: conn}, nil
1736+ }
1737 diff --git a/pkg/internal/store.go b/pkg/internal/store.go
1738new file mode 100644
1739index 0000000..76a0906
1740--- /dev/null
1741+++ b/pkg/internal/store.go
1742 @@ -0,0 +1,187 @@
1743+ package pomo
1744+
1745+ import (
1746+ "database/sql"
1747+ "strings"
1748+ "time"
1749+
1750+ _ "github.com/mattn/go-sqlite3"
1751+ )
1752+
1753+ // 2018-01-16 19:05:21.752851759+08:00
1754+ const datetimeFmt = "2006-01-02 15:04:05.999999999-07:00"
1755+
1756+ type StoreFunc func(tx *sql.Tx) error
1757+
1758+ type Store struct {
1759+ db *sql.DB
1760+ }
1761+
1762+ func NewStore(path string) (*Store, error) {
1763+ db, err := sql.Open("sqlite3", path)
1764+ if err != nil {
1765+ return nil, err
1766+ }
1767+ return &Store{db: db}, nil
1768+ }
1769+
1770+ // With applies all of the given functions with
1771+ // a single transaction, rolling back on failure
1772+ // and commiting on success.
1773+ func (s Store) With(fns ...func(tx *sql.Tx) error) error {
1774+ tx, err := s.db.Begin()
1775+ if err != nil {
1776+ return err
1777+ }
1778+ for _, fn := range fns {
1779+ err = fn(tx)
1780+ if err != nil {
1781+ tx.Rollback()
1782+ return err
1783+ }
1784+ }
1785+ return tx.Commit()
1786+ }
1787+
1788+ func (s Store) CreateTask(tx *sql.Tx, task Task) (int, error) {
1789+ var taskID int
1790+ _, err := tx.Exec(
1791+ "INSERT INTO task (message,pomodoros,duration,tags) VALUES ($1,$2,$3,$4)",
1792+ task.Message, task.NPomodoros, task.Duration.String(), strings.Join(task.Tags, ","))
1793+ if err != nil {
1794+ return -1, err
1795+ }
1796+ err = tx.QueryRow("SELECT last_insert_rowid() FROM task").Scan(&taskID)
1797+ if err != nil {
1798+ return -1, err
1799+ }
1800+ err = tx.QueryRow("SELECT last_insert_rowid() FROM task").Scan(&taskID)
1801+ if err != nil {
1802+ return -1, err
1803+ }
1804+ return taskID, nil
1805+ }
1806+
1807+ func (s Store) ReadTasks(tx *sql.Tx) ([]*Task, error) {
1808+ rows, err := tx.Query(`SELECT rowid,message,pomodoros,duration,tags FROM task`)
1809+ if err != nil {
1810+ return nil, err
1811+ }
1812+ tasks := []*Task{}
1813+ for rows.Next() {
1814+ var (
1815+ tags string
1816+ strDuration string
1817+ )
1818+ task := &Task{Pomodoros: []*Pomodoro{}}
1819+ err = rows.Scan(&task.ID, &task.Message, &task.NPomodoros, &strDuration, &tags)
1820+ if err != nil {
1821+ return nil, err
1822+ }
1823+ duration, _ := time.ParseDuration(strDuration)
1824+ task.Duration = duration
1825+ if tags != "" {
1826+ task.Tags = strings.Split(tags, ",")
1827+ }
1828+ pomodoros, err := s.ReadPomodoros(tx, task.ID)
1829+ if err != nil {
1830+ return nil, err
1831+ }
1832+ for _, pomodoro := range pomodoros {
1833+ task.Pomodoros = append(task.Pomodoros, pomodoro)
1834+ }
1835+ tasks = append(tasks, task)
1836+ }
1837+ return tasks, nil
1838+ }
1839+
1840+ func (s Store) DeleteTask(tx *sql.Tx, taskID int) error {
1841+ _, err := tx.Exec("DELETE FROM task WHERE rowid = $1", &taskID)
1842+ if err != nil {
1843+ return err
1844+ }
1845+ _, err = tx.Exec("DELETE FROM pomodoro WHERE task_id = $1", &taskID)
1846+ if err != nil {
1847+ return err
1848+ }
1849+ return nil
1850+ }
1851+
1852+ func (s Store) ReadTask(tx *sql.Tx, taskID int) (*Task, error) {
1853+ task := &Task{}
1854+ var (
1855+ tags string
1856+ strDuration string
1857+ )
1858+ err := tx.QueryRow(`SELECT rowid,message,pomodoros,duration,tags FROM task WHERE rowid = $1`, &taskID).
1859+ Scan(&task.ID, &task.Message, &task.NPomodoros, &strDuration, &tags)
1860+ if err != nil {
1861+ return nil, err
1862+ }
1863+ duration, _ := time.ParseDuration(strDuration)
1864+ task.Duration = duration
1865+ if tags != "" {
1866+ task.Tags = strings.Split(tags, ",")
1867+ }
1868+ return task, nil
1869+ }
1870+
1871+ func (s Store) CreatePomodoro(tx *sql.Tx, taskID int, pomodoro Pomodoro) error {
1872+ _, err := tx.Exec(
1873+ `INSERT INTO pomodoro (task_id, start, end) VALUES ($1, $2, $3)`,
1874+ taskID,
1875+ pomodoro.Start,
1876+ pomodoro.End,
1877+ )
1878+ return err
1879+ }
1880+
1881+ func (s Store) ReadPomodoros(tx *sql.Tx, taskID int) ([]*Pomodoro, error) {
1882+ rows, err := tx.Query(`SELECT start,end FROM pomodoro WHERE task_id = $1`, &taskID)
1883+ if err != nil {
1884+ return nil, err
1885+ }
1886+ pomodoros := []*Pomodoro{}
1887+ for rows.Next() {
1888+ var (
1889+ startStr string
1890+ endStr string
1891+ )
1892+ pomodoro := &Pomodoro{}
1893+ err = rows.Scan(&startStr, &endStr)
1894+ if err != nil {
1895+ return nil, err
1896+ }
1897+ start, _ := time.Parse(datetimeFmt, startStr)
1898+ end, _ := time.Parse(datetimeFmt, endStr)
1899+ pomodoro.Start = start
1900+ pomodoro.End = end
1901+ pomodoros = append(pomodoros, pomodoro)
1902+ }
1903+ return pomodoros, nil
1904+ }
1905+
1906+ func (s Store) DeletePomodoros(tx *sql.Tx, taskID int) error {
1907+ _, err := tx.Exec("DELETE FROM pomodoro WHERE task_id = $1", &taskID)
1908+ return err
1909+ }
1910+
1911+ func (s Store) Close() error { return s.db.Close() }
1912+
1913+ func InitDB(db *Store) error {
1914+ stmt := `
1915+ CREATE TABLE task (
1916+ message TEXT,
1917+ pomodoros INTEGER,
1918+ duration TEXT,
1919+ tags TEXT
1920+ );
1921+ CREATE TABLE pomodoro (
1922+ task_id INTEGER,
1923+ start DATETTIME,
1924+ end DATETTIME
1925+ );
1926+ `
1927+ _, err := db.db.Exec(stmt)
1928+ return err
1929+ }
1930 diff --git a/pkg/internal/types.go b/pkg/internal/types.go
1931new file mode 100644
1932index 0000000..d52c2e6
1933--- /dev/null
1934+++ b/pkg/internal/types.go
1935 @@ -0,0 +1,144 @@
1936+ package pomo
1937+
1938+ import (
1939+ "io/ioutil"
1940+ "os"
1941+ "time"
1942+
1943+ "github.com/0xAX/notificator"
1944+ )
1945+
1946+ type State int
1947+
1948+ func (s State) String() string {
1949+ switch s {
1950+ case RUNNING:
1951+ return "RUNNING"
1952+ case BREAKING:
1953+ return "BREAKING"
1954+ case COMPLETE:
1955+ return "COMPLETE"
1956+ case PAUSED:
1957+ return "PAUSED"
1958+ }
1959+ return ""
1960+ }
1961+
1962+ const (
1963+ RUNNING State = iota + 1
1964+ BREAKING
1965+ COMPLETE
1966+ PAUSED
1967+ )
1968+
1969+ // Wheel keeps track of an ASCII spinner
1970+ type Wheel int
1971+
1972+ func (w *Wheel) String() string {
1973+ switch int(*w) {
1974+ case 0:
1975+ *w++
1976+ return "|"
1977+ case 1:
1978+ *w++
1979+ return "/"
1980+ case 2:
1981+ *w++
1982+ return "-"
1983+ case 3:
1984+ *w = 0
1985+ return "\\"
1986+ }
1987+ return ""
1988+ }
1989+
1990+ // Task describes some activity
1991+ type Task struct {
1992+ ID int `json:"id"`
1993+ Message string `json:"message"`
1994+ // Array of completed pomodoros
1995+ Pomodoros []*Pomodoro `json:"pomodoros"`
1996+ // Free-form tags associated with this task
1997+ Tags []string `json:"tags"`
1998+ // Number of pomodoros for this task
1999+ NPomodoros int `json:"n_pomodoros"`
2000+ // Duration of each pomodoro
2001+ Duration time.Duration `json:"duration"`
2002+ }
2003+
2004+ // ByID is a sortable array of tasks
2005+ type ByID []*Task
2006+
2007+ func (b ByID) Len() int { return len(b) }
2008+ func (b ByID) Swap(i, j int) { b[i], b[j] = b[j], b[i] }
2009+ func (b ByID) Less(i, j int) bool { return b[i].ID < b[j].ID }
2010+
2011+ // After returns tasks that were started after the
2012+ // provided start time.
2013+ func After(start time.Time, tasks []*Task) []*Task {
2014+ filtered := []*Task{}
2015+ for _, task := range tasks {
2016+ if len(task.Pomodoros) > 0 {
2017+ if start.Before(task.Pomodoros[0].Start) {
2018+ filtered = append(filtered, task)
2019+ }
2020+ }
2021+ }
2022+ return filtered
2023+ }
2024+
2025+ // Pomodoro is a unit of time to spend working
2026+ // on a single task.
2027+ type Pomodoro struct {
2028+ Start time.Time `json:"start"`
2029+ End time.Time `json:"end"`
2030+ }
2031+
2032+ // Duration returns the runtime of the pomodoro
2033+ func (p Pomodoro) Duration() time.Duration {
2034+ return (p.End.Sub(p.Start))
2035+ }
2036+
2037+ // Status is used to communicate the state
2038+ // of a running Pomodoro session
2039+ type Status struct {
2040+ State State `json:"state"`
2041+ Remaining time.Duration `json:"remaining"`
2042+ Count int `json:"count"`
2043+ NPomodoros int `json:"n_pomodoros"`
2044+ }
2045+
2046+ // Notifier sends a system notification
2047+ type Notifier interface {
2048+ Notify(string, string) error
2049+ }
2050+
2051+ // NoopNotifier does nothing
2052+ type NoopNotifier struct{}
2053+
2054+ func (n NoopNotifier) Notify(string, string) error { return nil }
2055+
2056+ // Xnotifier can push notifications to mac, linux and windows.
2057+ type Xnotifier struct {
2058+ *notificator.Notificator
2059+ iconPath string
2060+ }
2061+
2062+ func NewXnotifier(iconPath string) Notifier {
2063+ // Write the built-in tomato icon if it
2064+ // doesn't already exist.
2065+ _, err := os.Stat(iconPath)
2066+ if os.IsNotExist(err) {
2067+ raw := MustAsset("tomato-icon.png")
2068+ _ = ioutil.WriteFile(iconPath, raw, 0644)
2069+ }
2070+ return Xnotifier{
2071+ Notificator: notificator.New(notificator.Options{}),
2072+ iconPath: iconPath,
2073+ }
2074+ }
2075+
2076+ // Notify sends a notification to the OS.
2077+ func (n Xnotifier) Notify(title, body string) error {
2078+ return n.Push(title, body, n.iconPath, notificator.UR_NORMAL)
2079+ }
2080 diff --git a/pkg/internal/ui.go b/pkg/internal/ui.go
2081new file mode 100644
2082index 0000000..4aa5f99
2083--- /dev/null
2084+++ b/pkg/internal/ui.go
2085 @@ -0,0 +1,123 @@
2086+ package pomo
2087+
2088+ import (
2089+ "fmt"
2090+
2091+ "github.com/gizak/termui"
2092+ )
2093+
2094+ func render(wheel *Wheel, status *Status) termui.GridBufferer {
2095+ var text string
2096+ switch status.State {
2097+ case RUNNING:
2098+ text = fmt.Sprintf(
2099+ `[%d/%d] Pomodoros completed
2100+
2101+ %s %s remaining
2102+
2103+
2104+ [q] - quit [p] - pause
2105+ `,
2106+ status.Count,
2107+ status.NPomodoros,
2108+ wheel,
2109+ status.Remaining,
2110+ )
2111+ case BREAKING:
2112+ text = `It is time to take a break!
2113+
2114+ Once you are ready, press [enter]
2115+ to begin the next Pomodoro.
2116+
2117+ [q] - quit [p] - pause
2118+ `
2119+ case PAUSED:
2120+ text = `Pomo is suspended.
2121+
2122+ Press [p] to continue.
2123+
2124+
2125+ [q] - quit [p] - unpause
2126+ `
2127+ case COMPLETE:
2128+ text = `This session has concluded.
2129+
2130+ Press [q] to exit.
2131+
2132+
2133+ [q] - quit
2134+ `
2135+ }
2136+ par := termui.NewPar(text)
2137+ par.Height = 8
2138+ par.BorderLabel = fmt.Sprintf("Pomo - %s", status.State)
2139+ par.BorderLabelFg = termui.ColorWhite
2140+ par.BorderFg = termui.ColorRed
2141+ if status.State == RUNNING {
2142+ par.BorderFg = termui.ColorGreen
2143+ }
2144+ return par
2145+ }
2146+
2147+ func newBlk() termui.GridBufferer {
2148+ blk := termui.NewBlock()
2149+ blk.Height = termui.TermHeight() / 3
2150+ blk.Border = false
2151+ return blk
2152+ }
2153+
2154+ func centered(part termui.GridBufferer) *termui.Grid {
2155+ grid := termui.NewGrid(
2156+ termui.NewRow(
2157+ termui.NewCol(12, 0, newBlk()),
2158+ ),
2159+ termui.NewRow(
2160+ termui.NewCol(3, 0, newBlk()),
2161+ termui.NewCol(6, 0, part),
2162+ termui.NewCol(3, 0, newBlk()),
2163+ ),
2164+ termui.NewRow(
2165+ termui.NewCol(12, 0, newBlk()),
2166+ ),
2167+ )
2168+ grid.BgColor = termui.ThemeAttr("bg")
2169+ grid.Width = termui.TermWidth()
2170+ grid.Align()
2171+ return grid
2172+ }
2173+
2174+ func StartUI(runner *TaskRunner) {
2175+ err := termui.Init()
2176+ if err != nil {
2177+ panic(err)
2178+ }
2179+ wheel := Wheel(0)
2180+
2181+ defer termui.Close()
2182+
2183+ termui.Render(centered(render(&wheel, runner.Status())))
2184+
2185+ termui.Handle("/timer/1s", func(termui.Event) {
2186+ termui.Render(centered(render(&wheel, runner.Status())))
2187+ })
2188+
2189+ termui.Handle("/sys/wnd/resize", func(termui.Event) {
2190+ termui.Render(centered(render(&wheel, runner.Status())))
2191+ })
2192+
2193+ termui.Handle("/sys/kbd/<enter>", func(termui.Event) {
2194+ runner.Toggle()
2195+ termui.Render(centered(render(&wheel, runner.Status())))
2196+ })
2197+
2198+ termui.Handle("/sys/kbd/p", func(termui.Event) {
2199+ runner.Pause()
2200+ termui.Render(centered(render(&wheel, runner.Status())))
2201+ })
2202+
2203+ termui.Handle("/sys/kbd/q", func(termui.Event) {
2204+ termui.StopLoop()
2205+ })
2206+
2207+ termui.Loop()
2208+ }
2209 diff --git a/pkg/internal/util.go b/pkg/internal/util.go
2210new file mode 100644
2211index 0000000..70b4433
2212--- /dev/null
2213+++ b/pkg/internal/util.go
2214 @@ -0,0 +1,81 @@
2215+ package pomo
2216+
2217+ import (
2218+ "fmt"
2219+ "time"
2220+
2221+ "github.com/fatih/color"
2222+ )
2223+
2224+
2225+ func SummerizeTasks(config *Config, tasks []*Task) {
2226+ for _, task := range tasks {
2227+ var start string
2228+ if len(task.Pomodoros) > 0 {
2229+ start = task.Pomodoros[0].Start.Format(config.DateTimeFmt)
2230+ }
2231+ fmt.Printf("%d: [%s] [%s] ", task.ID, start, task.Duration.Truncate(time.Second))
2232+ // a list of green/yellow/red pomodoros
2233+ // green indicates the pomodoro was finished normally
2234+ // yellow indicates the break was exceeded by +5minutes
2235+ // red indicates the pomodoro was never completed
2236+ fmt.Printf("[")
2237+ for i, pomodoro := range task.Pomodoros {
2238+ if i > 0 {
2239+ fmt.Printf(" ")
2240+ }
2241+ // pomodoro exceeded it's expected duration by more than 5m
2242+ if pomodoro.Duration() > task.Duration+5*time.Minute {
2243+ color.New(color.FgYellow).Printf("X")
2244+ } else {
2245+ // pomodoro completed normally
2246+ color.New(color.FgGreen).Printf("X")
2247+ }
2248+ }
2249+ // each missed pomodoro
2250+ for i := 0; i < task.NPomodoros-len(task.Pomodoros); i++ {
2251+ if i > 0 || i == 0 && len(task.Pomodoros) > 0 {
2252+ fmt.Printf(" ")
2253+ }
2254+ color.New(color.FgRed).Printf("X")
2255+ }
2256+ fmt.Printf("]")
2257+ // Tags
2258+ if len(task.Tags) > 0 {
2259+ fmt.Printf(" [")
2260+ for i, tag := range task.Tags {
2261+ if i > 0 && i != len(task.Tags) {
2262+ fmt.Printf(" ")
2263+ }
2264+ // user specified color mapping exists
2265+ if config.Colors != nil {
2266+ if color := config.Colors.Get(tag); color != nil {
2267+ color.Printf("%s", tag)
2268+ } else {
2269+ // no color mapping for tag
2270+ fmt.Printf("%s", tag)
2271+ }
2272+ } else {
2273+ // no color mapping
2274+ fmt.Printf("%s", tag)
2275+ }
2276+
2277+ }
2278+ fmt.Printf("]")
2279+ }
2280+ fmt.Printf(" - %s", task.Message)
2281+ fmt.Printf("\n")
2282+ }
2283+ }
2284+
2285+ func OutputStatus(status Status) {
2286+ state := "?"
2287+ if status.State >= RUNNING {
2288+ state = string(status.State.String()[0])
2289+ }
2290+ if status.State == RUNNING {
2291+ fmt.Printf("%s [%d/%d] %s", state, status.Count, status.NPomodoros, status.Remaining)
2292+ } else {
2293+ fmt.Printf("%s [%d/%d] -", state, status.Count, status.NPomodoros)
2294+ }
2295+ }
2296 diff --git a/pkg/internal/version.go b/pkg/internal/version.go
2297new file mode 100644
2298index 0000000..aad0e90
2299--- /dev/null
2300+++ b/pkg/internal/version.go
2301 @@ -0,0 +1,3 @@
2302+ package pomo
2303+
2304+ var Version = "undefined"
2305 diff --git a/runner.go b/runner.go
2306deleted file mode 100644
2307index c6376e1..0000000
2308--- a/runner.go
2309+++ /dev/null
2310 @@ -1,151 +0,0 @@
2311- package main
2312-
2313- import (
2314- "database/sql"
2315- "time"
2316- )
2317-
2318- type TaskRunner struct {
2319- count int
2320- taskID int
2321- taskMessage string
2322- nPomodoros int
2323- origDuration time.Duration
2324- state State
2325- store *Store
2326- started time.Time
2327- pause chan bool
2328- toggle chan bool
2329- notifier Notifier
2330- duration time.Duration
2331- }
2332-
2333- func NewMockedTaskRunner(task *Task, store *Store, notifier Notifier) (*TaskRunner, error) {
2334- tr := &TaskRunner{
2335- taskID: task.ID,
2336- taskMessage: task.Message,
2337- nPomodoros: task.NPomodoros,
2338- origDuration: task.Duration,
2339- store: store,
2340- state: State(0),
2341- pause: make(chan bool),
2342- toggle: make(chan bool),
2343- notifier: notifier,
2344- duration: task.Duration,
2345- }
2346- return tr, nil
2347- }
2348- func NewTaskRunner(task *Task, config *Config) (*TaskRunner, error) {
2349- store, err := NewStore(config.DBPath)
2350- if err != nil {
2351- return nil, err
2352- }
2353- tr := &TaskRunner{
2354- taskID: task.ID,
2355- taskMessage: task.Message,
2356- nPomodoros: task.NPomodoros,
2357- origDuration: task.Duration,
2358- store: store,
2359- state: State(0),
2360- pause: make(chan bool),
2361- toggle: make(chan bool),
2362- notifier: NewXnotifier(config.IconPath),
2363- duration: task.Duration,
2364- }
2365- return tr, nil
2366- }
2367-
2368- func (t *TaskRunner) Start() {
2369- go t.run()
2370- }
2371-
2372- func (t *TaskRunner) TimeRemaining() time.Duration {
2373- return (t.duration - time.Since(t.started)).Truncate(time.Second)
2374- }
2375-
2376- func (t *TaskRunner) SetState(state State) {
2377- t.state = state
2378- }
2379-
2380- func (t *TaskRunner) run() error {
2381- for t.count < t.nPomodoros {
2382- // Create a new pomodoro where we
2383- // track the start / end time of
2384- // of this session.
2385- pomodoro := &Pomodoro{}
2386- // Start this pomodoro
2387- pomodoro.Start = time.Now()
2388- // Set state to RUNNIN
2389- t.SetState(RUNNING)
2390- // Create a new timer
2391- timer := time.NewTimer(t.duration)
2392- // Record our started time
2393- t.started = pomodoro.Start
2394- loop:
2395- select {
2396- case <-timer.C:
2397- t.SetState(BREAKING)
2398- t.count++
2399- case <-t.toggle:
2400- // Catch any toggles when we
2401- // are not expecting them
2402- goto loop
2403- case <-t.pause:
2404- timer.Stop()
2405- // Record the remaining time of the current pomodoro
2406- remaining := t.TimeRemaining()
2407- // Change state to PAUSED
2408- t.SetState(PAUSED)
2409- // Wait for the user to press [p]
2410- <-t.pause
2411- // Resume the timer with previous
2412- // remaining time
2413- timer.Reset(remaining)
2414- // Change duration
2415- t.started = time.Now()
2416- t.duration = remaining
2417- // Restore state to RUNNING
2418- t.SetState(RUNNING)
2419- goto loop
2420- }
2421- pomodoro.End = time.Now()
2422- err := t.store.With(func(tx *sql.Tx) error {
2423- return t.store.CreatePomodoro(tx, t.taskID, *pomodoro)
2424- })
2425- if err != nil {
2426- return err
2427- }
2428- // All pomodoros completed
2429- if t.count == t.nPomodoros {
2430- break
2431- }
2432-
2433- t.notifier.Notify("Pomo", "It is time to take a break!")
2434- // Reset the duration incase it
2435- // was paused.
2436- t.duration = t.origDuration
2437- // User concludes the break
2438- <-t.toggle
2439-
2440- }
2441- t.notifier.Notify("Pomo", "Pomo session has completed!")
2442- t.SetState(COMPLETE)
2443- return nil
2444- }
2445-
2446- func (t *TaskRunner) Toggle() {
2447- t.toggle <- true
2448- }
2449-
2450- func (t *TaskRunner) Pause() {
2451- t.pause <- true
2452- }
2453-
2454- func (t *TaskRunner) Status() *Status {
2455- return &Status{
2456- State: t.state,
2457- Count: t.count,
2458- NPomodoros: t.nPomodoros,
2459- Remaining: t.TimeRemaining(),
2460- }
2461- }
2462 diff --git a/runner_test.go b/runner_test.go
2463deleted file mode 100644
2464index b11c628..0000000
2465--- a/runner_test.go
2466+++ /dev/null
2467 @@ -1,37 +0,0 @@
2468- package main
2469-
2470- import (
2471- "fmt"
2472- "io/ioutil"
2473- "path"
2474- "testing"
2475- "time"
2476- )
2477-
2478- func TestTaskRunner(t *testing.T) {
2479- baseDir, _ := ioutil.TempDir("/tmp", "")
2480- store, err := NewStore(path.Join(baseDir, "pomo.db"))
2481- if err != nil {
2482- t.Error(err)
2483- }
2484- err = initDB(store)
2485- if err != nil {
2486- t.Error(err)
2487- }
2488- runner, err := NewMockedTaskRunner(&Task{
2489- Duration: time.Second * 2,
2490- NPomodoros: 2,
2491- Message: fmt.Sprint("Test Task"),
2492- }, store, NoopNotifier{})
2493- if err != nil {
2494- t.Error(err)
2495- }
2496-
2497- runner.Start()
2498-
2499- runner.Toggle()
2500- runner.Toggle()
2501-
2502- runner.Toggle()
2503- runner.Toggle()
2504- }
2505 diff --git a/server.go b/server.go
2506deleted file mode 100644
2507index 87c5c7e..0000000
2508--- a/server.go
2509+++ /dev/null
2510 @@ -1,93 +0,0 @@
2511- package main
2512-
2513- import (
2514- "encoding/json"
2515- "errors"
2516- "fmt"
2517- "net"
2518- "os"
2519- )
2520-
2521- // Server listens on a Unix domain socket
2522- // for Pomo status requests
2523- type Server struct {
2524- listener net.Listener
2525- runner *TaskRunner
2526- running bool
2527- }
2528-
2529- func (s *Server) listen() {
2530- for s.running {
2531- conn, err := s.listener.Accept()
2532- if err != nil {
2533- break
2534- }
2535- buf := make([]byte, 512)
2536- // Ignore any content
2537- conn.Read(buf)
2538- raw, _ := json.Marshal(s.runner.Status())
2539- conn.Write(raw)
2540- conn.Close()
2541- }
2542- }
2543-
2544- func (s *Server) Start() {
2545- s.running = true
2546- go s.listen()
2547- }
2548-
2549- func (s *Server) Stop() {
2550- s.running = false
2551- s.listener.Close()
2552- }
2553-
2554- func NewServer(runner *TaskRunner, config *Config) (*Server, error) {
2555- //check if socket file exists
2556- if _, err := os.Stat(config.SocketPath); err == nil {
2557- _, err := net.Dial("unix", config.SocketPath)
2558- //if error then sock file was saved after crash
2559- if err != nil {
2560- os.Remove(config.SocketPath)
2561- } else {
2562- // another instance of pomo is running
2563- return nil, errors.New(fmt.Sprintf("Socket %s is already in use", config.SocketPath))
2564- }
2565- }
2566- listener, err := net.Listen("unix", config.SocketPath)
2567- if err != nil {
2568- return nil, err
2569- }
2570- return &Server{listener: listener, runner: runner}, nil
2571- }
2572-
2573- // Client makes requests to a listening
2574- // pomo server to check the status of
2575- // any currently running task session.
2576- type Client struct {
2577- conn net.Conn
2578- }
2579-
2580- func (c Client) read(statusCh chan *Status) {
2581- buf := make([]byte, 512)
2582- n, _ := c.conn.Read(buf)
2583- status := &Status{}
2584- json.Unmarshal(buf[0:n], status)
2585- statusCh <- status
2586- }
2587-
2588- func (c Client) Status() (*Status, error) {
2589- statusCh := make(chan *Status)
2590- c.conn.Write([]byte("status"))
2591- go c.read(statusCh)
2592- return <-statusCh, nil
2593- }
2594-
2595- func (c Client) Close() error { return c.conn.Close() }
2596-
2597- func NewClient(path string) (*Client, error) {
2598- conn, err := net.Dial("unix", path)
2599- if err != nil {
2600- return nil, err
2601- }
2602- return &Client{conn: conn}, nil
2603- }
2604 diff --git a/store.go b/store.go
2605deleted file mode 100644
2606index f3bf795..0000000
2607--- a/store.go
2608+++ /dev/null
2609 @@ -1,187 +0,0 @@
2610- package main
2611-
2612- import (
2613- "database/sql"
2614- "strings"
2615- "time"
2616-
2617- _ "github.com/mattn/go-sqlite3"
2618- )
2619-
2620- // 2018-01-16 19:05:21.752851759+08:00
2621- const datetimeFmt = "2006-01-02 15:04:05.999999999-07:00"
2622-
2623- type StoreFunc func(tx *sql.Tx) error
2624-
2625- type Store struct {
2626- db *sql.DB
2627- }
2628-
2629- func NewStore(path string) (*Store, error) {
2630- db, err := sql.Open("sqlite3", path)
2631- if err != nil {
2632- return nil, err
2633- }
2634- return &Store{db: db}, nil
2635- }
2636-
2637- // With applies all of the given functions with
2638- // a single transaction, rolling back on failure
2639- // and commiting on success.
2640- func (s Store) With(fns ...func(tx *sql.Tx) error) error {
2641- tx, err := s.db.Begin()
2642- if err != nil {
2643- return err
2644- }
2645- for _, fn := range fns {
2646- err = fn(tx)
2647- if err != nil {
2648- tx.Rollback()
2649- return err
2650- }
2651- }
2652- return tx.Commit()
2653- }
2654-
2655- func (s Store) CreateTask(tx *sql.Tx, task Task) (int, error) {
2656- var taskID int
2657- _, err := tx.Exec(
2658- "INSERT INTO task (message,pomodoros,duration,tags) VALUES ($1,$2,$3,$4)",
2659- task.Message, task.NPomodoros, task.Duration.String(), strings.Join(task.Tags, ","))
2660- if err != nil {
2661- return -1, err
2662- }
2663- err = tx.QueryRow("SELECT last_insert_rowid() FROM task").Scan(&taskID)
2664- if err != nil {
2665- return -1, err
2666- }
2667- err = tx.QueryRow("SELECT last_insert_rowid() FROM task").Scan(&taskID)
2668- if err != nil {
2669- return -1, err
2670- }
2671- return taskID, nil
2672- }
2673-
2674- func (s Store) ReadTasks(tx *sql.Tx) ([]*Task, error) {
2675- rows, err := tx.Query(`SELECT rowid,message,pomodoros,duration,tags FROM task`)
2676- if err != nil {
2677- return nil, err
2678- }
2679- tasks := []*Task{}
2680- for rows.Next() {
2681- var (
2682- tags string
2683- strDuration string
2684- )
2685- task := &Task{Pomodoros: []*Pomodoro{}}
2686- err = rows.Scan(&task.ID, &task.Message, &task.NPomodoros, &strDuration, &tags)
2687- if err != nil {
2688- return nil, err
2689- }
2690- duration, _ := time.ParseDuration(strDuration)
2691- task.Duration = duration
2692- if tags != "" {
2693- task.Tags = strings.Split(tags, ",")
2694- }
2695- pomodoros, err := s.ReadPomodoros(tx, task.ID)
2696- if err != nil {
2697- return nil, err
2698- }
2699- for _, pomodoro := range pomodoros {
2700- task.Pomodoros = append(task.Pomodoros, pomodoro)
2701- }
2702- tasks = append(tasks, task)
2703- }
2704- return tasks, nil
2705- }
2706-
2707- func (s Store) DeleteTask(tx *sql.Tx, taskID int) error {
2708- _, err := tx.Exec("DELETE FROM task WHERE rowid = $1", &taskID)
2709- if err != nil {
2710- return err
2711- }
2712- _, err = tx.Exec("DELETE FROM pomodoro WHERE task_id = $1", &taskID)
2713- if err != nil {
2714- return err
2715- }
2716- return nil
2717- }
2718-
2719- func (s Store) ReadTask(tx *sql.Tx, taskID int) (*Task, error) {
2720- task := &Task{}
2721- var (
2722- tags string
2723- strDuration string
2724- )
2725- err := tx.QueryRow(`SELECT rowid,message,pomodoros,duration,tags FROM task WHERE rowid = $1`, &taskID).
2726- Scan(&task.ID, &task.Message, &task.NPomodoros, &strDuration, &tags)
2727- if err != nil {
2728- return nil, err
2729- }
2730- duration, _ := time.ParseDuration(strDuration)
2731- task.Duration = duration
2732- if tags != "" {
2733- task.Tags = strings.Split(tags, ",")
2734- }
2735- return task, nil
2736- }
2737-
2738- func (s Store) CreatePomodoro(tx *sql.Tx, taskID int, pomodoro Pomodoro) error {
2739- _, err := tx.Exec(
2740- `INSERT INTO pomodoro (task_id, start, end) VALUES ($1, $2, $3)`,
2741- taskID,
2742- pomodoro.Start,
2743- pomodoro.End,
2744- )
2745- return err
2746- }
2747-
2748- func (s Store) ReadPomodoros(tx *sql.Tx, taskID int) ([]*Pomodoro, error) {
2749- rows, err := tx.Query(`SELECT start,end FROM pomodoro WHERE task_id = $1`, &taskID)
2750- if err != nil {
2751- return nil, err
2752- }
2753- pomodoros := []*Pomodoro{}
2754- for rows.Next() {
2755- var (
2756- startStr string
2757- endStr string
2758- )
2759- pomodoro := &Pomodoro{}
2760- err = rows.Scan(&startStr, &endStr)
2761- if err != nil {
2762- return nil, err
2763- }
2764- start, _ := time.Parse(datetimeFmt, startStr)
2765- end, _ := time.Parse(datetimeFmt, endStr)
2766- pomodoro.Start = start
2767- pomodoro.End = end
2768- pomodoros = append(pomodoros, pomodoro)
2769- }
2770- return pomodoros, nil
2771- }
2772-
2773- func (s Store) DeletePomodoros(tx *sql.Tx, taskID int) error {
2774- _, err := tx.Exec("DELETE FROM pomodoro WHERE task_id = $1", &taskID)
2775- return err
2776- }
2777-
2778- func (s Store) Close() error { return s.db.Close() }
2779-
2780- func initDB(db *Store) error {
2781- stmt := `
2782- CREATE TABLE task (
2783- message TEXT,
2784- pomodoros INTEGER,
2785- duration TEXT,
2786- tags TEXT
2787- );
2788- CREATE TABLE pomodoro (
2789- task_id INTEGER,
2790- start DATETTIME,
2791- end DATETTIME
2792- );
2793- `
2794- _, err := db.db.Exec(stmt)
2795- return err
2796- }
2797 diff --git a/types.go b/types.go
2798deleted file mode 100644
2799index d248aed..0000000
2800--- a/types.go
2801+++ /dev/null
2802 @@ -1,144 +0,0 @@
2803- package main
2804-
2805- import (
2806- "io/ioutil"
2807- "os"
2808- "time"
2809-
2810- "github.com/0xAX/notificator"
2811- )
2812-
2813- type State int
2814-
2815- func (s State) String() string {
2816- switch s {
2817- case RUNNING:
2818- return "RUNNING"
2819- case BREAKING:
2820- return "BREAKING"
2821- case COMPLETE:
2822- return "COMPLETE"
2823- case PAUSED:
2824- return "PAUSED"
2825- }
2826- return ""
2827- }
2828-
2829- const (
2830- RUNNING State = iota + 1
2831- BREAKING
2832- COMPLETE
2833- PAUSED
2834- )
2835-
2836- // Wheel keeps track of an ASCII spinner
2837- type Wheel int
2838-
2839- func (w *Wheel) String() string {
2840- switch int(*w) {
2841- case 0:
2842- *w++
2843- return "|"
2844- case 1:
2845- *w++
2846- return "/"
2847- case 2:
2848- *w++
2849- return "-"
2850- case 3:
2851- *w = 0
2852- return "\\"
2853- }
2854- return ""
2855- }
2856-
2857- // Task describes some activity
2858- type Task struct {
2859- ID int `json:"id"`
2860- Message string `json:"message"`
2861- // Array of completed pomodoros
2862- Pomodoros []*Pomodoro `json:"pomodoros"`
2863- // Free-form tags associated with this task
2864- Tags []string `json:"tags"`
2865- // Number of pomodoros for this task
2866- NPomodoros int `json:"n_pomodoros"`
2867- // Duration of each pomodoro
2868- Duration time.Duration `json:"duration"`
2869- }
2870-
2871- // ByID is a sortable array of tasks
2872- type ByID []*Task
2873-
2874- func (b ByID) Len() int { return len(b) }
2875- func (b ByID) Swap(i, j int) { b[i], b[j] = b[j], b[i] }
2876- func (b ByID) Less(i, j int) bool { return b[i].ID < b[j].ID }
2877-
2878- // After returns tasks that were started after the
2879- // provided start time.
2880- func After(start time.Time, tasks []*Task) []*Task {
2881- filtered := []*Task{}
2882- for _, task := range tasks {
2883- if len(task.Pomodoros) > 0 {
2884- if start.Before(task.Pomodoros[0].Start) {
2885- filtered = append(filtered, task)
2886- }
2887- }
2888- }
2889- return filtered
2890- }
2891-
2892- // Pomodoro is a unit of time to spend working
2893- // on a single task.
2894- type Pomodoro struct {
2895- Start time.Time `json:"start"`
2896- End time.Time `json:"end"`
2897- }
2898-
2899- // Duration returns the runtime of the pomodoro
2900- func (p Pomodoro) Duration() time.Duration {
2901- return (p.End.Sub(p.Start))
2902- }
2903-
2904- // Status is used to communicate the state
2905- // of a running Pomodoro session
2906- type Status struct {
2907- State State `json:"state"`
2908- Remaining time.Duration `json:"remaining"`
2909- Count int `json:"count"`
2910- NPomodoros int `json:"n_pomodoros"`
2911- }
2912-
2913- // Notifier sends a system notification
2914- type Notifier interface {
2915- Notify(string, string) error
2916- }
2917-
2918- // NoopNotifier does nothing
2919- type NoopNotifier struct{}
2920-
2921- func (n NoopNotifier) Notify(string, string) error { return nil }
2922-
2923- // Xnotifier can push notifications to mac, linux and windows.
2924- type Xnotifier struct {
2925- *notificator.Notificator
2926- iconPath string
2927- }
2928-
2929- func NewXnotifier(iconPath string) Notifier {
2930- // Write the built-in tomato icon if it
2931- // doesn't already exist.
2932- _, err := os.Stat(iconPath)
2933- if os.IsNotExist(err) {
2934- raw := MustAsset("tomato-icon.png")
2935- _ = ioutil.WriteFile(iconPath, raw, 0644)
2936- }
2937- return Xnotifier{
2938- Notificator: notificator.New(notificator.Options{}),
2939- iconPath: iconPath,
2940- }
2941- }
2942-
2943- // Notify sends a notification to the OS.
2944- func (n Xnotifier) Notify(title, body string) error {
2945- return n.Push(title, body, n.iconPath, notificator.UR_NORMAL)
2946- }
2947 diff --git a/ui.go b/ui.go
2948deleted file mode 100644
2949index f9b42fc..0000000
2950--- a/ui.go
2951+++ /dev/null
2952 @@ -1,123 +0,0 @@
2953- package main
2954-
2955- import (
2956- "fmt"
2957-
2958- "github.com/gizak/termui"
2959- )
2960-
2961- func render(wheel *Wheel, status *Status) termui.GridBufferer {
2962- var text string
2963- switch status.State {
2964- case RUNNING:
2965- text = fmt.Sprintf(
2966- `[%d/%d] Pomodoros completed
2967-
2968- %s %s remaining
2969-
2970-
2971- [q] - quit [p] - pause
2972- `,
2973- status.Count,
2974- status.NPomodoros,
2975- wheel,
2976- status.Remaining,
2977- )
2978- case BREAKING:
2979- text = `It is time to take a break!
2980-
2981- Once you are ready, press [enter]
2982- to begin the next Pomodoro.
2983-
2984- [q] - quit [p] - pause
2985- `
2986- case PAUSED:
2987- text = `Pomo is suspended.
2988-
2989- Press [p] to continue.
2990-
2991-
2992- [q] - quit [p] - unpause
2993- `
2994- case COMPLETE:
2995- text = `This session has concluded.
2996-
2997- Press [q] to exit.
2998-
2999-
3000- [q] - quit
3001- `
3002- }
3003- par := termui.NewPar(text)
3004- par.Height = 8
3005- par.BorderLabel = fmt.Sprintf("Pomo - %s", status.State)
3006- par.BorderLabelFg = termui.ColorWhite
3007- par.BorderFg = termui.ColorRed
3008- if status.State == RUNNING {
3009- par.BorderFg = termui.ColorGreen
3010- }
3011- return par
3012- }
3013-
3014- func newBlk() termui.GridBufferer {
3015- blk := termui.NewBlock()
3016- blk.Height = termui.TermHeight() / 3
3017- blk.Border = false
3018- return blk
3019- }
3020-
3021- func centered(part termui.GridBufferer) *termui.Grid {
3022- grid := termui.NewGrid(
3023- termui.NewRow(
3024- termui.NewCol(12, 0, newBlk()),
3025- ),
3026- termui.NewRow(
3027- termui.NewCol(3, 0, newBlk()),
3028- termui.NewCol(6, 0, part),
3029- termui.NewCol(3, 0, newBlk()),
3030- ),
3031- termui.NewRow(
3032- termui.NewCol(12, 0, newBlk()),
3033- ),
3034- )
3035- grid.BgColor = termui.ThemeAttr("bg")
3036- grid.Width = termui.TermWidth()
3037- grid.Align()
3038- return grid
3039- }
3040-
3041- func startUI(runner *TaskRunner) {
3042- err := termui.Init()
3043- if err != nil {
3044- panic(err)
3045- }
3046- wheel := Wheel(0)
3047-
3048- defer termui.Close()
3049-
3050- termui.Render(centered(render(&wheel, runner.Status())))
3051-
3052- termui.Handle("/timer/1s", func(termui.Event) {
3053- termui.Render(centered(render(&wheel, runner.Status())))
3054- })
3055-
3056- termui.Handle("/sys/wnd/resize", func(termui.Event) {
3057- termui.Render(centered(render(&wheel, runner.Status())))
3058- })
3059-
3060- termui.Handle("/sys/kbd/<enter>", func(termui.Event) {
3061- runner.Toggle()
3062- termui.Render(centered(render(&wheel, runner.Status())))
3063- })
3064-
3065- termui.Handle("/sys/kbd/p", func(termui.Event) {
3066- runner.Pause()
3067- termui.Render(centered(render(&wheel, runner.Status())))
3068- })
3069-
3070- termui.Handle("/sys/kbd/q", func(termui.Event) {
3071- termui.StopLoop()
3072- })
3073-
3074- termui.Loop()
3075- }
3076 diff --git a/util.go b/util.go
3077deleted file mode 100644
3078index 4c2425f..0000000
3079--- a/util.go
3080+++ /dev/null
3081 @@ -1,96 +0,0 @@
3082- package main
3083-
3084- import (
3085- "fmt"
3086- "os"
3087- "os/user"
3088- "path"
3089- "time"
3090-
3091- "github.com/fatih/color"
3092- )
3093-
3094- func maybe(err error) {
3095- if err != nil {
3096- fmt.Printf("Error:\n%s\n", err)
3097- os.Exit(1)
3098- }
3099- }
3100-
3101- func defaultConfigPath() string {
3102- u, err := user.Current()
3103- maybe(err)
3104- return path.Join(u.HomeDir, "/.pomo/config.json")
3105- }
3106-
3107- func summerizeTasks(config *Config, tasks []*Task) {
3108- for _, task := range tasks {
3109- var start string
3110- if len(task.Pomodoros) > 0 {
3111- start = task.Pomodoros[0].Start.Format(config.DateTimeFmt)
3112- }
3113- fmt.Printf("%d: [%s] [%s] ", task.ID, start, task.Duration.Truncate(time.Second))
3114- // a list of green/yellow/red pomodoros
3115- // green indicates the pomodoro was finished normally
3116- // yellow indicates the break was exceeded by +5minutes
3117- // red indicates the pomodoro was never completed
3118- fmt.Printf("[")
3119- for i, pomodoro := range task.Pomodoros {
3120- if i > 0 {
3121- fmt.Printf(" ")
3122- }
3123- // pomodoro exceeded it's expected duration by more than 5m
3124- if pomodoro.Duration() > task.Duration+5*time.Minute {
3125- color.New(color.FgYellow).Printf("X")
3126- } else {
3127- // pomodoro completed normally
3128- color.New(color.FgGreen).Printf("X")
3129- }
3130- }
3131- // each missed pomodoro
3132- for i := 0; i < task.NPomodoros-len(task.Pomodoros); i++ {
3133- if i > 0 || i == 0 && len(task.Pomodoros) > 0 {
3134- fmt.Printf(" ")
3135- }
3136- color.New(color.FgRed).Printf("X")
3137- }
3138- fmt.Printf("]")
3139- // Tags
3140- if len(task.Tags) > 0 {
3141- fmt.Printf(" [")
3142- for i, tag := range task.Tags {
3143- if i > 0 && i != len(task.Tags) {
3144- fmt.Printf(" ")
3145- }
3146- // user specified color mapping exists
3147- if config.Colors != nil {
3148- if color := config.Colors.Get(tag); color != nil {
3149- color.Printf("%s", tag)
3150- } else {
3151- // no color mapping for tag
3152- fmt.Printf("%s", tag)
3153- }
3154- } else {
3155- // no color mapping
3156- fmt.Printf("%s", tag)
3157- }
3158-
3159- }
3160- fmt.Printf("]")
3161- }
3162- fmt.Printf(" - %s", task.Message)
3163- fmt.Printf("\n")
3164- }
3165- }
3166-
3167- func outputStatus(status Status) {
3168- state := "?"
3169- if status.State >= RUNNING {
3170- state = string(status.State.String()[0])
3171- }
3172- if status.State == RUNNING {
3173- fmt.Printf("%s [%d/%d] %s", state, status.Count, status.NPomodoros, status.Remaining)
3174- } else {
3175- fmt.Printf("%s [%d/%d] -", state, status.Count, status.NPomodoros)
3176- }
3177- }
3178 diff --git a/version.go b/version.go
3179deleted file mode 100644
3180index 3cb07c8..0000000
3181--- a/version.go
3182+++ /dev/null
3183 @@ -1,3 +0,0 @@
3184- package main
3185-
3186- var Version = "undefined"