todo-txt 0.9 → 0.10
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 +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
|