Author: Kevin Schoon [kevinschoon@gmail.com]
Hash: 2fc414efddb48a975a2305bae2672f924e1f4e83
Timestamp: Sun, 04 Feb 2018 03:13:46 +0000 (6 years ago)

+143 -12 +/-6 browse
add socket server for scriptable status output
1diff --git a/main.go b/main.go
2index 402024a..2da6240 100644
3--- a/main.go
4+++ b/main.go
5 @@ -32,6 +32,10 @@ func start(path *string) func(*cli.Cmd) {
6 }
7 runner, err := NewTaskRunner(task, db, NewXnotifier(*path+"/icon.png"))
8 maybe(err)
9+ server, err := NewServer(*path+"/pomo.sock", runner)
10+ maybe(err)
11+ server.Start()
12+ defer server.Stop()
13 runner.Start()
14 startUI(runner)
15 }
16 @@ -94,6 +98,23 @@ func _delete(path *string) func(*cli.Cmd) {
17 }
18 }
19
20+ func _status(path *string) func(*cli.Cmd) {
21+ return func(cmd *cli.Cmd) {
22+ cmd.Spec = "[OPTIONS]"
23+ cmd.Action = func() {
24+ client, err := NewClient(*path + "/pomo.sock")
25+ if err != nil {
26+ outputStatus(Status{})
27+ return
28+ }
29+ defer client.Close()
30+ status, err := client.Status()
31+ maybe(err)
32+ outputStatus(*status)
33+ }
34+ }
35+ }
36+
37 func main() {
38 app := cli.App("pomo", "Pomodoro CLI")
39 app.Spec = "[OPTIONS]"
40 @@ -105,5 +126,6 @@ func main() {
41 app.Command("init", "initialize the sqlite database", initialize(path))
42 app.Command("list l", "list historical tasks", list(path))
43 app.Command("delete d", "delete a stored task", _delete(path))
44+ app.Command("status st", "output the current status", _status(path))
45 app.Run(os.Args)
46 }
47 diff --git a/server.go b/server.go
48new file mode 100644
49index 0000000..8c6719a
50--- /dev/null
51+++ b/server.go
52 @@ -0,0 +1,79 @@
53+ package main
54+
55+ import (
56+ "encoding/json"
57+ "net"
58+ )
59+
60+ // Server listens on a Unix domain socket
61+ // for Pomo status requests
62+ type Server struct {
63+ listener net.Listener
64+ runner *TaskRunner
65+ running bool
66+ }
67+
68+ func (s *Server) listen() {
69+ for s.running {
70+ conn, err := s.listener.Accept()
71+ if err != nil {
72+ panic(err)
73+ }
74+ buf := make([]byte, 512)
75+ // Ignore any content
76+ conn.Read(buf)
77+ raw, _ := json.Marshal(s.runner.Status())
78+ conn.Write(raw)
79+ conn.Close()
80+ }
81+ }
82+
83+ func (s *Server) Start() {
84+ s.running = true
85+ go s.listen()
86+ }
87+
88+ func (s *Server) Stop() {
89+ s.running = false
90+ s.listener.Close()
91+ }
92+
93+ func NewServer(path string, runner *TaskRunner) (*Server, error) {
94+ listener, err := net.Listen("unix", path)
95+ if err != nil {
96+ return nil, err
97+ }
98+ return &Server{listener: listener, runner: runner}, nil
99+ }
100+
101+ // Client makes requests to a listening
102+ // pomo server to check the status of
103+ // any currently running task session.
104+ type Client struct {
105+ conn net.Conn
106+ }
107+
108+ func (c Client) read(statusCh chan *Status) {
109+ buf := make([]byte, 512)
110+ n, _ := c.conn.Read(buf)
111+ status := &Status{}
112+ json.Unmarshal(buf[0:n], status)
113+ statusCh <- status
114+ }
115+
116+ func (c Client) Status() (*Status, error) {
117+ statusCh := make(chan *Status)
118+ c.conn.Write([]byte("status"))
119+ go c.read(statusCh)
120+ return <-statusCh, nil
121+ }
122+
123+ func (c Client) Close() error { return c.conn.Close() }
124+
125+ func NewClient(path string) (*Client, error) {
126+ conn, err := net.Dial("unix", path)
127+ if err != nil {
128+ return nil, err
129+ }
130+ return &Client{conn: conn}, nil
131+ }
132 diff --git a/task.go b/task.go
133index 82316de..9823484 100644
134--- a/task.go
135+++ b/task.go
136 @@ -118,3 +118,12 @@ func (t *TaskRunner) Toggle() {
137 func (t *TaskRunner) Pause() {
138 t.pause <- true
139 }
140+
141+ func (t *TaskRunner) Status() *Status {
142+ return &Status{
143+ State: t.state,
144+ Count: t.count,
145+ NPomodoros: t.nPomodoros,
146+ Remaining: t.TimeRemaining(),
147+ }
148+ }
149 diff --git a/types.go b/types.go
150index b99be81..67a5dff 100644
151--- a/types.go
152+++ b/types.go
153 @@ -163,6 +163,15 @@ func (p Pomodoro) Duration() time.Duration {
154 return (p.End.Sub(p.Start))
155 }
156
157+ // Status is used to communicate the state
158+ // of a running Pomodoro session
159+ type Status struct {
160+ State State `json:"state"`
161+ Remaining time.Duration `json:"remaining"`
162+ Count int `json:"count"`
163+ NPomodoros int `json:"n_pomodoros"`
164+ }
165+
166 // Notifier sends a system notification
167 type Notifier interface {
168 Notify(string, string) error
169 diff --git a/ui.go b/ui.go
170index b1243e0..d36b95b 100644
171--- a/ui.go
172+++ b/ui.go
173 @@ -5,9 +5,9 @@ import (
174 "github.com/gizak/termui"
175 )
176
177- func status(wheel *Wheel, runner *TaskRunner) termui.GridBufferer {
178+ func render(wheel *Wheel, status *Status) termui.GridBufferer {
179 var text string
180- switch runner.state {
181+ switch status.State {
182 case RUNNING:
183 text = fmt.Sprintf(
184 `[%d/%d] Pomodoros completed
185 @@ -17,10 +17,10 @@ func status(wheel *Wheel, runner *TaskRunner) termui.GridBufferer {
186
187 [q] - quit [p] - pause
188 `,
189- runner.count,
190- runner.nPomodoros,
191+ status.Count,
192+ status.NPomodoros,
193 wheel,
194- runner.TimeRemaining(),
195+ status.Remaining,
196 )
197 case BREAKING:
198 text = `It is time to take a break!
199 @@ -49,10 +49,10 @@ func status(wheel *Wheel, runner *TaskRunner) termui.GridBufferer {
200 }
201 par := termui.NewPar(text)
202 par.Height = 8
203- par.BorderLabel = fmt.Sprintf("Pomo - %s", runner.state)
204+ par.BorderLabel = fmt.Sprintf("Pomo - %s", status.State)
205 par.BorderLabelFg = termui.ColorWhite
206 par.BorderFg = termui.ColorRed
207- if runner.state == RUNNING {
208+ if status.State == RUNNING {
209 par.BorderFg = termui.ColorGreen
210 }
211 return par
212 @@ -94,24 +94,24 @@ func startUI(runner *TaskRunner) {
213
214 defer termui.Close()
215
216- termui.Render(centered(status(&wheel, runner)))
217+ termui.Render(centered(render(&wheel, runner.Status())))
218
219 termui.Handle("/timer/1s", func(termui.Event) {
220- termui.Render(centered(status(&wheel, runner)))
221+ termui.Render(centered(render(&wheel, runner.Status())))
222 })
223
224 termui.Handle("/sys/wnd/resize", func(termui.Event) {
225- termui.Render(centered(status(&wheel, runner)))
226+ termui.Render(centered(render(&wheel, runner.Status())))
227 })
228
229 termui.Handle("/sys/kbd/<enter>", func(termui.Event) {
230 runner.Toggle()
231- termui.Render(centered(status(&wheel, runner)))
232+ termui.Render(centered(render(&wheel, runner.Status())))
233 })
234
235 termui.Handle("/sys/kbd/p", func(termui.Event) {
236 runner.Pause()
237- termui.Render(centered(status(&wheel, runner)))
238+ termui.Render(centered(render(&wheel, runner.Status())))
239 })
240
241 termui.Handle("/sys/kbd/q", func(termui.Event) {
242 diff --git a/util.go b/util.go
243index a2de6ec..f7a5680 100644
244--- a/util.go
245+++ b/util.go
246 @@ -74,3 +74,15 @@ func summerizeTasks(config *Config, tasks []*Task) {
247 fmt.Printf("\n")
248 }
249 }
250+
251+ func outputStatus(status Status) {
252+ state := "?"
253+ if status.State >= RUNNING {
254+ state = string(status.State.String()[0])
255+ }
256+ if status.State == RUNNING {
257+ fmt.Printf("%s [%d/%d] %s", state, status.Count, status.NPomodoros, status.Remaining)
258+ } else {
259+ fmt.Printf("%s [%d/%d] -", state, status.Count, status.NPomodoros)
260+ }
261+ }