todotxt 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +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
|