todo-jsonl 1.0.7 → 1.0.8
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/bin/todo +2 -2
- data/bin/todo.rb +414 -407
- metadata +8 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 01a891896f26ef6117a6cbc3f62c7f1473e83c6c7cb12a93b79134be5d25bd6e
|
4
|
+
data.tar.gz: 9fed56888aca1e9330e1da951ea1457af30849f46a4cca4335867a60fb039b1c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 13f21e68b1e5f1b7d11c8f351c225c79d71759baddd5ac4af00b238a7b1868c6ea0542d22b1d346b04dd6e5e3c87a9bb221f4743fba5b583ed8f58a45d5f2d28
|
7
|
+
data.tar.gz: c28c4622a9897633c28ec629616386d985ffbc9ccdbf890e0d726fd9673ebbaef3fca3a921b8bd9089fbb985088731029717609f15913f827e1f0d713c592647
|
data/bin/todo
CHANGED
@@ -1,2 +1,2 @@
|
|
1
|
-
#!/usr/bin/env ruby
|
2
|
-
require_relative 'todo.rb'
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require_relative 'todo.rb'
|
data/bin/todo.rb
CHANGED
@@ -1,407 +1,414 @@
|
|
1
|
-
#!/usr/bin/env ruby
|
2
|
-
|
3
|
-
# todo.rb - todo list manager on the command-line
|
4
|
-
# inspired by todo.txt using the jsonl format.
|
5
|
-
#
|
6
|
-
# Copyright (c) 2020-2021 Gabor Bata
|
7
|
-
#
|
8
|
-
# Permission is hereby granted, free of charge, to any person
|
9
|
-
# obtaining a copy of this software and associated documentation files
|
10
|
-
# (the "Software"), to deal in the Software without restriction,
|
11
|
-
# including without limitation the rights to use, copy, modify, merge,
|
12
|
-
# publish, distribute, sublicense, and/or sell copies of the Software,
|
13
|
-
# and to permit persons to whom the Software is furnished to do so,
|
14
|
-
# subject to the following conditions:
|
15
|
-
#
|
16
|
-
# The above copyright notice and this permission notice shall be
|
17
|
-
# included in all copies or substantial portions of the Software.
|
18
|
-
#
|
19
|
-
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
20
|
-
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
21
|
-
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
22
|
-
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
23
|
-
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
24
|
-
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
25
|
-
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
26
|
-
# SOFTWARE.
|
27
|
-
|
28
|
-
require 'json'
|
29
|
-
require 'date'
|
30
|
-
|
31
|
-
class Todo
|
32
|
-
COLOR_CODES = {
|
33
|
-
black: 30,
|
34
|
-
red: 31,
|
35
|
-
green: 32,
|
36
|
-
yellow: 33,
|
37
|
-
blue: 34,
|
38
|
-
magenta: 35,
|
39
|
-
cyan: 36,
|
40
|
-
white: 37
|
41
|
-
}
|
42
|
-
|
43
|
-
STATES = {
|
44
|
-
'new' => '[ ]',
|
45
|
-
'done' => '[x]',
|
46
|
-
'started' => '[>]',
|
47
|
-
'blocked' => '[!]',
|
48
|
-
'
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
'
|
54
|
-
'
|
55
|
-
'
|
56
|
-
'
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
'
|
63
|
-
'
|
64
|
-
'
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
when '
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
when '
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
when '
|
115
|
-
raise action + ' command requires
|
116
|
-
|
117
|
-
when '
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
*
|
148
|
-
*
|
149
|
-
|
150
|
-
*
|
151
|
-
*
|
152
|
-
*
|
153
|
-
*
|
154
|
-
*
|
155
|
-
|
156
|
-
*
|
157
|
-
*
|
158
|
-
*
|
159
|
-
*
|
160
|
-
*
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
':
|
189
|
-
':
|
190
|
-
':
|
191
|
-
':
|
192
|
-
':
|
193
|
-
':
|
194
|
-
':
|
195
|
-
':
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
end
|
236
|
-
|
237
|
-
def
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
end
|
253
|
-
end
|
254
|
-
|
255
|
-
def
|
256
|
-
update_task(item, :list, lambda do |task|
|
257
|
-
task[:title] = text
|
258
|
-
postprocess_tags(task)
|
259
|
-
end)
|
260
|
-
end
|
261
|
-
|
262
|
-
def
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
end
|
268
|
-
|
269
|
-
def
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
end)
|
295
|
-
end
|
296
|
-
|
297
|
-
def
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
end
|
340
|
-
|
341
|
-
def
|
342
|
-
update_task(item, :show, lambda do |task|
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# todo.rb - todo list manager on the command-line
|
4
|
+
# inspired by todo.txt using the jsonl format.
|
5
|
+
#
|
6
|
+
# Copyright (c) 2020-2021 Gabor Bata
|
7
|
+
#
|
8
|
+
# Permission is hereby granted, free of charge, to any person
|
9
|
+
# obtaining a copy of this software and associated documentation files
|
10
|
+
# (the "Software"), to deal in the Software without restriction,
|
11
|
+
# including without limitation the rights to use, copy, modify, merge,
|
12
|
+
# publish, distribute, sublicense, and/or sell copies of the Software,
|
13
|
+
# and to permit persons to whom the Software is furnished to do so,
|
14
|
+
# subject to the following conditions:
|
15
|
+
#
|
16
|
+
# The above copyright notice and this permission notice shall be
|
17
|
+
# included in all copies or substantial portions of the Software.
|
18
|
+
#
|
19
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
20
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
21
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
22
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
23
|
+
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
24
|
+
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
25
|
+
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
26
|
+
# SOFTWARE.
|
27
|
+
|
28
|
+
require 'json'
|
29
|
+
require 'date'
|
30
|
+
|
31
|
+
class Todo
|
32
|
+
COLOR_CODES = {
|
33
|
+
black: 30,
|
34
|
+
red: 31,
|
35
|
+
green: 32,
|
36
|
+
yellow: 33,
|
37
|
+
blue: 34,
|
38
|
+
magenta: 35,
|
39
|
+
cyan: 36,
|
40
|
+
white: 37
|
41
|
+
}
|
42
|
+
|
43
|
+
STATES = {
|
44
|
+
'new' => '[ ]',
|
45
|
+
'done' => '[x]',
|
46
|
+
'started' => '[>]',
|
47
|
+
'blocked' => '[!]',
|
48
|
+
'waiting' => '[@]',
|
49
|
+
'default' => '[?]'
|
50
|
+
}
|
51
|
+
|
52
|
+
ORDER = {
|
53
|
+
'new' => 3,
|
54
|
+
'done' => 5,
|
55
|
+
'started' => 2,
|
56
|
+
'blocked' => 1,
|
57
|
+
'waiting' => 4,
|
58
|
+
'default' => 100
|
59
|
+
}
|
60
|
+
|
61
|
+
COLORS = {
|
62
|
+
'new' => :white,
|
63
|
+
'done' => :blue,
|
64
|
+
'started' => :green,
|
65
|
+
'blocked' => :yellow,
|
66
|
+
'waiting' => :cyan,
|
67
|
+
'default' => :magenta
|
68
|
+
}
|
69
|
+
|
70
|
+
DATE_FORMAT = '%Y-%m-%d'
|
71
|
+
DUE_DATE_DAYS_SIMPLE = ['today', 'tomorrow']
|
72
|
+
DUE_DATE_TAG_PATTERN = /(^| )due:([a-zA-Z0-9-]+)/
|
73
|
+
CONTEXT_TAG_PATTERN = /(^| )[@+][\w-]+/
|
74
|
+
PRIORITY_FLAG = '*'
|
75
|
+
TODO_FILE = File.join(Dir.home, 'todo.jsonl')
|
76
|
+
|
77
|
+
def execute(arguments)
|
78
|
+
begin
|
79
|
+
setup
|
80
|
+
action = arguments.first
|
81
|
+
args = arguments.drop(1)
|
82
|
+
case action
|
83
|
+
when 'add'
|
84
|
+
raise action + ' command requires at least one parameter' if args.empty?
|
85
|
+
add(args.join(' '))
|
86
|
+
when 'start'
|
87
|
+
args.length > 0 ? change_state(args.first.to_i, 'started', args.drop(1).join(' ')) : list(nil, [':started'])
|
88
|
+
when 'done'
|
89
|
+
args.length > 0 ? change_state(args.first.to_i, 'done', args.drop(1).join(' ')) : list(nil, [':done'])
|
90
|
+
when 'block'
|
91
|
+
args.length > 0 ? change_state(args.first.to_i, 'blocked', args.drop(1).join(' ')) : list(nil, [':blocked'])
|
92
|
+
when 'wait'
|
93
|
+
args.length > 0 ? change_state(args.first.to_i, 'waiting', args.drop(1).join(' ')) : list(nil, [':waiting'])
|
94
|
+
when 'reset'
|
95
|
+
args.length > 0 ? change_state(args.first.to_i, 'new', args.drop(1).join(' ')) : list(nil, [':new'])
|
96
|
+
when 'prio'
|
97
|
+
raise action + ' command requires at least one parameter' if args.length < 1
|
98
|
+
set_priority(args.first.to_i, args.drop(1).join(' '))
|
99
|
+
when 'due'
|
100
|
+
raise action + ' command requires at least one parameter' if args.length < 1
|
101
|
+
due_date(args.first.to_i, args.drop(1).join(' '))
|
102
|
+
when 'append'
|
103
|
+
raise action + ' command requires at least two parameters' if args.length < 2
|
104
|
+
append(args.first.to_i, args.drop(1).join(' '))
|
105
|
+
when 'rename'
|
106
|
+
raise action + ' command requires at least two parameters' if args.length < 2
|
107
|
+
rename(args.first.to_i, args.drop(1).join(' '))
|
108
|
+
when 'del'
|
109
|
+
raise action + ' command requires exactly one parameter' if args.length != 1
|
110
|
+
delete(args.first.to_i)
|
111
|
+
when 'note'
|
112
|
+
raise action + ' command requires at least two parameters' if args.length < 2
|
113
|
+
add_note(args.first.to_i, args.drop(1).join(' '))
|
114
|
+
when 'delnote'
|
115
|
+
raise action + ' command requires one or two parameters' if args.length < 1 || args.length > 2
|
116
|
+
delete_note(args.first.to_i, args[1])
|
117
|
+
when 'list'
|
118
|
+
list(nil, args)
|
119
|
+
when 'show'
|
120
|
+
raise action + ' command requires exactly one parameter' if args.length != 1
|
121
|
+
show(args.first.to_i)
|
122
|
+
when 'help'
|
123
|
+
raise action + ' command has no parameters' if args.length > 0
|
124
|
+
puts usage
|
125
|
+
when 'repl'
|
126
|
+
raise action + ' command has no parameters' if args.length > 0
|
127
|
+
start_repl
|
128
|
+
when 'cleanup'
|
129
|
+
raise action + ' command requires at least one parameter' if args.empty?
|
130
|
+
cleanup(args)
|
131
|
+
else
|
132
|
+
list(nil, arguments)
|
133
|
+
end
|
134
|
+
rescue StandardError => error
|
135
|
+
puts "#{colorize('ERROR:', :red)} #{error}"
|
136
|
+
end
|
137
|
+
self
|
138
|
+
end
|
139
|
+
|
140
|
+
private
|
141
|
+
|
142
|
+
def usage
|
143
|
+
<<~USAGE
|
144
|
+
Usage: todo <command> <arguments>
|
145
|
+
|
146
|
+
Commands:
|
147
|
+
* add <text> add new task
|
148
|
+
* start <tasknumber> [text] mark task as started, with optional note
|
149
|
+
* done <tasknumber> [text] mark task as completed, with optional note
|
150
|
+
* block <tasknumber> [text] mark task as blocked, with optional note
|
151
|
+
* wait <tasknumber> [text] mark task as waiting, with optional note
|
152
|
+
* reset <tasknumber> [text] reset task to new state, with optional note
|
153
|
+
* prio <tasknumber> [text] toggle high priority flag, with optional note
|
154
|
+
* due <tasknumber> [date] set/unset due date (in YYYY-MM-DD format)
|
155
|
+
|
156
|
+
* append <tasknumber> <text> append text to task title
|
157
|
+
* rename <tasknumber> <text> rename task
|
158
|
+
* del <tasknumber> delete task
|
159
|
+
* note <tasknumber> <text> add note to task
|
160
|
+
* delnote <tasknumber> [number] delete a specific or all notes from task
|
161
|
+
|
162
|
+
* list <regex> [regex...] list tasks (only active tasks by default)
|
163
|
+
* show <tasknumber> show all task details
|
164
|
+
* repl enter read-eval-print loop mode
|
165
|
+
* cleanup <regex> [regex...] cleanup completed tasks by regex
|
166
|
+
* help this help screen
|
167
|
+
|
168
|
+
With list command the following pre-defined queries can be also used:
|
169
|
+
#{@queries.keys.each_with_index.map { |k, i| (i == 8 ? "\n" : '') + k }.join(', ')}
|
170
|
+
|
171
|
+
Due dates can be also added via tags in task title: "due:YYYY-MM-DD"
|
172
|
+
In addition to formatted dates, you can use date synonyms:
|
173
|
+
"due:today", "due:tomorrow", and day names e.g. "due:monday" or "due:tue"
|
174
|
+
|
175
|
+
Legend: #{STATES.select { |k, v| k != 'default' }.map { |k, v| "#{k} #{v}" }.join(', ') }, priority #{PRIORITY_FLAG}
|
176
|
+
|
177
|
+
Todo file: #{TODO_FILE}
|
178
|
+
USAGE
|
179
|
+
end
|
180
|
+
|
181
|
+
def setup
|
182
|
+
@today = Date.today
|
183
|
+
next_7_days = (0..6).map { |day| @today + day }
|
184
|
+
@due_date_days = next_7_days.map { |day| day.strftime('%A').downcase }
|
185
|
+
due_dates_for_queries = next_7_days.map { |day| day.strftime(DATE_FORMAT) }
|
186
|
+
recent_date = (@today - 7).strftime(DATE_FORMAT)
|
187
|
+
@queries = {
|
188
|
+
':active' => lambda { |task| /(new|started|blocked|waiting)/.match(task[:state]) },
|
189
|
+
':done' => lambda { |task| 'done' == task[:state] },
|
190
|
+
':blocked' => lambda { |task| 'blocked' == task[:state] },
|
191
|
+
':waiting' => lambda { |task| 'waiting' == task[:state] },
|
192
|
+
':started' => lambda { |task| 'started' == task[:state] },
|
193
|
+
':new' => lambda { |task| 'new' == task[:state] },
|
194
|
+
':all' => lambda { |task| /\w+/.match(task[:state]) },
|
195
|
+
':priority' => lambda { |task| task[:priority] },
|
196
|
+
':note' => lambda { |task| task[:note] && !task[:note].empty? },
|
197
|
+
':today' => lambda { |task| due_dates_for_queries[0] == task[:due] },
|
198
|
+
':tomorrow' => lambda { |task| due_dates_for_queries[1] == task[:due] },
|
199
|
+
':next7days' => lambda { |task| /(#{due_dates_for_queries.join('|')})/.match(task[:due]) },
|
200
|
+
':overdue' => lambda { |task| task[:due] && task[:due] < due_dates_for_queries[0] },
|
201
|
+
':due' => lambda { |task| task[:due] },
|
202
|
+
':recent' => lambda { |task| recent_date <= task[:modified] }
|
203
|
+
}
|
204
|
+
end
|
205
|
+
|
206
|
+
def load_tasks(item_to_check = nil)
|
207
|
+
count = 0
|
208
|
+
tasks = {}
|
209
|
+
if File.exist?(TODO_FILE)
|
210
|
+
File.open(TODO_FILE, 'r:UTF-8') do |file|
|
211
|
+
file.each_line do |line|
|
212
|
+
next if line.strip == ''
|
213
|
+
count += 1
|
214
|
+
tasks[count] = JSON.parse(line.chomp, :symbolize_names => true)
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
218
|
+
raise "#{item_to_check}: No such todo" if item_to_check && !tasks.has_key?(item_to_check)
|
219
|
+
tasks
|
220
|
+
end
|
221
|
+
|
222
|
+
def write_tasks(tasks)
|
223
|
+
File.open(TODO_FILE, 'w:UTF-8') do |file|
|
224
|
+
tasks.keys.sort.each { |key| file.write(JSON.generate(tasks[key]) + "\n") }
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
def postprocess_tags(task)
|
229
|
+
match_data = task[:title].match(DUE_DATE_TAG_PATTERN)
|
230
|
+
if match_data
|
231
|
+
task[:title] = task[:title].gsub(DUE_DATE_TAG_PATTERN, '')
|
232
|
+
task[:due] = convert_due_date(match_data[2])
|
233
|
+
end
|
234
|
+
raise 'title must not be empty' if task[:title].empty?
|
235
|
+
end
|
236
|
+
|
237
|
+
def add(text)
|
238
|
+
task = { state: 'new', title: text, modified: @today.strftime(DATE_FORMAT) }
|
239
|
+
postprocess_tags(task)
|
240
|
+
File.open(TODO_FILE, 'a:UTF-8') { |file| file.write(JSON.generate(task) + "\n") }
|
241
|
+
list
|
242
|
+
end
|
243
|
+
|
244
|
+
def update_task(item, post_action, update_function)
|
245
|
+
tasks = load_tasks(item)
|
246
|
+
update_function.call(tasks[item])
|
247
|
+
tasks[item][:modified] = @today.strftime(DATE_FORMAT)
|
248
|
+
write_tasks(tasks)
|
249
|
+
case post_action
|
250
|
+
when :show then show(item, tasks)
|
251
|
+
when :list then list(tasks)
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
def append(item, text)
|
256
|
+
update_task(item, :list, lambda do |task|
|
257
|
+
task[:title] = [task[:title], text].join(' ')
|
258
|
+
postprocess_tags(task)
|
259
|
+
end)
|
260
|
+
end
|
261
|
+
|
262
|
+
def rename(item, text)
|
263
|
+
update_task(item, :list, lambda do |task|
|
264
|
+
task[:title] = text
|
265
|
+
postprocess_tags(task)
|
266
|
+
end)
|
267
|
+
end
|
268
|
+
|
269
|
+
def delete(item)
|
270
|
+
tasks = load_tasks(item)
|
271
|
+
tasks.delete(item)
|
272
|
+
write_tasks(tasks)
|
273
|
+
list
|
274
|
+
end
|
275
|
+
|
276
|
+
def change_state(item, state, note = nil)
|
277
|
+
update_task(item, :list, lambda do |task|
|
278
|
+
task[:state] = state
|
279
|
+
if !note.nil? && !note.empty?
|
280
|
+
task[:note] ||= []
|
281
|
+
task[:note].push(note)
|
282
|
+
end
|
283
|
+
end)
|
284
|
+
end
|
285
|
+
|
286
|
+
def set_priority(item, note = nil)
|
287
|
+
update_task(item, :list, lambda do |task|
|
288
|
+
task[:priority] = !task[:priority]
|
289
|
+
task.delete(:priority) if !task[:priority]
|
290
|
+
if !note.nil? && !note.empty?
|
291
|
+
task[:note] ||= []
|
292
|
+
task[:note].push(note)
|
293
|
+
end
|
294
|
+
end)
|
295
|
+
end
|
296
|
+
|
297
|
+
def due_date(item, date = '')
|
298
|
+
update_task(item, :list, lambda do |task|
|
299
|
+
task[:due] = convert_due_date(date)
|
300
|
+
task.delete(:due) if task[:due].nil?
|
301
|
+
end)
|
302
|
+
end
|
303
|
+
|
304
|
+
def list(tasks = nil, patterns = nil)
|
305
|
+
tasks ||= load_tasks
|
306
|
+
task_indent = [tasks.keys.max.to_s.size, 4].max
|
307
|
+
patterns ||= []
|
308
|
+
patterns += [':active'] if (patterns & [':active', ':done', ':blocked', ':started', ':new', ':all', ':waiting']).empty?
|
309
|
+
items = filter_tasks(tasks, patterns).sort_by do |num, task|
|
310
|
+
[
|
311
|
+
task[:priority] && task[:state] != 'done' ? 0 : 1,
|
312
|
+
ORDER[task[:state] || 'default'] || ORDER['default'],
|
313
|
+
task[:state] != 'done' ? task[:due] || 'n/a' : task[:modified],
|
314
|
+
num
|
315
|
+
]
|
316
|
+
end
|
317
|
+
items.each do |num, task|
|
318
|
+
state = task[:state] || 'default'
|
319
|
+
display_state = colorize(STATES[state], COLORS[state])
|
320
|
+
title = task[:title].gsub(CONTEXT_TAG_PATTERN) do |tag|
|
321
|
+
(tag.start_with?(' ') ? ' ' : '') + colorize(tag.strip, :cyan)
|
322
|
+
end
|
323
|
+
priority_flag = task[:priority] && state != 'done' ? colorize(PRIORITY_FLAG, :red) : ' '
|
324
|
+
due_date = ''
|
325
|
+
if task[:due] && state != 'done'
|
326
|
+
date_diff = (Date.strptime(task[:due], DATE_FORMAT) - @today).to_i
|
327
|
+
if date_diff < 0
|
328
|
+
due_date = colorize("(#{date_diff.abs}d overdue)", :red)
|
329
|
+
elsif date_diff == 0 || date_diff == 1
|
330
|
+
due_date = colorize("(#{DUE_DATE_DAYS_SIMPLE[date_diff]})", :yellow)
|
331
|
+
else
|
332
|
+
due_date = colorize("(#{@due_date_days[date_diff] || task[:due]})", :magenta) if date_diff > 1
|
333
|
+
end
|
334
|
+
due_date = ' ' + due_date
|
335
|
+
end
|
336
|
+
puts "#{num.to_s.rjust(task_indent)}:#{priority_flag}#{display_state} #{title}#{due_date}"
|
337
|
+
end
|
338
|
+
puts 'No todos found' if items.empty?
|
339
|
+
end
|
340
|
+
|
341
|
+
def add_note(item, text)
|
342
|
+
update_task(item, :show, lambda do |task|
|
343
|
+
task[:note] ||= []
|
344
|
+
task[:note].push(text)
|
345
|
+
end)
|
346
|
+
end
|
347
|
+
|
348
|
+
def delete_note(item, num = nil)
|
349
|
+
update_task(item, :show, lambda do |task|
|
350
|
+
if num.to_s.empty?
|
351
|
+
task.delete(:note)
|
352
|
+
else
|
353
|
+
raise "#{num.to_i}: Note does not exist" if num.to_i <= 0 || task[:note].to_a.size < num.to_i
|
354
|
+
task[:note].delete_at(num.to_i - 1)
|
355
|
+
task.delete(:note) if task[:note].empty?
|
356
|
+
end
|
357
|
+
end)
|
358
|
+
end
|
359
|
+
|
360
|
+
def show(item, tasks = nil)
|
361
|
+
tasks ||= load_tasks(item)
|
362
|
+
tasks[item].each do |k, v|
|
363
|
+
v = "\n" + v.each_with_index.
|
364
|
+
map { |n, i| v.size > 1 ? "#{(i + 1).to_s.rjust(v.size.to_s.size)}: #{n}" : n }.
|
365
|
+
join("\n") if v.is_a?(Array)
|
366
|
+
puts "#{colorize(k.to_s.rjust(10) + ':', :cyan)} #{v}"
|
367
|
+
end
|
368
|
+
end
|
369
|
+
|
370
|
+
def start_repl
|
371
|
+
command = ''
|
372
|
+
while !['exit', 'quit'].include?(command)
|
373
|
+
if ['clear', 'cls'].include?(command)
|
374
|
+
print "\e[H\e[2J"
|
375
|
+
else
|
376
|
+
execute(command == 'repl' ? [] : command.split(/\s+/))
|
377
|
+
end
|
378
|
+
print "\ntodo> "
|
379
|
+
command = STDIN.gets.chomp.strip
|
380
|
+
end
|
381
|
+
end
|
382
|
+
|
383
|
+
def cleanup(patterns)
|
384
|
+
tasks = load_tasks
|
385
|
+
patterns = [':done'] + patterns.to_a
|
386
|
+
items = filter_tasks(tasks, patterns)
|
387
|
+
items.each_key { |num| tasks.delete(num) }
|
388
|
+
write_tasks(tasks)
|
389
|
+
puts "Deleted #{items.size} todo(s)"
|
390
|
+
end
|
391
|
+
|
392
|
+
def filter_tasks(tasks, patterns)
|
393
|
+
patterns = patterns.uniq
|
394
|
+
tasks.select do |num, task|
|
395
|
+
patterns.all? do |pattern|
|
396
|
+
@queries[pattern] ? @queries[pattern].call(task) : /#{pattern}/ix.match(task[:title])
|
397
|
+
end
|
398
|
+
end
|
399
|
+
end
|
400
|
+
|
401
|
+
def colorize(text, color)
|
402
|
+
"\e[#{COLOR_CODES[color] || 37}m#{text}\e[0m"
|
403
|
+
end
|
404
|
+
|
405
|
+
def convert_due_date(date)
|
406
|
+
day_index = @due_date_days.index(date.to_s.downcase) ||
|
407
|
+
DUE_DATE_DAYS_SIMPLE.index(date.to_s.downcase) ||
|
408
|
+
@due_date_days.map { |day| day[0..2] }.index(date.to_s.downcase)
|
409
|
+
return (@today + day_index).strftime(DATE_FORMAT) if day_index
|
410
|
+
date.nil? || date.empty? ? nil : Date.strptime(date, DATE_FORMAT).strftime(DATE_FORMAT)
|
411
|
+
end
|
412
|
+
end
|
413
|
+
|
414
|
+
Todo.new.execute(ARGV)
|
metadata
CHANGED
@@ -1,17 +1,17 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: todo-jsonl
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.8
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Gabor Bata
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-
|
11
|
+
date: 2021-08-19 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
|
-
description:
|
14
|
-
email:
|
13
|
+
description:
|
14
|
+
email:
|
15
15
|
executables:
|
16
16
|
- todo.rb
|
17
17
|
- todo
|
@@ -24,7 +24,7 @@ homepage: https://github.com/gaborbata/todo
|
|
24
24
|
licenses:
|
25
25
|
- MIT
|
26
26
|
metadata: {}
|
27
|
-
post_install_message:
|
27
|
+
post_install_message:
|
28
28
|
rdoc_options: []
|
29
29
|
require_paths:
|
30
30
|
- lib
|
@@ -39,8 +39,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '0'
|
41
41
|
requirements: []
|
42
|
-
rubygems_version: 3.
|
43
|
-
signing_key:
|
42
|
+
rubygems_version: 3.1.6
|
43
|
+
signing_key:
|
44
44
|
specification_version: 4
|
45
45
|
summary: todo list manager on the command-line inspired by todo.txt using the jsonl
|
46
46
|
format
|