aboutsummaryrefslogtreecommitdiff
path: root/gromo
blob: f5da8d91ee77fd712b47846f6662aeb8dd655c1c (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
#!/usr/bin/env bash
#
# gromo - To count your daily Gromodoros :thumbsup. A Gromo is a short unit of
#         time, usually 20 minutes, in which you are committed to do your work
#         and only your work. It's basically the Pomodoro technique, but more
#         opinionated. In particular, it's designed to implement the advice an
#         optician once gave me, the "20/20/20" rule, which is: for each 20
#         minutes looking at the screen, look away at least 20 meters away for
#         at least 20 seconds. Also, not every type of work benefits from the
#         interruptions 5-minute rests provoke. So, if the mainstream is not
#         enough for you, enter the all-flexible, eye-careful, non-interrupting
#         Gromodoros.
#
# Dependencies: curl and mpv for the 'ding' sound; slock to force you to rest :)
################################################################################

shopt -s nullglob

DEFAULT_DING=https://gramos.me/sounds/ding.opus

DATA_DIR=${XDG_DATA_HOME:-$HOME/.local/share}/gromo
CACHE_DIR=${XDG_CACHE_HOME:-$HOME/.cache}/gromo

TODAY_DIR=$DATA_DIR/$(date '+%Y-%m-%d')
DING_FILE=$CACHE_DIR/ding.opus
STATE_FILE=${XDG_STATE_HOME:-$HOME/.local/state}/gromo

FS=:

gromo_duration=$((60 * 20))
stop_duration=20

cmd_help() {
    cmd=$(basename "$0")

    echo -e "Usage:
  $cmd -h              \tShow this help
  $cmd -x              \tShow status formatted for xmobar
  $cmd -l              \tList past gromos

  $cmd                 \tShow status
  $cmd [options] <task>\tStart task

  Options:

    -1
        Oneshot: stop after first gromo is completed without blocking the screen.

    -t
        test the 'ding' sound

    -d <dur>
        Set custom gromo duration (format: Ns, Nm, Nh).

    -s <dur>
        Set custom stop duration (format: Ns, Nm, Nh). Ignored if combined with -1.

Tasks can be divided in subtasks by using dots (.), e.g., 'gromo work.retro'. Only the first level
is taken into account for organization purposes: the top-level task is considered the 'main task'
and a file is created after it to account for its subtasks.
    "
}

cmd_list() {
    find "$DATA_DIR/" -type f -print0 | xargs -0 -n 1 basename | sort -u
}

cmd_status() {
    format=$1

    status=idle
    [ -f "$STATE_FILE" ] && status=$(cat "$STATE_FILE")

    if [ "$format" = "xmobar" ]; then
        color=red
        [ "$status" = "idle" ] && color=green

        if [ -d "$TODAY_DIR" ]; then
            today=""
            # needs nullglob
            for task_file in "$TODAY_DIR"/*; do
                today+="$(basename "$task_file"): $(sum_subtasks "$task_file"), "
            done
            [ -n "$today" ] && today=" (${today%, })"
        fi

        echo "<fc=$color>$status</fc>$today"
    else
        echo -e "status: $status\n"

        if [ -d "$TODAY_DIR" ]; then
            # needs nullglob
            for task_file in "$TODAY_DIR"/*; do
                out+=$(
                    gawk -F $FS -f - "$task_file" <<EOF
{ tasks[\$1][\$2] = \$3 }
END {
    for (task in tasks) {
        gromos = time = 0
        for (dur in tasks[task]) {
            gromos += tasks[task][dur];
            time += dur * tasks[task][dur]
        };
        print task ":" time ":" gromos
    }
}
EOF
                )
                out+="\n"
            done

            sortedout=$(printf '%s' "$out" | sort -t $FS -k 2 -rn)
            if [ -n "$sortedout" ]; then
                while IFS=$FS read -r task dur gromos; do
                    echo -e "$task\n\t$gromos gromos, $(pp_seconds "$dur")"
                done <<<"$sortedout"
            fi
        fi
    fi
}

parse_duration() {
    duration=$1
    case ${duration:0,-1} in
        s)
            echo "${duration%s}"
            ;;
        m)
            echo $((${duration%m} * 60))
            ;;
        h)
            echo $((${duration%h} * 60 * 60))
            ;;
        *)
            echo "ERROR: Invalid duration format. Must be <dur>[smh]." >&2
            return 1
            ;;
    esac
}

sum_subtasks() {
    file=$1
    awk -F $FS '{ sum += $3 } END { print sum }' "$file"
}

inc_subtask() {
    task=$1
    maintask="${fulltask%%.*}"
    task_file="$TODAY_DIR/$maintask"
    if [ ! -f "$task_file" ]; then
        touch "$task_file"
    fi

    awk -F $FS -f - "$task_file" > tmp <<EOF
BEGIN { OFS = FS }
\$1 == "$task" && \$2 == "$gromo_duration" { \$3 += 1; added=1 }
{ print }
END { if (!added) { print "$task" FS $gromo_duration FS 1 } }
EOF
    mv tmp "$task_file"
}

pp_seconds() {
    seconds=$1
    if [ "$seconds" -lt 60 ]; then
        echo "$seconds seconds"
    elif [ "$seconds" -lt 3600 ]; then
        echo "$((seconds / 60)) minutes"
    else
        echo "$((seconds / 3600)) hours"
    fi
}

optspec="1hxltd:s:"
while getopts "$optspec" optchar; do
    case "$optchar" in
        1)
            oneshot=1
            ;;
        h)
            cmd_help
            exit 0
            ;;
        x)
            cmd_status xmobar
            exit 0
            ;;
        l)
            cmd_list
            exit 0
            ;;
        t)
            mpv --no-terminal "$DING_FILE" &
            exit 0
            ;;
        d)
            gromo_duration=$(parse_duration "${OPTARG}") || exit 1
            ;;
        s)
            stop_duration=$(parse_duration "${OPTARG}") || exit 1
            ;;
        *)
            cmd_help
            exit 1
            ;;
    esac
done
shift $((OPTIND - 1))

if [ "$#" -eq 1 ]; then
    fulltask="$1"
else
    cmd_status
    exit 0
fi

mkdir -p "$DATA_DIR" "$TODAY_DIR" "$(dirname "$STATE_FILE")"

[ -f "$DING_FILE" ] || curl $DEFAULT_DING --create-dirs -so "$DING_FILE"

if [ -f "$STATE_FILE" ]; then
    echo "Another instance is currently running; exiting (remove $STATE_FILE to override)"
    exit 1
fi
echo idle > "$STATE_FILE"
trap 'rm $STATE_FILE 2> /dev/null; exit' INT TERM EXIT

while true; do
    echo -ne "\r\033[K[** IN PROGRESS: $fulltask **] "

    echo "$fulltask" > "$STATE_FILE"
    sleep "$gromo_duration"
    echo idle > "$STATE_FILE"

    inc_subtask "$fulltask"

    if [ "$oneshot" = "1" ]; then
        mpv --no-terminal "$DING_FILE" &
        exit 0
    fi

    (sleep "$stop_duration" && mpv --no-terminal "$DING_FILE") &
    slock || exit
done