todotxt 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +6 -14
- data/.gitignore +2 -0
- data/README.md +40 -18
- data/Rakefile +1 -4
- data/bin/todotxt +5 -5
- data/features/edit.feature +1 -1
- data/features/files.feature +53 -0
- data/features/initialize.feature +0 -11
- data/features/move.feature +13 -0
- data/features/step_definitions/environment_steps.rb +8 -4
- data/features/step_definitions/list_steps.rb +5 -1
- data/lib/todotxt.rb +9 -8
- data/lib/todotxt/cli.rb +96 -142
- data/lib/todotxt/clihelpers.rb +21 -24
- data/lib/todotxt/config.rb +45 -16
- data/lib/todotxt/regex.rb +5 -5
- data/lib/todotxt/todo.rb +44 -30
- data/lib/todotxt/todofile.rb +11 -11
- data/lib/todotxt/todolist.rb +31 -19
- data/lib/todotxt/version.rb +1 -1
- data/spec/config_spec.rb +41 -21
- data/spec/fixtures/config_no_todo.cfg +2 -0
- data/spec/todo_spec.rb +77 -90
- data/spec/todofile_spec.rb +17 -12
- data/spec/todolist_spec.rb +65 -66
- data/todotxt.gemspec +19 -21
- metadata +50 -61
data/lib/todotxt/clihelpers.rb
CHANGED
@@ -1,55 +1,52 @@
|
|
1
1
|
module Todotxt
|
2
2
|
module CLIHelpers
|
3
|
-
|
4
|
-
def format_todo(todo, number_padding=nil)
|
3
|
+
def format_todo(todo, number_padding = nil)
|
5
4
|
line = todo.line.to_s
|
6
|
-
if number_padding
|
7
|
-
line = line.rjust number_padding
|
8
|
-
end
|
5
|
+
line = line.rjust number_padding if number_padding
|
9
6
|
|
10
7
|
text = todo.to_s
|
11
8
|
|
12
|
-
|
9
|
+
if todo.done
|
10
|
+
text = text.color(:black).bright
|
11
|
+
else
|
13
12
|
text.gsub! PRIORITY_REGEX do |p|
|
14
|
-
case p[1]
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
13
|
+
color = case p[1]
|
14
|
+
when 'A'
|
15
|
+
:red
|
16
|
+
when 'B'
|
17
|
+
:yellow
|
18
|
+
when 'C'
|
19
|
+
:green
|
20
|
+
else
|
21
|
+
:white
|
22
|
+
end
|
24
23
|
|
25
24
|
p.to_s.color(color)
|
26
25
|
end
|
27
26
|
|
28
27
|
text.gsub! PROJECT_REGEX, '\1'.color(:green)
|
29
28
|
text.gsub! CONTEXT_REGEX, '\1'.color(:blue)
|
30
|
-
else
|
31
|
-
text = text.color(:black).bright
|
32
29
|
end
|
33
30
|
|
34
|
-
ret =
|
31
|
+
ret = ''
|
35
32
|
|
36
33
|
ret << "#{line}. ".color(:black).bright
|
37
|
-
ret <<
|
34
|
+
ret << text.to_s
|
38
35
|
end
|
39
36
|
|
40
|
-
def warn
|
37
|
+
def warn(message = '')
|
41
38
|
puts "WARN: #{message}".color(:yellow)
|
42
39
|
end
|
43
40
|
|
44
|
-
def notice
|
41
|
+
def notice(message = '')
|
45
42
|
puts "=> #{message}".color(:green)
|
46
43
|
end
|
47
44
|
|
48
|
-
def error
|
45
|
+
def error(message = '')
|
49
46
|
puts "ERROR: #{message}".color(:red)
|
50
47
|
end
|
51
48
|
|
52
|
-
def error_and_exit
|
49
|
+
def error_and_exit(message = '')
|
53
50
|
error message
|
54
51
|
exit
|
55
52
|
end
|
data/lib/todotxt/config.rb
CHANGED
@@ -1,35 +1,63 @@
|
|
1
|
-
require
|
2
|
-
require
|
1
|
+
require 'parseconfig'
|
2
|
+
require 'fileutils'
|
3
3
|
|
4
4
|
module Todotxt
|
5
|
+
# Todotxt relies on a configuration file (`.todotxt.cfg`) in your home directory,
|
6
|
+
# which points to the location of your todo.txt. You can run:
|
7
|
+
#
|
8
|
+
# $ todotxt generate_cfg
|
9
|
+
#
|
10
|
+
# to generate this file, which will then point to `~/todo.txt`.
|
5
11
|
class Config < ParseConfig
|
6
|
-
def initialize
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
@config_file = config_file
|
11
|
-
end
|
12
|
+
def initialize(options = {})
|
13
|
+
@options = options
|
14
|
+
|
15
|
+
@config_file = options[:config_file] || Config.config_path
|
12
16
|
|
13
17
|
if file_exists?
|
14
18
|
super @config_file
|
15
19
|
validate
|
16
20
|
else
|
21
|
+
# Initialize mandatory values for `ParseConfig`
|
17
22
|
@params = {}
|
18
23
|
@groups = []
|
24
|
+
@splitRegex = '\s*=\s*'
|
25
|
+
@comments = [';']
|
19
26
|
end
|
20
27
|
end
|
21
28
|
|
22
29
|
def file_exists?
|
23
|
-
File.
|
30
|
+
File.exist? @config_file
|
24
31
|
end
|
25
32
|
|
26
33
|
def files
|
27
|
-
|
34
|
+
files = {}
|
35
|
+
(params['files'] || { 'todo' => params['todo_txt_path'] }).each do |k, p|
|
36
|
+
files[k] = TodoFile.new(p)
|
37
|
+
end
|
38
|
+
|
39
|
+
files
|
40
|
+
end
|
41
|
+
|
42
|
+
def file
|
43
|
+
if @options[:file].nil?
|
44
|
+
files['todo'] || raise("Bad configuration file: 'todo' is a required file.")
|
45
|
+
elsif files[@options[:file]]
|
46
|
+
files[@options[:file]]
|
47
|
+
elsif File.exist?(File.expand_path(@options[:file]))
|
48
|
+
TodoFile.new(File.expand_path(@options[:file]))
|
49
|
+
else
|
50
|
+
raise("\"#{@options[:file]}\" is not defined in the config and not a valid filename.")
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def editor
|
55
|
+
params['editor'] || ENV['EDITOR']
|
28
56
|
end
|
29
57
|
|
30
58
|
def generate!
|
31
|
-
|
32
|
-
|
59
|
+
FileUtils.copy File.join(File.dirname(File.expand_path(__FILE__)), '..', '..', 'conf', 'todotxt.cfg'), @config_file
|
60
|
+
import_config
|
33
61
|
end
|
34
62
|
|
35
63
|
def path
|
@@ -41,17 +69,18 @@ module Todotxt
|
|
41
69
|
end
|
42
70
|
|
43
71
|
def self.config_path
|
44
|
-
File.join ENV[
|
72
|
+
File.join ENV['HOME'], '.todotxt.cfg'
|
45
73
|
end
|
46
74
|
|
47
75
|
def deprecated?
|
48
|
-
params[
|
76
|
+
params['files'].nil?
|
49
77
|
end
|
50
78
|
|
51
79
|
private
|
80
|
+
|
52
81
|
def validate
|
53
|
-
if params[
|
54
|
-
raise
|
82
|
+
if params['files'] && params['todo_txt_path']
|
83
|
+
raise 'Bad configuration file: use either files or todo_txt_path'
|
55
84
|
end
|
56
85
|
end
|
57
86
|
end
|
data/lib/todotxt/regex.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
module Todotxt
|
2
|
-
PRIORITY_REGEX = /^\(([A-Z])\)
|
3
|
-
PROJECT_REGEX = /(\+\w+)
|
4
|
-
CONTEXT_REGEX = /(@\w+)
|
5
|
-
DATE_REGEX = /^(\([A-Z]\) )?(x )?((\d{4}-)(\d{1,2}-)(\d{1,2}))\s
|
6
|
-
DONE_REGEX = /^(\([A-Z]\) )?x
|
2
|
+
PRIORITY_REGEX = /^\(([A-Z])\) /.freeze
|
3
|
+
PROJECT_REGEX = /(\+\w+)/.freeze
|
4
|
+
CONTEXT_REGEX = /(@\w+)/.freeze
|
5
|
+
DATE_REGEX = /^(\([A-Z]\) )?(x )?((\d{4}-)(\d{1,2}-)(\d{1,2}))\s?/.freeze
|
6
|
+
DONE_REGEX = /^(\([A-Z]\) )?x /.freeze
|
7
7
|
end
|
data/lib/todotxt/todo.rb
CHANGED
@@ -1,8 +1,16 @@
|
|
1
|
-
require
|
1
|
+
require 'todotxt/regex'
|
2
2
|
|
3
3
|
module Todotxt
|
4
|
+
# Represent a task formatted according to
|
5
|
+
# [todo.txt format rules](https://github.com/todotxt/todo.txt#todotxt-format-rules)
|
6
|
+
#
|
7
|
+
# @attr [String] the complete text definition of this task
|
8
|
+
# @attr [Integer] line the line number of this task
|
9
|
+
# @attr [Char] the character which defines the task's priority
|
10
|
+
# @attr [Array] projects list of linked projects
|
11
|
+
# @attr [Array] contexts list of linked contexts
|
12
|
+
# @attr [Boolean] done `true` if task is done
|
4
13
|
class Todo
|
5
|
-
|
6
14
|
attr_accessor :text
|
7
15
|
attr_accessor :line
|
8
16
|
attr_accessor :priority
|
@@ -10,25 +18,21 @@ module Todotxt
|
|
10
18
|
attr_accessor :contexts
|
11
19
|
attr_accessor :done
|
12
20
|
|
13
|
-
|
21
|
+
# @param[String] text
|
22
|
+
def initialize(text, line = nil)
|
14
23
|
@line = line
|
15
24
|
|
16
25
|
create_from_text text
|
17
26
|
end
|
18
27
|
|
19
|
-
|
20
|
-
|
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
|
-
|
28
|
+
# Get due date if set
|
29
|
+
# @return [Date|Nil]
|
27
30
|
def due
|
28
31
|
date = Chronic.parse(text.scan(DATE_REGEX).flatten[2])
|
29
32
|
date.nil? ? nil : date.to_date
|
30
33
|
end
|
31
34
|
|
35
|
+
# Mark this task as done
|
32
36
|
def do
|
33
37
|
unless done
|
34
38
|
@text = "x #{text}".strip
|
@@ -36,23 +40,20 @@ module Todotxt
|
|
36
40
|
end
|
37
41
|
end
|
38
42
|
|
43
|
+
# Mark this task as not done
|
39
44
|
def undo
|
40
45
|
if done
|
41
|
-
@text = text.sub(DONE_REGEX,
|
46
|
+
@text = text.sub(DONE_REGEX, '').strip
|
42
47
|
@done = false
|
43
48
|
end
|
44
49
|
end
|
45
50
|
|
46
|
-
def prioritize
|
47
|
-
if new_priority && !new_priority.match(/^[A-Z]$/i)
|
48
|
-
return
|
49
|
-
end
|
51
|
+
def prioritize(new_priority = nil, opts = {})
|
52
|
+
return if new_priority && !new_priority.match(/^[A-Z]$/i)
|
50
53
|
|
51
|
-
if new_priority
|
52
|
-
new_priority = new_priority.upcase
|
53
|
-
end
|
54
|
+
new_priority = new_priority.upcase if new_priority
|
54
55
|
|
55
|
-
priority_string = new_priority ? "(#{new_priority}) " :
|
56
|
+
priority_string = new_priority ? "(#{new_priority}) " : ''
|
56
57
|
|
57
58
|
if priority && !opts[:force]
|
58
59
|
@text.gsub! PRIORITY_REGEX, priority_string
|
@@ -63,33 +64,46 @@ module Todotxt
|
|
63
64
|
@priority = new_priority
|
64
65
|
end
|
65
66
|
|
66
|
-
|
67
|
-
|
67
|
+
# Add some text to the end of this task definition
|
68
|
+
def append(appended_text = '')
|
69
|
+
@text << ' ' << appended_text
|
68
70
|
end
|
69
71
|
|
70
|
-
|
72
|
+
# Add some text to the beginning of this task definition
|
73
|
+
def prepend(prepended_text = '')
|
71
74
|
@text = "#{prepended_text} #{text.gsub(PRIORITY_REGEX, '')}"
|
72
|
-
prioritize priority, :
|
75
|
+
prioritize priority, force: true
|
73
76
|
end
|
74
77
|
|
75
|
-
def replace
|
78
|
+
def replace(text)
|
76
79
|
create_from_text text
|
77
80
|
end
|
78
81
|
|
82
|
+
# @return [String]
|
79
83
|
def to_s
|
80
84
|
text.clone
|
81
85
|
end
|
82
86
|
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
+
# Compare with another `Todo` based on `line` and `priority` attributes
|
88
|
+
# @param [Todo] b
|
89
|
+
def <=>(b)
|
90
|
+
return 1 unless b.is_a? Todo
|
91
|
+
return line <=> b.line if priority.nil? && b.priority.nil?
|
87
92
|
|
88
93
|
return 1 if priority.nil?
|
89
94
|
return -1 if b.priority.nil?
|
90
95
|
|
91
|
-
|
96
|
+
priority <=> b.priority
|
92
97
|
end
|
93
98
|
|
99
|
+
private
|
100
|
+
|
101
|
+
def create_from_text(text)
|
102
|
+
@text = text
|
103
|
+
@priority = text.scan(PRIORITY_REGEX).flatten.first || nil
|
104
|
+
@projects = text.scan(PROJECT_REGEX).flatten.uniq || []
|
105
|
+
@contexts = text.scan(CONTEXT_REGEX).flatten.uniq || []
|
106
|
+
@done = !text.scan(DONE_REGEX).empty?
|
107
|
+
end
|
94
108
|
end
|
95
109
|
end
|
data/lib/todotxt/todofile.rb
CHANGED
@@ -1,40 +1,40 @@
|
|
1
1
|
module Todotxt
|
2
2
|
class TodoFile
|
3
|
-
|
4
|
-
def initialize path
|
3
|
+
def initialize(path)
|
5
4
|
@path = File.expand_path(path)
|
6
5
|
end
|
7
6
|
|
8
7
|
# Generate a file from template
|
9
8
|
def generate!
|
10
|
-
FileUtils.copy File.join(File.dirname(File.expand_path(__FILE__)),
|
9
|
+
FileUtils.copy File.join(File.dirname(File.expand_path(__FILE__)), '..', '..', 'conf', 'todo.txt'), @path
|
11
10
|
end
|
12
11
|
|
13
|
-
|
14
|
-
@path
|
15
|
-
end
|
12
|
+
attr_reader :path
|
16
13
|
|
17
14
|
def basename
|
18
15
|
File.basename @path
|
19
16
|
end
|
20
17
|
|
21
18
|
def exists?
|
22
|
-
File.
|
19
|
+
File.exist? File.expand_path(@path)
|
23
20
|
end
|
24
21
|
|
25
22
|
def self.from_key(key)
|
26
23
|
config = Todotxt::Config.new
|
27
|
-
if config.files.
|
24
|
+
if config.files.key? key
|
28
25
|
path = config.files[key]
|
29
|
-
|
26
|
+
new path
|
30
27
|
else
|
31
|
-
raise
|
28
|
+
raise 'Key not found in config'
|
32
29
|
end
|
33
30
|
end
|
34
31
|
|
35
|
-
|
36
32
|
def to_s
|
37
33
|
@path
|
38
34
|
end
|
35
|
+
|
36
|
+
def ==(other)
|
37
|
+
@path == other.path
|
38
|
+
end
|
39
39
|
end
|
40
40
|
end
|
data/lib/todotxt/todolist.rb
CHANGED
@@ -1,13 +1,19 @@
|
|
1
|
-
require
|
1
|
+
require 'todotxt/todo'
|
2
2
|
|
3
3
|
module Todotxt
|
4
|
-
|
4
|
+
# Represent a collection of `Todo` items
|
5
|
+
# TODO merge with TodoFile, both overlap too much
|
5
6
|
class TodoList
|
6
7
|
include Enumerable
|
7
8
|
|
8
|
-
|
9
|
+
attr_reader :todos
|
9
10
|
|
10
|
-
|
11
|
+
# @INK: refactor TodoList and TodoFile
|
12
|
+
# So that TodoFile contains all IO ad List is no longer dependent on file.
|
13
|
+
# That way, todolist lsa|listall can use multiple TodoFiles to generate one TodoList
|
14
|
+
|
15
|
+
def initialize(file, line = nil)
|
16
|
+
@line = line || 0
|
11
17
|
@todos = []
|
12
18
|
@file = file
|
13
19
|
|
@@ -16,44 +22,51 @@ module Todotxt
|
|
16
22
|
end
|
17
23
|
end
|
18
24
|
|
19
|
-
|
20
|
-
|
25
|
+
# @param [String] str add the given todo string definition to the list
|
26
|
+
# TODO also support `Todo` object
|
27
|
+
def add(str)
|
28
|
+
todo = Todo.new str, (@line += 1)
|
21
29
|
@todos.push todo
|
22
30
|
@todos.sort!
|
23
31
|
|
24
|
-
|
32
|
+
todo
|
25
33
|
end
|
26
34
|
|
27
|
-
|
35
|
+
# @param [Todo|String] line remove the given todo to the list
|
36
|
+
def remove(line)
|
28
37
|
@todos.reject! { |t| t.line.to_s == line.to_s }
|
29
38
|
end
|
30
39
|
|
31
|
-
def move
|
40
|
+
def move(line, other_list)
|
32
41
|
other_list.add find_by_line(line).to_s
|
33
42
|
remove line
|
34
43
|
end
|
35
44
|
|
45
|
+
# Get all projects from todo definitions
|
46
|
+
# @return[Array<String>]
|
36
47
|
def projects
|
37
|
-
map
|
48
|
+
map(&:projects).flatten.uniq.sort
|
38
49
|
end
|
39
50
|
|
51
|
+
# Get all contexts from todo definitions
|
52
|
+
# @return[Array<String>]
|
40
53
|
def contexts
|
41
|
-
map
|
54
|
+
map(&:contexts).flatten.uniq.sort
|
42
55
|
end
|
43
56
|
|
44
|
-
def find_by_line
|
57
|
+
def find_by_line(line)
|
45
58
|
@todos.find { |t| t.line.to_s == line.to_s }
|
46
59
|
end
|
47
60
|
|
48
61
|
def save
|
49
|
-
File.open(@file.path,
|
62
|
+
File.open(@file.path, 'w') { |f| f.write to_txt }
|
50
63
|
end
|
51
64
|
|
52
|
-
def each
|
65
|
+
def each(&block)
|
53
66
|
@todos.each &block
|
54
67
|
end
|
55
68
|
|
56
|
-
def filter
|
69
|
+
def filter(search = '', opts = {})
|
57
70
|
@todos.select! do |t|
|
58
71
|
select = false
|
59
72
|
|
@@ -77,16 +90,16 @@ module Todotxt
|
|
77
90
|
self
|
78
91
|
end
|
79
92
|
|
80
|
-
def on_date
|
93
|
+
def on_date(date)
|
81
94
|
@todos.select { |t| t.due == date }
|
82
95
|
end
|
83
96
|
|
84
|
-
def before_date
|
97
|
+
def before_date(date)
|
85
98
|
@todos.reject { |t| t.due.nil? || t.due >= date }
|
86
99
|
end
|
87
100
|
|
88
101
|
def to_txt
|
89
|
-
@todos.
|
102
|
+
@todos.sort_by(&:line).map { |t| t.to_s.strip }.join("\n")
|
90
103
|
end
|
91
104
|
|
92
105
|
def to_s
|
@@ -96,6 +109,5 @@ module Todotxt
|
|
96
109
|
def to_a
|
97
110
|
map { |t| ["#{t.line}. ", t.to_s] }
|
98
111
|
end
|
99
|
-
|
100
112
|
end
|
101
113
|
end
|