Add todo.txt pom and its dependencies

This commit is contained in:
Ethan Lane 2025-05-02 10:47:44 +01:00
parent 1e7adfa51c
commit af5315c5b9
10 changed files with 403 additions and 0 deletions

View file

@ -0,0 +1,7 @@
#!/bin/bash
if command -v ruby >/dev/null 2>&1; then
exit 0
else
sudo pacman -S ruby
gem install rainbow terminal-notifier
fi

View file

@ -0,0 +1,36 @@
## Pomodori-todo.txt
A pomodoro counter implementation for [Todo.txt](http://todotxt.com/).
## What it features
* easily plan, run timers, and show current tasks status via the command line
* allows tmux integration to display the remaining pomodoro timer in your tmux statusbar
* logs the pomodori start time and end time to a log file for an easy history
* uses the terminal-notifier gem to notify you when your pomodoro starts and ends
## Installation
* Make sure you have ruby installed
* run `gem install rainbow terminal-notifier`
* Download this repo somewhere on your filesystem
* symlink the `pom` executable into your TODO_ACTIONS_DIR
## Usage:
todo.sh pom action [task_number] [args]
Actions:
ls
lists tasks with pomodori count and estimates
log
displays a log of your pomodori
start ITEM#
start a pomodoro timer for item ITEM#
add ITEM#
add one pomodoro to task on line ITEM# without running a timer
plan ITEM# PLANNED_POMODORI
estimate ITEM# will take PLANNED_POMODORI to complete
## Example:
<img src="https://raw.github.com/metalelf0/pomodori-todo.txt/master/screenshot.png">

View file

@ -0,0 +1,39 @@
class Countdown
def run seconds, options={}
seconds.downto(0) do |current_seconds|
sleep 1
write_tmux(current_seconds) if (options[:services] || []).include?(:tmux)
set_window_title(current_seconds) if (options[:services] || []).include?(:iterm2)
STDOUT.write "[RUNNING] #{to_minutes(current_seconds)}\r"
end
end
private
def write_tmux(current_seconds)
`echo #{to_minutes(current_seconds, :tmux)} > ~/.pomo.txt.tmux`
end
def set_window_title(current_seconds)
title = to_minutes(current_seconds, :tmux)
`echo -ne "\e]1;#{title}\a"`
end
def to_minutes seconds, format=:standard
minutes = seconds / 60
mins = sprintf("%02d", minutes)
secs = sprintf("%02d", seconds % 60)
if format == :standard
"#{mins}:#{secs} [#{dots_for(minutes)}]"
else
"#{mins}:#{secs}"
end
end
def dots_for mins
"#{'.' * (25 - mins)}#{' ' * mins}"
end
end

View file

@ -0,0 +1,29 @@
require 'fileutils'
class FileLogger
attr_accessor :pomodoro_log_file
def initialize log_file_path
@pomodoro_log_file = log_file_path
FileUtils.touch pomodoro_log_file
end
def notify_start task
add_separator_if_new_day
File.open(pomodoro_log_file, "a") { |file| file.puts "#{Time.now.strftime('%Y/%m/%d %H:%M')} Pomodoro nr. #{sprintf("% 2d", task.pomodori + 1)} started: #{task.text}" }
end
def notify_completed task
File.open(pomodoro_log_file, "a") { |file| file.puts "#{Time.now.strftime('%Y/%m/%d %H:%M')} Pomodoro nr. #{sprintf("% 2d", task.pomodori + 1)} completed: #{task.text}" }
end
private
def add_separator_if_new_day
return if File.zero?(pomodoro_log_file)
last_day = File.open(pomodoro_log_file, "r") { |file| file.readlines.last.split(" ")[0]}
today = Time.now.strftime('%Y/%m/%d')
File.open(pomodoro_log_file, "a") { |file| file.puts "\n-----------\n\n" } if last_day != today
end
end

View file

@ -0,0 +1,19 @@
class PomoLogger
attr_accessor :options
def initialize opts
@options = opts
end
def log_pomodoro_started task
TerminalNotifierLogger.new.notify_start(task)
FileLogger.new(options[:pomodoro_log_file]).notify_start(task)
end
def log_pomodoro_completed task
TerminalNotifierLogger.new.notify_completed(task)
FileLogger.new(options[:pomodoro_log_file]).notify_completed(task)
end
end

View file

@ -0,0 +1,63 @@
require 'rainbow'
class Task
attr_accessor :text
attr_accessor :index
attr_accessor :pomodori
attr_accessor :planned
POMO_REGEXP = / \(#pomo: (\d+)\/(\d+)\)$/
PRIORITY_REGEXP = /^\([A-Z]+\) /
STATUS_COLORS = {
new: :white,
planned: :green,
in_progress: :yellow,
completed: :blue,
underestimated: :red
}
def initialize index, text
@index = index.to_i
if text =~ POMO_REGEXP
@pomodori, @planned = text.match(POMO_REGEXP)[1].to_i, text.match(POMO_REGEXP)[2].to_i
else
@pomodori, @planned = 0, 0
end
@text = text.gsub(POMO_REGEXP, '').gsub(PRIORITY_REGEXP, '')
end
def to_s
"#{text} (#pomo: #{pomodori}/#{planned})"
end
def add_pomo
@pomodori += 1
end
def plan planned
@planned = planned.to_i
end
def puts_highlighted
return if blank?(text)
print "#{index} #{text} "
color = STATUS_COLORS[self.status]
puts Rainbow("(#pomo: #{pomodori}/#{planned})").send(color)
end
def status
return :new if pomodori == 0 && planned == 0
return :planned if pomodori == 0 && planned != 0
return :completed if pomodori == planned
return :in_progress if pomodori != 0 && planned != 0 && pomodori < planned
return :underestimated if pomodori != 0 && pomodori > planned
end
private
def blank?(string)
string == nil || string == ""
end
end

View file

@ -0,0 +1,9 @@
class TerminalNotifierLogger
def notify_start task
TerminalNotifier.notify('Pomodoro started', title: 'Pomotxt', sound: 'Glass')
end
def notify_completed task
TerminalNotifier.notify('Pomodoro completed!', title: 'Pomotxt', sound: 'Glass')
end
end

94
.todo.actions.d/pom/pom Executable file
View file

@ -0,0 +1,94 @@
#!/usr/bin/env ruby
require 'date'
require 'rainbow'
require 'terminal-notifier'
require_relative 'lib/task'
require_relative 'lib/countdown'
require_relative 'lib/pomo_logger'
require_relative 'lib/terminal_notifier_logger'
require_relative 'lib/file_logger'
POMODORO_SECONDS = 25 * 60
# TODO: either move this to a env variable or a yml configuration file
POMODORO_LOG_FILE = "#{ENV['HOME']}/Documents/pomodoro_log.txt"
def logger
PomoLogger.new(
:pomodoro_log_file => POMODORO_LOG_FILE
)
end
def read_todos
todos = File.read(ENV['TODO_FILE'])
todos.split(/\r?\n/)
end
def increase_pomos todos, index
todo_to_update = todos[index]
task = Task.new(index + 1, todo_to_update)
task.add_pomo
task.puts_highlighted
`todo.sh replace #{index + 1} "#{task.to_s}"`
end
def plan_task todos, index, planned
todo_to_update = todos[index]
task = Task.new(index + 1, todo_to_update)
task.plan(planned)
task.puts_highlighted
`todo.sh replace #{index + 1} "#{task.to_s}"`
end
def puts_and_exit string
puts string
exit
end
case ARGV[1]
when 'ls'
todos = read_todos
todos.each_with_index do |todo, index|
Task.new(index + 1, todo).puts_highlighted
end
when 'log'
puts `cat #{POMODORO_LOG_FILE}`
when 'add'
puts_and_exit "You must insert the task number" unless index = ARGV[2]
puts_and_exit "Task number must be... a number" unless index =~ /\d+/
index = index.to_i - 1
todos = read_todos
increase_pomos todos, index
when 'plan'
puts_and_exit "You must insert the task number" unless index = ARGV[2]
puts_and_exit "Task number must be... a number" unless index =~ /\d+/
puts_and_exit "You must insert the task pomodori estimate" unless planned = ARGV[3]
puts_and_exit "The task pomodori estimate must be a number" unless planned =~ /\d+/
index = index.to_i - 1
todos = read_todos
plan_task todos, index, planned
when 'start'
puts_and_exit "You must insert the task number" unless index = ARGV[2]
puts_and_exit "Task number must be... a number" unless index =~ /\d+/
index = index.to_i - 1
todos = read_todos
task_being_worked = Task.new(index + 1, todos[index])
logger.log_pomodoro_started(task_being_worked)
Countdown.new.run(POMODORO_SECONDS, services: [ :tmux, :iterm2 ])
logger.log_pomodoro_completed(task_being_worked)
increase_pomos todos, index
when 'help'
puts "Usage: todo.sh pomo action [task_number] [args]\n"
puts "Actions:"
puts " ls"
puts " lists tasks with pomodori count and estimates"
puts " log"
puts " displays a log of your pomodori"
puts " start ITEM#"
puts " start a pomodoro timer for item ITEM#"
puts " add ITEM#"
puts " add one pomodoro to task on line ITEM# without running a timer"
puts " plan ITEM# PLANNED_POMODORI"
puts " estimate ITEM# will take PLANNED_POMODORI to complete"
end

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

View file

@ -0,0 +1,107 @@
require_relative '../lib/task'
describe Task do
context "Task#new" do
context "When task already includes pomodori" do
subject { Task.new(1, "Write Task spec (#pomo: 3/5)")}
specify { expect(subject.pomodori).to eq(3) }
specify { expect(subject.planned).to eq(5) }
specify { expect(subject.text).to eq("Write Task spec") }
specify { expect(subject.to_s).to eq("Write Task spec (#pomo: 3/5)") }
end
context "When task does not include pomodori" do
subject { Task.new(1, "Write Task spec")}
specify { expect(subject.pomodori).to eq(0) }
specify { expect(subject.planned).to eq(0) }
specify { expect(subject.text).to eq("Write Task spec") }
specify { expect(subject.to_s).to eq("Write Task spec (#pomo: 0/0)") }
end
end
context "Task#add_pomo" do
context "Prioritized task" do
subject { Task.new(1, "(A) Write Task spec (#pomo: 3/5)")}
specify { expect(subject.pomodori).to eq(3) }
context "Adding pomo to a tagged task" do
before { subject.add_pomo }
specify { expect(subject.pomodori).to eq(4) }
specify { expect(subject.to_s).to eq("Write Task spec (#pomo: 4/5)") }
specify { subject.add_pomo; expect(subject.to_s).to eq("Write Task spec (#pomo: 5/5)") }
end
end
context "Already tagged task" do
subject { Task.new(1, "Write Task spec (#pomo: 3/5)")}
specify { expect(subject.pomodori).to eq(3) }
context "Adding pomo to a tagged task" do
before { subject.add_pomo }
specify { expect(subject.pomodori).to eq(4) }
specify { expect(subject.to_s).to eq("Write Task spec (#pomo: 4/5)") }
end
end
context "Normal task" do
subject { Task.new(1, "Write Task spec")}
specify { expect(subject.pomodori).to eq(0) }
context "Adding pomo to a normal task" do
before { subject.add_pomo }
specify { expect(subject.pomodori).to eq(1) }
specify { expect(subject.to_s).to eq("Write Task spec (#pomo: 1/0)") }
end
end
end
context "Task#plan" do
context "When task already includes pomodori" do
subject { Task.new(1, "Write Task spec (#pomo: 3/5)")}
before { subject.plan(10) }
specify { expect(subject.pomodori).to eq(3) }
specify { expect(subject.planned).to eq(10) }
specify { expect(subject.text).to eq("Write Task spec") }
specify { expect(subject.to_s).to eq("Write Task spec (#pomo: 3/10)") }
end
context "When task does not include pomodori" do
subject { Task.new(1, "Write Task spec")}
before { subject.plan(10) }
specify { expect(subject.pomodori).to eq(0) }
specify { expect(subject.planned).to eq(10) }
specify { expect(subject.text).to eq("Write Task spec") }
specify { expect(subject.to_s).to eq("Write Task spec (#pomo: 0/10)") }
end
end
context "Task#status" do
subject { Task.new(1, "Write Task spec")}
context "new task" do
specify { expect(subject.status).to eq :new }
end
context "planned task" do
before { subject.plan(10) }
specify { expect(subject.status).to eq :planned }
end
context "in progress task" do
before { subject.plan(10); subject.add_pomo }
specify { expect(subject.status).to eq :in_progress }
end
context "completed task" do
before { subject.plan(1); subject.add_pomo }
specify { expect(subject.status).to eq :completed }
end
context "underestimated task" do
before { subject.plan(1); 2.times { subject.add_pomo } }
specify { expect(subject.status).to eq :underestimated }
end
end
end