now-task-manager 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +66 -0
- data/bin/git-commit-pomodoro +4 -0
- data/bin/pomodoro +246 -0
- data/doc/benefits.md +91 -0
- data/doc/bitbar.md +2 -0
- data/doc/curses.md +5 -0
- data/doc/example/rules.rb +7 -0
- data/doc/formats/scheduled.md +32 -0
- data/doc/formats/today.md +115 -0
- data/doc/getting-started.md +41 -0
- data/doc/integrations.md +68 -0
- data/doc/rules.md +0 -0
- data/doc/schedules.md +0 -0
- data/doc/terms.md +32 -0
- data/doc/workflow.md +60 -0
- data/lib/pomodoro.rb +6 -0
- data/lib/pomodoro/commands/bitbar.rb +87 -0
- data/lib/pomodoro/config.rb +30 -0
- data/lib/pomodoro/exts/hour.rb +63 -0
- data/lib/pomodoro/formats/scheduled.rb +37 -0
- data/lib/pomodoro/formats/scheduled/parser/parser.rb +26 -0
- data/lib/pomodoro/formats/scheduled/parser/transformer.rb +21 -0
- data/lib/pomodoro/formats/scheduled/task_group.rb +40 -0
- data/lib/pomodoro/formats/scheduled/task_list.rb +90 -0
- data/lib/pomodoro/formats/today.rb +12 -0
- data/lib/pomodoro/formats/today/formatter.rb +26 -0
- data/lib/pomodoro/formats/today/parser/parser.rb +73 -0
- data/lib/pomodoro/formats/today/parser/transformer.rb +65 -0
- data/lib/pomodoro/formats/today/task.rb +92 -0
- data/lib/pomodoro/formats/today/task/dynamic_additions.rb +5 -0
- data/lib/pomodoro/formats/today/task/metadata.rb +37 -0
- data/lib/pomodoro/formats/today/task/statuses.rb +63 -0
- data/lib/pomodoro/formats/today/task_list.rb +50 -0
- data/lib/pomodoro/formats/today/time_frame.rb +113 -0
- data/lib/pomodoro/schedule/dsl.rb +68 -0
- data/lib/pomodoro/scheduler.rb +46 -0
- metadata +124 -0
@@ -0,0 +1,92 @@
|
|
1
|
+
require 'pomodoro/exts/hour'
|
2
|
+
require 'pomodoro/formats/today'
|
3
|
+
require 'pomodoro/formats/today/task/statuses'
|
4
|
+
require 'pomodoro/formats/today/task/dynamic_additions'
|
5
|
+
require 'pomodoro/formats/today/task/metadata'
|
6
|
+
|
7
|
+
module Pomodoro::Formats::Today
|
8
|
+
class Task
|
9
|
+
STATUS_MAPPING ||= {
|
10
|
+
not_done: ['-'],
|
11
|
+
done: ['✓', '✔', '☑'],
|
12
|
+
failed: ['✕', '☓', '✖', '✗', '✘', '☒'],
|
13
|
+
# wip: ['☐', '⛶', '⚬']
|
14
|
+
}
|
15
|
+
|
16
|
+
STATUS_LIST ||= STATUS_MAPPING.keys
|
17
|
+
|
18
|
+
include TaskStatuses
|
19
|
+
include DynamicAdditions
|
20
|
+
|
21
|
+
attr_reader :status, :body, :start_time, :end_time, :fixed_start_time, :duration, :tags, :lines
|
22
|
+
def initialize(status:, body:, start_time: nil, end_time: nil, fixed_start_time: nil, duration: nil, tags: [], lines: [])
|
23
|
+
@status, @body, @tags, @duration = status, body, tags, duration
|
24
|
+
@start_time, @end_time, @fixed_start_time = start_time, end_time, fixed_start_time
|
25
|
+
validate_data_integrity
|
26
|
+
end
|
27
|
+
|
28
|
+
def remaining_duration(current_time_frame)
|
29
|
+
@start_time || raise("The task #{self.inspect} hasn't been started yet.")
|
30
|
+
|
31
|
+
closing_time = @start_time + duration
|
32
|
+
interval_end_time = current_time_frame.interval[1]
|
33
|
+
end
|
34
|
+
|
35
|
+
def to_s
|
36
|
+
Formatter.format(self)
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
def format_duration
|
41
|
+
if @start_time && @end_time
|
42
|
+
[@start_time, @end_time].join('-')
|
43
|
+
elsif @start_time
|
44
|
+
"started at #{@start_time}"
|
45
|
+
elsif @end_time
|
46
|
+
raise 'nonsense'
|
47
|
+
else # nil
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def validate_nil_or_instance_of(expected_class, instance, var_name)
|
52
|
+
instance && (instance.is_a?(expected_class) ||
|
53
|
+
raise(ArgumentError.new("#{var_name} has to be an instance of #{expected_class}.")))
|
54
|
+
end
|
55
|
+
|
56
|
+
def validate_data_integrity
|
57
|
+
validate_nil_or_instance_of(Hour, @start_time, :start_time)
|
58
|
+
validate_nil_or_instance_of(Hour, @end_time, :end_time)
|
59
|
+
validate_nil_or_instance_of(Hour, @fixed_start_time, :fixed_start_time)
|
60
|
+
|
61
|
+
if @start_time.nil? && @end_time
|
62
|
+
raise ArgumentError.new("Setting end_time without start_time is invalid.")
|
63
|
+
end
|
64
|
+
|
65
|
+
if @start_time && @end_time && @start_time >= @end_time
|
66
|
+
raise ArgumentError.new("start_time has to be smaller than end_time.")
|
67
|
+
end
|
68
|
+
|
69
|
+
if @duration && ! (@duration.respond_to?(:integer?) && @duration.integer?)
|
70
|
+
raise ArgumentError.new("Duration has to be an integer.")
|
71
|
+
end
|
72
|
+
|
73
|
+
if @duration && ! (5..90).include?(@duration)
|
74
|
+
raise ArgumentError.new("Duration has between 5 and 90 minutes.")
|
75
|
+
end
|
76
|
+
|
77
|
+
unless STATUS_LIST.include?(@status)
|
78
|
+
raise ArgumentError.new("Status has to be one of #{STATUS_SYMBOLS.keys.inspect}.")
|
79
|
+
end
|
80
|
+
|
81
|
+
# Unstarted or in progress.
|
82
|
+
if @status == :not_done && @end_time
|
83
|
+
raise ArgumentError.new("A task with status :not_done cannot have an end_time!")
|
84
|
+
end
|
85
|
+
|
86
|
+
if [:done, :failed].include?(@status) && ! (
|
87
|
+
(@start_time && @end_time) || (! @start_time && ! @end_time))
|
88
|
+
raise ArgumentError.new("Task has ended. It can either have start_time and end_time or neither, not only one of them.")
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'pomodoro/formats/today'
|
2
|
+
|
3
|
+
module Pomodoro::Formats::Today
|
4
|
+
# - A task. #cool
|
5
|
+
# Log about how is it going with the task.
|
6
|
+
#
|
7
|
+
# One more paragraph.
|
8
|
+
#
|
9
|
+
# ✔ Subtask 1.
|
10
|
+
# ✘ Subtask 2.
|
11
|
+
# - Subtask 3.
|
12
|
+
#
|
13
|
+
# Postponed: I got bored of it.
|
14
|
+
class Metadata
|
15
|
+
attr_reader :lines, :subtasks, :metadata
|
16
|
+
def initialize(lines: [], subtasks: [], metadata: {})
|
17
|
+
@lines, @subtasks, @metadata = lines, subtasks, metadata
|
18
|
+
@lines.is_a?(Array) || raise(ArgumentError.new("Lines must be an array!"))
|
19
|
+
@subtasks.is_a?(Array) || raise(ArgumentError.new("Subtasks must be an array!"))
|
20
|
+
@metadata.is_a?(Hash) || raise(ArgumentError.new("Metadata must be an array!"))
|
21
|
+
end
|
22
|
+
|
23
|
+
def []=(key, value)
|
24
|
+
if @metadata['Postponed'] && key == 'Failed'
|
25
|
+
raise ArgumentError.new("....")
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def to_s
|
30
|
+
@lines.join("\n\n")
|
31
|
+
@subtasks.join("\n")
|
32
|
+
@metadata.reduce(Array.new) do |buffer, (key, value)|
|
33
|
+
buffer << "#{key}: #{value}"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module Pomodoro::Formats::Today
|
2
|
+
module TaskStatuses
|
3
|
+
# @!group Status checking methods
|
4
|
+
def ended?
|
5
|
+
[:done, :failed].include?(@status)
|
6
|
+
end
|
7
|
+
|
8
|
+
def unstarted?
|
9
|
+
@start_time.nil? && @status == :not_done
|
10
|
+
end
|
11
|
+
|
12
|
+
def skipped?(time_frame)
|
13
|
+
self.unstarted? && time_frame.ended?
|
14
|
+
end
|
15
|
+
|
16
|
+
def in_progress?
|
17
|
+
(! @start_time.nil? && @end_time.nil?) && @status == :not_done
|
18
|
+
end
|
19
|
+
|
20
|
+
alias_method :started?, :in_progress?
|
21
|
+
|
22
|
+
def completed?
|
23
|
+
@status == :done && ! self.metadata['Reason']
|
24
|
+
end
|
25
|
+
|
26
|
+
def progress_made_but_not_finished?
|
27
|
+
@status == :done && self.metadata['Reason']
|
28
|
+
end
|
29
|
+
|
30
|
+
def postponed?
|
31
|
+
@status == :failed && self.metadata['Postponed']
|
32
|
+
end
|
33
|
+
|
34
|
+
def deleted?
|
35
|
+
@status == :failed && self.metadata['Deleted']
|
36
|
+
end
|
37
|
+
|
38
|
+
# @!group Status setters
|
39
|
+
def start!
|
40
|
+
@status, @start_time = :in_progress, Hour.now
|
41
|
+
end
|
42
|
+
|
43
|
+
def finish_for_the_day!
|
44
|
+
@end_time = Hour.now if @start_time
|
45
|
+
@status = :progress_made
|
46
|
+
end
|
47
|
+
|
48
|
+
def complete!
|
49
|
+
@end_time = Hour.now if @start_time
|
50
|
+
@status = :completed
|
51
|
+
end
|
52
|
+
|
53
|
+
def postpone!(reason, next_review)
|
54
|
+
@end_time = Hour.now if @start_time
|
55
|
+
@status = :failed
|
56
|
+
end
|
57
|
+
|
58
|
+
def delete!(reason)
|
59
|
+
@end_time = Hour.now if @start_time
|
60
|
+
@status = :failed
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'pomodoro/formats/today'
|
2
|
+
|
3
|
+
module Pomodoro::Formats::Today
|
4
|
+
class TaskList
|
5
|
+
attr_reader :time_frame_list
|
6
|
+
def initialize(time_frame_list)
|
7
|
+
@time_frame_list = time_frame_list
|
8
|
+
|
9
|
+
time_frame_list.each do |time_frame|
|
10
|
+
self.define_singleton_method(time_frame.method_name) do
|
11
|
+
time_frame
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def get_current_time_frame(current_time = Time.now)
|
17
|
+
@time_frame_list.find do |time_frame|
|
18
|
+
starting_time, closing_time = time_frame.interval
|
19
|
+
starting_time < current_time && (closing_time.nil? || closing_time > current_time)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
include Enumerable
|
24
|
+
def each(&block)
|
25
|
+
@time_frame_list.each(&block)
|
26
|
+
end
|
27
|
+
|
28
|
+
|
29
|
+
def duration
|
30
|
+
self.time_frame_list.sum { |time_frame| time_frame.duration }
|
31
|
+
end
|
32
|
+
|
33
|
+
# def has_unfinished_tasks?
|
34
|
+
# @time_frame_list.any?(&:has_unfinished_tasks?)
|
35
|
+
# end
|
36
|
+
|
37
|
+
def to_s
|
38
|
+
self.time_frame_list.reduce(nil) do |buffer, time_frame|
|
39
|
+
buffer ? "#{buffer}\n\n#{time_frame.to_s}" : "#{time_frame.to_s}"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def save(path)
|
44
|
+
data = self.to_s
|
45
|
+
File.open(path, 'w') do |file|
|
46
|
+
file.puts(data)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
require 'pomodoro/exts/hour'
|
2
|
+
require 'pomodoro/formats/today'
|
3
|
+
|
4
|
+
module Pomodoro::Formats::Today
|
5
|
+
class TimeFrame
|
6
|
+
ALLOWED_OPTIONS ||= [:online, :writeable, :note, :tags]
|
7
|
+
|
8
|
+
attr_reader :name, :tasks, :interval, :options
|
9
|
+
attr_reader :start_time, :end_time, :header
|
10
|
+
def initialize(header:, start_time: nil, end_time: nil, task_list: Array.new, **shit)
|
11
|
+
# tag, interval_from, interval_to, options = Hash.new
|
12
|
+
@name, @tag, @options = header, nil, {}
|
13
|
+
@interval = [start_time, end_time]
|
14
|
+
@start_time, @end_time = start_time, end_time
|
15
|
+
@header = header
|
16
|
+
# @interval = [interval_from && Hour.parse(interval_from), interval_to && Hour.parse(interval_to)]
|
17
|
+
@tasks = task_list
|
18
|
+
|
19
|
+
# if @options.has_key?(:writeable) && ! @options[:writeable]
|
20
|
+
# @tasks.freeze
|
21
|
+
# end
|
22
|
+
|
23
|
+
unless (unrecognised_options = options.keys - ALLOWED_OPTIONS).empty?
|
24
|
+
raise ArgumentError.new("Unrecognised options: #{unrecognised_options.inspect}")
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def create_task(header, duration = nil, tags = Array.new)
|
29
|
+
@tasks << Task.new(header: header, tags: tags)
|
30
|
+
end
|
31
|
+
|
32
|
+
def unshift_task(*args)
|
33
|
+
@tasks.unshift(Task.new(*args))
|
34
|
+
end
|
35
|
+
|
36
|
+
def header
|
37
|
+
if @interval[0] && @interval[1]
|
38
|
+
[@name, "(#{@interval[0]} – #{@interval[1]})"].compact.join(' ')
|
39
|
+
elsif @interval[0] && ! @interval[1]
|
40
|
+
[@name, "(from #{@interval[0]})"].compact.join(' ')
|
41
|
+
elsif ! @interval[0] && @interval[1]
|
42
|
+
[@name, "(until #{@interval[1]})"].compact.join(' ')
|
43
|
+
else
|
44
|
+
[@name, @options[:online] && '#online'].compact.join(' ')
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def to_s
|
49
|
+
if @tasks.empty?
|
50
|
+
self.header
|
51
|
+
else
|
52
|
+
["#{self.header}", self.tasks.map(&:to_s)].join("\n")
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def method_name
|
57
|
+
if @name
|
58
|
+
@name.downcase.tr(' ', '_').to_sym
|
59
|
+
else
|
60
|
+
:default
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def active_task
|
65
|
+
self.tasks.find do |task|
|
66
|
+
task.in_progress?
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def first_unstarted_task
|
71
|
+
self.tasks.find do |task|
|
72
|
+
task.unstarted?
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def remaining_duration
|
77
|
+
@interval[1] && (@interval[1] - Hour.now)
|
78
|
+
end
|
79
|
+
|
80
|
+
include Enumerable
|
81
|
+
def each(&block)
|
82
|
+
@tasks.each do |task|
|
83
|
+
block.call(task)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def duration
|
88
|
+
self.tasks.reduce(0) do |sum, task|
|
89
|
+
(task.finished? && task.duration) ? sum + task.duration : sum
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# def has_unfinished_tasks?
|
94
|
+
# self.tasks.any? do |task|
|
95
|
+
# ! task.finished?
|
96
|
+
# end
|
97
|
+
# end
|
98
|
+
|
99
|
+
|
100
|
+
# def mark_active_task_as_done # TODO: WIP
|
101
|
+
# #Time.now.strftime('%H:%M')
|
102
|
+
# self.active_task.tags.push(:done)
|
103
|
+
# end
|
104
|
+
#
|
105
|
+
# def active_task
|
106
|
+
# self.today_tasks.find { |task| ! task.tags.include?(:done) }
|
107
|
+
# end
|
108
|
+
#
|
109
|
+
# def finished_tasks
|
110
|
+
# self.today_tasks.select { |task| task.tags.include?(:done) }
|
111
|
+
# end
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'date'
|
2
|
+
require 'pomodoro/formats/today'
|
3
|
+
|
4
|
+
module Pomodoro
|
5
|
+
module Schedule
|
6
|
+
class Thing
|
7
|
+
def initialize(condition, &block)
|
8
|
+
@condition, @callable = condition, block
|
9
|
+
end
|
10
|
+
|
11
|
+
def true?
|
12
|
+
@condition.call
|
13
|
+
end
|
14
|
+
|
15
|
+
def call(tasks)
|
16
|
+
@callable.call(tasks)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class Rule < Thing
|
21
|
+
end
|
22
|
+
|
23
|
+
class Schedule < Thing
|
24
|
+
def call
|
25
|
+
list = @callable.call
|
26
|
+
TaskList.new(list)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class DSL
|
31
|
+
attr_reader :rules, :schedules, :today
|
32
|
+
def initialize(schedule_dir, today = Date.today)
|
33
|
+
@schedule_dir, @today = schedule_dir, today
|
34
|
+
@rules, @schedules = Hash.new, Hash.new
|
35
|
+
end
|
36
|
+
|
37
|
+
alias_method :_require, :require
|
38
|
+
def require(schedule)
|
39
|
+
path = File.expand_path("#{@schedule_dir}/#{schedule}.rb")
|
40
|
+
self.instance_eval(File.read(path), path)
|
41
|
+
rescue Errno::ENOENT # require 'pry'
|
42
|
+
_require schedule
|
43
|
+
end
|
44
|
+
|
45
|
+
def schedule(name, condition, &block)
|
46
|
+
@schedules[name] = Schedule.new(condition, &block)
|
47
|
+
end
|
48
|
+
|
49
|
+
def rule(name, condition, &block)
|
50
|
+
@rules[name] = Rule.new(condition, &block)
|
51
|
+
end
|
52
|
+
|
53
|
+
def last_day_of_a_month
|
54
|
+
Date.new(today.year, today.month, -1)
|
55
|
+
end
|
56
|
+
|
57
|
+
def last_work_day_of_a_month
|
58
|
+
if last_day_of_a_month.saturday?
|
59
|
+
last_work_day_of_a_month = last_day_of_a_month.prev_day
|
60
|
+
elsif last_day_of_a_month.sunday?
|
61
|
+
last_work_day_of_a_month = last_day_of_a_month.prev_day.prev_day
|
62
|
+
else
|
63
|
+
last_work_day_of_a_month = last_day_of_a_month
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'date'
|
2
|
+
require 'refined-refinements/date'
|
3
|
+
|
4
|
+
require 'pomodoro/schedule/dsl'
|
5
|
+
|
6
|
+
module Pomodoro
|
7
|
+
class Scheduler
|
8
|
+
using RR::DateExts
|
9
|
+
|
10
|
+
def self.load(paths, today = Date.today)
|
11
|
+
dir = File.expand_path("#{paths.first}/..") # HACK This way we don't have to merge multiple contexts or reset its path.
|
12
|
+
context = Pomodoro::Schedule::DSL.new(dir, today)
|
13
|
+
paths.each do |path|
|
14
|
+
context.instance_eval(File.read(path), path)
|
15
|
+
end
|
16
|
+
|
17
|
+
self.new(context)
|
18
|
+
end
|
19
|
+
|
20
|
+
def initialize(schedule)
|
21
|
+
@schedule = schedule
|
22
|
+
end
|
23
|
+
|
24
|
+
def rules
|
25
|
+
@schedule.rules
|
26
|
+
end
|
27
|
+
|
28
|
+
def schedules
|
29
|
+
@schedule.schedules
|
30
|
+
end
|
31
|
+
|
32
|
+
def schedule_for_date(date)
|
33
|
+
self.schedules.each do |name, schedule|
|
34
|
+
return schedule if schedule.true?
|
35
|
+
end
|
36
|
+
|
37
|
+
return nil
|
38
|
+
end
|
39
|
+
|
40
|
+
def populate_from_rules(task_list)
|
41
|
+
self.rules.each do |rule_name, rule|
|
42
|
+
rule.true? && rule.call(task_list)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|