diff --git a/.config/yadm/bootstrap.d/11-ruby b/.config/yadm/bootstrap.d/11-ruby new file mode 100755 index 0000000..9400891 --- /dev/null +++ b/.config/yadm/bootstrap.d/11-ruby @@ -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 diff --git a/.todo.actions.d/pom/README.md b/.todo.actions.d/pom/README.md new file mode 100644 index 0000000..330a648 --- /dev/null +++ b/.todo.actions.d/pom/README.md @@ -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: + + diff --git a/.todo.actions.d/pom/lib/countdown.rb b/.todo.actions.d/pom/lib/countdown.rb new file mode 100644 index 0000000..570f3dc --- /dev/null +++ b/.todo.actions.d/pom/lib/countdown.rb @@ -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 diff --git a/.todo.actions.d/pom/lib/file_logger.rb b/.todo.actions.d/pom/lib/file_logger.rb new file mode 100644 index 0000000..09f9d0b --- /dev/null +++ b/.todo.actions.d/pom/lib/file_logger.rb @@ -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 diff --git a/.todo.actions.d/pom/lib/pomo_logger.rb b/.todo.actions.d/pom/lib/pomo_logger.rb new file mode 100644 index 0000000..ef869f1 --- /dev/null +++ b/.todo.actions.d/pom/lib/pomo_logger.rb @@ -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 diff --git a/.todo.actions.d/pom/lib/task.rb b/.todo.actions.d/pom/lib/task.rb new file mode 100644 index 0000000..e356af5 --- /dev/null +++ b/.todo.actions.d/pom/lib/task.rb @@ -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 diff --git a/.todo.actions.d/pom/lib/terminal_notifier_logger.rb b/.todo.actions.d/pom/lib/terminal_notifier_logger.rb new file mode 100644 index 0000000..7a28022 --- /dev/null +++ b/.todo.actions.d/pom/lib/terminal_notifier_logger.rb @@ -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 diff --git a/.todo.actions.d/pom/pom b/.todo.actions.d/pom/pom new file mode 100755 index 0000000..cfd939d --- /dev/null +++ b/.todo.actions.d/pom/pom @@ -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 diff --git a/.todo.actions.d/pom/screenshot.png b/.todo.actions.d/pom/screenshot.png new file mode 100644 index 0000000..2fc3063 Binary files /dev/null and b/.todo.actions.d/pom/screenshot.png differ diff --git a/.todo.actions.d/pom/spec/task_spec.rb b/.todo.actions.d/pom/spec/task_spec.rb new file mode 100644 index 0000000..3efbb07 --- /dev/null +++ b/.todo.actions.d/pom/spec/task_spec.rb @@ -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 +