todotxt 0.0.1
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.
- data/.gitignore +4 -0
- data/Gemfile +4 -0
- data/MIT-LICENSE.txt +20 -0
- data/README.md +65 -0
- data/Rakefile +1 -0
- data/bin/todotxt +13 -0
- data/conf/todo.txt +1 -0
- data/conf/todotxt.cfg +1 -0
- data/fixtures/complex_todo.txt +3 -0
- data/fixtures/simple_todo.txt +4 -0
- data/lib/todotxt/cli.rb +289 -0
- data/lib/todotxt/clihelpers.rb +54 -0
- data/lib/todotxt/regex.rb +6 -0
- data/lib/todotxt/todo.rb +90 -0
- data/lib/todotxt/todolist.rb +83 -0
- data/lib/todotxt/version.rb +3 -0
- data/lib/todotxt.rb +13 -0
- data/spec/todo_spec.rb +129 -0
- data/spec/todolist_spec.rb +99 -0
- data/todotxt.gemspec +26 -0
- metadata +112 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/MIT-LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2011 Johan Sahlén, http://jsahlen.se/
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
# Todotxt.rb
|
2
|
+
|
3
|
+
*todo.txt with a ruby flair*
|
4
|
+
|
5
|
+
|
6
|
+
## About
|
7
|
+
|
8
|
+
Todotxt is a ruby CLI interface to work with a
|
9
|
+
[todo.txt](http://www.todotxt.com) file
|
10
|
+
|
11
|
+
|
12
|
+
## Install
|
13
|
+
|
14
|
+
Clone from [jsahlen/todotxt](http://github.com/jsahlen/todotxt) and do
|
15
|
+
|
16
|
+
rake install
|
17
|
+
|
18
|
+
I might release it as a proper gem in the future…
|
19
|
+
|
20
|
+
Todotxt relies on a configuration file (`.todotxt.cfg`) in your home directory,
|
21
|
+
which points to the location of your todo.txt. You can run
|
22
|
+
|
23
|
+
todotxt generate_cfg
|
24
|
+
|
25
|
+
to generate this file, which will then point to `~/todo.txt`.
|
26
|
+
|
27
|
+
|
28
|
+
## Usage
|
29
|
+
|
30
|
+
The gem will install a command, `todotxt` which is used to interact with your
|
31
|
+
todo.txt.
|
32
|
+
|
33
|
+
Tasks:
|
34
|
+
todotxt add | a TEXT # Add a new Todo item
|
35
|
+
todotxt append | app ITEM# STRING # Append STRING to ITEM#
|
36
|
+
todotxt del | rm ITEM#[, ITEM#, ITEM#, ...] # Remove ITEM#
|
37
|
+
todotxt do ITEM#[, ITEM#, ITEM#, ...] # Mark ITEM# as done
|
38
|
+
todotxt dp | depri ITEM#[, ITEM#, ITEM#, ...] # Remove priority for ITEM#
|
39
|
+
todotxt generate_cfg # Create a sample todo.txt
|
40
|
+
todotxt generate_config # Create a .todotxt.cfg file in your home folder, containing the path to todo.txt
|
41
|
+
todotxt help [TASK] # Describe available tasks or one specific task
|
42
|
+
todotxt list | ls [SEARCH] # List all todos, or todos matching SEARCH
|
43
|
+
todotxt listproj | lsproj # List all projects
|
44
|
+
todotxt lscon | lsc # List all contexts
|
45
|
+
todotxt lsdone | lsd # List all done items
|
46
|
+
todotxt prepend | prep ITEM# STRING # Prepend STRING to ITEM#
|
47
|
+
todotxt pri | p ITEM# PRIORITY # Set priority of ITEM# to PRIORITY
|
48
|
+
todotxt replace ITEM# TEXT # Completely replace ITEM# text with TEXT
|
49
|
+
todotxt undo | u ITEM#[, ITEM#, ITEM#, ...] # Mark ITEM# item as not done
|
50
|
+
todotxt version # Show todotxt version
|
51
|
+
|
52
|
+
Calling simply `todotxt` will automatically run the `ls` command.
|
53
|
+
|
54
|
+
|
55
|
+
## Dependencies
|
56
|
+
|
57
|
+
* [Thor](http://github.com/wycats/thor)
|
58
|
+
* [Rainbow](http://github.com/sickill/rainbow)
|
59
|
+
* [ParseConfig](http://www.5dollarwhitebox.org/drupal/?q=node/21)
|
60
|
+
|
61
|
+
|
62
|
+
## Bugs
|
63
|
+
|
64
|
+
Please report any bugs using the
|
65
|
+
[GitHub Issue Tracker](http://github.com/jsahlen/todotxt/issues).
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'bundler/gem_tasks'
|
data/bin/todotxt
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
libdir = File.join(File.dirname(File.dirname(__FILE__)), "lib")
|
4
|
+
$LOAD_PATH.unshift(libdir) unless $LOAD_PATH.include?(libdir)
|
5
|
+
|
6
|
+
args = ARGV.clone
|
7
|
+
|
8
|
+
if args.empty?
|
9
|
+
args.push "ls"
|
10
|
+
end
|
11
|
+
|
12
|
+
require "todotxt"
|
13
|
+
Todotxt::CLI.start(args)
|
data/conf/todo.txt
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
Learn todo.txt +myproject @home
|
data/conf/todotxt.cfg
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
todo_txt_path = ~/todo.txt
|
data/lib/todotxt/cli.rb
ADDED
@@ -0,0 +1,289 @@
|
|
1
|
+
require "thor"
|
2
|
+
require "rainbow"
|
3
|
+
require "parseconfig"
|
4
|
+
|
5
|
+
module Todotxt
|
6
|
+
CFG_PATH = File.expand_path("~/.todotxt.cfg")
|
7
|
+
|
8
|
+
class CLI < Thor
|
9
|
+
include Thor::Actions
|
10
|
+
include Todotxt::CLIHelpers
|
11
|
+
|
12
|
+
def self.source_root
|
13
|
+
File.join File.dirname(__FILE__), "..", "..", "conf"
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize(*args)
|
17
|
+
super
|
18
|
+
|
19
|
+
unless ["help", "generate_config"].include? ARGV[0]
|
20
|
+
parse_config
|
21
|
+
|
22
|
+
@list = TodoList.new @txt_path
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
#
|
27
|
+
# Listing
|
28
|
+
#
|
29
|
+
|
30
|
+
desc "list | ls [SEARCH]", "List all todos, or todos matching SEARCH"
|
31
|
+
method_option :done, :type => :boolean, :aliases => "-d", :desc => "Include todo items that have been marked as done"
|
32
|
+
method_option :simple, :type => :boolean, :desc => "Simple output (for scripts, etc)"
|
33
|
+
def list search=""
|
34
|
+
with_done = false
|
35
|
+
|
36
|
+
with_done = true if options[:done]
|
37
|
+
|
38
|
+
@list.filter(search, :with_done => with_done)
|
39
|
+
|
40
|
+
render_list :simple => !!options[:simple]
|
41
|
+
end
|
42
|
+
map "ls" => :list
|
43
|
+
|
44
|
+
desc "lsdone | lsd", "List all done items"
|
45
|
+
def lsdone search=""
|
46
|
+
@list.filter(search, :only_done => true)
|
47
|
+
|
48
|
+
render_list
|
49
|
+
end
|
50
|
+
map "lsd" => :lsdone
|
51
|
+
|
52
|
+
desc "listproj | lsproj", "List all projects"
|
53
|
+
def listproj
|
54
|
+
@list.projects.each { |p| say p }
|
55
|
+
end
|
56
|
+
map "lsproj" => :listproj
|
57
|
+
|
58
|
+
desc "lscon | lsc", "List all contexts"
|
59
|
+
def lscon
|
60
|
+
@list.contexts.each { |c| say c }
|
61
|
+
end
|
62
|
+
map "lsc" => :lscon
|
63
|
+
|
64
|
+
#
|
65
|
+
# Todo management
|
66
|
+
#
|
67
|
+
|
68
|
+
desc "add | a TEXT", "Add a new Todo item"
|
69
|
+
def add(str, *str2)
|
70
|
+
string = "#{str} #{str2.join(' ')}"
|
71
|
+
todo = @list.add string
|
72
|
+
|
73
|
+
puts format_todo(todo)
|
74
|
+
|
75
|
+
@list.save
|
76
|
+
end
|
77
|
+
map "a" => :add
|
78
|
+
|
79
|
+
desc "do ITEM#[, ITEM#, ITEM#, ...]", "Mark ITEM# as done"
|
80
|
+
def do line1, *lines
|
81
|
+
lines.unshift(line1).each do |line|
|
82
|
+
todo = @list.find_by_line line
|
83
|
+
if todo
|
84
|
+
todo.do
|
85
|
+
puts format_todo(todo)
|
86
|
+
|
87
|
+
@list.save
|
88
|
+
else
|
89
|
+
error "No todo found at line #{line}"
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
desc "undo | u ITEM#[, ITEM#, ITEM#, ...]", "Mark ITEM# item as not done"
|
95
|
+
def undo line1, *lines
|
96
|
+
lines.unshift(line1).each do |line|
|
97
|
+
todo = @list.find_by_line line
|
98
|
+
if todo
|
99
|
+
todo.undo
|
100
|
+
puts format_todo(todo)
|
101
|
+
|
102
|
+
@list.save
|
103
|
+
else
|
104
|
+
error "No todo found at line #{line}"
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
map "u" => :undo
|
109
|
+
|
110
|
+
desc "pri | p ITEM# PRIORITY", "Set priority of ITEM# to PRIORITY"
|
111
|
+
def pri line, priority
|
112
|
+
todo = @list.find_by_line line
|
113
|
+
if todo
|
114
|
+
todo.prioritize priority
|
115
|
+
puts format_todo(todo)
|
116
|
+
|
117
|
+
@list.save
|
118
|
+
else
|
119
|
+
error "No todo found at line #{line}"
|
120
|
+
end
|
121
|
+
end
|
122
|
+
map "p" => :pri
|
123
|
+
|
124
|
+
desc "dp | depri ITEM#[, ITEM#, ITEM#, ...]", "Remove priority for ITEM#"
|
125
|
+
def dp line1, *lines
|
126
|
+
lines.unshift(line1).each do |line|
|
127
|
+
todo = @list.find_by_line line
|
128
|
+
if todo
|
129
|
+
todo.prioritize
|
130
|
+
puts format_todo(todo)
|
131
|
+
|
132
|
+
@list.save
|
133
|
+
else
|
134
|
+
error "No todo found at line #{line}"
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
map "depri" => :dp
|
139
|
+
|
140
|
+
desc "append | app ITEM# STRING", "Append STRING to ITEM#"
|
141
|
+
def append line, str, *str2
|
142
|
+
string = "#{str} #{str2.join(' ')}"
|
143
|
+
todo = @list.find_by_line line
|
144
|
+
if todo
|
145
|
+
todo.append string
|
146
|
+
puts format_todo(todo)
|
147
|
+
|
148
|
+
@list.save
|
149
|
+
else
|
150
|
+
error "No todo found at line #{line}"
|
151
|
+
end
|
152
|
+
end
|
153
|
+
map "app" => :append
|
154
|
+
|
155
|
+
desc "prepend | prep ITEM# STRING", "Prepend STRING to ITEM#"
|
156
|
+
def prepend line, str, *str2
|
157
|
+
string = "#{str} #{str2.join(' ')}"
|
158
|
+
todo = @list.find_by_line line
|
159
|
+
if todo
|
160
|
+
todo.prepend string
|
161
|
+
puts format_todo(todo)
|
162
|
+
|
163
|
+
@list.save
|
164
|
+
else
|
165
|
+
error "No todo found at line #{line}"
|
166
|
+
end
|
167
|
+
end
|
168
|
+
map "prep" => :prepend
|
169
|
+
|
170
|
+
desc "replace ITEM# TEXT", "Completely replace ITEM# text with TEXT"
|
171
|
+
def replace line, str, *str2
|
172
|
+
string = "#{str} #{str2.join(' ')}"
|
173
|
+
todo = @list.find_by_line line
|
174
|
+
if todo
|
175
|
+
todo.replace string
|
176
|
+
puts format_todo(todo)
|
177
|
+
|
178
|
+
@list.save
|
179
|
+
else
|
180
|
+
error "No todo found at line #{line}"
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
desc "del | rm ITEM#[, ITEM#, ITEM#, ...]", "Remove ITEM#"
|
185
|
+
method_option :force, :type => :boolean, :aliases => "-f", :desc => "Don't confirm removal"
|
186
|
+
def del line1, *lines
|
187
|
+
lines.unshift(line1).each do |line|
|
188
|
+
todo = @list.find_by_line line
|
189
|
+
if todo
|
190
|
+
say format_todo(todo)
|
191
|
+
if options[:force] || yes?("Remove this item? [y/N]")
|
192
|
+
@list.remove line
|
193
|
+
notice "Removed from list"
|
194
|
+
|
195
|
+
@list.save
|
196
|
+
end
|
197
|
+
else
|
198
|
+
error "No todo found at line #{line}"
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
map "rm" => :del
|
203
|
+
|
204
|
+
#
|
205
|
+
# File generation
|
206
|
+
#
|
207
|
+
|
208
|
+
desc "generate_config", "Create a .todotxt.cfg file in your home folder, containing the path to todo.txt"
|
209
|
+
def generate_config
|
210
|
+
copy_file "todotxt.cfg", CFG_PATH
|
211
|
+
puts ""
|
212
|
+
|
213
|
+
parse_config
|
214
|
+
end
|
215
|
+
|
216
|
+
desc "generate_cfg", "Create a sample todo.txt"
|
217
|
+
def generate_txt
|
218
|
+
copy_file "todo.txt", @txt_path
|
219
|
+
puts ""
|
220
|
+
end
|
221
|
+
|
222
|
+
#
|
223
|
+
# Extras
|
224
|
+
#
|
225
|
+
|
226
|
+
desc "version", "Show todotxt version"
|
227
|
+
def version
|
228
|
+
say "todotxt #{VERSION}"
|
229
|
+
end
|
230
|
+
|
231
|
+
private
|
232
|
+
|
233
|
+
def render_list opts={}
|
234
|
+
numsize = @list.count + 1
|
235
|
+
numsize = numsize.to_s.length + 0
|
236
|
+
|
237
|
+
@list.each do |t|
|
238
|
+
if opts[:simple]
|
239
|
+
say "#{t.line} #{t.to_s}"
|
240
|
+
else
|
241
|
+
say format_todo(t, numsize)
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
unless opts[:simple]
|
246
|
+
puts "TODO: #{@list.count} items".color(:black).bright
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
def parse_config
|
251
|
+
unless File.exist? CFG_PATH
|
252
|
+
puts "You need a .todotxt.cfg file in your home folder to continue (used to determine the path of your todo.txt.) Answer yes to have it generated for you (pointing to ~/todo.txt), or no to create it yourself.\n\n"
|
253
|
+
confirm_generate = yes? "Create ~/.todotxt.cfg? [y/N]"
|
254
|
+
|
255
|
+
if confirm_generate
|
256
|
+
generate_config
|
257
|
+
else
|
258
|
+
puts ""
|
259
|
+
exit
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
cfg = ParseConfig.new(CFG_PATH)
|
264
|
+
|
265
|
+
txt = cfg.get_value "todo_txt_path"
|
266
|
+
|
267
|
+
if txt
|
268
|
+
@txt_path = File.expand_path(txt)
|
269
|
+
|
270
|
+
unless File.exist? @txt_path
|
271
|
+
puts "#{txt} doesn't exist yet. Would you like to generate a sample file?"
|
272
|
+
confirm_generate = yes? "Create #{txt}? [y/N]"
|
273
|
+
|
274
|
+
if confirm_generate
|
275
|
+
generate_txt
|
276
|
+
else
|
277
|
+
puts ""
|
278
|
+
exit
|
279
|
+
end
|
280
|
+
end
|
281
|
+
else
|
282
|
+
error "Couldn't find todo_txt_path setting in ~/.todotxt.cfg."
|
283
|
+
puts "Please run the following to create a new configuration file:"
|
284
|
+
puts " todotxt generate_config"
|
285
|
+
exit
|
286
|
+
end
|
287
|
+
end
|
288
|
+
end
|
289
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Todotxt
|
2
|
+
module CLIHelpers
|
3
|
+
|
4
|
+
def format_todo(todo, number_padding=nil)
|
5
|
+
line = todo.line.to_s
|
6
|
+
|
7
|
+
if number_padding
|
8
|
+
line = line.rjust number_padding
|
9
|
+
end
|
10
|
+
|
11
|
+
text = todo.to_s
|
12
|
+
|
13
|
+
unless todo.done
|
14
|
+
text.gsub! PRIORITY_REGEX do |p|
|
15
|
+
case p[1]
|
16
|
+
when "A"
|
17
|
+
color = :red
|
18
|
+
when "B"
|
19
|
+
color = :yellow
|
20
|
+
when "C"
|
21
|
+
color = :green
|
22
|
+
else
|
23
|
+
color = :white
|
24
|
+
end
|
25
|
+
|
26
|
+
p.to_s.color(color)
|
27
|
+
end
|
28
|
+
|
29
|
+
text.gsub! PROJECT_REGEX, '\1'.color(:green)
|
30
|
+
text.gsub! CONTEXT_REGEX, '\1'.color(:blue)
|
31
|
+
else
|
32
|
+
text = text.color(:black).bright
|
33
|
+
end
|
34
|
+
|
35
|
+
ret = ""
|
36
|
+
|
37
|
+
ret << "#{line}. ".color(:black).bright
|
38
|
+
ret << "#{text}"
|
39
|
+
end
|
40
|
+
|
41
|
+
def warn message=""
|
42
|
+
puts "WARN: #{message}".color(:yellow)
|
43
|
+
end
|
44
|
+
|
45
|
+
def notice message=""
|
46
|
+
puts "=> #{message}".color(:green)
|
47
|
+
end
|
48
|
+
|
49
|
+
def error message=""
|
50
|
+
puts "ERROR: #{message}".color(:red)
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
end
|
data/lib/todotxt/todo.rb
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
require "todotxt/regex"
|
2
|
+
|
3
|
+
module Todotxt
|
4
|
+
class Todo
|
5
|
+
|
6
|
+
attr_accessor :text
|
7
|
+
attr_accessor :line
|
8
|
+
attr_accessor :priority
|
9
|
+
attr_accessor :projects
|
10
|
+
attr_accessor :contexts
|
11
|
+
attr_accessor :done
|
12
|
+
|
13
|
+
def initialize text, line=nil
|
14
|
+
@line = line
|
15
|
+
|
16
|
+
create_from_text text
|
17
|
+
end
|
18
|
+
|
19
|
+
def create_from_text text
|
20
|
+
@text = text
|
21
|
+
@priority = text.scan(PRIORITY_REGEX).flatten.first || nil
|
22
|
+
@projects = text.scan(PROJECT_REGEX).flatten.uniq || []
|
23
|
+
@contexts = text.scan(CONTEXT_REGEX).flatten.uniq || []
|
24
|
+
@done = !text.scan(DONE_REGEX).empty?
|
25
|
+
end
|
26
|
+
|
27
|
+
def do
|
28
|
+
unless done
|
29
|
+
@text = "x #{text}".strip
|
30
|
+
@done = true
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def undo
|
35
|
+
if done
|
36
|
+
@text = text.sub(DONE_REGEX, "").strip
|
37
|
+
@done = false
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def prioritize new_priority=nil, opts={}
|
42
|
+
if new_priority && !new_priority.match(/^[A-Z]$/i)
|
43
|
+
return
|
44
|
+
end
|
45
|
+
|
46
|
+
if new_priority
|
47
|
+
new_priority = new_priority.upcase
|
48
|
+
end
|
49
|
+
|
50
|
+
priority_string = new_priority ? "(#{new_priority}) " : ""
|
51
|
+
|
52
|
+
if priority && !opts[:force]
|
53
|
+
@text.gsub! PRIORITY_REGEX, priority_string
|
54
|
+
else
|
55
|
+
@text = "#{priority_string}#{text}".strip
|
56
|
+
end
|
57
|
+
|
58
|
+
@priority = new_priority
|
59
|
+
end
|
60
|
+
|
61
|
+
def append appended_text=""
|
62
|
+
@text << " " << appended_text
|
63
|
+
end
|
64
|
+
|
65
|
+
def prepend prepended_text=""
|
66
|
+
@text = "#{prepended_text} #{text.gsub(PRIORITY_REGEX, '')}"
|
67
|
+
prioritize priority, :force => true
|
68
|
+
end
|
69
|
+
|
70
|
+
def replace text
|
71
|
+
create_from_text text
|
72
|
+
end
|
73
|
+
|
74
|
+
def to_s
|
75
|
+
text.clone
|
76
|
+
end
|
77
|
+
|
78
|
+
def <=> b
|
79
|
+
if priority.nil? && b.priority.nil?
|
80
|
+
return line <=> b.line
|
81
|
+
end
|
82
|
+
|
83
|
+
return 1 if priority.nil?
|
84
|
+
return -1 if b.priority.nil?
|
85
|
+
|
86
|
+
return priority <=> b.priority
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
require "todotxt/todo"
|
2
|
+
|
3
|
+
module Todotxt
|
4
|
+
class TodoList
|
5
|
+
include Enumerable
|
6
|
+
|
7
|
+
attr_accessor :todos
|
8
|
+
|
9
|
+
def initialize file
|
10
|
+
@todos = []
|
11
|
+
@file = file
|
12
|
+
|
13
|
+
File.open(file).read.each_line do |l|
|
14
|
+
add l.strip unless l.empty?
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def add str
|
19
|
+
todo = Todo.new str, (@todos.count + 1)
|
20
|
+
@todos.push todo
|
21
|
+
@todos.sort!
|
22
|
+
|
23
|
+
return todo
|
24
|
+
end
|
25
|
+
|
26
|
+
def remove line
|
27
|
+
@todos.reject! { |t| t.line.to_s == line.to_s }
|
28
|
+
end
|
29
|
+
|
30
|
+
def projects
|
31
|
+
map { |t| t.projects }.flatten.uniq.sort
|
32
|
+
end
|
33
|
+
|
34
|
+
def contexts
|
35
|
+
map { |t| t.contexts }.flatten.uniq.sort
|
36
|
+
end
|
37
|
+
|
38
|
+
def find_by_line line
|
39
|
+
@todos.find { |t| t.line.to_s == line.to_s }
|
40
|
+
end
|
41
|
+
|
42
|
+
def save
|
43
|
+
File.open(@file, "w") { |f| f.write to_txt }
|
44
|
+
end
|
45
|
+
|
46
|
+
def each &block
|
47
|
+
@todos.each &block
|
48
|
+
end
|
49
|
+
|
50
|
+
def filter search="", opts={}
|
51
|
+
@todos.select! do |t|
|
52
|
+
select = false
|
53
|
+
|
54
|
+
text = t.to_s.downcase
|
55
|
+
|
56
|
+
if opts[:only_done]
|
57
|
+
select = true if t.done
|
58
|
+
else
|
59
|
+
if opts[:with_done]
|
60
|
+
select = true
|
61
|
+
else
|
62
|
+
select = true unless t.done
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
select = false unless text.include?(search.downcase)
|
67
|
+
|
68
|
+
select
|
69
|
+
end
|
70
|
+
|
71
|
+
self
|
72
|
+
end
|
73
|
+
|
74
|
+
def to_txt
|
75
|
+
@todos.sort { |a,b| a.line <=> b.line }.map { |t| t.to_s.strip }.join("\n")
|
76
|
+
end
|
77
|
+
|
78
|
+
def to_a
|
79
|
+
map { |t| ["#{t.line}. ", t.to_s] }
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
end
|
data/lib/todotxt.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
# Setup our load paths
|
2
|
+
libdir = File.dirname(__FILE__)
|
3
|
+
$LOAD_PATH.unshift(libdir) unless $LOAD_PATH.include?(libdir)
|
4
|
+
|
5
|
+
module Todotxt
|
6
|
+
autoload :Todo, "todotxt/todo"
|
7
|
+
autoload :TodoList, "todotxt/todolist"
|
8
|
+
autoload :CLI, "todotxt/cli"
|
9
|
+
autoload :CLIHelpers, "todotxt/clihelpers"
|
10
|
+
end
|
11
|
+
|
12
|
+
require "todotxt/regex"
|
13
|
+
require "todotxt/version"
|
data/spec/todo_spec.rb
ADDED
@@ -0,0 +1,129 @@
|
|
1
|
+
require "todotxt/todo"
|
2
|
+
|
3
|
+
describe Todotxt::Todo do
|
4
|
+
|
5
|
+
it "should create a todo item string" do
|
6
|
+
todo = Todotxt::Todo.new "an item"
|
7
|
+
todo.to_s.should eql("an item")
|
8
|
+
end
|
9
|
+
|
10
|
+
it "should parse metadata when creating a simple item" do
|
11
|
+
todo = Todotxt::Todo.new "x an item +project1 +project2 @context1 @context2"
|
12
|
+
|
13
|
+
todo.to_s.should eql "x an item +project1 +project2 @context1 @context2"
|
14
|
+
todo.priority.should eql nil
|
15
|
+
todo.projects.should eql ["+project1", "+project2"]
|
16
|
+
todo.contexts.should eql ["@context1", "@context2"]
|
17
|
+
todo.done.should eql true
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should parse metadata when creating an item with priority" do
|
21
|
+
todo = Todotxt::Todo.new "(A) x an item +project1 +project2 @context1 @context2"
|
22
|
+
|
23
|
+
todo.to_s.should eql "(A) x an item +project1 +project2 @context1 @context2"
|
24
|
+
todo.priority.should eql "A"
|
25
|
+
todo.projects.should eql ["+project1", "+project2"]
|
26
|
+
todo.contexts.should eql ["@context1", "@context2"]
|
27
|
+
todo.done.should eql true
|
28
|
+
end
|
29
|
+
|
30
|
+
it "should store line number when creating an item" do
|
31
|
+
todo = Todotxt::Todo.new "an item", "2"
|
32
|
+
|
33
|
+
todo.line.should eql "2"
|
34
|
+
end
|
35
|
+
|
36
|
+
it "should set an item as done" do
|
37
|
+
todo = Todotxt::Todo.new "an item"
|
38
|
+
|
39
|
+
todo.do
|
40
|
+
|
41
|
+
todo.to_s.should eql "x an item"
|
42
|
+
todo.done.should eql true
|
43
|
+
end
|
44
|
+
|
45
|
+
it "should set an item as not done" do
|
46
|
+
todo = Todotxt::Todo.new "x an item"
|
47
|
+
|
48
|
+
todo.undo
|
49
|
+
|
50
|
+
todo.to_s.should eql "an item"
|
51
|
+
todo.done.should eql false
|
52
|
+
end
|
53
|
+
|
54
|
+
it "should add priority to an item" do
|
55
|
+
todo = Todotxt::Todo.new "an item"
|
56
|
+
|
57
|
+
todo.prioritize "a"
|
58
|
+
|
59
|
+
todo.to_s.should eql "(A) an item"
|
60
|
+
todo.priority.should eql "A"
|
61
|
+
end
|
62
|
+
|
63
|
+
it "should change priority of an item" do
|
64
|
+
todo = Todotxt::Todo.new "(A) an item"
|
65
|
+
|
66
|
+
todo.prioritize "z"
|
67
|
+
|
68
|
+
todo.to_s.should eql "(Z) an item"
|
69
|
+
todo.priority.should eql "Z"
|
70
|
+
end
|
71
|
+
|
72
|
+
it "should remove priority from an item" do
|
73
|
+
todo = Todotxt::Todo.new "(A) an item"
|
74
|
+
|
75
|
+
todo.prioritize
|
76
|
+
|
77
|
+
todo.to_s.should eql "an item"
|
78
|
+
todo.priority.should eql nil
|
79
|
+
end
|
80
|
+
|
81
|
+
it "should append text to an item" do
|
82
|
+
todo = Todotxt::Todo.new "an item"
|
83
|
+
|
84
|
+
todo.append "more text"
|
85
|
+
|
86
|
+
todo.to_s.should eql "an item more text"
|
87
|
+
end
|
88
|
+
|
89
|
+
it "should prepend text to an item" do
|
90
|
+
todo = Todotxt::Todo.new "an item"
|
91
|
+
|
92
|
+
todo.prepend "more text"
|
93
|
+
|
94
|
+
todo.to_s.should eql "more text an item"
|
95
|
+
end
|
96
|
+
|
97
|
+
it "should preserve priority when prepending text to an item" do
|
98
|
+
todo = Todotxt::Todo.new "(A) an item"
|
99
|
+
|
100
|
+
todo.prepend "more text"
|
101
|
+
|
102
|
+
todo.to_s.should eql "(A) more text an item"
|
103
|
+
todo.priority.should eql "A"
|
104
|
+
end
|
105
|
+
|
106
|
+
it "should replace an item with new text" do
|
107
|
+
todo = Todotxt::Todo.new "an item"
|
108
|
+
|
109
|
+
todo.replace "(A) a replacement item"
|
110
|
+
|
111
|
+
todo.to_s.should eql "(A) a replacement item"
|
112
|
+
todo.priority.should eql "A"
|
113
|
+
end
|
114
|
+
|
115
|
+
it "should sort based on line number" do
|
116
|
+
todo1 = Todotxt::Todo.new "an item 1", 1
|
117
|
+
todo2 = Todotxt::Todo.new "an item 2", 2
|
118
|
+
|
119
|
+
(todo1 <=> todo2).should eql -1
|
120
|
+
end
|
121
|
+
|
122
|
+
it "should value items with priority higher when sorting" do
|
123
|
+
todo1 = Todotxt::Todo.new "an item 1", 1
|
124
|
+
todo2 = Todotxt::Todo.new "(A) an item 2", 2
|
125
|
+
|
126
|
+
(todo1 <=> todo2).should eql 1
|
127
|
+
end
|
128
|
+
|
129
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
require "todotxt/todolist"
|
2
|
+
|
3
|
+
describe Todotxt::TodoList do
|
4
|
+
|
5
|
+
describe "with simple list" do
|
6
|
+
before :each do
|
7
|
+
@list = Todotxt::TodoList.new File.join(File.dirname(__FILE__), "..", "fixtures", "simple_todo.txt")
|
8
|
+
end
|
9
|
+
|
10
|
+
it "should parse a file on creation" do
|
11
|
+
@list.todos[0].to_s.should eql "First item"
|
12
|
+
@list.todos[1].to_s.should eql "Second item"
|
13
|
+
@list.todos[2].to_s.should eql "Third item"
|
14
|
+
@list.todos[3].to_s.should eql "x First done item"
|
15
|
+
|
16
|
+
@list.todos[0].line.should eql 1
|
17
|
+
@list.todos[1].line.should eql 2
|
18
|
+
@list.todos[2].line.should eql 3
|
19
|
+
@list.todos[3].line.should eql 4
|
20
|
+
end
|
21
|
+
|
22
|
+
it "should add a new item" do
|
23
|
+
@list.add "Fourth item"
|
24
|
+
|
25
|
+
@list.todos[4].to_s.should eql "Fourth item"
|
26
|
+
@list.todos[4].line.should eql 5
|
27
|
+
end
|
28
|
+
|
29
|
+
it "should remove an item" do
|
30
|
+
@list.remove 1
|
31
|
+
|
32
|
+
@list.todos[0].to_s.should eql "Second item"
|
33
|
+
end
|
34
|
+
|
35
|
+
it "should find item by line" do
|
36
|
+
todo = @list.find_by_line 3
|
37
|
+
|
38
|
+
todo.to_s.should eql "Third item"
|
39
|
+
end
|
40
|
+
|
41
|
+
it "should filter list when searching" do
|
42
|
+
@list.filter "First"
|
43
|
+
|
44
|
+
@list.todos.count.should eql 1
|
45
|
+
end
|
46
|
+
|
47
|
+
it "should filter list when searching case-sensitive" do
|
48
|
+
@list.filter "first"
|
49
|
+
|
50
|
+
@list.todos.count.should eql 1
|
51
|
+
end
|
52
|
+
|
53
|
+
it "should include done items in search when told to do so" do
|
54
|
+
@list.filter "first", :with_done => true
|
55
|
+
|
56
|
+
@list.todos.count.should eql 2
|
57
|
+
end
|
58
|
+
|
59
|
+
it "should only include done items in search when told to do so" do
|
60
|
+
@list.filter "first", :only_done => true
|
61
|
+
|
62
|
+
@list.todos.count.should eql 1
|
63
|
+
end
|
64
|
+
|
65
|
+
it "should render plain text" do
|
66
|
+
comparison_string = <<EOF
|
67
|
+
First item
|
68
|
+
Second item
|
69
|
+
Third item
|
70
|
+
x First done item
|
71
|
+
EOF
|
72
|
+
@list.to_txt.should eql comparison_string.strip
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
describe "with complex list" do
|
77
|
+
before :each do
|
78
|
+
@list = Todotxt::TodoList.new File.join(File.dirname(__FILE__), "..", "fixtures", "complex_todo.txt")
|
79
|
+
end
|
80
|
+
|
81
|
+
it "should sort itself automatically on parse" do
|
82
|
+
@list.todos[0].to_s.should eql "(A) an item"
|
83
|
+
@list.todos[0].line.should eql 3
|
84
|
+
end
|
85
|
+
|
86
|
+
it "should re-sort itself after adding a new item" do
|
87
|
+
@list.add "(B) A new item"
|
88
|
+
|
89
|
+
@list.todos[1].to_s.should eql "(B) A new item"
|
90
|
+
@list.todos[1].line.should eql 4
|
91
|
+
end
|
92
|
+
|
93
|
+
it "should list all projects and contexts in the list" do
|
94
|
+
@list.projects.should eql ["+project1", "+project2"]
|
95
|
+
@list.contexts.should eql ["@context1", "@context2"]
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
end
|
data/todotxt.gemspec
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "todotxt/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "todotxt"
|
7
|
+
s.version = Todotxt::VERSION
|
8
|
+
s.authors = ["Johan Sahlén"]
|
9
|
+
s.email = ["johan@monospace.se"]
|
10
|
+
s.homepage = "http://github.com/jsahlen/todotxt"
|
11
|
+
s.summary = %q{todo.txt with a Ruby flair}
|
12
|
+
s.description = %q{Interact with your todo.txt using a Ruby-base command-line tool}
|
13
|
+
|
14
|
+
s.rubyforge_project = "todotxt"
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
|
21
|
+
s.add_dependency "thor", ">= 0.14.6"
|
22
|
+
s.add_dependency "rainbow", ">= 1.1.1"
|
23
|
+
s.add_dependency "parseconfig", ">= 0.5.2"
|
24
|
+
|
25
|
+
s.add_development_dependency "rspec", ">= 2.6.0"
|
26
|
+
end
|
metadata
ADDED
@@ -0,0 +1,112 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: todotxt
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Johan Sahlén
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2011-08-09 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: thor
|
16
|
+
requirement: &2164860840 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 0.14.6
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *2164860840
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: rainbow
|
27
|
+
requirement: &2164860340 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 1.1.1
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *2164860340
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: parseconfig
|
38
|
+
requirement: &2164859880 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ! '>='
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: 0.5.2
|
44
|
+
type: :runtime
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *2164859880
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: rspec
|
49
|
+
requirement: &2164859420 !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 2.6.0
|
55
|
+
type: :development
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: *2164859420
|
58
|
+
description: Interact with your todo.txt using a Ruby-base command-line tool
|
59
|
+
email:
|
60
|
+
- johan@monospace.se
|
61
|
+
executables:
|
62
|
+
- todotxt
|
63
|
+
extensions: []
|
64
|
+
extra_rdoc_files: []
|
65
|
+
files:
|
66
|
+
- .gitignore
|
67
|
+
- Gemfile
|
68
|
+
- MIT-LICENSE.txt
|
69
|
+
- README.md
|
70
|
+
- Rakefile
|
71
|
+
- bin/todotxt
|
72
|
+
- conf/todo.txt
|
73
|
+
- conf/todotxt.cfg
|
74
|
+
- fixtures/complex_todo.txt
|
75
|
+
- fixtures/simple_todo.txt
|
76
|
+
- lib/todotxt.rb
|
77
|
+
- lib/todotxt/cli.rb
|
78
|
+
- lib/todotxt/clihelpers.rb
|
79
|
+
- lib/todotxt/regex.rb
|
80
|
+
- lib/todotxt/todo.rb
|
81
|
+
- lib/todotxt/todolist.rb
|
82
|
+
- lib/todotxt/version.rb
|
83
|
+
- spec/todo_spec.rb
|
84
|
+
- spec/todolist_spec.rb
|
85
|
+
- todotxt.gemspec
|
86
|
+
homepage: http://github.com/jsahlen/todotxt
|
87
|
+
licenses: []
|
88
|
+
post_install_message:
|
89
|
+
rdoc_options: []
|
90
|
+
require_paths:
|
91
|
+
- lib
|
92
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
93
|
+
none: false
|
94
|
+
requirements:
|
95
|
+
- - ! '>='
|
96
|
+
- !ruby/object:Gem::Version
|
97
|
+
version: '0'
|
98
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
99
|
+
none: false
|
100
|
+
requirements:
|
101
|
+
- - ! '>='
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
requirements: []
|
105
|
+
rubyforge_project: todotxt
|
106
|
+
rubygems_version: 1.8.6
|
107
|
+
signing_key:
|
108
|
+
specification_version: 3
|
109
|
+
summary: todo.txt with a Ruby flair
|
110
|
+
test_files:
|
111
|
+
- spec/todo_spec.rb
|
112
|
+
- spec/todolist_spec.rb
|