Compare commits
10 Commits
1cac8904da
...
309277a115
Author | SHA1 | Date |
---|---|---|
Kevin Schoon | 309277a115 | |
Sam Boysel | 8113c0a933 | |
Kevin Schoon | e6aa45152c | |
Sam Boysel | f86c5a6436 | |
Sam Boysel | 81cb8f568f | |
Sam Boysel | 3ba07e9a87 | |
Sam Boysel | caded9b68b | |
Sam Boysel | 3d3a2bc152 | |
Kevin Schoon | 6236144041 | |
Kevin Schoon | 1b321198fb |
8
Makefile
8
Makefile
|
@ -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
|
||||||
|
|
||||||
|
|
29
README.md
29
README.md
|
@ -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,0 +1,3 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
echo -e '\e'
|
|
@ -0,0 +1,5 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
if [ "$POMO_STATE" == "BREAKING" ] ; then
|
||||||
|
swaynag -m "It's time to take a break!"
|
||||||
|
fi
|
|
@ -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.\&
|
|
@ -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.
|
|
@ -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"`
|
||||||
|
|
|
@ -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.toggle <- true
|
t.mu.Lock()
|
||||||
|
defer t.mu.Unlock()
|
||||||
|
if t.state == BREAKING {
|
||||||
|
t.toggle <- true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *TaskRunner) Pause() {
|
func (t *TaskRunner) Pause() {
|
||||||
t.pause <- true
|
t.mu.Lock()
|
||||||
|
defer t.mu.Unlock()
|
||||||
|
if t.state == PAUSED || t.state == RUNNING {
|
||||||
|
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,
|
||||||
|
|
|
@ -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"`
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in New Issue