todo-txt 0.9 → 0.10
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +3 -0
- data/.travis.yml +0 -1
- data/Gemfile +1 -1
- data/Rakefile +1 -1
- data/lib/todo-txt.rb +1 -6
- data/lib/todo.rb +38 -0
- data/lib/todo/file.rb +83 -0
- data/lib/todo/list.rb +127 -0
- data/lib/{todo-txt → todo}/logger.rb +0 -0
- data/lib/{todo-txt → todo}/options.rb +0 -13
- data/lib/{todo-txt → todo}/syntax.rb +11 -13
- data/lib/{todo-txt → todo}/task.rb +104 -82
- data/spec/todo-txt/file_spec.rb +75 -0
- data/spec/todo-txt/list_spec.rb +11 -16
- data/spec/todo-txt/syntax_spec.rb +13 -5
- data/spec/todo-txt/task_spec.rb +15 -4
- data/todo-txt.gemspec +6 -6
- data/todo.txt +8 -2
- metadata +11 -9
- data/lib/todo-txt/list.rb +0 -122
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0e6194a6c093149543776903fec449488ff35e12
|
4
|
+
data.tar.gz: 1ff563eca42c38f95a55eda4732f4c43005a2079
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0d614c4914dfc5e7e84addd6185f79c52270a4196003be1bef3f88452063e4d549062855d3229b92413dfe1f9b0f896ff6fe12cc7b64786767300aa9ff430a56
|
7
|
+
data.tar.gz: 0b8307a2946e632a5a68fb1b03b1df5d2fcbf56317087ebac52e7d3317b742ba8e9a5f7e31de9ff8723683e940a50d2d11bd39e58a4b806265a05180f6fa123e
|
data/.rubocop.yml
CHANGED
data/.travis.yml
CHANGED
data/Gemfile
CHANGED
data/Rakefile
CHANGED
data/lib/todo-txt.rb
CHANGED
data/lib/todo.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'logger'
|
2
|
+
require 'todo/logger'
|
3
|
+
require 'todo/options'
|
4
|
+
require 'todo/syntax'
|
5
|
+
require 'todo/list'
|
6
|
+
require 'todo/task'
|
7
|
+
require 'todo/file'
|
8
|
+
|
9
|
+
# Allows for easy management of task lists and tasks in the todo.txt format.
|
10
|
+
module Todo
|
11
|
+
# Provides global options for customizing todo list behaviour.
|
12
|
+
class << self
|
13
|
+
attr_accessor :options_instance
|
14
|
+
end
|
15
|
+
|
16
|
+
# Global access to custom options.
|
17
|
+
#
|
18
|
+
# Example:
|
19
|
+
#
|
20
|
+
# if Todo.options.require_completed_on
|
21
|
+
# # Do something
|
22
|
+
# end
|
23
|
+
def self.options
|
24
|
+
self.options_instance ||= Options.new
|
25
|
+
end
|
26
|
+
|
27
|
+
# Customize global list behaviour.
|
28
|
+
#
|
29
|
+
# Example:
|
30
|
+
#
|
31
|
+
# Todo.customize do |opts|
|
32
|
+
# opts.require_completed_on = false
|
33
|
+
# end
|
34
|
+
def self.customize
|
35
|
+
self.options_instance ||= Options.new
|
36
|
+
yield(options_instance)
|
37
|
+
end
|
38
|
+
end
|
data/lib/todo/file.rb
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
module Todo
|
2
|
+
# A high level wrapper around the Ruby File interface which supports reading
|
3
|
+
# from and writing to an IO handle with Todo::Task objects.
|
4
|
+
class File
|
5
|
+
# Open a list file handle and pass it to the given block. The file is
|
6
|
+
# automatically closed when the block returns.
|
7
|
+
#
|
8
|
+
# The file is opened in read-only mode by default.
|
9
|
+
#
|
10
|
+
# Todo::File.open("~/Dropbox/todo/todo.txt") do |file|
|
11
|
+
# file.each_task do |task|
|
12
|
+
# puts task.done?
|
13
|
+
# end
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# @param [String, Pathname] path
|
17
|
+
# @param [String] mode
|
18
|
+
def self.open(path, mode = 'r')
|
19
|
+
ios = new(path, mode)
|
20
|
+
|
21
|
+
if block_given?
|
22
|
+
yield ios
|
23
|
+
return ios.close
|
24
|
+
end
|
25
|
+
|
26
|
+
ios
|
27
|
+
end
|
28
|
+
|
29
|
+
# @param [String, Pathname] path
|
30
|
+
def self.read(path)
|
31
|
+
list = []
|
32
|
+
|
33
|
+
open(path) do |file|
|
34
|
+
file.each_task do |task|
|
35
|
+
list << task
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
list
|
40
|
+
end
|
41
|
+
|
42
|
+
# @param [String, Pathname] path
|
43
|
+
# @param [Array, Todo::List] list
|
44
|
+
def self.write(path, list)
|
45
|
+
open(path, 'w') do |file|
|
46
|
+
list.each do |task|
|
47
|
+
file.puts(task)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# @param [String, Pathname] path
|
53
|
+
# @param [String] mode
|
54
|
+
def initialize(path, mode = 'r')
|
55
|
+
@ios = ::File.open(path, mode)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Executes the block for every task in the list.
|
59
|
+
def each
|
60
|
+
return enum_for(:each) unless block_given?
|
61
|
+
|
62
|
+
@ios.each_line do |line|
|
63
|
+
yield Task.new(line)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
alias each_task each
|
68
|
+
|
69
|
+
# Writes the given tasks to the underlying IO handle.
|
70
|
+
#
|
71
|
+
# @overload puts(task, ...)
|
72
|
+
# @param [Todo::Task] task
|
73
|
+
# @param [Todo::Task] ...
|
74
|
+
def puts(*tasks)
|
75
|
+
@ios.puts(tasks.map(&:to_s))
|
76
|
+
end
|
77
|
+
|
78
|
+
# Closes the IO handle and flushes any pending writes.
|
79
|
+
def close
|
80
|
+
@ios.close
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
data/lib/todo/list.rb
ADDED
@@ -0,0 +1,127 @@
|
|
1
|
+
module Todo
|
2
|
+
# Initializes a Todo List object with a path to the corresponding todo.txt
|
3
|
+
# file. For example, if your todo.txt file is located at:
|
4
|
+
#
|
5
|
+
# /home/sam/Dropbox/todo/todo.txt
|
6
|
+
#
|
7
|
+
# You would initialize the list object like:
|
8
|
+
#
|
9
|
+
# list = Todo::List.new("/home/sam/Dropbox/todo/todo.txt")
|
10
|
+
#
|
11
|
+
# Alternately, you can initialize the object with an array of strings or
|
12
|
+
# tasks. If the array is of strings, the strings will be converted into
|
13
|
+
# tasks. You can supply a mixed list of string and tasks if you wish.
|
14
|
+
#
|
15
|
+
# Example:
|
16
|
+
#
|
17
|
+
# tasks = []
|
18
|
+
# tasks << "A task line"
|
19
|
+
# tasks << Todo::Task.new("A task object")
|
20
|
+
#
|
21
|
+
# list = Todo::List.new(tasks)
|
22
|
+
class List < Array
|
23
|
+
def initialize(list)
|
24
|
+
case list
|
25
|
+
when Array
|
26
|
+
tasks = list.map do |item|
|
27
|
+
case item
|
28
|
+
when String then Task.new(item)
|
29
|
+
when Task then item
|
30
|
+
else
|
31
|
+
raise "Cannot add #{item.class} to list."
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
concat(tasks)
|
36
|
+
|
37
|
+
when String
|
38
|
+
@path = list
|
39
|
+
concat(File.read(list))
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# The path to the todo.txt file that you supplied when you created the
|
44
|
+
# Todo::List object.
|
45
|
+
attr_reader :path
|
46
|
+
|
47
|
+
# Filters the list by priority and returns a new list.
|
48
|
+
#
|
49
|
+
# Example:
|
50
|
+
#
|
51
|
+
# list = Todo::List.new("/path/to/list")
|
52
|
+
# list.by_priority("A")
|
53
|
+
# # => Will be a new list with only priority 'A' tasks
|
54
|
+
#
|
55
|
+
# @param priority [String]
|
56
|
+
# @return [Todo::List]
|
57
|
+
def by_priority(priority)
|
58
|
+
List.new(select { |task| task.priority == priority })
|
59
|
+
end
|
60
|
+
|
61
|
+
# Filters the list by context and returns a new list.
|
62
|
+
#
|
63
|
+
# Example:
|
64
|
+
#
|
65
|
+
# list = Todo::List.new("/path/to/list")
|
66
|
+
# list.by_context("@admin")
|
67
|
+
# # => <Todo::List> filtered by '@admin'
|
68
|
+
#
|
69
|
+
# @param context [String]
|
70
|
+
# @return [Todo::List]
|
71
|
+
def by_context(context)
|
72
|
+
List.new(select { |task| task.contexts.include? context })
|
73
|
+
end
|
74
|
+
|
75
|
+
# Filters the list by project and returns a new list.
|
76
|
+
#
|
77
|
+
# Example:
|
78
|
+
#
|
79
|
+
# list = Todo::List.new("/path/to/list")
|
80
|
+
# list.by_project("+blog")
|
81
|
+
# # => <Todo::List> filtered by '+blog'
|
82
|
+
#
|
83
|
+
# @param project [String]
|
84
|
+
# @return [Todo::List]
|
85
|
+
def by_project(project)
|
86
|
+
List.new(select { |task| task.projects.include? project })
|
87
|
+
end
|
88
|
+
|
89
|
+
# Filters the list by completed tasks and returns a new list.
|
90
|
+
#
|
91
|
+
# Example:
|
92
|
+
#
|
93
|
+
# list = Todo::List.new("/path/to/list")
|
94
|
+
# list.by_done
|
95
|
+
# # => <Todo::List> filtered by tasks that are done
|
96
|
+
#
|
97
|
+
# @return [Todo::List]
|
98
|
+
def by_done
|
99
|
+
List.new(select(&:done?))
|
100
|
+
end
|
101
|
+
|
102
|
+
# Filters the list by incomplete tasks and returns a new list.
|
103
|
+
#
|
104
|
+
# Example:
|
105
|
+
#
|
106
|
+
# list = Todo::List.new("/path/to/list")
|
107
|
+
# list.by_not_done
|
108
|
+
# # => <Todo::List> filtered by tasks that are not done
|
109
|
+
#
|
110
|
+
# @return [Todo::List]
|
111
|
+
def by_not_done
|
112
|
+
List.new(select { |task| task.done? == false })
|
113
|
+
end
|
114
|
+
|
115
|
+
# Saves the list to the original file location.
|
116
|
+
#
|
117
|
+
# Warning: This is a destructive operation and will overwrite any existing
|
118
|
+
# content in the file. It does not attempt to diff and append changes.
|
119
|
+
#
|
120
|
+
# If no `path` is specified in the constructor then an error is raised.
|
121
|
+
def save!
|
122
|
+
raise "No path specified." unless path
|
123
|
+
|
124
|
+
File.write(path, self)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
File without changes
|
@@ -1,17 +1,4 @@
|
|
1
1
|
module Todo
|
2
|
-
class << self
|
3
|
-
attr_accessor :options_instance
|
4
|
-
end
|
5
|
-
|
6
|
-
def self.options
|
7
|
-
self.options_instance ||= Options.new
|
8
|
-
end
|
9
|
-
|
10
|
-
def self.customize
|
11
|
-
self.options_instance ||= Options.new
|
12
|
-
yield(options_instance)
|
13
|
-
end
|
14
|
-
|
15
2
|
# Options for default preferences and library settings that can be customized
|
16
3
|
# by clients of the gem.
|
17
4
|
class Options
|
@@ -1,5 +1,5 @@
|
|
1
1
|
module Todo
|
2
|
-
#
|
2
|
+
# Supports parsing and extracting core syntax fragments from todo.txt lines.
|
3
3
|
module Syntax
|
4
4
|
# The regex used to match contexts.
|
5
5
|
CONTEXTS_PATTERN = /(?:\s+|^)@[^\s]+/
|
@@ -19,6 +19,9 @@ module Todo
|
|
19
19
|
# The regex used to match due date.
|
20
20
|
DUE_ON_PATTERN = /(?:due:)(\d{4}-\d{2}-\d{2})(?:\s+|$)/i
|
21
21
|
|
22
|
+
# The regex used to match generic tags.
|
23
|
+
TAGS_PATTERN = /([a-z]+):([a-z0-9_-]+)/i
|
24
|
+
|
22
25
|
# Extracts the readable text content of a task line, stripping out all the
|
23
26
|
# discrete pieces of metadata (priority, dates, completion flag, projects,
|
24
27
|
# contexts, etc).
|
@@ -76,8 +79,8 @@ module Todo
|
|
76
79
|
|
77
80
|
# Checks whether the given todo item has a completion flag set.
|
78
81
|
#
|
79
|
-
# This provides support for ad-hoc handwritten lists where the completed
|
80
|
-
# is set but there is no completed date.
|
82
|
+
# This provides support for ad-hoc handwritten lists where the completed
|
83
|
+
# flag is set but there is no completed date.
|
81
84
|
#
|
82
85
|
# @param line [String] the todo item to be processed
|
83
86
|
# @return [Boolean]
|
@@ -85,18 +88,13 @@ module Todo
|
|
85
88
|
line[0] == COMPLETED_FLAG && line[1] == SINGLE_SPACE
|
86
89
|
end
|
87
90
|
|
88
|
-
# Extracts the
|
89
|
-
# Returns
|
91
|
+
# Extracts the collection of tag annotations for the given todo item.
|
92
|
+
# Returns an empty hash if no tags are found.
|
90
93
|
#
|
91
94
|
# @param line [String] the todo item to be processed
|
92
|
-
# @return [
|
93
|
-
def
|
94
|
-
|
95
|
-
begin
|
96
|
-
Date.parse(date[1]) if date
|
97
|
-
rescue ArgumentError
|
98
|
-
return nil # The given date is not valid
|
99
|
-
end
|
95
|
+
# @return [Hash<Symbol,String>] the collection of tag annotations
|
96
|
+
def extract_tags(line)
|
97
|
+
line.scan(TAGS_PATTERN).map { |tag, val| [tag.downcase.to_sym, val] }.to_h
|
100
98
|
end
|
101
99
|
|
102
100
|
# Extract the list of `@context` tags out of the task line.
|
@@ -12,36 +12,37 @@ module Todo
|
|
12
12
|
include Todo::Logger
|
13
13
|
include Todo::Syntax
|
14
14
|
|
15
|
-
def initialize(
|
16
|
-
@
|
17
|
-
@priority = extract_priority(
|
18
|
-
@created_on = extract_created_on(
|
19
|
-
@
|
20
|
-
@contexts ||= extract_contexts(
|
21
|
-
@projects ||= extract_projects(
|
15
|
+
def initialize(line)
|
16
|
+
@raw = line
|
17
|
+
@priority = extract_priority(raw)
|
18
|
+
@created_on = extract_created_on(raw)
|
19
|
+
@tags = extract_tags(raw)
|
20
|
+
@contexts ||= extract_contexts(raw)
|
21
|
+
@projects ||= extract_projects(raw)
|
22
22
|
|
23
23
|
if Todo.options.require_completed_on
|
24
|
-
@completed_on = extract_completed_date(
|
24
|
+
@completed_on = extract_completed_date(raw)
|
25
25
|
@is_completed = !@completed_on.nil?
|
26
26
|
else
|
27
|
-
@completed_on = extract_completed_date(
|
28
|
-
@is_completed = check_completed_flag(
|
27
|
+
@completed_on = extract_completed_date(raw)
|
28
|
+
@is_completed = check_completed_flag(raw)
|
29
29
|
end
|
30
30
|
end
|
31
31
|
|
32
|
-
# Returns the
|
32
|
+
# Returns the raw content of the original task line.
|
33
33
|
#
|
34
34
|
# Example:
|
35
35
|
#
|
36
|
-
# task = Todo::Task.new
|
37
|
-
# task.
|
38
|
-
|
36
|
+
# task = Todo::Task.new("(A) @context +project Hello!")
|
37
|
+
# task.raw
|
38
|
+
# # => "(A) @context +project Hello!"
|
39
|
+
attr_reader :raw
|
39
40
|
|
40
41
|
# Returns the task's creation date, if any.
|
41
42
|
#
|
42
43
|
# Example:
|
43
44
|
#
|
44
|
-
# task = Todo::Task.new
|
45
|
+
# task = Todo::Task.new("(A) 2012-03-04 Task.")
|
45
46
|
# task.created_on
|
46
47
|
# #=> <Date: 2012-03-04 (4911981/2,0,2299161)>
|
47
48
|
#
|
@@ -54,53 +55,57 @@ module Todo
|
|
54
55
|
#
|
55
56
|
# Example:
|
56
57
|
#
|
57
|
-
# task = Todo::Task.new
|
58
|
+
# task = Todo::Task.new("x 2012-03-04 Task.")
|
58
59
|
# task.completed_on
|
59
|
-
#
|
60
|
+
# # => <Date: 2012-03-04 (4911981/2,0,2299161)>
|
60
61
|
#
|
61
62
|
# Dates _must_ be in the YYYY-MM-DD format as specified in the todo.txt
|
62
63
|
# format. Dates in any other format will be classed as malformed and this
|
63
64
|
# attribute will be nil.
|
64
65
|
attr_reader :completed_on
|
65
66
|
|
66
|
-
# Returns
|
67
|
+
# Returns tag annotations embedded in the list item.
|
67
68
|
#
|
68
69
|
# Example:
|
69
70
|
#
|
70
|
-
# task = Todo::Task.new
|
71
|
-
# task.
|
72
|
-
#
|
71
|
+
# task = Todo::Task.new("Some task. due:2016-06-16 hello:world")
|
72
|
+
# task.tags
|
73
|
+
# # => { :due => '2016-06-16', :hello => 'world' }
|
73
74
|
#
|
74
|
-
#
|
75
|
-
#
|
76
|
-
#
|
77
|
-
attr_reader :
|
75
|
+
# task = Todo::Task.new("Some task.")
|
76
|
+
# task.tags.empty?
|
77
|
+
# # => true
|
78
|
+
attr_reader :tags
|
78
79
|
|
79
80
|
# Returns the priority, if any.
|
80
81
|
#
|
81
82
|
# Example:
|
82
83
|
#
|
83
|
-
# task = Todo::Task.new
|
84
|
-
# task.priority
|
84
|
+
# task = Todo::Task.new("(A) Some task.")
|
85
|
+
# task.priority
|
86
|
+
# # => "A"
|
85
87
|
#
|
86
88
|
# task = Todo::Task.new "Some task."
|
87
|
-
# task.priority
|
89
|
+
# task.priority
|
90
|
+
# # => nil
|
88
91
|
attr_reader :priority
|
89
92
|
|
90
93
|
# Returns an array of all the @context annotations.
|
91
94
|
#
|
92
95
|
# Example:
|
93
96
|
#
|
94
|
-
# task = Todo:Task.new
|
95
|
-
# task.context
|
97
|
+
# task = Todo:Task.new("(A) @context Testing!")
|
98
|
+
# task.context
|
99
|
+
# # => ["@context"]
|
96
100
|
attr_reader :contexts
|
97
101
|
|
98
102
|
# Returns an array of all the +project annotations.
|
99
103
|
#
|
100
104
|
# Example:
|
101
105
|
#
|
102
|
-
# task = Todo:Task.new
|
103
|
-
# task.projects
|
106
|
+
# task = Todo:Task.new("(A) +test Testing!")
|
107
|
+
# task.projects
|
108
|
+
# # => ["+test"]
|
104
109
|
attr_reader :projects
|
105
110
|
|
106
111
|
# Gets just the text content of the todo, without the priority, contexts
|
@@ -108,29 +113,44 @@ module Todo
|
|
108
113
|
#
|
109
114
|
# Example:
|
110
115
|
#
|
111
|
-
# task = Todo::Task.new
|
112
|
-
# task.text
|
116
|
+
# task = Todo::Task.new("(A) @test Testing!")
|
117
|
+
# task.text
|
118
|
+
# # => "Testing!"
|
113
119
|
def text
|
114
|
-
@text ||= extract_item_text(
|
120
|
+
@text ||= extract_item_text(raw)
|
115
121
|
end
|
116
122
|
|
117
|
-
#
|
123
|
+
# Deprecated. See: #created_on
|
124
|
+
def date
|
125
|
+
logger.warn("`Task#date` is deprecated, use `Task#created_on` instead.")
|
126
|
+
|
127
|
+
@created_on
|
128
|
+
end
|
129
|
+
|
130
|
+
# Deprecated. See: #raw
|
131
|
+
def orig
|
132
|
+
logger.warn("`Task#orig` is deprecated, use `Task#raw` instead.")
|
133
|
+
|
134
|
+
raw
|
135
|
+
end
|
136
|
+
|
137
|
+
# Returns the task's due date, if any.
|
118
138
|
#
|
119
139
|
# Example:
|
120
140
|
#
|
121
|
-
# task = Todo::Task.new
|
122
|
-
# task.
|
123
|
-
#
|
141
|
+
# task = Todo::Task.new("(A) This is a task. due:2012-03-04")
|
142
|
+
# task.due_on
|
143
|
+
# # => <Date: 2012-03-04 (4911981/2,0,2299161)>
|
124
144
|
#
|
125
145
|
# Dates _must_ be in the YYYY-MM-DD format as specified in the todo.txt
|
126
146
|
# format. Dates in any other format will be classed as malformed and this
|
127
|
-
#
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
147
|
+
# attribute will be nil.
|
148
|
+
def due_on
|
149
|
+
begin
|
150
|
+
Date.parse(tags[:due]) if tags[:due] =~ /(\d{4}-\d{2}-\d{2})/
|
151
|
+
rescue ArgumentError
|
152
|
+
return nil
|
153
|
+
end
|
134
154
|
end
|
135
155
|
|
136
156
|
# Returns whether a task's due date is in the past.
|
@@ -139,22 +159,22 @@ module Todo
|
|
139
159
|
#
|
140
160
|
# task = Todo::Task.new("This task is overdue! due:#{Date.today - 1}")
|
141
161
|
# task.overdue?
|
142
|
-
#
|
162
|
+
# # => true
|
143
163
|
def overdue?
|
144
164
|
!due_on.nil? && due_on < Date.today
|
145
165
|
end
|
146
166
|
|
147
|
-
# Returns if the task is
|
167
|
+
# Returns true if the task is completed.
|
148
168
|
#
|
149
169
|
# Example:
|
150
170
|
#
|
151
|
-
# task = Todo::Task.new
|
171
|
+
# task = Todo::Task.new("x 2012-12-08 Task.")
|
152
172
|
# task.done?
|
153
|
-
#
|
173
|
+
# # => true
|
154
174
|
#
|
155
|
-
# task = Todo::Task.new
|
175
|
+
# task = Todo::Task.new("Task.")
|
156
176
|
# task.done?
|
157
|
-
#
|
177
|
+
# # => false
|
158
178
|
def done?
|
159
179
|
@is_completed
|
160
180
|
end
|
@@ -163,17 +183,16 @@ module Todo
|
|
163
183
|
#
|
164
184
|
# Example:
|
165
185
|
#
|
166
|
-
# task = Todo::Task.new
|
186
|
+
# task = Todo::Task.new("2012-12-08 Task.")
|
187
|
+
#
|
167
188
|
# task.done?
|
168
|
-
#
|
189
|
+
# # => false
|
169
190
|
#
|
191
|
+
# # Complete the task
|
170
192
|
# task.do!
|
193
|
+
#
|
171
194
|
# task.done?
|
172
|
-
#
|
173
|
-
# task.created_on
|
174
|
-
# #=> <Date: 2012-12-08 (4911981/2,0,2299161)>
|
175
|
-
# task.completed_on
|
176
|
-
# #=> # the current date
|
195
|
+
# # => true
|
177
196
|
def do!
|
178
197
|
@completed_on = Date.today
|
179
198
|
@is_completed = true
|
@@ -184,21 +203,19 @@ module Todo
|
|
184
203
|
#
|
185
204
|
# Example:
|
186
205
|
#
|
187
|
-
# task = Todo::Task.new
|
206
|
+
# task = Todo::Task.new("x 2012-12-08 2012-03-04 Task.")
|
188
207
|
# task.done?
|
189
|
-
#
|
208
|
+
# # => true
|
190
209
|
#
|
210
|
+
# # Undo the completed task
|
191
211
|
# task.undo!
|
212
|
+
#
|
192
213
|
# task.done?
|
193
|
-
#
|
194
|
-
# task.created_on
|
195
|
-
# #=> <Date: 2012-03-04 (4911981/2,0,2299161)>
|
196
|
-
# task.completed_on
|
197
|
-
# #=> nil
|
214
|
+
# # => false
|
198
215
|
def undo!
|
199
216
|
@completed_on = nil
|
200
217
|
@is_completed = false
|
201
|
-
@priority = extract_priority(
|
218
|
+
@priority = extract_priority(raw)
|
202
219
|
end
|
203
220
|
|
204
221
|
# Increases the priority until A. If it's nil, it sets it to A.
|
@@ -225,17 +242,20 @@ module Todo
|
|
225
242
|
#
|
226
243
|
# Example:
|
227
244
|
#
|
228
|
-
# task = Todo::Task.new
|
245
|
+
# task = Todo::Task.new("x 2012-12-08 Task.")
|
229
246
|
# task.done?
|
230
|
-
#
|
247
|
+
# # => true
|
231
248
|
#
|
249
|
+
# # Toggle between complete and incomplete
|
232
250
|
# task.toggle!
|
251
|
+
#
|
233
252
|
# task.done?
|
234
|
-
#
|
253
|
+
# # => false
|
235
254
|
#
|
236
255
|
# task.toggle!
|
256
|
+
#
|
237
257
|
# task.done?
|
238
|
-
#
|
258
|
+
# # => true
|
239
259
|
def toggle!
|
240
260
|
done? ? undo! : do!
|
241
261
|
end
|
@@ -244,25 +264,27 @@ module Todo
|
|
244
264
|
#
|
245
265
|
# Example:
|
246
266
|
#
|
247
|
-
# task = Todo::Task.new
|
267
|
+
# task = Todo::Task.new("(A) 2012-12-08 Task")
|
248
268
|
# task.to_s
|
249
|
-
#
|
269
|
+
# # => "(A) 2012-12-08 Task"
|
250
270
|
def to_s
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
271
|
+
[
|
272
|
+
done? && "x #{completed_on}",
|
273
|
+
priority && "(#{priority})",
|
274
|
+
created_on.to_s,
|
275
|
+
text,
|
276
|
+
contexts.join(' '),
|
277
|
+
projects.join(' '),
|
278
|
+
tags.map { |tag,val| "#{tag}:#{val}" }.join(' ')
|
279
|
+
].grep(String).join(' ').strip
|
258
280
|
end
|
259
281
|
|
260
282
|
# Compares the priorities of two tasks.
|
261
283
|
#
|
262
284
|
# Example:
|
263
285
|
#
|
264
|
-
# task1 = Todo::Task.new
|
265
|
-
# task2 = Todo::Task.new
|
286
|
+
# task1 = Todo::Task.new("(A) Priority A.")
|
287
|
+
# task2 = Todo::Task.new("(B) Priority B.")
|
266
288
|
#
|
267
289
|
# task1 > task2
|
268
290
|
# # => true
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Todo::File do
|
4
|
+
let(:path) { File.dirname(__FILE__) + '/../data/todo.txt' }
|
5
|
+
|
6
|
+
context 'reading input' do
|
7
|
+
it 'should raise a system error when file not found' do
|
8
|
+
expect { Todo::File.open('not_found') }.to raise_error Errno::ENOENT
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'should yield each task from block passed to open' do
|
12
|
+
Todo::File.open(path) do |file|
|
13
|
+
file.each do |task|
|
14
|
+
expect(task).to be_a Todo::Task
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'should return a file object when no block is passed to open' do
|
20
|
+
file = Todo::File.open(path)
|
21
|
+
expect(file).to be_a Todo::File
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'should return an enumerator when no block is passed to each' do
|
25
|
+
enumerator = Todo::File.open(path).each
|
26
|
+
expect(enumerator).to be_a Enumerator
|
27
|
+
expect(enumerator.next).to be_a Todo::Task
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'should read a file into an array of tasks' do
|
31
|
+
list = Todo::File.read(path)
|
32
|
+
expect(list).to be_a Array
|
33
|
+
expect(list.first).to be_a Todo::Task
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
context 'writing' do
|
38
|
+
let(:path) { File.dirname(__FILE__) + '/../data/test.txt' }
|
39
|
+
|
40
|
+
it 'should write to a list file when initialized in writeable mode' do
|
41
|
+
file = Todo::File.new(path, 'w')
|
42
|
+
file.puts(Todo::Task.new('Task 1'))
|
43
|
+
file.puts(Todo::Task.new('Task 2'))
|
44
|
+
file.close
|
45
|
+
|
46
|
+
target = File.open(path)
|
47
|
+
expect(target.gets.rstrip).to eq("Task 1")
|
48
|
+
expect(target.gets.rstrip).to eq("Task 2")
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'should write to a list file when opened in writeable mode' do
|
52
|
+
Todo::File.open(path, 'w') do |file|
|
53
|
+
file.puts(Todo::Task.new('Task 1'))
|
54
|
+
file.puts(Todo::Task.new('Task 2'))
|
55
|
+
end
|
56
|
+
|
57
|
+
target = File.open(path)
|
58
|
+
expect(target.gets.rstrip).to eq("Task 1")
|
59
|
+
expect(target.gets.rstrip).to eq("Task 2")
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'should write each task in the list to a line in the file' do
|
63
|
+
list = Todo::List.new([Todo::Task.new('Task 1'), Todo::Task.new('Task 2')])
|
64
|
+
Todo::File.write(path, list)
|
65
|
+
|
66
|
+
target = File.open(path)
|
67
|
+
expect(target.gets.rstrip).to eq("Task 1")
|
68
|
+
expect(target.gets.rstrip).to eq("Task 2")
|
69
|
+
end
|
70
|
+
|
71
|
+
after do
|
72
|
+
File.delete(path)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
data/spec/todo-txt/list_spec.rb
CHANGED
@@ -7,17 +7,17 @@ describe Todo::List do
|
|
7
7
|
|
8
8
|
context 'create with an array' do
|
9
9
|
it 'successfully creates an object' do
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
expect(Todo::List.new
|
10
|
+
items = []
|
11
|
+
items << "(A) A string task!"
|
12
|
+
items << Todo::Task.new("(A) An actual task!")
|
13
|
+
expect(Todo::List.new(items).length).to eq(2)
|
14
14
|
end
|
15
15
|
|
16
16
|
it 'does not have a path' do
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
expect(Todo::List.new(
|
17
|
+
items = []
|
18
|
+
items << "(A) A string task!"
|
19
|
+
items << Todo::Task.new("(A) An actual task!")
|
20
|
+
expect(Todo::List.new(items).path).to eq(nil)
|
21
21
|
end
|
22
22
|
end
|
23
23
|
|
@@ -38,12 +38,9 @@ describe Todo::List do
|
|
38
38
|
end
|
39
39
|
|
40
40
|
it 'accepts a mix of tasks and strings' do
|
41
|
-
|
42
|
-
"A task!",
|
43
|
-
Todo::Task.new("Another task!"),
|
44
|
-
])
|
41
|
+
mixed = Todo::List.new(["A task!", Todo::Task.new("Another task!")])
|
45
42
|
|
46
|
-
expect(
|
43
|
+
expect(mixed.length).to eq(2)
|
47
44
|
end
|
48
45
|
|
49
46
|
it 'has the correct path' do
|
@@ -97,9 +94,7 @@ describe Todo::List do
|
|
97
94
|
end
|
98
95
|
|
99
96
|
it 'should be able to filter by project, context and priority' do
|
100
|
-
filtered = list.by_project('+project').
|
101
|
-
by_context('@context').
|
102
|
-
by_priority('C')
|
97
|
+
filtered = list.by_project('+project').by_context('@context').by_priority('C')
|
103
98
|
|
104
99
|
# Make sure some data was actually checked
|
105
100
|
expect(filtered.length).to be > 0
|
@@ -115,17 +115,25 @@ describe Todo::Syntax do
|
|
115
115
|
end
|
116
116
|
end
|
117
117
|
|
118
|
-
describe '#
|
118
|
+
describe '#extract_tags' do
|
119
119
|
specify 'empty task' do
|
120
|
-
expect(
|
120
|
+
expect(extract_tags('')).to eq({})
|
121
121
|
end
|
122
122
|
|
123
|
-
specify 'task without
|
124
|
-
expect(
|
123
|
+
specify 'task without tag' do
|
124
|
+
expect(extract_tags('something to do')).to be {}
|
125
125
|
end
|
126
126
|
|
127
127
|
specify 'task with due date' do
|
128
|
-
expect(
|
128
|
+
expect(extract_tags('something to do due:2016-03-30')).to eq(:due => '2016-03-30')
|
129
|
+
end
|
130
|
+
|
131
|
+
specify 'task with case-insensitive due date' do
|
132
|
+
expect(extract_tags('something to do DUE:2016-03-30')).to eq(:due => '2016-03-30')
|
133
|
+
end
|
134
|
+
|
135
|
+
specify 'task with multiple tags' do
|
136
|
+
expect(extract_tags('something to do hello:world and foo:bar')).to eq(:hello => 'world', :foo => 'bar')
|
129
137
|
end
|
130
138
|
end
|
131
139
|
|
data/spec/todo-txt/task_spec.rb
CHANGED
@@ -11,7 +11,7 @@ describe Todo::Task do
|
|
11
11
|
task = Todo::Task.new '(A) 2012-12-08 My task @test +test2'
|
12
12
|
Timecop.freeze(2013, 12, 8) do
|
13
13
|
task.do!
|
14
|
-
expect(task.
|
14
|
+
expect(task.raw).to eq('(A) 2012-12-08 My task @test +test2')
|
15
15
|
end
|
16
16
|
end
|
17
17
|
|
@@ -32,7 +32,7 @@ describe Todo::Task do
|
|
32
32
|
|
33
33
|
it 'should retain the original task creation string' do
|
34
34
|
task = Todo::Task.new '(A) This is an awesome task, yo. +winning'
|
35
|
-
expect(task.
|
35
|
+
expect(task.raw).to eq('(A) This is an awesome task, yo. +winning')
|
36
36
|
end
|
37
37
|
|
38
38
|
it 'should be able to get just the text, no contexts etc.' do
|
@@ -411,11 +411,22 @@ describe Todo::Task do
|
|
411
411
|
logger = double(Logger)
|
412
412
|
Todo::Logger.logger = logger
|
413
413
|
|
414
|
-
task = Todo::Task.new
|
415
|
-
error_message = 'Task#date is deprecated, use created_on instead.'
|
414
|
+
task = Todo::Task.new('x 2012-09-11 (B) 2012-03-04 This is a sweet task. @context due:2012-02-31 +project')
|
415
|
+
error_message = '`Task#date` is deprecated, use `Task#created_on` instead.'
|
416
416
|
|
417
417
|
expect(logger).to receive(:warn).with(error_message)
|
418
418
|
task.date
|
419
419
|
end
|
420
|
+
|
421
|
+
it 'should call @logger.warn if #orig called as deprecated method' do
|
422
|
+
logger = double(Logger)
|
423
|
+
Todo::Logger.logger = logger
|
424
|
+
|
425
|
+
task = Todo::Task.new('x 2012-09-11 (B) 2012-03-04 This is a sweet task. @context due:2012-02-31 +project')
|
426
|
+
error_message = '`Task#orig` is deprecated, use `Task#raw` instead.'
|
427
|
+
|
428
|
+
expect(logger).to receive(:warn).with(error_message)
|
429
|
+
task.orig
|
430
|
+
end
|
420
431
|
end
|
421
432
|
end
|
data/todo-txt.gemspec
CHANGED
@@ -1,11 +1,11 @@
|
|
1
1
|
Gem::Specification.new do |s|
|
2
2
|
s.name = 'todo-txt'
|
3
|
-
s.version = '0.
|
4
|
-
s.authors = [
|
5
|
-
s.email =
|
6
|
-
s.summary =
|
7
|
-
s.homepage =
|
8
|
-
s.description =
|
3
|
+
s.version = '0.10'
|
4
|
+
s.authors = ['Sam Rose']
|
5
|
+
s.email = 'samwho@lbak.co.uk'
|
6
|
+
s.summary = 'A client library for parsing todo.txt files.'
|
7
|
+
s.homepage = 'http://github.com/samwho/todo-txt-gem'
|
8
|
+
s.description = 'Allows for simple parsing of todo.txt files, as per Gina Trapani\'s todo.txt project.'
|
9
9
|
s.required_ruby_version = '>= 2.0'
|
10
10
|
s.license = 'GPL-2'
|
11
11
|
s.files = `git ls-files`.split(/\n/)
|
data/todo.txt
CHANGED
@@ -1,13 +1,19 @@
|
|
1
1
|
x 2016-04-21 modify list#save! to work with windows line separators +bug
|
2
2
|
determine correct/expected behaviour for missing file paths +feature +api
|
3
3
|
dirty flag on lists that have modifications after loading +feature
|
4
|
-
extract file handling into separate IO-like module +refactor +api
|
4
|
+
x 2016-04-27 extract file handling into separate IO-like module +refactor +api
|
5
5
|
implement enumerable API on list rather than inherit from array +api
|
6
6
|
list operations should be algebraically closed +api
|
7
7
|
include rubocop in travis builds +build
|
8
8
|
use << operation to move tasks between lists +api +feature
|
9
9
|
create github issue with roadmap for 1.0 release +feature +api
|
10
10
|
consider rewriting file write tests with mocking and stringio +refactor +build
|
11
|
-
remove deprecated date
|
11
|
+
remove deprecated date and orig methods from task +api
|
12
12
|
pass in options as an instance rather than make it always global +refactor +api
|
13
13
|
save a backup of an existing todo file when the IO object is read in +feature
|
14
|
+
x 2016-06-16 extract `due:` formatting into a more general syntax for adding tags +feature +refactor +api
|
15
|
+
x rename lib path to match standard gem conventions +refactor
|
16
|
+
x 2016-04-23 tidy up code examples in doc comments to match ruby styleguide conventions +refactor +docs
|
17
|
+
consider renaming or aliasing `#done?` to `#complete?` +api +refactor
|
18
|
+
add complementary method returning opposite of `#done?` to provide symmetry +api
|
19
|
+
restructure test specs to reflect different core concerns and areas of functionality +refactor
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: todo-txt
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: '0.
|
4
|
+
version: '0.10'
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sam Rose
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-
|
11
|
+
date: 2016-07-02 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: Allows for simple parsing of todo.txt files, as per Gina Trapani's todo.txt
|
14
14
|
project.
|
@@ -26,14 +26,17 @@ files:
|
|
26
26
|
- README.md
|
27
27
|
- Rakefile
|
28
28
|
- lib/todo-txt.rb
|
29
|
-
- lib/todo
|
30
|
-
- lib/todo
|
31
|
-
- lib/todo
|
32
|
-
- lib/todo
|
33
|
-
- lib/todo
|
29
|
+
- lib/todo.rb
|
30
|
+
- lib/todo/file.rb
|
31
|
+
- lib/todo/list.rb
|
32
|
+
- lib/todo/logger.rb
|
33
|
+
- lib/todo/options.rb
|
34
|
+
- lib/todo/syntax.rb
|
35
|
+
- lib/todo/task.rb
|
34
36
|
- spec/data/tasks.txt
|
35
37
|
- spec/data/todo.txt
|
36
38
|
- spec/spec_helper.rb
|
39
|
+
- spec/todo-txt/file_spec.rb
|
37
40
|
- spec/todo-txt/list_spec.rb
|
38
41
|
- spec/todo-txt/options/require_completed_on_spec.rb
|
39
42
|
- spec/todo-txt/options_spec.rb
|
@@ -61,9 +64,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
61
64
|
version: '0'
|
62
65
|
requirements: []
|
63
66
|
rubyforge_project:
|
64
|
-
rubygems_version: 2.
|
67
|
+
rubygems_version: 2.4.5
|
65
68
|
signing_key:
|
66
69
|
specification_version: 4
|
67
70
|
summary: A client library for parsing todo.txt files.
|
68
71
|
test_files: []
|
69
|
-
has_rdoc:
|
data/lib/todo-txt/list.rb
DELETED
@@ -1,122 +0,0 @@
|
|
1
|
-
module Todo
|
2
|
-
# Initializes a Todo List object with a path to the corresponding todo.txt
|
3
|
-
# file. For example, if your todo.txt file is located at:
|
4
|
-
#
|
5
|
-
# /home/sam/Dropbox/todo/todo.txt
|
6
|
-
#
|
7
|
-
# You would initialize this object like:
|
8
|
-
#
|
9
|
-
# list = Todo::List.new "/home/sam/Dropbox/todo/todo-txt"
|
10
|
-
#
|
11
|
-
# Alternately, you can initialize this object with an array of strings or
|
12
|
-
# tasks. If the array is of strings, the strings will be converted into
|
13
|
-
# tasks. You can supply a mixed list of string and tasks if you wish.
|
14
|
-
#
|
15
|
-
# Example:
|
16
|
-
#
|
17
|
-
# array = Array.new
|
18
|
-
# array.push "(A) A string task!"
|
19
|
-
# array.push Todo::Task.new("(A) An actual task!")
|
20
|
-
#
|
21
|
-
# list = Todo::List.new array
|
22
|
-
class List < Array
|
23
|
-
def initialize(list)
|
24
|
-
if list.is_a? Array
|
25
|
-
# No file path was given.
|
26
|
-
@path = nil
|
27
|
-
|
28
|
-
# If path is an array, loop over it, adding to self.
|
29
|
-
list.each do |task|
|
30
|
-
# If it's a string, make a new task out of it.
|
31
|
-
if task.is_a? String
|
32
|
-
push Task.new task
|
33
|
-
# If it's a task, just add it.
|
34
|
-
elsif task.is_a? Todo::Task
|
35
|
-
push task
|
36
|
-
end
|
37
|
-
end
|
38
|
-
elsif list.is_a? String
|
39
|
-
@path = list
|
40
|
-
|
41
|
-
# Read in lines from file, create Todo::Tasks out of them and push them
|
42
|
-
# onto self.
|
43
|
-
File.open(list) do |file|
|
44
|
-
file.each_line { |line| push Task.new(line) }
|
45
|
-
end
|
46
|
-
end
|
47
|
-
end
|
48
|
-
|
49
|
-
# The path to the todo.txt file that you supplied when you created the
|
50
|
-
# Todo::List object.
|
51
|
-
attr_reader :path
|
52
|
-
|
53
|
-
# Filters the list by priority and returns a new list.
|
54
|
-
#
|
55
|
-
# Example:
|
56
|
-
#
|
57
|
-
# list = Todo::List.new "/path/to/list"
|
58
|
-
# list.by_priority "A" #=> Will be a new list with only priority A tasks
|
59
|
-
def by_priority(priority)
|
60
|
-
List.new(select { |task| task.priority == priority })
|
61
|
-
end
|
62
|
-
|
63
|
-
# Filters the list by context and returns a new list.
|
64
|
-
#
|
65
|
-
# Example:
|
66
|
-
#
|
67
|
-
# list = Todo::List.new "/path/to/list"
|
68
|
-
# list.by_context "@context" #=> Will be a new list with only tasks
|
69
|
-
# containing "@context"
|
70
|
-
def by_context(context)
|
71
|
-
List.new(select { |task| task.contexts.include? context })
|
72
|
-
end
|
73
|
-
|
74
|
-
# Filters the list by project and returns a new list.
|
75
|
-
#
|
76
|
-
# Example:
|
77
|
-
#
|
78
|
-
# list = Todo::List.new "/path/to/list"
|
79
|
-
# list.by_project "+project" #=> Will be a new list with only tasks
|
80
|
-
# containing "+project"
|
81
|
-
def by_project(project)
|
82
|
-
List.new(select { |task| task.projects.include? project })
|
83
|
-
end
|
84
|
-
|
85
|
-
# Filters the list by completed tasks and returns a new list.
|
86
|
-
#
|
87
|
-
# Example:
|
88
|
-
#
|
89
|
-
# list = Todo::List.new "/path/to/list"
|
90
|
-
# list.by_done #=> Will be a new list with only tasks marked with
|
91
|
-
# an [x]
|
92
|
-
def by_done
|
93
|
-
List.new(select(&:done?))
|
94
|
-
end
|
95
|
-
|
96
|
-
# Filters the list by incomplete tasks and returns a new list.
|
97
|
-
#
|
98
|
-
# Example:
|
99
|
-
#
|
100
|
-
# list = Todo::List.new "/path/to/list"
|
101
|
-
# list.by_not_done #=> Will be a new list with only incomplete tasks
|
102
|
-
def by_not_done
|
103
|
-
List.new(select { |task| task.done? == false })
|
104
|
-
end
|
105
|
-
|
106
|
-
# Saves the list to the original file location.
|
107
|
-
#
|
108
|
-
# Warning: This is a destructive operation and will overwrite what is
|
109
|
-
# currently there.
|
110
|
-
#
|
111
|
-
# If no `path` is specified in the constructor then an error is raised.
|
112
|
-
def save!
|
113
|
-
raise "No path specified." unless path
|
114
|
-
|
115
|
-
File.open(path, 'w') do |outfile|
|
116
|
-
each do |task|
|
117
|
-
outfile.puts(task.to_s)
|
118
|
-
end
|
119
|
-
end
|
120
|
-
end
|
121
|
-
end
|
122
|
-
end
|