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,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
|