xtdo 0.2
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/HISTORY +7 -0
- data/LICENSE +19 -0
- data/README.rdoc +58 -0
- data/Rakefile +10 -0
- data/TODO +4 -0
- data/bin/xtdo +8 -0
- data/bin/xtdo_completion.zsh +26 -0
- data/lib/xtdo.rb +203 -0
- data/spec/add_spec.rb +18 -0
- data/spec/bump_spec.rb +19 -0
- data/spec/completion_spec.rb +18 -0
- data/spec/done_spec.rb +14 -0
- data/spec/list_spec.rb +19 -0
- data/spec/recurring_spec.rb +93 -0
- data/spec/spec_helper.rb +112 -0
- metadata +105 -0
data/HISTORY
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Copyright (c) 2010 Xavier Shay
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
5
|
+
in the Software without restriction, including without limitation the rights
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
11
|
+
all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
= xtdo
|
2
|
+
|
3
|
+
An ultra fast command line todo list manager, with some strong opinions about workflow.
|
4
|
+
|
5
|
+
== Usage
|
6
|
+
|
7
|
+
Every command is mapped to a single character. If you want to be fast, you need to get the key strokes down.
|
8
|
+
|
9
|
+
l = list
|
10
|
+
a = add (or all when used with list)
|
11
|
+
d = done
|
12
|
+
b = bump
|
13
|
+
r = recur
|
14
|
+
c = completion (to help enable shell tab completion)
|
15
|
+
|
16
|
+
Here is how it fits together:
|
17
|
+
|
18
|
+
gem install xtdo
|
19
|
+
|
20
|
+
alias t=xtdo # Recommended!
|
21
|
+
|
22
|
+
t a some task # Add to list
|
23
|
+
t l a # All tasks
|
24
|
+
t b 0 some task # Move task to TODAY
|
25
|
+
t a 0 another task # New task on today list, shortcut letter
|
26
|
+
t l # Today's tasks
|
27
|
+
t d another task # It is done!
|
28
|
+
t b 1w some task # Actually we will to it next week
|
29
|
+
t r a 1d,thu bin night # Bin night every Thursday
|
30
|
+
t r a 1m,4 pay rent # Pay rent on the 4th of the month
|
31
|
+
t r d pay rent # Bah who wants to pay rent
|
32
|
+
|
33
|
+
t l c # List all tasks in a format suitable for tab completion
|
34
|
+
t r c # List all recurring tasks for completion
|
35
|
+
|
36
|
+
There is a command line completion script for ZSH in bin/xtdo_completion.zsh that you should use, since it auto completes task names for you. I don't know the best way to install this yet.
|
37
|
+
|
38
|
+
By default tasks are stored in ~/.xtdo. Customize this by setting the XTDO_PATH environment variable. I store mine in Dropbox.
|
39
|
+
|
40
|
+
== Why?
|
41
|
+
|
42
|
+
I used to use a small subset of Things.app, and I really dug the workflow. I spend my life on the command-line though. Existing command line apps didn't quite match my workflow, or were too slow.
|
43
|
+
|
44
|
+
Xtdo is fast in the sense that you type very little to make it do things, but also in that it is extremely quick to respond to commands - you will never notice any delay.
|
45
|
+
|
46
|
+
== Compatibility
|
47
|
+
|
48
|
+
Requires ruby 1.9.2. Will not run on any variant of 1.8.
|
49
|
+
|
50
|
+
== Developing
|
51
|
+
|
52
|
+
Refactors accepted, as long as they don't turn everything into a First Class Object. I see no reason why this code should require more than one file. I will likely not accept any features that I will not use personally.
|
53
|
+
|
54
|
+
There are no runtime external dependencies. Let's keep it that way.
|
55
|
+
|
56
|
+
== Status
|
57
|
+
|
58
|
+
Almost, but not quite, useable. This file is the only documentation you'll get currently.
|
data/Rakefile
ADDED
data/TODO
ADDED
data/bin/xtdo
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
autoload -U compinit
|
2
|
+
compinit
|
3
|
+
|
4
|
+
_xtdo() {
|
5
|
+
# TODO: Complete a bare 'xtdo' with commands + description
|
6
|
+
|
7
|
+
if [[ $words[2] == b ]]; then
|
8
|
+
if (( CURRENT == 4 )); then
|
9
|
+
compadd `bin/xtdo l c`
|
10
|
+
fi
|
11
|
+
fi
|
12
|
+
|
13
|
+
if [[ $words[2] == d ]]; then
|
14
|
+
if (( CURRENT == 3 )); then
|
15
|
+
compadd `bin/xtdo l c`
|
16
|
+
fi
|
17
|
+
fi
|
18
|
+
|
19
|
+
if [[ $words[2] == r && $words[3] == d ]]; then
|
20
|
+
if (( CURRENT == 4 )); then
|
21
|
+
compadd `bin/xtdo r c`
|
22
|
+
fi
|
23
|
+
fi
|
24
|
+
}
|
25
|
+
|
26
|
+
compdef _xtdo xtdo
|
data/lib/xtdo.rb
ADDED
@@ -0,0 +1,203 @@
|
|
1
|
+
require 'date'
|
2
|
+
require 'yaml'
|
3
|
+
|
4
|
+
class Xtdo
|
5
|
+
def self.run(store, operation)
|
6
|
+
operation = operation.split(/\s+/)
|
7
|
+
verb = operation.shift
|
8
|
+
|
9
|
+
# tasks = {
|
10
|
+
# 'T1' => {:scheduled => Date.today}
|
11
|
+
# }
|
12
|
+
store = File.expand_path(store)
|
13
|
+
tasks = if File.exists?(store)
|
14
|
+
YAML.load(File.open(store))
|
15
|
+
else
|
16
|
+
{}
|
17
|
+
end
|
18
|
+
tasks[:tasks] ||= {}
|
19
|
+
tasks[:recurring] ||= {}
|
20
|
+
|
21
|
+
manager = Xtdo.new(tasks)
|
22
|
+
|
23
|
+
ret = case verb
|
24
|
+
when 'a' then # A is for add!
|
25
|
+
manager.add operation.join(' ')
|
26
|
+
when 'b' then # B is for bump!
|
27
|
+
manager.bump operation.join(' ')
|
28
|
+
when 'd' then # D is for done!
|
29
|
+
manager.done operation.join(' ')
|
30
|
+
when 'l' then # L is for list!
|
31
|
+
case operation[0]
|
32
|
+
when 'a' then
|
33
|
+
manager.list [:today, :next, :scheduled]
|
34
|
+
when 'c' then
|
35
|
+
manager.list [:today, :next, :scheduled], :format => :completion
|
36
|
+
else
|
37
|
+
manager.list [:today]
|
38
|
+
end
|
39
|
+
when 'r' then # R is for recur!
|
40
|
+
manager.recur operation.join(' ')
|
41
|
+
end
|
42
|
+
|
43
|
+
manager.save(store)
|
44
|
+
ret
|
45
|
+
end
|
46
|
+
|
47
|
+
attr_reader :tasks, :recurring
|
48
|
+
|
49
|
+
def initialize(tasks)
|
50
|
+
@tasks = tasks[:tasks]
|
51
|
+
@recurring = tasks[:recurring]
|
52
|
+
end
|
53
|
+
|
54
|
+
def parse_relative_time(number, period)
|
55
|
+
adj = {
|
56
|
+
'd' => 1,
|
57
|
+
'w' => 7,
|
58
|
+
'm' => 30,
|
59
|
+
'y' => 365
|
60
|
+
}[period] || 1
|
61
|
+
Date.today + adj * number
|
62
|
+
end
|
63
|
+
|
64
|
+
def add(task)
|
65
|
+
number, period, task = /^(?:(\d+)([dwmy])? )?(.*)/.match(task).captures
|
66
|
+
@tasks[make_key task] = {:name => task}
|
67
|
+
@tasks[make_key task][:scheduled] = parse_relative_time(number.to_i, period) if number
|
68
|
+
end
|
69
|
+
|
70
|
+
def bump(task)
|
71
|
+
number, period, task = /^(?:(\d+)([dwmy])? )?(.*)/.match(task).captures
|
72
|
+
if !number
|
73
|
+
"Invalid time"
|
74
|
+
elsif tasks[make_key task]
|
75
|
+
tasks[make_key task][:scheduled] = parse_relative_time(number.to_i, period)
|
76
|
+
else
|
77
|
+
"No such task"
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def done(task)
|
82
|
+
if tasks.delete(make_key task)
|
83
|
+
"Task done"
|
84
|
+
else
|
85
|
+
"No such task"
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def list(groups, opts = {})
|
90
|
+
# Check for recurring
|
91
|
+
recurring.each do |name, task|
|
92
|
+
if task[:next] <= Date.today
|
93
|
+
tasks[make_key name] = {:scheduled => Date.today, :name => task[:name] }
|
94
|
+
number, period, start, _ = self.class.extract_recur_tokens(task[:period] + ' ' + name)
|
95
|
+
|
96
|
+
task[:next] = self.class.calculate_starting_day(Date.today, number, period, start)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
if opts[:format] == :completion
|
101
|
+
tasks.keys.join "\n"
|
102
|
+
else
|
103
|
+
# Print groups
|
104
|
+
task_selector = {
|
105
|
+
:today => lambda {|x| x && x <= Date.today },
|
106
|
+
:next => lambda {|x| !x },
|
107
|
+
:scheduled => lambda {|x| x && x > Date.today }
|
108
|
+
}
|
109
|
+
|
110
|
+
groups.map do |group|
|
111
|
+
t = tasks.select {|name, opts| task_selector[group][opts[:scheduled]] }
|
112
|
+
next if t.empty?
|
113
|
+
"===== #{group.to_s.upcase}\n" + t.map { |name, attrs|
|
114
|
+
attrs[:name]
|
115
|
+
}.join("\n")
|
116
|
+
end.join("\n")
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def recur(task)
|
121
|
+
tokens = task.split(/\s+/)
|
122
|
+
verb = tokens.shift
|
123
|
+
task = tokens.join(' ')
|
124
|
+
case verb
|
125
|
+
when 'a' then
|
126
|
+
number, period, start, name = self.class.extract_recur_tokens(task)
|
127
|
+
|
128
|
+
period_string = "#{number}#{period}"
|
129
|
+
period_string += ",#{start}" if start
|
130
|
+
recurring[make_key name] = {
|
131
|
+
:name => name,
|
132
|
+
:next => Date.today + 1,
|
133
|
+
:period => period_string
|
134
|
+
}
|
135
|
+
when 'd' then
|
136
|
+
if recurring.delete make_key(task)
|
137
|
+
"Recurring task removed"
|
138
|
+
else
|
139
|
+
"No such recurring task"
|
140
|
+
end
|
141
|
+
when 'l' then
|
142
|
+
"===== RECURRING\n" + recurring.map do |name, task|
|
143
|
+
"%-6s%s" % [task[:period], task[:name]]
|
144
|
+
end.join("\n")
|
145
|
+
when 'c' then
|
146
|
+
recurring.keys.join "\n"
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def make_key(name)
|
151
|
+
name.gsub /[^a-zA-Z0-9,.]/, '-'
|
152
|
+
end
|
153
|
+
|
154
|
+
def self.calculate_starting_day(date, number, period, start = nil)
|
155
|
+
days = %w(sun mon tue wed thu fri sat)
|
156
|
+
number = (number || 1).to_i
|
157
|
+
|
158
|
+
adjust = case period
|
159
|
+
when 'd' then 1
|
160
|
+
when 'w' then
|
161
|
+
if days.index(start)
|
162
|
+
(days.index(start) - date.wday) % 7 + (number - 1) * 7
|
163
|
+
end
|
164
|
+
when 'm' then
|
165
|
+
start = start.to_i
|
166
|
+
if start > 0
|
167
|
+
year = date.year
|
168
|
+
if start.to_i <= date.day
|
169
|
+
month = date.month + 1
|
170
|
+
if month > 12
|
171
|
+
month = 1
|
172
|
+
year += 1
|
173
|
+
end
|
174
|
+
else
|
175
|
+
month = date.month
|
176
|
+
end
|
177
|
+
Date.new(year, month, start.to_i) - date
|
178
|
+
end
|
179
|
+
end
|
180
|
+
if adjust
|
181
|
+
if adjust == 0
|
182
|
+
date + 7
|
183
|
+
else
|
184
|
+
date + adjust
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
def self.extract_recur_tokens(task)
|
190
|
+
/^(\d+)([dwmy])?(?:,(#{days.join('|')}|\d{1,2}))? (.*)/.match(task).captures
|
191
|
+
end
|
192
|
+
def self.days
|
193
|
+
days = %w(sun mon tue wed thu fri sat)
|
194
|
+
end
|
195
|
+
|
196
|
+
def save(file)
|
197
|
+
File.open(file, 'w') {|f| f.write({
|
198
|
+
:tasks => tasks,
|
199
|
+
:recurring => recurring,
|
200
|
+
:version => 1
|
201
|
+
}.to_yaml) }
|
202
|
+
end
|
203
|
+
end
|
data/spec/add_spec.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
feature 'add' do
|
4
|
+
scenario 'to next' do
|
5
|
+
t('a T1')
|
6
|
+
t('l a').should have_task('T1', :in => :next)
|
7
|
+
end
|
8
|
+
|
9
|
+
scenario 'to today' do
|
10
|
+
t('a 0 T1')
|
11
|
+
t('l a').should have_task('T1', :in => :today)
|
12
|
+
end
|
13
|
+
|
14
|
+
scenario 'to scheduled' do
|
15
|
+
t('a 1w T1')
|
16
|
+
t('l a').should have_task('T1', :in => :scheduled, :for => Date.today + 7)
|
17
|
+
end
|
18
|
+
end
|
data/spec/bump_spec.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
feature 'bumping tasks' do
|
4
|
+
scenario 'moving around' do
|
5
|
+
t('a T1')
|
6
|
+
t('l').should_not have_task('T1', :in => :today)
|
7
|
+
t('b 0 T1')
|
8
|
+
t('l').should have_task('T1', :in => :today)
|
9
|
+
t('b 1 T1')
|
10
|
+
t('l a').should have_task('T1', :in => :scheduled, :for => Date.today + 1)
|
11
|
+
t('b 1w T1')
|
12
|
+
t('l a').should have_task('T1', :in => :scheduled, :for => Date.today + 7)
|
13
|
+
end
|
14
|
+
|
15
|
+
scenario 'invalid input' do
|
16
|
+
t('b 0 T1').should == 'No such task'
|
17
|
+
t('b T1').should == 'Invalid time'
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
feature 'completion' do
|
4
|
+
scenario 'normal' do
|
5
|
+
t('a 0 T1') # Today
|
6
|
+
t('a T2') # Next
|
7
|
+
t('a 1 T3') # Scheduled
|
8
|
+
|
9
|
+
t('l c').should have_completion_task('T1')
|
10
|
+
t('l c').should have_completion_task('T2')
|
11
|
+
t('l c').should have_completion_task('T3')
|
12
|
+
end
|
13
|
+
|
14
|
+
scenario 'recurring' do
|
15
|
+
t('r a 1d T1')
|
16
|
+
t('r c').should have_completion_task('T1')
|
17
|
+
end
|
18
|
+
end
|
data/spec/done_spec.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
feature 'done' do
|
4
|
+
scenario 'marking off tasks' do
|
5
|
+
t('a T1')
|
6
|
+
t('l a').should have_task('T1')
|
7
|
+
t('d T1').should == "Task done"
|
8
|
+
t('l').should_not have_task('T1')
|
9
|
+
end
|
10
|
+
|
11
|
+
scenario 'invalid input' do
|
12
|
+
t('d T1').should == "No such task"
|
13
|
+
end
|
14
|
+
end
|
data/spec/list_spec.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
feature 'list' do
|
4
|
+
scenario 'options' do
|
5
|
+
t('a 0 T1') # Today
|
6
|
+
t('a T2') # Next
|
7
|
+
t('a 1 T3') # Scheduled
|
8
|
+
|
9
|
+
# Today
|
10
|
+
t('l').should have_task('T1')
|
11
|
+
t('l').should_not have_task('T2')
|
12
|
+
t('l').should_not have_task('T3')
|
13
|
+
|
14
|
+
# All
|
15
|
+
t('l a').should have_task('T1')
|
16
|
+
t('l a').should have_task('T2')
|
17
|
+
t('l a').should have_task('T3')
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
feature 'recurring' do
|
4
|
+
describe 'extract_r_tokens' do
|
5
|
+
it { Xtdo.extract_recur_tokens('1d T1').should == ['1', 'd', nil, 'T1'] }
|
6
|
+
it { Xtdo.extract_recur_tokens('1w,thu T1').should == ['1', 'w', 'thu', 'T1'] }
|
7
|
+
it { Xtdo.extract_recur_tokens('1m,2 T1').should == ['1', 'm', '2', 'T1'] }
|
8
|
+
it { Xtdo.extract_recur_tokens('1m,20 T1').should == ['1', 'm', '20', 'T1'] }
|
9
|
+
end
|
10
|
+
|
11
|
+
describe 'starting day' do
|
12
|
+
let(:today) { Date.new(2010,11,22) } # Monday
|
13
|
+
|
14
|
+
it { Xtdo.calculate_starting_day(today, 1, 'd').should == today + 1 }
|
15
|
+
it { Xtdo.calculate_starting_day(today, 1, 'w', 'mon').should == today + 7}
|
16
|
+
it { Xtdo.calculate_starting_day(today, 1, 'w', 'tue').should == today + 1}
|
17
|
+
it { Xtdo.calculate_starting_day(today, 1, 'w', 'sun').should == today + 6}
|
18
|
+
it { Xtdo.calculate_starting_day(today, 1, 'w', '0').should == nil }
|
19
|
+
it { Xtdo.calculate_starting_day(today, 1, 'm', '1').should == Date.new(2010,12,1)}
|
20
|
+
it { Xtdo.calculate_starting_day(today, 1, 'm', '22').should == Date.new(2010,12,22)}
|
21
|
+
it { Xtdo.calculate_starting_day(today, 1, 'm', '23').should == Date.new(2010,11,23)}
|
22
|
+
it { Xtdo.calculate_starting_day(today, 1, 'm', '0').should == nil }
|
23
|
+
end
|
24
|
+
|
25
|
+
let(:today) { Date.new(2010,11,17) } # Monday
|
26
|
+
|
27
|
+
scenario 'list' do
|
28
|
+
t('r a 1d T1')
|
29
|
+
t('r l').should have_task('1d T1')
|
30
|
+
end
|
31
|
+
|
32
|
+
scenario 'daily' do
|
33
|
+
|
34
|
+
time_travel today
|
35
|
+
t('r a 1d T1')
|
36
|
+
|
37
|
+
time_travel today + 1
|
38
|
+
t('l').should have_task('T1')
|
39
|
+
t('b 1 T1')
|
40
|
+
t('l').should_not have_task('T1')
|
41
|
+
t('d T1')
|
42
|
+
t('l').should_not have_task('T1')
|
43
|
+
|
44
|
+
time_travel today + 2
|
45
|
+
t('l').should have_task('T1')
|
46
|
+
t('d T1')
|
47
|
+
|
48
|
+
time_travel today + 4
|
49
|
+
t('l').should have_task('T1')
|
50
|
+
end
|
51
|
+
|
52
|
+
scenario 'remove' do
|
53
|
+
time_travel today
|
54
|
+
t('r a 1d T1')
|
55
|
+
t('r d T1')
|
56
|
+
time_travel today + 1
|
57
|
+
t('l a').should_not have_task('T1')
|
58
|
+
end
|
59
|
+
|
60
|
+
scenario 'weekly' do
|
61
|
+
time_travel today
|
62
|
+
t('r a 1w,thu T1')
|
63
|
+
|
64
|
+
time_travel Date.new(2010,11,18) # Thursday
|
65
|
+
t('l').should have_task('T1')
|
66
|
+
t('b 1 T1')
|
67
|
+
t('l').should_not have_task('T1')
|
68
|
+
t('d T1')
|
69
|
+
t('l').should_not have_task('T1') # Do not recreate the task
|
70
|
+
|
71
|
+
time_travel Date.new(2010,11,25) # Next Thursday
|
72
|
+
t('l').should have_task('T1')
|
73
|
+
t('d T1')
|
74
|
+
|
75
|
+
time_travel Date.new(2010,12,3) # Friday after
|
76
|
+
t('l').should have_task('T1')
|
77
|
+
end
|
78
|
+
|
79
|
+
scenario 'monthly' do
|
80
|
+
time_travel today
|
81
|
+
t('r a 1m,3 T1')
|
82
|
+
|
83
|
+
time_travel Date.new(2010,12,3)
|
84
|
+
t('l').should have_task('T1')
|
85
|
+
t('b 1 T1')
|
86
|
+
t('l').should_not have_task('T1')
|
87
|
+
t('d T1')
|
88
|
+
t('l').should_not have_task('T1')
|
89
|
+
|
90
|
+
time_travel Date.new(2011,1,4)
|
91
|
+
t('l').should have_task('T1')
|
92
|
+
end
|
93
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,112 @@
|
|
1
|
+
require 'rspec'
|
2
|
+
require 'tempfile'
|
3
|
+
require 'date'
|
4
|
+
require 'timecop'
|
5
|
+
|
6
|
+
$LOAD_PATH.unshift File.expand_path("../../lib", __FILE__)
|
7
|
+
require 'xtdo'
|
8
|
+
|
9
|
+
RSpec.configure do |config|
|
10
|
+
config.before :each do
|
11
|
+
@file = File.join(Dir.tmpdir, 'xtdo_test.yml')
|
12
|
+
end
|
13
|
+
|
14
|
+
config.after :each do
|
15
|
+
File.unlink @file if File.exists? @file
|
16
|
+
Timecop.return
|
17
|
+
end
|
18
|
+
|
19
|
+
def t(operation)
|
20
|
+
Xtdo.run @file, operation
|
21
|
+
end
|
22
|
+
|
23
|
+
def time_travel(time)
|
24
|
+
Timecop.travel(time)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
RSpec::Matchers.define(:have_completion_task) do |task|
|
29
|
+
match do |result|
|
30
|
+
parse(result).include? task
|
31
|
+
end
|
32
|
+
|
33
|
+
failure_message_for_should do |result|
|
34
|
+
buffer = "Expected to find #{task}. Instead found:\n"
|
35
|
+
parse(result).each do |found|
|
36
|
+
buffer += " #{found}"
|
37
|
+
end
|
38
|
+
buffer
|
39
|
+
end
|
40
|
+
|
41
|
+
def parse(data)
|
42
|
+
data.to_s.lines.map(&:chomp)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
RSpec::Matchers.define(:have_task) do |task, opts = {}|
|
47
|
+
match do |result|
|
48
|
+
parsed = parse(result)
|
49
|
+
tasks = opts[:in] ? parsed[opts[:in]] : parsed.values.flatten
|
50
|
+
tasks && tasks.include?(task)
|
51
|
+
end
|
52
|
+
|
53
|
+
failure_message_for_should do |result|
|
54
|
+
in_string = " in #{opts[:in]}" if opts[:in]
|
55
|
+
buffer = "Expected to find #{task}#{in_string}. Instead found:\n"
|
56
|
+
parse(result).each do |group, tasks|
|
57
|
+
buffer += " #{group}\n"
|
58
|
+
buffer += tasks.map {|x| " #{x}" }.join("\n")
|
59
|
+
buffer += "\n"
|
60
|
+
end
|
61
|
+
buffer
|
62
|
+
end
|
63
|
+
|
64
|
+
failure_message_for_should_not do |result|
|
65
|
+
in_string = " in #{opts[:in]}" if opts[:in]
|
66
|
+
buffer = "Expected not to find #{task}#{in_string}:\n"
|
67
|
+
parse(result).each do |group, tasks|
|
68
|
+
buffer += " #{group}\n"
|
69
|
+
buffer += tasks.map {|x| " #{x}" }.join("\n")
|
70
|
+
buffer += "\n"
|
71
|
+
end
|
72
|
+
buffer
|
73
|
+
end
|
74
|
+
|
75
|
+
def parse(data)
|
76
|
+
@parsed ||= begin
|
77
|
+
group = nil
|
78
|
+
data.to_s.lines.inject({}) do |a, line|
|
79
|
+
if line =~ /^=+ (.+)/
|
80
|
+
group = line[/^=+ (.+)/, 1].downcase.to_sym
|
81
|
+
else
|
82
|
+
a[group] ||= []
|
83
|
+
a[group] << line.chomp
|
84
|
+
end
|
85
|
+
a
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
module Steak
|
92
|
+
module AcceptanceExampleGroup
|
93
|
+
def self.included(base)
|
94
|
+
base.instance_eval do
|
95
|
+
alias scenario example
|
96
|
+
alias background before
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
module DSL
|
102
|
+
def feature(*args, &block)
|
103
|
+
args << {} unless args.last.is_a?(Hash)
|
104
|
+
args.last.update :type => :acceptance, :steak => true, :caller => caller
|
105
|
+
describe(*args, &block)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
extend Steak::DSL
|
111
|
+
|
112
|
+
RSpec.configuration.include Steak::AcceptanceExampleGroup, :type => :acceptance
|
metadata
ADDED
@@ -0,0 +1,105 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: xtdo
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 2
|
8
|
+
version: "0.2"
|
9
|
+
platform: ruby
|
10
|
+
authors:
|
11
|
+
- Xavier Shay
|
12
|
+
autorequire:
|
13
|
+
bindir: bin
|
14
|
+
cert_chain: []
|
15
|
+
|
16
|
+
date: 2010-11-28 00:00:00 +11:00
|
17
|
+
default_executable:
|
18
|
+
dependencies:
|
19
|
+
- !ruby/object:Gem::Dependency
|
20
|
+
name: rspec
|
21
|
+
prerelease: false
|
22
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
23
|
+
none: false
|
24
|
+
requirements:
|
25
|
+
- - ~>
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
segments:
|
28
|
+
- 2
|
29
|
+
- 1
|
30
|
+
- 0
|
31
|
+
version: 2.1.0
|
32
|
+
type: :development
|
33
|
+
version_requirements: *id001
|
34
|
+
- !ruby/object:Gem::Dependency
|
35
|
+
name: timecop
|
36
|
+
prerelease: false
|
37
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
38
|
+
none: false
|
39
|
+
requirements:
|
40
|
+
- - ">="
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
segments:
|
43
|
+
- 0
|
44
|
+
version: "0"
|
45
|
+
type: :development
|
46
|
+
version_requirements: *id002
|
47
|
+
description:
|
48
|
+
email:
|
49
|
+
- hello@xaviershay.com
|
50
|
+
executables:
|
51
|
+
- xtdo
|
52
|
+
extensions: []
|
53
|
+
|
54
|
+
extra_rdoc_files: []
|
55
|
+
|
56
|
+
files:
|
57
|
+
- spec/add_spec.rb
|
58
|
+
- spec/bump_spec.rb
|
59
|
+
- spec/completion_spec.rb
|
60
|
+
- spec/done_spec.rb
|
61
|
+
- spec/list_spec.rb
|
62
|
+
- spec/recurring_spec.rb
|
63
|
+
- spec/spec_helper.rb
|
64
|
+
- lib/xtdo.rb
|
65
|
+
- bin/xtdo
|
66
|
+
- bin/xtdo_completion.zsh
|
67
|
+
- README.rdoc
|
68
|
+
- HISTORY
|
69
|
+
- LICENSE
|
70
|
+
- Rakefile
|
71
|
+
- TODO
|
72
|
+
has_rdoc: false
|
73
|
+
homepage: http://github.com/xaviershay/xtdo
|
74
|
+
licenses: []
|
75
|
+
|
76
|
+
post_install_message:
|
77
|
+
rdoc_options: []
|
78
|
+
|
79
|
+
require_paths:
|
80
|
+
- lib
|
81
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
82
|
+
none: false
|
83
|
+
requirements:
|
84
|
+
- - ">="
|
85
|
+
- !ruby/object:Gem::Version
|
86
|
+
segments:
|
87
|
+
- 0
|
88
|
+
version: "0"
|
89
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
90
|
+
none: false
|
91
|
+
requirements:
|
92
|
+
- - ">="
|
93
|
+
- !ruby/object:Gem::Version
|
94
|
+
segments:
|
95
|
+
- 0
|
96
|
+
version: "0"
|
97
|
+
requirements: []
|
98
|
+
|
99
|
+
rubyforge_project:
|
100
|
+
rubygems_version: 1.3.7
|
101
|
+
signing_key:
|
102
|
+
specification_version: 3
|
103
|
+
summary: Minimal and fast command line todo manager
|
104
|
+
test_files: []
|
105
|
+
|