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.
Files changed (38) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +66 -0
  3. data/bin/git-commit-pomodoro +4 -0
  4. data/bin/pomodoro +246 -0
  5. data/doc/benefits.md +91 -0
  6. data/doc/bitbar.md +2 -0
  7. data/doc/curses.md +5 -0
  8. data/doc/example/rules.rb +7 -0
  9. data/doc/formats/scheduled.md +32 -0
  10. data/doc/formats/today.md +115 -0
  11. data/doc/getting-started.md +41 -0
  12. data/doc/integrations.md +68 -0
  13. data/doc/rules.md +0 -0
  14. data/doc/schedules.md +0 -0
  15. data/doc/terms.md +32 -0
  16. data/doc/workflow.md +60 -0
  17. data/lib/pomodoro.rb +6 -0
  18. data/lib/pomodoro/commands/bitbar.rb +87 -0
  19. data/lib/pomodoro/config.rb +30 -0
  20. data/lib/pomodoro/exts/hour.rb +63 -0
  21. data/lib/pomodoro/formats/scheduled.rb +37 -0
  22. data/lib/pomodoro/formats/scheduled/parser/parser.rb +26 -0
  23. data/lib/pomodoro/formats/scheduled/parser/transformer.rb +21 -0
  24. data/lib/pomodoro/formats/scheduled/task_group.rb +40 -0
  25. data/lib/pomodoro/formats/scheduled/task_list.rb +90 -0
  26. data/lib/pomodoro/formats/today.rb +12 -0
  27. data/lib/pomodoro/formats/today/formatter.rb +26 -0
  28. data/lib/pomodoro/formats/today/parser/parser.rb +73 -0
  29. data/lib/pomodoro/formats/today/parser/transformer.rb +65 -0
  30. data/lib/pomodoro/formats/today/task.rb +92 -0
  31. data/lib/pomodoro/formats/today/task/dynamic_additions.rb +5 -0
  32. data/lib/pomodoro/formats/today/task/metadata.rb +37 -0
  33. data/lib/pomodoro/formats/today/task/statuses.rb +63 -0
  34. data/lib/pomodoro/formats/today/task_list.rb +50 -0
  35. data/lib/pomodoro/formats/today/time_frame.rb +113 -0
  36. data/lib/pomodoro/schedule/dsl.rb +68 -0
  37. data/lib/pomodoro/scheduler.rb +46 -0
  38. 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,5 @@
1
+ module Pomodoro::Formats::Today
2
+ module DynamicAdditions
3
+ attr_accessor :command
4
+ end
5
+ 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