#!/bin/bash
#
# Tatsumato - Tatsumoto's Pomodoro timer.
#
# Copyright (C) 2020-2023 Ren Tatsumoto
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
readonly lock_timeout=10s
readonly lock_color=111111
readonly ankiconnect_url=127.0.0.1:8765
while getopts 'a A p H v h s i d t: b: l: L: k:' flag; do
case $flag in
H) human=false ;;
v) verbose=true ;;
h) disphelp=true ;;
s) silent=true ;;
i) use_i3lock=true ;;
p) control_player=true ;;
a) control_anki=true ;;
A) focus_anki=true ;;
d) dmenu_nag=true ;;
t) pomtime=${OPTARG} ;;
b) brktime=${OPTARG} ;;
l) lngbrkt=${OPTARG} ;;
L) longbrk=${OPTARG} ;;
k) endtime=${OPTARG} ;;
*) echo "Unknown argument ${flag}" && exit 1 ;;
esac
done
readonly human=${human:-true}
readonly verbose=${verbose:-false}
readonly disphelp=${disphelp:-false}
readonly silent=${silent:-false}
readonly use_i3lock=${use_i3lock:-false}
readonly control_player=${control_player:-false}
readonly control_anki=${control_anki:-false}
readonly focus_anki=${focus_anki:-false}
readonly dmenu_nag=${dmenu_nag:-false}
readonly pomtime=${pomtime:-25}
readonly brktime=${brktime:-5}
readonly lngbrkt=${lngbrkt:-10}
readonly longbrk=${longbrk:-3}
readonly endtime=${endtime:-0}
show_help() {
local -r prog=$(basename -- "$0")
cat <<-END
$prog - Pomodoro productivity shell script.
Options:
END
column -N key,description -W description -d -t -s'|' <<-'EOF'
-t [minutes]|Set the amount of minutes a pomodoro lasts. Default 25.
-b [minutes]|Set the amount of minutes a short break lasts. Default 5.
-l [minutes]|Set the amount of minutes a long break lasts. Default 10.
-L [number]|Set the amount of pomodoros before triggering a long break. Default 3.
-k [number]|Set the amount of pomodoros before ending the script. 0 means the script will run until stopped by the user. Default 0.
-H|Disable human mode.
-h|Display this help text and exit.
-v|Enable verbose mode. Echo the pomodoro status to your terminal. Off by default.
-s|Silent mode. Notification sound is not played.
-i|Run i3lock when a pomodoro is over.
-p|Pause/unpause mpv between breaks.
-a|Control Anki. Close Anki's review screen before a break starts.
-A|Focus Anki. Focus Anki's window after a break ends.
-d|Show a dmenu (or rofi) dialog after each pomodoro.
EOF
cat <<-END
Notes:
Create a shell alias with the options you like. For example:
alias pom="$prog -a -i -p -t 13 -b 2 -l 3"
END
}
assert_installed() {
local x
for x; do
if ! command -v "$x" >/dev/null 2>&1; then
echo "Error: $x is not installed." >&2
exit 1
fi
done
}
play_bell() {
assert_installed paplay
paplay /usr/share/sounds/freedesktop/stereo/complete.oga &
}
notify() {
assert_installed notify-send
$verbose && echo "$@"
notify-send "Pomodoro" "$*" >/dev/null 2>&1 &
$silent || play_bell
}
do_pomodoro() {
local -r time=$1 mode=$2
for ((i = time; i > 0; i--)); do
$human && printf -- '\r'
printf -- '%im left of %s ' "$i" "${mode,,}"
$human || printf -- '\n'
sleep 1m
done
}
close_review_window() {
assert_installed curl
# If you use Pomodoro while doing your Anki reps,
# this function will close the review window before a break starts.
# Requires AnkiConnect to work.
curl \
-fsS "$ankiconnect_url" \
-X POST \
-d '{ "action": "guiDeckBrowser", "version": 6 }' >/dev/null &
}
focus_window() {
assert_installed i3-msg
i3-msg -q "[class=\"$1\"] focus" || true
}
setallmpv() {
# Sends pause/play commands to mpv.
# Requires the mpvSockets user-script for mpv.
assert_installed socat i3-msg
local cmd='{ "command": ["set_property", "pause", ] }'
if [[ $1 == play ]]; then
local -r cmd=${cmd//false}
focus_window mpv
else
local -r cmd=${cmd//true}
fi
for i in /tmp/mpvSockets/*; do
echo "$cmd" | socat - "$i"
done
}
resume_player() {
setallmpv play >/dev/null 2>&1
}
pause_player() {
setallmpv pause >/dev/null 2>&1
}
lock_screen() {
assert_installed i3lock
(
sleep "$lock_timeout"
i3lock --color="$lock_color"
) &
}
unlock_screen() {
killall i3lock 2>/dev/null || true
}
call_dmenu() {
if command -v rofi >/dev/null; then
rofi -dmenu "$@"
else
assert_installed dmenu
dmenu "$@"
fi
}
dmenu_report() {
cat <<-EOF | call_dmenu -l 30 -i -p "Finished 🍠$pomcount pomodoros."
☕ start ${mode,,} ($time min)
🚪 exit
EOF
}
dmenu_nagscreen() {
$silent || play_bell
case $(dmenu_report) in
*start*) return ;;
*exit) exit ;;
*) echo "Invalid command or no command provided." && exit 1 ;;
esac
}
main() {
$disphelp && show_help && exit
local endcount=0 pomcount=0 mode=Pomodoro time=$pomtime
while true; do
notify "${mode^} will last for $time minute(s)."
do_pomodoro "$time" "$mode"
if [[ ${mode,,} == "pomodoro" ]]; then
# after pomodoro
pomcount=$((pomcount + 1)) endcount=$((endcount + 1))
if [[ $endtime -gt 0 ]] && [[ $endcount -ge $endtime ]]; then
echo "Finished."
return
fi
if [[ $pomcount -eq $longbrk ]]; then
mode="Long break" time=$lngbrkt
else
mode="Short break" time=$brktime
fi
$dmenu_nag && dmenu_nagscreen
$control_player && resume_player
$use_i3lock && lock_screen
$control_anki && close_review_window
else
# after break
mode="Pomodoro" time=$pomtime
$use_i3lock && unlock_screen
$control_player && pause_player
$dmenu_nag && dmenu_nagscreen
$focus_anki && focus_window Anki
fi
if [[ ${mode,,} == "long break" ]]; then
pomcount=0
fi
done
}
main