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,63 @@
1
+ class Hour
2
+ def self.parse(string)
3
+ hours, minutes = string.split(':')
4
+ self.new(hours.to_i, minutes.to_i)
5
+ end
6
+
7
+ def self.now
8
+ now = Time.now
9
+ self.new(now.hour, now.min)
10
+ end
11
+
12
+ attr_reader :minutes
13
+ def initialize(hours, minutes = 0)
14
+ @minutes = (hours * 60) + minutes
15
+ end
16
+
17
+ [:+, :-].each do |method_name|
18
+ define_method(method_name) do |hour_or_minutes|
19
+ if hour_or_minutes.is_a?(self.class)
20
+ self.class.new(0, @minutes.send(method_name, hour_or_minutes.minutes))
21
+ elsif hour_or_minutes.is_a?(Integer)
22
+ self.class.new(0, @minutes.send(method_name, hour_or_minutes))
23
+ else
24
+ raise TypeError.new
25
+ end
26
+ end
27
+ end
28
+
29
+ def hours
30
+ if (@minutes / 60).round > (@minutes / 60)
31
+ (@minutes / 60).round - 1
32
+ else
33
+ (@minutes / 60).round
34
+ end
35
+ end
36
+
37
+ # Currently unused, but it might be in the future.
38
+ # def *(rate)
39
+ # (@minutes * (rate / 60.0)).round(2)
40
+ # end
41
+
42
+ [:==, :eql?, :<, :<=, :>, :>=, :<=>].each do |method_name|
43
+ define_method(method_name) do |anotherHour|
44
+ if anotherHour.is_a?(self.class)
45
+ self.minutes.send(method_name, anotherHour.minutes)
46
+ elsif anotherHour.is_a?(Time)
47
+ self.send(method_name, Hour.now)
48
+ else
49
+ raise TypeError.new("#{self.class}##{method_name} expects #{self.class} or Time object.")
50
+ end
51
+ end
52
+ end
53
+
54
+ def inspect
55
+ "#{self.hours}:#{format('%02d', self.minutes_over_the_hour)}"
56
+ end
57
+ alias_method :to_s, :inspect
58
+
59
+ protected
60
+ def minutes_over_the_hour
61
+ @minutes - (self.hours * 60)
62
+ end
63
+ end
@@ -0,0 +1,37 @@
1
+ require 'parslet'
2
+ require 'parslet/convenience' # parse_with_debug
3
+
4
+ module Pomodoro
5
+ module Formats
6
+ # {include:file:doc/formats/scheduled.md}
7
+ module Scheduled
8
+ # The entry point method for parsing this format.
9
+ #
10
+ # @param string [String] string in the scheduled task list format
11
+ # @return [TaskList, nil]
12
+ #
13
+ # @example
14
+ # require 'pomodoro/formats/scheduled'
15
+ #
16
+ # task_list = Pomodoro::Formats::Scheduled.parse <<-EOF.gsub(/^\s*/, '')
17
+ # Tomorrow
18
+ # - Buy milk. #errands
19
+ # - [9:20] Call with Mike.
20
+ #
21
+ # Prague
22
+ # - Pick up my shoes. #errands
23
+ # EOF
24
+ # @since 1.0
25
+ def self.parse(string)
26
+ tree = Parser.new.parse_with_debug(string)
27
+ nodes = Transformer.new.apply(tree)
28
+ TaskList.new(nodes) unless nodes.empty?
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+ require 'pomodoro/formats/scheduled/parser/parser'
35
+ require 'pomodoro/formats/scheduled/parser/transformer'
36
+ require 'pomodoro/formats/scheduled/task_list'
37
+ require 'pomodoro/formats/scheduled/task_group'
@@ -0,0 +1,26 @@
1
+ require 'parslet'
2
+ require 'pomodoro/formats/scheduled'
3
+
4
+ module Pomodoro::Formats::Scheduled
5
+ # @api private
6
+ class Parser < Parslet::Parser
7
+ rule(:header) {
8
+ # match['^\n'].repeat # This makes it hang!
9
+ (str("\n").absent? >> any).repeat(1).as(:header) >> str("\n")
10
+ }
11
+
12
+ rule(:task) {
13
+ str('- ') >> match['^\n'].repeat.as(:task) >> str("\n").repeat
14
+ }
15
+
16
+ rule(:task_group) {
17
+ (header >> task.repeat.as(:task_list)).as(:task_group)
18
+ }
19
+
20
+ rule(:task_groups) {
21
+ task_group.repeat(0)
22
+ }
23
+
24
+ root(:task_groups)
25
+ end
26
+ end
@@ -0,0 +1,21 @@
1
+ require 'parslet'
2
+ require 'pomodoro/formats/scheduled'
3
+
4
+ module Pomodoro::Formats::Scheduled
5
+ # @api private
6
+ class Transformer < Parslet::Transform
7
+ rule(task: simple(:task)) { task.to_s }
8
+
9
+ # Doesn't work. Has multiple keys by the way.
10
+ rule(header: simple(:header)) { header.to_s }
11
+
12
+ rule(task_group: subtree(:task_group)) {
13
+ options = {
14
+ header: task_group[:header].to_s,
15
+ tasks: task_group[:task_list]
16
+ }
17
+
18
+ TaskGroup.new(**options)
19
+ }
20
+ end
21
+ end
@@ -0,0 +1,40 @@
1
+ module Pomodoro::Formats::Scheduled
2
+ class TaskGroup
3
+ # @since 1.0
4
+ attr_reader :header, :tasks
5
+
6
+ # @param header [String] header of the task group.
7
+ # @param tasks [Array<String>] tasks of the group.
8
+ # @since 1.0
9
+ #
10
+ # @example
11
+ # require 'pomodoro/formats/scheduled'
12
+ #
13
+ # tasks = ['Buy milk. #errands', '[9:20] Call with Mike.']
14
+ # group = Pomodoro::Formats::Scheduled::TaskGroup.new(header: 'Tomorrow', tasks: tasks)
15
+ def initialize(header:, tasks: Array.new)
16
+ @header, @tasks = header, tasks
17
+ end
18
+
19
+ # Add a task to the task group.
20
+ #
21
+ # @since 1.0
22
+ def <<(task)
23
+ @tasks << task unless @tasks.include?(task)
24
+ end
25
+
26
+ # Remove a task from the task group.
27
+ #
28
+ # @since 1.0
29
+ def delete(task)
30
+ @tasks.delete(task)
31
+ end
32
+
33
+ # Return a scheduled task list formatted string.
34
+ #
35
+ # @since 1.0
36
+ def to_s
37
+ [@header, @tasks.map { |task| "- #{task}" }, nil].flatten.join("\n")
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,90 @@
1
+ module Pomodoro::Formats::Scheduled
2
+ class TaskList
3
+ include Enumerable
4
+
5
+ # List of {TaskGroup task groups}. Or more precisely objects responding to `#header` and `#tasks`.
6
+ # @since 1.0
7
+ attr_reader :data
8
+
9
+ # @param [Array<TaskGroup>] data List of task groups.
10
+ # Or more precisely objects responding to `#header` and `#tasks`.
11
+ # @raise [ArgumentError] if data is not an array or if its content doesn't
12
+ # respond to `#header` and `#tasks`.
13
+ #
14
+ # @example
15
+ # require 'pomodoro/formats/scheduled'
16
+ #
17
+ # tasks = ['Buy milk. #errands', '[9:20] Call with Mike.']
18
+ # group = Pomodoro::Formats::Scheduled::TaskGroup.new(header: 'Tomorrow', tasks: tasks)
19
+ # list = Pomodoro::Formats::Scheduled::TaskList.new([group])
20
+ # @since 1.0
21
+ def initialize(data)
22
+ @data = data
23
+
24
+ unless data.is_a?(Array) && data.all? { |item| item.respond_to?(:header) && item.respond_to?(:tasks) }
25
+ raise ArgumentError.new("Data is supposed to be an array of TaskGroup instances.")
26
+ end
27
+ end
28
+
29
+ # Find a task group that matches given header.
30
+ #
31
+ # @return [TaskGroup, nil] matching the header.
32
+ # @since 1.0
33
+ #
34
+ # @example
35
+ # # Using the code from the initialiser.
36
+ # list['Tomorrow']
37
+ def [](header)
38
+ @data.find do |task_group|
39
+ task_group.header == header
40
+ end
41
+ end
42
+
43
+ # Add a task group onto the task list.
44
+ #
45
+ # @raise [ArgumentError] if the task group is already in the list.
46
+ # @param [TaskGroup] task_group the task group.
47
+ # @since 1.0
48
+ def <<(task_group)
49
+ if self[task_group.header]
50
+ raise ArgumentError.new("Task group with header #{task_group.header} is already on the list.")
51
+ end
52
+
53
+ @data << task_group
54
+ end
55
+
56
+ # Remove a task group from the task list.
57
+ #
58
+ # @param [TaskGroup] task_group the task group.
59
+ # @since 1.0
60
+ def delete(task_group)
61
+ @data.delete(task_group)
62
+ end
63
+
64
+ # Iterate over the task groups.
65
+ #
66
+ # @yieldparam [TaskGroup] task_group
67
+ # @since 1.0
68
+ def each(&block)
69
+ @data.each(&block)
70
+ end
71
+
72
+ # Return a scheduled task list formatted string.
73
+ #
74
+ # @since 1.0
75
+ def to_s
76
+ @data.map(&:to_s).join("\n")
77
+ end
78
+
79
+ # Save scheduled task list formatted string into a file.
80
+ #
81
+ # @param [String] destination_path
82
+ # @since 1.0
83
+ def save(destination_path)
84
+ data = self.to_s
85
+ File.open(destination_path, 'w') do |file|
86
+ file.puts(data)
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,12 @@
1
+ module Pomodoro
2
+ module Formats
3
+ module Today
4
+ end
5
+ end
6
+ end
7
+
8
+ require 'pomodoro/formats/today/task_list'
9
+ require 'pomodoro/formats/today/time_frame'
10
+ require 'pomodoro/formats/today/task'
11
+ require 'pomodoro/formats/today/parser/parser'
12
+ require 'pomodoro/formats/today/parser/transformer'
@@ -0,0 +1,26 @@
1
+ module Pomodoro::Formats::Today
2
+ class Formatter
3
+ STATUS_SYMBOLS ||= {
4
+ # Unfinished statuses.
5
+ # Unstarted can be either tasks to be started,
6
+ # or tasks skipped when time frame changed.
7
+ unstarted: '-', in_progress: '-',
8
+
9
+ # maybe status :finished and :unfinished, but :unfinished would require Postponed/Deleted etc. (check dynamically)
10
+ # Finished, as in done for the day.
11
+ completed: '✔', progress_made: '✔', postponed: '✘', deleted: '✘'
12
+ }
13
+
14
+ def self.format(task)
15
+ output = [STATUS_SYMBOLS[self.status]]
16
+ if @start_time || @end_time
17
+ output << "[#{self.class.format_interval(@start_time, @end_time)}]"
18
+ else
19
+ output << "[#{@duration}]" unless @duration == DEFAULT_DURATION
20
+ end
21
+ output << @body
22
+ output << @tags.map { |tag| "##{tag}"}.join(' ') unless @tags.empty?
23
+ output.join(' ')
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,73 @@
1
+ require 'parslet'
2
+ require 'pomodoro/formats/today'
3
+
4
+ module Pomodoro::Formats::Today
5
+ class Parser < Parslet::Parser
6
+ # Primitives.
7
+ rule(:integer) { match['0-9'].repeat(1) }
8
+
9
+ rule(:nl) { str("\n").repeat(1) }
10
+ rule(:nl_or_eof) { any.absent? | nl.maybe }
11
+
12
+ rule(:space) { match('\s').repeat(1) }
13
+ rule(:space?) { space.maybe }
14
+
15
+ rule(:lparen) { str('(') }
16
+ rule(:rparen) { str(')') }
17
+ rule(:time_delimiter) { match['-–'] }
18
+ rule(:colon) { str(':') }
19
+
20
+ rule(:hour) { (integer.repeat >> (colon >> integer).maybe).as(:hour) }
21
+ rule(:hour_strict) { (integer.repeat >> colon >> integer).as(:hour) }
22
+ rule(:indent) { match['-✓✔✕✖✗✘'].as(:indent).repeat(1, 1) >> space }
23
+
24
+ rule(:task_desc) { (match['#\n'].absent? >> any).repeat.as(:desc) }
25
+ rule(:time_frame_desc) { (match['(\n'].absent? >> any).repeat.as(:desc) }
26
+ # rule(:task_desc) { (str(' #').absent? >> match['^\n']).repeat.as(:desc) }
27
+ # rule(:time_frame_desc) { (str(' (').absent? >> match['^\n']).repeat.as(:desc) }
28
+ # rule(:task_desc) { (str("\n").absent? >> str(' #').absent? >> any).repeat.as(:desc) }
29
+ # rule(:time_frame_desc) { (str("\n").absent? >> str(' (').absent? >> any).repeat.as(:desc) }
30
+
31
+ rule(:tag) { str('#') >> match['^\s'].repeat.as(:tag) >> space? }
32
+
33
+ rule(:duration) do
34
+ # ✔ 9:20
35
+ # ✔ 9:20–10:00
36
+ # ✔ started at 9:20 (this is not the same as just 9:20)
37
+ # ✖ 9-10
38
+ # There was an issue with parsing that compared to 9 as duration.
39
+ (hour_strict.as(:start_time) >> (time_delimiter >> hour_strict.as(:end_time)).maybe) | str('started at') >> hour_strict.as(:start_time)
40
+ end
41
+
42
+ rule(:task_time_info) do
43
+ str('[') >> (duration | integer.as(:duration)) >> str(']') >> space
44
+ end
45
+
46
+ rule(:metadata) { (str("\n").absent? >> any).repeat.as(:line) }
47
+
48
+ rule(:task_body) { indent >> task_time_info.maybe >> task_desc >> tag.repeat }
49
+ rule(:metadata_block) { (nl >> str(' ') >> metadata).repeat(0) }
50
+
51
+ rule(:task) do
52
+ (task_body >> metadata_block).as(:task) >> nl.maybe # replaced nl_or_eof to fix the hang.
53
+ # IMPORTANT NOTE: nl.maybe is because the tag definition eats up \n's for unknown reason.
54
+ end
55
+
56
+ rule(:time_range) do
57
+ hour.as(:start_time) >> space? >> time_delimiter >> space? >> hour.as(:end_time)
58
+ end
59
+
60
+ rule(:time_from) do
61
+ (str('from') | str('after')) >> space >> hour.as(:start_time)
62
+ end
63
+
64
+ rule(:time_frame_header) do
65
+ time_frame_desc >> (lparen >> (time_range | time_from) >> rparen).maybe >> nl # replaced nl_or_eof to fix the hang.
66
+ end
67
+
68
+ rule(:time_frame_with_tasks) { (time_frame_header >> task.repeat.as(:task_list)).as(:time_frame) } # ...
69
+ rule(:time_frames_with_tasks) { time_frame_with_tasks.repeat(0) }
70
+
71
+ root(:time_frames_with_tasks)
72
+ end
73
+ end
@@ -0,0 +1,65 @@
1
+ require 'parslet'
2
+ require 'pomodoro/exts/hour'
3
+ require 'pomodoro/formats/today'
4
+
5
+ module Pomodoro::Formats::Today
6
+ class Transformer < Parslet::Transform
7
+ STATUS_MAPPING ||= {
8
+ finished: ['✓', '✔', '☑'],
9
+ failed: ['✕', '☓', '✖', '✗', '✘', '☒'],
10
+ wip: ['☐', '⛶', '⚬']
11
+ }
12
+
13
+ rule(body: simple(:body)) {
14
+ {body: body.to_s.strip}
15
+ }
16
+
17
+ rule(duration: simple(:duration)) {
18
+ {duration: Integer(duration)}
19
+ }
20
+
21
+ rule(hour: simple(:hour_string)) {
22
+ Hour.parse(hour_string.to_s)
23
+ }
24
+
25
+ rule(tag: simple(:tag)) {
26
+ {tag: tag.to_sym}
27
+ }
28
+
29
+ rule(indent: simple(:char)) {
30
+ status, _ = Task::STATUS_MAPPING.find { |status, i| i.include?(char) }
31
+ {status: status}
32
+ }
33
+
34
+ rule(task: subtree(:hashes)) {
35
+ data = hashes.reduce(Hash.new) do |buffer, hash|
36
+ key = hash.keys.first
37
+ if buffer.has_key?(key)
38
+ buffer.merge(key => [buffer[key], hash[key]].flatten)
39
+ else
40
+ buffer.merge(hash)
41
+ end
42
+ end
43
+
44
+ if data[:tag]
45
+ data[:tags] = [data.delete(:tag)].flatten
46
+ end
47
+
48
+ if data[:line]
49
+ data[:lines] = [data.delete(:line)].flatten
50
+ end
51
+
52
+ begin
53
+ Task.new(**data)
54
+ rescue ArgumentError => error
55
+ message = [error.message, "Arguments were: #{data.inspect}"].join("\n")
56
+ raise ArgumentError.new(message)
57
+ end
58
+ }
59
+
60
+ rule(time_frame: subtree(:data)) {
61
+ data[:body] = data.delete(:desc).to_s.strip # WTH? All the other nodes are processed correctly?
62
+ TimeFrame.new(**data)
63
+ }
64
+ end
65
+ end