todo-jsonl 0.1.9 → 0.1.15

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.
Files changed (3) hide show
  1. checksums.yaml +4 -4
  2. data/bin/todo.rb +69 -36
  3. metadata +2 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 889bafbe16e82bf4bc1d64a7cc4991a582693677c02a1a6ace65bdd04f20f4b6
4
- data.tar.gz: 67def741aa050ac753c55af39488712aa3aed696f17c28366020233a59673810
3
+ metadata.gz: eac1c59e2d905a14881aae2903048c63b0b3ff796a1d8511cff286e0d64052fa
4
+ data.tar.gz: b7105e1b10437375614ea5f696533613100a81eaa57cc11590a1248b2f1ad082
5
5
  SHA512:
6
- metadata.gz: 5de24eee9c08a01b1abfe1315bbc75444d0d7fa26e0f3c6c99f25911104ec5b3d428e4ae13c23cb3f7d6b931c6b059759b870b65af33ff795786676545813e2c
7
- data.tar.gz: 4b10d821af4c52d466f621cc77961551556f915f063032b5e7525c8f5d3fff84b838d1564eeabf899c1480e64235654d1ca669922552ddb53bc4be6941033aab
6
+ metadata.gz: ef12c1dd2fd26e691a68b8e7934e431cc3cca2eb6542c03671481ecfdbcc29515b133b9db45d103e7cf554da9b12be20139463254192fb55a8fb381fc97345fc
7
+ data.tar.gz: bcabeafc8735514c3b4f4f19e2c70a970588b7bff168f1f1e928a1b06a79726527cb9df4d3a7250648d256de9749290b70a712bb19f01479481531884536570a
data/bin/todo.rb CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  # todo.rb - todo list manager inspired by todo.txt using the jsonl format.
4
4
  #
5
- # Copyright (c) 2020 Gabor Bata
5
+ # Copyright (c) 2020-2021 Gabor Bata
6
6
  #
7
7
  # Permission is hereby granted, free of charge, to any person
8
8
  # obtaining a copy of this software and associated documentation files
@@ -27,8 +27,6 @@
27
27
  require 'json'
28
28
  require 'date'
29
29
 
30
- DATE_FORMAT = '%Y-%m-%d'
31
-
32
30
  COLOR_CODES = {
33
31
  black: 30,
34
32
  red: 31,
@@ -64,19 +62,28 @@ COLORS = {
64
62
  'default' => :magenta
65
63
  }
66
64
 
67
- QUERIES = {
68
- ':active' => 'state=(new|started|blocked)',
69
- ':done' => 'state=done',
70
- ':blocked' => 'state=blocked',
71
- ':started' => 'state=started',
72
- ':new' => 'state=new',
73
- ':all' => 'state=\w+'
74
- }
75
-
76
65
  TODAY = DateTime.now
77
- DUE_DATE_DAYS = (0..6).map do |day| (TODAY.to_date + day).strftime('%A').downcase end
66
+ NEXT_7_DAYS = (0..6).map do |day| (TODAY.to_date + day) end
67
+ DATE_FORMAT = '%Y-%m-%d'
68
+ DUE_DATE_DAYS = NEXT_7_DAYS.map do |day| day.strftime('%A').downcase end
69
+ DUE_DATES_FOR_QUERIES = NEXT_7_DAYS.map do |day| day.strftime(DATE_FORMAT) end
78
70
  DUE_DATE_DAYS_SIMPLE = ['today', 'tomorrow']
79
71
 
72
+ DUE_DATE_TAG_PATTERN = /(^| )due:([a-zA-Z0-9-]+)/
73
+ CONTEXT_TAG_PATTERN = /(^| )[@+][\w-]+/
74
+
75
+ QUERIES = {
76
+ ':active' => 'state=(new|started|blocked)',
77
+ ':done' => 'state=done',
78
+ ':blocked' => 'state=blocked',
79
+ ':started' => 'state=started',
80
+ ':new' => 'state=new',
81
+ ':all' => 'state=\w+',
82
+ ':today' => "due=#{DUE_DATES_FOR_QUERIES[0]}",
83
+ ':tomorrow' => "due=#{DUE_DATES_FOR_QUERIES[1]}",
84
+ ':next7days' => "due=(#{DUE_DATES_FOR_QUERIES.join('|')})"
85
+ }
86
+
80
87
  PRIORITY_FLAG = '*'
81
88
 
82
89
  TODO_FILE = "#{ENV['HOME']}/todo.jsonl"
@@ -87,12 +94,12 @@ def usage
87
94
 
88
95
  Commands:
89
96
  * add <text> add new task
90
- * start <tasknumber> mark task as started
91
- * done <tasknumber> mark task as completed
92
- * block <tasknumber> mark task as blocked
93
- * reset <tasknumber> reset task to new state
97
+ * start <tasknumber> [text] mark task as started, with optional note
98
+ * done <tasknumber> [text] mark task as completed, with optional note
99
+ * block <tasknumber> [text] mark task as blocked, with optional note
100
+ * reset <tasknumber> [text] reset task to new state, with optional note
94
101
  * prio <tasknumber> toggle high priority flag
95
- * due <tasknumber> <date> set due date (in YYYY-MM-DD format)
102
+ * due <tasknumber> [date] set/unset due date (in YYYY-MM-DD format)
96
103
 
97
104
  * append <tasknumber> <text> append text to task title
98
105
  * rename <tasknumber> <text> rename task
@@ -108,8 +115,12 @@ def usage
108
115
  With list command the following pre-defined regex patterns can be also used:
109
116
  #{QUERIES.keys.join(', ')}
110
117
 
118
+ Due dates can be also added via tags in task title: "due:YYYY-MM-DD"
119
+
111
120
  Legend:
112
121
  #{STATES.select { |k, v| k != 'default' }.map { |k, v| "#{k} #{v}" }.join(', ') }, priority #{PRIORITY_FLAG}
122
+
123
+ Todo file: #{TODO_FILE}
113
124
  USAGE
114
125
  end
115
126
 
@@ -139,12 +150,22 @@ def write_tasks(tasks)
139
150
  end
140
151
  end
141
152
 
153
+ def postprocess_tags(task)
154
+ match_data = task[:title].match(DUE_DATE_TAG_PATTERN)
155
+ if match_data
156
+ task[:title] = task[:title].gsub(DUE_DATE_TAG_PATTERN, '')
157
+ task[:due] = convert_due_date(match_data[2])
158
+ end
159
+ raise 'title must not be empty' if task[:title].empty?
160
+ end
161
+
142
162
  def add(text)
143
163
  task = {
144
164
  state: 'new',
145
165
  title: text,
146
166
  modified: Time.now.strftime(DATE_FORMAT)
147
167
  }
168
+ postprocess_tags(task)
148
169
  File.open(TODO_FILE, 'a:UTF-8') do |file|
149
170
  file.write(JSON.generate(task) + "\n")
150
171
  end
@@ -155,6 +176,7 @@ def append(item, text = '')
155
176
  tasks = load_tasks(item)
156
177
  tasks[item][:title] = [tasks[item][:title], text].join(' ')
157
178
  tasks[item][:modified] = Time.now.strftime(DATE_FORMAT)
179
+ postprocess_tags(tasks[item])
158
180
  write_tasks(tasks)
159
181
  list(tasks)
160
182
  end
@@ -163,6 +185,7 @@ def rename(item, text)
163
185
  tasks = load_tasks(item)
164
186
  tasks[item][:title] = text
165
187
  tasks[item][:modified] = Time.now.strftime(DATE_FORMAT)
188
+ postprocess_tags(tasks[item])
166
189
  write_tasks(tasks)
167
190
  list(tasks)
168
191
  end
@@ -174,10 +197,14 @@ def delete(item)
174
197
  list
175
198
  end
176
199
 
177
- def change_state(item, state)
200
+ def change_state(item, state, note = nil)
178
201
  tasks = load_tasks(item)
179
202
  tasks[item][:state] = state
180
203
  tasks[item][:modified] = Time.now.strftime(DATE_FORMAT)
204
+ if !note.nil? && !note.empty?
205
+ tasks[item][:note] ||= []
206
+ tasks[item][:note].push(note)
207
+ end
181
208
  write_tasks(tasks)
182
209
  list(tasks)
183
210
  end
@@ -190,14 +217,9 @@ def set_priority(item)
190
217
  list(tasks)
191
218
  end
192
219
 
193
- def due_date(item, date = '')
220
+ def due_date(item, date = '', task = nil)
194
221
  tasks = load_tasks(item)
195
- day_index = DUE_DATE_DAYS.index(date.to_s.downcase) || DUE_DATE_DAYS_SIMPLE.index(date.to_s.downcase)
196
- if day_index
197
- tasks[item][:due] = (TODAY.to_date + day_index).strftime(DATE_FORMAT)
198
- else
199
- tasks[item][:due] = date.nil? || date.empty? ? nil : Date.parse(date).strftime(DATE_FORMAT)
200
- end
222
+ tasks[item][:due] = convert_due_date(date)
201
223
  tasks[item][:modified] = Time.now.strftime(DATE_FORMAT)
202
224
  write_tasks(tasks)
203
225
  list(tasks)
@@ -209,7 +231,7 @@ def list(tasks = nil, patterns = nil)
209
231
  task_indent = [tasks.keys.max.to_s.size, 4].max
210
232
  patterns = patterns.nil? || patterns.empty? ? [QUERIES[':active']] : patterns
211
233
  tasks.each do |num, task|
212
- normalized_task = "state=#{task[:state]} #{task[:title]}"
234
+ normalized_task = "state=#{task[:state]} due=#{task[:due]} #{task[:title]}"
213
235
  match = true
214
236
  patterns.each do |pattern|
215
237
  match = false unless /#{QUERIES[pattern] || pattern}/ix.match(normalized_task)
@@ -223,7 +245,9 @@ def list(tasks = nil, patterns = nil)
223
245
  state = task[:state] || 'default'
224
246
  color = COLORS[state]
225
247
  display_state = colorize(STATES[state], color)
226
- title = task[:title].gsub(/@\w+/) { |tag| colorize(tag, :cyan) }
248
+ title = task[:title].gsub(CONTEXT_TAG_PATTERN) do |tag|
249
+ (tag.start_with?(' ') ? ' ' : '') + colorize(tag.strip, :cyan)
250
+ end
227
251
  priority_flag = task[:priority] ? colorize(PRIORITY_FLAG, :red) : ' '
228
252
  due_date = ''
229
253
  if task[:due] && state != 'done'
@@ -284,6 +308,19 @@ def colorize(text, color)
284
308
  "\e[#{COLOR_CODES[color]}m#{text}\e[0m"
285
309
  end
286
310
 
311
+ def convert_due_date(date = '')
312
+ due = nil
313
+ day_index = DUE_DATE_DAYS.index(date.to_s.downcase) ||
314
+ DUE_DATE_DAYS_SIMPLE.index(date.to_s.downcase) ||
315
+ DUE_DATE_DAYS.map do |day| day[0..2] end.index(date.to_s.downcase)
316
+ if day_index
317
+ due = (TODAY.to_date + day_index).strftime(DATE_FORMAT)
318
+ else
319
+ due = date.nil? || date.empty? ? nil : Date.parse(date).strftime(DATE_FORMAT)
320
+ end
321
+ return due
322
+ end
323
+
287
324
  def read(arguments)
288
325
  begin
289
326
  action = arguments.first
@@ -293,17 +330,13 @@ def read(arguments)
293
330
  raise action + ' command requires at least one parameter' if args.nil? || args.empty?
294
331
  add(args.join(' '))
295
332
  when 'start'
296
- raise action + ' command can receive only one task number' if args.length > 1
297
- args.length == 1 ? change_state(args.first.to_i, 'started') : list(nil, [':started'])
333
+ args.length > 0 ? change_state(args.first.to_i, 'started', args[1..-1].join(' ')) : list(nil, [':started'])
298
334
  when 'done'
299
- raise action + ' command can receive only one task number' if args.length > 1
300
- args.length == 1 ? change_state(args.first.to_i, 'done') : list(nil, [':done'])
335
+ args.length > 0 ? change_state(args.first.to_i, 'done', args[1..-1].join(' ')) : list(nil, [':done'])
301
336
  when 'block'
302
- raise action + ' command can receive only one task number' if args.length > 1
303
- args.length == 1 ? change_state(args.first.to_i, 'blocked') : list(nil, [':blocked'])
337
+ args.length > 0 ? change_state(args.first.to_i, 'blocked', args[1..-1].join(' ')) : list(nil, [':blocked'])
304
338
  when 'reset'
305
- raise action + ' command can receive only one task number' if args.length > 1
306
- args.length == 1 ? change_state(args.first.to_i, 'new') : list(nil, [':new'])
339
+ args.length > 0 ? change_state(args.first.to_i, 'new', args[1..-1].join(' ')) : list(nil, [':new'])
307
340
  when 'prio'
308
341
  raise action + ' command requires exactly one parameter' if args.length != 1
309
342
  set_priority(args.first.to_i)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: todo-jsonl
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.9
4
+ version: 0.1.15
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gabor Bata
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-10-05 00:00:00.000000000 Z
11
+ date: 2021-02-25 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description:
14
14
  email: