now-task-manager 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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