Compare commits

...

10 Commits

Author SHA1 Message Date
Kevin Schoon 309277a115
Merge pull request #62 from sboysel/feature/ui-task-message
Display current task message while running or paused
2022-06-01 09:00:46 -05:00
Sam Boysel 8113c0a933 display current task message while running or paused 2022-05-31 23:00:34 -07:00
Kevin Schoon e6aa45152c
Merge pull request #61 from sboysel/feature/exec-on-event
Execute command on state change
2022-05-31 20:35:23 -05:00
Sam Boysel f86c5a6436 adds contrib dir for user contributed scripts 2022-05-31 18:10:10 -07:00
Sam Boysel 81cb8f568f execute onEvent command in Go routine 2022-05-31 18:06:43 -07:00
Sam Boysel 3ba07e9a87 check if onEvent is set, parse argument array 2022-05-31 13:35:15 -07:00
Sam Boysel caded9b68b fix README typos and edit for clarity 2022-05-30 23:36:33 -07:00
Sam Boysel 3d3a2bc152 execute command on state change 2022-05-30 23:21:29 -07:00
Kevin Schoon 6236144041 add man page 2022-05-30 15:34:55 -05:00
Kevin Schoon 1b321198fb fix issue where interface can block
Fixed an issue where the UI can block when certain key combinations are
pressed in different states. An alternative and more robust approach would
likely be to re-write the runner code as a finite state machine, however
these quick fixes work okay.

Additionally cleaned up some spacing in console messages and added a CREATED
state which is the default state of a pomodoro.
2022-05-30 14:59:03 -05:00
11 changed files with 285 additions and 21 deletions

View File

@ -10,7 +10,8 @@ LDFLAGS=\
test \ test \
docs \ docs \
pomo-build \ pomo-build \
readme readme \
bin/pomo
default: bin/pomo test default: bin/pomo test
@ -28,6 +29,11 @@ test:
install: install:
go install ./cmd/... go install ./cmd/...
man/pomo.1: man/pomo.1.scd
scdoc < $< > $@
manpages: man/pomo.1
docs: www/data/readme.json docs: www/data/readme.json
cd www && hugo -d ../docs cd www && hugo -d ../docs

View File

@ -49,7 +49,7 @@ pomo start -t my-project "write some codes"
## Configuration ## Configuration
Pomo has a few configuration options which can be read from a JSON file in Pomo's state directory `~/.pomo/config.json`. Pomo has a few configuration options which can be read from a JSON file in Pomo's config directory `~/.config/pomo/config.json`.
### colors ### colors
@ -65,6 +65,33 @@ Example:
} }
``` ```
### Execute command on state change
Pomo will execute an arbitrary command specified in the array argument `onEvent`
when the state changes. The first element of this array should be the
executable to run while the remaining elements are space delimited arguments.
The new state will be exported as an environment variable `POMO_STATE` for this
command. Possible state values are `RUNNING`, `PAUSED`, `BREAKING`, or
`COMPLETE`.
For example, to trigger a terminal bell when a session completes, add the
following to `config.json`:
```json
...
"onEvent": ["/bin/sh", "/path/to/script/my_script.sh"],
...
```
where the contents of `my_script.sh` are
```bash
#!/bin/sh
if [ "$POMO_STATE" == "COMPLETE" ] ; then
echo -e '\a'
fi
```
See the `contrib` directory for user contributed scripts for use with `onEvent`.
## Integrations ## Integrations
By default pomo will setup a Unix socket and serve it's status there. By default pomo will setup a Unix socket and serve it's status there.

0
contrib/.gitkeep Normal file
View File

3
contrib/bell Executable file
View File

@ -0,0 +1,3 @@
#!/bin/sh
echo -e '\e'

5
contrib/pomonag Executable file
View File

@ -0,0 +1,5 @@
#!/bin/sh
if [ "$POMO_STATE" == "BREAKING" ] ; then
swaynag -m "It's time to take a break!"
fi

89
man/pomo.1 Normal file
View File

@ -0,0 +1,89 @@
.\" Generated by scdoc 1.11.2
.\" Complete documentation for this program is not available as a GNU info page
.ie \n(.g .ds Aq \(aq
.el .ds Aq '
.nh
.ad l
.\" Begin generated content:
.TH "pomo" "1" "2022-05-30"
.P
.SH NAME
.P
\fBPomo\fR is a simple CLI for using the Pomodoro Technique.\&
.P
.SH SYNOPSIS
.P
\fBpomo\fR [OPTIONS] COMMAND [arg.\&.\&.\&]
.P
.SH DESCRIPTION
.P
\fBpomo\fR helps you track what you did, how long it took you to do it,
and how much effort you expect it to take.\&
.P
The Pomodoro Technique is simple and effective:
.P
.RS 4
\fB\fR Decide on a task you want to accomplish
\fB\fR Break the task into timed intervals (pomodoros), [approx.\& 25 min]
\fB\fR After each pomodoro take a short break [approx.\& 3 - 5 min]
\fB\fR Once all pomodoros are completed take a longer break [approx 15 - 20 min]
\fB\fR Repeat
.P
.RE
.SH SUBCOMMANDS
.P
See --help for the complete command usage
.P
.nf
.RS 4
start, s start a new task
init initialize the sqlite database
config, cf display the current configuration
create, c create a new task without starting
begin, b begin requested pomodoro
list, l list historical tasks
delete, d delete a stored task
status, st output the current status
.fi
.RE
.P
.SH CONFIGURATION
.P
Pomo has a configuration file that is stored in \fB~/.\&config/pomo/config.\&json\fR.\&
.P
.nf
.RS 4
{
"colors": null,
"dateTimeFmt": "2006-01-02 15:04",
"publish": false,
"publishJson": false,
"publishSocketPath": ""
}
.fi
.RE
.P
.SH EXAMPLES
.P
.SS GETTING STARTED
.P
.nf
.RS 4
# ensure your database has been initialized
pomo init
# run a new pomodoro
pomo start -t my-project "write some code"
# once finished view previously completed pomodoros
pomo list
.fi
.RE
.P
.SH SEE ALSO
.P
See the pomo source repository on Github at https://github.\&com/kevinschoon/pomo for complete documentation.\&
.P
.SH AUTHORS
.P
Written by Kevin Schoon <me@kevinschoon.\&com> with help from the open source
community.\&

74
man/pomo.1.scd Normal file
View File

@ -0,0 +1,74 @@
pomo(1)
# NAME
*Pomo* is a simple CLI for using the Pomodoro Technique.
# SYNOPSIS
*pomo* [OPTIONS] COMMAND [arg...]
# DESCRIPTION
*pomo* helps you track what you did, how long it took you to do it,
and how much effort you expect it to take.
The Pomodoro Technique is simple and effective:
** Decide on a task you want to accomplish
** Break the task into timed intervals (pomodoros), [approx. 25 min]
** After each pomodoro take a short break [approx. 3 - 5 min]
** Once all pomodoros are completed take a longer break [approx 15 - 20 min]
** Repeat
# SUBCOMMANDS
See --help for the complete command usage
```
start, s start a new task
init initialize the sqlite database
config, cf display the current configuration
create, c create a new task without starting
begin, b begin requested pomodoro
list, l list historical tasks
delete, d delete a stored task
status, st output the current status
```
# CONFIGURATION
Pomo has a configuration file that is stored in *~/.config/pomo/config.json*.
```
{
"colors": null,
"dateTimeFmt": "2006-01-02 15:04",
"publish": false,
"publishJson": false,
"publishSocketPath": ""
}
```
# EXAMPLES
## GETTING STARTED
```
# ensure your database has been initialized
pomo init
# run a new pomodoro
pomo start -t my-project "write some code"
# once finished view previously completed pomodoros
pomo list
```
# SEE ALSO
See the pomo source repository on Github at https://github.com/kevinschoon/pomo for complete documentation.
# AUTHORS
Written by Kevin Schoon <me@kevinschoon.com> with help from the open source
community.

View File

@ -23,6 +23,7 @@ type Config struct {
DBPath string `json:"dbPath"` DBPath string `json:"dbPath"`
SocketPath string `json:"socketPath"` SocketPath string `json:"socketPath"`
IconPath string `json:"iconPath"` IconPath string `json:"iconPath"`
OnEvent []string `json:"onEvent"`
// Publish pushes updates to the configured // Publish pushes updates to the configured
// SocketPath rather than listening for requests // SocketPath rather than listening for requests
Publish bool `json:"publish"` Publish bool `json:"publish"`

View File

@ -2,6 +2,10 @@ package pomo
import ( import (
"database/sql" "database/sql"
"fmt"
"os"
"os/exec"
"sync"
"time" "time"
) )
@ -19,6 +23,8 @@ type TaskRunner struct {
toggle chan bool toggle chan bool
notifier Notifier notifier Notifier
duration time.Duration duration time.Duration
mu sync.Mutex
onEvent []string
} }
func NewMockedTaskRunner(task *Task, store *Store, notifier Notifier) (*TaskRunner, error) { func NewMockedTaskRunner(task *Task, store *Store, notifier Notifier) (*TaskRunner, error) {
@ -28,7 +34,7 @@ func NewMockedTaskRunner(task *Task, store *Store, notifier Notifier) (*TaskRunn
nPomodoros: task.NPomodoros, nPomodoros: task.NPomodoros,
origDuration: task.Duration, origDuration: task.Duration,
store: store, store: store,
state: State(0), state: CREATED,
pause: make(chan bool), pause: make(chan bool),
toggle: make(chan bool), toggle: make(chan bool),
notifier: notifier, notifier: notifier,
@ -53,6 +59,7 @@ func NewTaskRunner(task *Task, config *Config) (*TaskRunner, error) {
toggle: make(chan bool), toggle: make(chan bool),
notifier: NewXnotifier(config.IconPath), notifier: NewXnotifier(config.IconPath),
duration: task.Duration, duration: task.Duration,
onEvent: config.OnEvent,
} }
return tr, nil return tr, nil
} }
@ -71,6 +78,34 @@ func (t *TaskRunner) TimePauseDuration() time.Duration {
func (t *TaskRunner) SetState(state State) { func (t *TaskRunner) SetState(state State) {
t.state = state t.state = state
// execute onEvent command if variable is set
if t.onEvent != nil {
go t.runOnEvent()
}
}
// execute script command specified by `onEvent` on state change
func (t *TaskRunner) runOnEvent() error {
var cmd *exec.Cmd
// parse command arguments
numArgs := len(t.onEvent)
app := t.onEvent[0]
if numArgs > 1 {
args := t.onEvent[1:(numArgs + 1)]
cmd = exec.Command(app, args...)
} else {
cmd = exec.Command(app)
}
// set state in command environment
cmd.Env = append(os.Environ(),
fmt.Sprintf("POMO_STATE=%s", t.state),
)
// run command
err := cmd.Run()
if err != nil {
return err
}
return nil
} }
func (t *TaskRunner) run() error { func (t *TaskRunner) run() error {
@ -90,7 +125,6 @@ func (t *TaskRunner) run() error {
loop: loop:
select { select {
case <-timer.C: case <-timer.C:
t.SetState(BREAKING)
t.stopped = time.Now() t.stopped = time.Now()
t.count++ t.count++
case <-t.toggle: case <-t.toggle:
@ -126,7 +160,7 @@ func (t *TaskRunner) run() error {
if t.count == t.nPomodoros { if t.count == t.nPomodoros {
break break
} }
t.SetState(BREAKING)
t.notifier.Notify("Pomo", "It is time to take a break!") t.notifier.Notify("Pomo", "It is time to take a break!")
// Reset the duration incase it // Reset the duration incase it
// was paused. // was paused.
@ -141,15 +175,25 @@ func (t *TaskRunner) run() error {
} }
func (t *TaskRunner) Toggle() { func (t *TaskRunner) Toggle() {
t.mu.Lock()
defer t.mu.Unlock()
if t.state == BREAKING {
t.toggle <- true t.toggle <- true
} }
}
func (t *TaskRunner) Pause() { func (t *TaskRunner) Pause() {
t.mu.Lock()
defer t.mu.Unlock()
if t.state == PAUSED || t.state == RUNNING {
t.pause <- true t.pause <- true
} }
}
func (t *TaskRunner) Status() *Status { func (t *TaskRunner) Status() *Status {
return &Status{ return &Status{
TaskID: t.taskID,
TaskMessage: t.taskMessage,
State: t.state, State: t.state,
Count: t.count, Count: t.count,
NPomodoros: t.nPomodoros, NPomodoros: t.nPomodoros,

View File

@ -12,6 +12,8 @@ type State int
func (s State) String() string { func (s State) String() string {
switch s { switch s {
case CREATED:
return "CREATED"
case RUNNING: case RUNNING:
return "RUNNING" return "RUNNING"
case BREAKING: case BREAKING:
@ -25,7 +27,8 @@ func (s State) String() string {
} }
const ( const (
RUNNING State = iota + 1 CREATED State = iota
RUNNING
BREAKING BREAKING
COMPLETE COMPLETE
PAUSED PAUSED
@ -102,6 +105,8 @@ func (p Pomodoro) Duration() time.Duration {
// Status is used to communicate the state // Status is used to communicate the state
// of a running Pomodoro session // of a running Pomodoro session
type Status struct { type Status struct {
TaskID int `json:"task_id"`
TaskMessage string `json:"task_message"`
State State `json:"state"` State State `json:"state"`
Remaining time.Duration `json:"remaining"` Remaining time.Duration `json:"remaining"`
Pauseduration time.Duration `json:"pauseduration"` Pauseduration time.Duration `json:"pauseduration"`

View File

@ -14,6 +14,8 @@ func setContent(wheel *Wheel, status *Status, par *widgets.Paragraph) {
par.Text = fmt.Sprintf( par.Text = fmt.Sprintf(
`[%d/%d] Pomodoros completed `[%d/%d] Pomodoros completed
Current Task: %s
%s %s remaining %s %s remaining
@ -21,6 +23,7 @@ func setContent(wheel *Wheel, status *Status, par *widgets.Paragraph) {
`, `,
status.Count, status.Count,
status.NPomodoros, status.NPomodoros,
status.TaskMessage,
wheel, wheel,
status.Remaining, status.Remaining,
) )
@ -29,26 +32,29 @@ func setContent(wheel *Wheel, status *Status, par *widgets.Paragraph) {
par.Text = fmt.Sprintf( par.Text = fmt.Sprintf(
`It is time to take a break! `It is time to take a break!
Once you are ready, press [Enter] Once you are ready, press [Enter]
to begin the next Pomodoro to begin the next Pomodoro
%s %s pause duration %s %s break duration
[q] - quit [p] - pause [q] - quit
`, `,
wheel, wheel,
status.Pauseduration, status.Pauseduration,
) )
case PAUSED: case PAUSED:
par.Text = `Pomo is suspended. par.Text = fmt.Sprintf(`Pomo is suspended.
Current Task: %s
Press [p] to continue. Press [p] to continue.
[q] - quit [p] - unpause [q] - quit [p] - unpause
` `,
status.TaskMessage,
)
case COMPLETE: case COMPLETE:
par.Text = `This session has concluded. par.Text = `This session has concluded.
@ -83,16 +89,20 @@ func StartUI(runner *TaskRunner) {
resize := func() { resize := func() {
termWidth, termHeight := ui.TerminalDimensions() termWidth, termHeight := ui.TerminalDimensions()
// for RUNNING and PAUSED states
x1 := (termWidth - 50) / 2 x1 := (termWidth - 50) / 2
x2 := x1 + 50 x2 := x1 + 50
y1 := (termHeight - 8) / 2 y1 := (termHeight - 10) / 2
y2 := y1 + 8 y2 := y1 + 10
switch runner.state { switch runner.state {
case BREAKING: case BREAKING:
y1 = (termHeight - 12) / 2 y1 = (termHeight - 11) / 2
y2 = y1 + 12 y2 = y1 + 11
case COMPLETE:
y1 = (termHeight - 8) / 2
y2 = y1 + 8
} }
par.SetRect(x1, y1, x2, y2) par.SetRect(x1, y1, x2, y2)