now-task-manager 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|