From 55f2a8537831c631133ef9f287226254f7f5f40f Mon Sep 17 00:00:00 2001 From: Kevin Schoon Date: Fri, 26 Jan 2018 23:07:38 +0800 Subject: [PATCH] redesign around termui --- main.go | 4 +- task.go | 117 ++++++++++++++++++++++++++++++++++++++++----------- task_test.go | 36 ++++++++++++++++ types.go | 60 ++++++++++++++------------ ui.go | 64 ++++++++++++++++++++++++++++ 5 files changed, 228 insertions(+), 53 deletions(-) create mode 100644 task_test.go create mode 100644 ui.go diff --git a/main.go b/main.go index 4669ba7..6078ef8 100644 --- a/main.go +++ b/main.go @@ -31,7 +31,9 @@ func start(path *string) func(*cli.Cmd) { } runner, err := NewTaskRunner(task, db) maybe(err) - maybe(runner.Run()) + runner.Start() + startUI(runner) + //maybe(runner.Run()) } } } diff --git a/task.go b/task.go index 9859d85..4a3c54f 100644 --- a/task.go +++ b/task.go @@ -1,19 +1,19 @@ package main import ( - "fmt" - "github.com/gosuri/uilive" "time" ) type TaskRunner struct { count int + state State task *Task store *Store - writer *uilive.Writer - timer *time.Timer - ticker *time.Ticker + started time.Time + pause chan bool + toggle chan bool notifier Notifier + duration time.Duration } func NewTaskRunner(task *Task, store *Store) (*TaskRunner, error) { @@ -25,22 +25,98 @@ func NewTaskRunner(task *Task, store *Store) (*TaskRunner, error) { tr := &TaskRunner{ task: task, store: store, + state: State(0), + pause: make(chan bool), + toggle: make(chan bool), notifier: NewLibNotifier(), - writer: uilive.New(), - timer: time.NewTimer(task.Duration), - ticker: time.NewTicker(RefreshInterval), + duration: task.Duration, } - tr.writer.Start() return tr, nil } +func (t *TaskRunner) Start() { + go t.run() +} + +func (t *TaskRunner) TimeRemaining() time.Duration { + return (t.duration - time.Since(t.started)).Truncate(time.Second) +} + +func (t *TaskRunner) run() error { + for t.count < t.task.NPomodoros { + // Create a new pomodoro where we + // track the start / end time of + // of this session. + pomodoro := &Pomodoro{} + // Start this pomodoro + pomodoro.Start = time.Now() + // Set state to RUNNING + t.state = RUNNING + // Create a new timer + timer := time.NewTimer(t.duration) + // Record our started time + t.started = pomodoro.Start + loop: + select { + case <-timer.C: + // Timer ended so now we set the + // state as BREAKING + t.state = BREAKING + case <-t.toggle: + // Catch any toggles when we + // are not expecting them + goto loop + case <-t.pause: + timer.Stop() + // Record the remaining time of this pomodoro + remaining := t.TimeRemaining() + // Change state to PAUSED + t.state = PAUSED + // Wait for the user to press [p] + <-t.pause + // Resume the timer at previous + // remaining time + timer.Reset(remaining) + t.started = time.Now() + t.duration = remaining + // Restore state to RUNNING + t.state = RUNNING + goto loop + } + if t.count+1 == t.task.NPomodoros { + break + } + // User concludes the break + <-t.toggle + pomodoro.End = time.Now() + err := t.store.CreatePomodoro(t.task.ID, *pomodoro) + if err != nil { + return err + } + t.duration = t.task.Duration + t.count++ + } + t.state = COMPLETE + return nil +} + +func (t *TaskRunner) Toggle() { + t.toggle <- true +} + +func (t *TaskRunner) Pause() { + t.pause <- true +} + +/* + func (t *TaskRunner) Run() error { for t.count < t.task.NPomodoros { // ASCII spinner wheel := &Wheel{} // This pomodoro pomodoro := &Pomodoro{} - prompt("press enter to begin") + //prompt("press enter to begin") // Emit a desktop notification // that the task is beginning. t.notifier.Begin(t.count, *t.task) @@ -53,21 +129,22 @@ func (t *TaskRunner) Run() error { loop: select { case <-t.ticker.C: - t.updateUI(Message{ + t.msgCh <- Message{ Start: pomodoro.Start, Duration: t.task.Duration, Pomodoros: t.task.NPomodoros, Wheel: wheel, CurrentPomodoro: t.count, - }) + State: RUNNING, + } goto loop case <-t.timer.C: // Send a notification for the // user to take a break. We record // how long it actually takes for // them to initiate the break. - t.notifier.Break(*t.task) - prompt("press enter to take a break") + //t.notifier.Break(*t.task) + //prompt("press enter to take a break") // Record the task as complete pomodoro.End = time.Now() // Record the session in the db @@ -81,14 +158,4 @@ func (t *TaskRunner) Run() error { } return nil } - -func (t *TaskRunner) updateUI(msg Message) { - fmt.Fprintf( - t.writer, - "%s %s remaining [ pomodoro %d/%d ]\n", - msg.Wheel, - (msg.Duration - time.Since(msg.Start)).Truncate(time.Second), - msg.CurrentPomodoro, - msg.Pomodoros, - ) -} +*/ diff --git a/task_test.go b/task_test.go new file mode 100644 index 0000000..07765d9 --- /dev/null +++ b/task_test.go @@ -0,0 +1,36 @@ +package main + +import ( + "fmt" + "io/ioutil" + "testing" + "time" +) + +func TestTaskRunner(t *testing.T) { + path, _ := ioutil.TempDir("/tmp", "") + store, err := NewStore(path) + if err != nil { + t.Error(err) + } + err = initDB(store) + if err != nil { + t.Error(err) + } + runner, err := NewTaskRunner(&Task{ + Duration: time.Second * 2, + NPomodoros: 2, + Message: fmt.Sprint("Test Task"), + }, store) + if err != nil { + t.Error(err) + } + + runner.Start() + + runner.Toggle() + runner.Toggle() + + runner.Toggle() + runner.Toggle() +} diff --git a/types.go b/types.go index 212fc73..d0807d6 100644 --- a/types.go +++ b/types.go @@ -10,6 +10,29 @@ import ( "github.com/kevinschoon/pomo/libnotify" ) +type State int + +func (s State) String() string { + switch s { + case RUNNING: + return "RUNNING" + case BREAKING: + return "BREAKING" + case COMPLETE: + return "COMPLETE" + case PAUSED: + return "PAUSED" + } + return "" +} + +const ( + RUNNING State = iota + 1 + BREAKING + COMPLETE + PAUSED +) + // RefreshInterval is the frequency at which // the display is updated. const RefreshInterval = 800 * time.Millisecond @@ -21,6 +44,7 @@ type Message struct { Duration time.Duration Pomodoros int CurrentPomodoro int + State State Wheel *Wheel } @@ -125,9 +149,7 @@ func (p Pomodoro) Duration() time.Duration { // notification. On Linux this libnotify. // TODO: OSX, Windows(?) type Notifier interface { - Begin(int, Task) error - Break(Task) error - Finish(Task) error + Notify(string, string) error } // LibNotifier implements a Linux @@ -150,28 +172,12 @@ func NewLibNotifier() Notifier { return ln } -func (ln LibNotifier) Begin(count int, t Task) error { - return ln.client.Notify(libnotify.Notification{ - Title: t.Message, - Body: fmt.Sprintf("Task is starting (%d/%d pomodoros)", count, t.NPomodoros), - Icon: ln.iconPath, - }) -} - -func (ln LibNotifier) Break(t Task) error { - return ln.client.Notify(libnotify.Notification{ - Title: t.Message, - Urgency: "critical", - Body: fmt.Sprintf("Time to take a break!\nPress enter at the console to initiate the break."), - Icon: ln.iconPath, - }) -} - -func (ln LibNotifier) Finish(t Task) error { - return ln.client.Notify(libnotify.Notification{ - Title: t.Message, - Urgency: "critical", - Body: fmt.Sprintf("This task session is complete!"), - Icon: ln.iconPath, - }) +func (ln LibNotifier) Notify(title, body string) error { + return ln.client.Notify( + libnotify.Notification{ + Title: title, + Body: body, + Icon: ln.iconPath, + }, + ) } diff --git a/ui.go b/ui.go new file mode 100644 index 0000000..e250809 --- /dev/null +++ b/ui.go @@ -0,0 +1,64 @@ +package main + +import ( + "fmt" + "github.com/gizak/termui" +) + +func getText(runner *TaskRunner) *termui.Par { + par := termui.NewPar("") + switch runner.state { + case RUNNING: + par.Text = fmt.Sprintf( + "%s %s remaining [ pomodoro %d/%d ]", + "X", + runner.TimeRemaining(), + runner.count, + runner.task.NPomodoros, + ) + case BREAKING: + par.Text = "Time to take a break.\nPress [enter] to begin the next Pomodoro!" + case PAUSED: + par.Text = "Press p to resume" + case COMPLETE: + par.Text = "Press q to quit" + } + par.Height = 8 + par.Width = 55 + par.BorderLabel = fmt.Sprintf("Pomo - %s", runner.state) + par.BorderLabelFg = termui.ColorWhite + par.BorderFg = termui.ColorRed + if runner.state == RUNNING { + par.BorderFg = termui.ColorGreen + } + return par +} + +func startUI(runner *TaskRunner) { + err := termui.Init() + if err != nil { + panic(err) + } + + defer termui.Close() + + termui.Handle("/timer/1s", func(termui.Event) { + termui.Render(getText(runner)) + }) + + termui.Handle("/sys/kbd/", func(termui.Event) { + runner.Toggle() + termui.Render(getText(runner)) + }) + + termui.Handle("/sys/kbd/p", func(termui.Event) { + runner.Pause() + termui.Render(getText(runner)) + }) + + termui.Handle("/sys/kbd/q", func(termui.Event) { + termui.StopLoop() + }) + + termui.Loop() +}