todo-jsonl 0.1.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.
Files changed (4) hide show
  1. checksums.yaml +7 -0
  2. data/bin/todo +2 -0
  3. data/bin/todo.rb +295 -0
  4. metadata +71 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 266e5f102d3397fcbe41fe5af932ca7e3245b3bca0f0c022eba7a08d42e58909
4
+ data.tar.gz: bd8425b2e83ec7dd8bea45e2e54adebc9e4aaf11e554b850d5dc6d3679dcb845
5
+ SHA512:
6
+ metadata.gz: 478c00716781148b1fd9736dea4afbae040dd817ec5562f31db23eaa8d3069af2210f8e023ab52460723099f98e28cb87ed4694e82fdfd29c8e3a2c74f465a68
7
+ data.tar.gz: dcc56754c7a74b86e03411440ef9c8b5a703069dfb4e85b23a9b169a3727207ffe64caadcb74bc8c1ac2e7237ecbbd5e8389ff8eac1a26fc6f21a79539590603
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env ruby
2
+ require_relative 'todo.rb'
@@ -0,0 +1,295 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # todo.rb - todo list manager inspired by todo.txt using the jsonl format.
4
+ #
5
+ # Copyright (c) 2020 Gabor Bata
6
+ #
7
+ # Permission is hereby granted, free of charge, to any person
8
+ # obtaining a copy of this software and associated documentation files
9
+ # (the "Software"), to deal in the Software without restriction,
10
+ # including without limitation the rights to use, copy, modify, merge,
11
+ # publish, distribute, sublicense, and/or sell copies of the Software,
12
+ # and to permit persons to whom the Software is furnished to do so,
13
+ # subject to the following conditions:
14
+ #
15
+ # The above copyright notice and this permission notice shall be
16
+ # included in all copies or substantial portions of the Software.
17
+ #
18
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
19
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
20
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
21
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
22
+ # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
23
+ # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
24
+ # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25
+ # SOFTWARE.
26
+
27
+ require 'json'
28
+
29
+ DATE_FORMAT = '%Y-%m-%d'
30
+
31
+ COLOR_CODES = {
32
+ black: 30,
33
+ red: 31,
34
+ green: 32,
35
+ yellow: 33,
36
+ blue: 34,
37
+ magenta: 35,
38
+ cyan: 36,
39
+ white: 37
40
+ }
41
+
42
+ STATES = {
43
+ 'new' => '[ ]',
44
+ 'done' => '[x]',
45
+ 'started' => '[>]',
46
+ 'blocked' => '[!]',
47
+ 'default' => '[?]'
48
+ }
49
+
50
+ ORDER = {
51
+ 'new' => 3,
52
+ 'done' => 4,
53
+ 'started' => 2,
54
+ 'blocked' => 1,
55
+ 'default' => 100
56
+ }
57
+
58
+ COLORS = {
59
+ 'new' => :white,
60
+ 'done' => :blue,
61
+ 'started' => :green,
62
+ 'blocked' => :yellow,
63
+ 'default' => :magenta
64
+ }
65
+
66
+ QUERIES = {
67
+ ':active' => 'state=(new|started|blocked)',
68
+ ':done' => 'state=done',
69
+ ':blocked' => 'state=blocked',
70
+ ':started' => 'state=started',
71
+ ':new' => 'state=new',
72
+ ':all' => 'state=\w+'
73
+ }
74
+
75
+ PRIORITY_FLAG = '*'
76
+
77
+ TODO_FILE = "#{ENV['HOME']}/todo.jsonl"
78
+
79
+ def usage
80
+ <<~USAGE
81
+ Usage: todo <command> <arguments>
82
+
83
+ Commands:
84
+ * add <text> add new task
85
+ * start <tasknumber> mark task as started
86
+ * done <tasknumber> mark task as completed
87
+ * block <tasknumber> mark task as blocked
88
+ * prio <tasknumber> toggle high priority flag
89
+
90
+ * append <tasknumber> <text> append text to task title
91
+ * rename <tasknumber> <text> rename task
92
+ * del <tasknumber> delete task
93
+ * note <tasknumber> <text> add note to task
94
+ * delnote <tasknumber> <text> delete all notes from task
95
+
96
+ * list <regex> [regex...] list tasks (only active tasks by default)
97
+ * show <tasknumber> show all task details
98
+ * repl enter read–eval–print loop mode
99
+ * help this help screen
100
+
101
+ With list command the following pre-defined regex patterns can be also used:
102
+ #{QUERIES.keys.join(', ')}
103
+
104
+ Legend:
105
+ #{STATES.select { |k, v| k != 'default' }.map { |k, v| "#{k} #{v}" }.join(', ') }, priority #{PRIORITY_FLAG}
106
+ USAGE
107
+ end
108
+
109
+ def load_tasks(item_to_check = nil)
110
+ count = 0
111
+ tasks = {}
112
+ if File.exist?(TODO_FILE)
113
+ File.open(TODO_FILE, 'r:UTF-8') do |file|
114
+ file.each_line do |line|
115
+ next if line.strip == ''
116
+ count += 1
117
+ tasks[count] = JSON.parse(line.chomp, :symbolize_names => true)
118
+ end
119
+ end
120
+ end
121
+ if item_to_check && !tasks.has_key?(item_to_check)
122
+ raise "#{item_to_check}: No such todo"
123
+ end
124
+ tasks
125
+ end
126
+
127
+ def write_tasks(tasks)
128
+ File.open(TODO_FILE, 'w:UTF-8') do |file|
129
+ tasks.keys.sort.each do |key|
130
+ file.write(JSON.generate(tasks[key]) + "\n")
131
+ end
132
+ end
133
+ end
134
+
135
+ def add(text)
136
+ task = {
137
+ state: 'new',
138
+ title: text,
139
+ modified: Time.now.strftime(DATE_FORMAT)
140
+ }
141
+ File.open(TODO_FILE, 'a:UTF-8') do |file|
142
+ file.write(JSON.generate(task) + "\n")
143
+ end
144
+ list
145
+ end
146
+
147
+ def append(item, text = '')
148
+ tasks = load_tasks(item)
149
+ tasks[item][:title] = [tasks[item][:title], text].join(' ')
150
+ tasks[item][:modified] = Time.now.strftime(DATE_FORMAT)
151
+ write_tasks(tasks)
152
+ list(tasks)
153
+ end
154
+
155
+ def rename(item, text)
156
+ tasks = load_tasks(item)
157
+ tasks[item][:title] = text
158
+ tasks[item][:modified] = Time.now.strftime(DATE_FORMAT)
159
+ write_tasks(tasks)
160
+ list(tasks)
161
+ end
162
+
163
+ def delete(item)
164
+ tasks = load_tasks(item)
165
+ tasks.delete(item)
166
+ write_tasks(tasks)
167
+ list
168
+ end
169
+
170
+ def change_state(item, state)
171
+ tasks = load_tasks(item)
172
+ tasks[item][:state] = state
173
+ tasks[item][:modified] = Time.now.strftime(DATE_FORMAT)
174
+ write_tasks(tasks)
175
+ list(tasks)
176
+ end
177
+
178
+ def set_priority(item)
179
+ tasks = load_tasks(item)
180
+ tasks[item][:priority] = !tasks[item][:priority]
181
+ tasks[item][:modified] = Time.now.strftime(DATE_FORMAT)
182
+ write_tasks(tasks)
183
+ list(tasks)
184
+ end
185
+
186
+ def list(tasks = nil, patterns = nil)
187
+ items = {}
188
+ tasks = tasks || load_tasks
189
+ patterns = patterns.nil? || patterns.empty? ? [QUERIES[':active']] : patterns
190
+ tasks.each do |num, task|
191
+ normalized_task = "state=#{task[:state]} #{task[:title]}"
192
+ match = true
193
+ patterns.each do |pattern|
194
+ match = false unless /#{QUERIES[pattern] || pattern}/ix.match(normalized_task)
195
+ end
196
+ items[num] = task if match
197
+ end
198
+ items = items.sort_by do |num, task|
199
+ [task[:priority] ? 0 : 1, ORDER[task[:state] || 'default'], num]
200
+ end
201
+ items.each do |num, task|
202
+ state = task[:state] || 'default'
203
+ color = COLORS[state]
204
+ display_state = colorize(STATES[state], color)
205
+ title = task[:title].gsub(/@\w+/) { |tag| colorize(tag, :cyan) }
206
+ priority_flag = task[:priority] ? colorize(PRIORITY_FLAG, :red) : ' '
207
+ puts "#{num.to_s.rjust(4, ' ')}:#{priority_flag}#{display_state} #{title}"
208
+ end
209
+ puts 'No todos found' if items.empty?
210
+ end
211
+
212
+ def add_note(item, text)
213
+ tasks = load_tasks(item)
214
+ tasks[item][:note] ||= []
215
+ tasks[item][:note].push(text)
216
+ tasks[item][:modified] = Time.now.strftime(DATE_FORMAT)
217
+ write_tasks(tasks)
218
+ show(item)
219
+ end
220
+
221
+ def delete_note(item)
222
+ tasks = load_tasks(item)
223
+ tasks[item][:note] = []
224
+ tasks[item][:modified] = Time.now.strftime(DATE_FORMAT)
225
+ write_tasks(tasks)
226
+ show(item)
227
+ end
228
+
229
+ def show(item)
230
+ tasks = load_tasks(item)
231
+ tasks[item].each do |key, value|
232
+ val = value.kind_of?(Array) ? "\n" + value.join("\n") : value
233
+ puts "#{colorize(key.to_s.rjust(10, ' ') + ':', :cyan)} #{val}"
234
+ end
235
+ end
236
+
237
+ def start_repl
238
+ command = ''
239
+ while !['exit', 'quit'].include?(command)
240
+ if ['clear', 'cls'].include?(command)
241
+ print "\e[H\e[2J"
242
+ else
243
+ read(command == 'repl' ? [] : command.split(/\s+/))
244
+ end
245
+ print "\ntodo> "
246
+ command = STDIN.gets.chomp
247
+ end
248
+ end
249
+
250
+ def colorize(text, color)
251
+ "\e[#{COLOR_CODES[color]}m#{text}\e[0m"
252
+ end
253
+
254
+ def read(arguments)
255
+ begin
256
+ action = arguments.first
257
+ args = arguments[1..-1]
258
+ case action
259
+ when 'add'
260
+ add(args.join(' ')) unless args.nil? || args.empty?
261
+ when 'start'
262
+ args.length == 1 ? change_state(args.first.to_i, 'started') : list(nil, [':started'])
263
+ when 'done'
264
+ args.length == 1 ? change_state(args.first.to_i, 'done') : list(nil, [':done'])
265
+ when 'block'
266
+ args.length == 1 ? change_state(args.first.to_i, 'blocked') : list(nil, [':blocked'])
267
+ when 'prio'
268
+ set_priority(args.first.to_i) if args.length == 1
269
+ when 'append'
270
+ append(args.first.to_i, args[1..-1].join(' ')) unless args.length < 1
271
+ when 'rename'
272
+ rename(args.first.to_i, args[1..-1].join(' ')) unless args.length < 1
273
+ when 'del'
274
+ delete(args.first.to_i) if args.length == 1
275
+ when 'note'
276
+ add_note(args.first.to_i, args[1..-1].join(' ')) unless args.length < 1
277
+ when 'delnote'
278
+ delete_note(args.first.to_i) if args.length == 1
279
+ when 'list'
280
+ list(nil, args)
281
+ when 'show'
282
+ show(args.first.to_i) if args.length == 1
283
+ when 'help'
284
+ puts usage
285
+ when 'repl'
286
+ start_repl
287
+ else
288
+ list(nil, arguments)
289
+ end
290
+ rescue => error
291
+ puts "#{colorize('ERROR:', :red)} #{error}"
292
+ end
293
+ end
294
+
295
+ read(ARGV)
metadata ADDED
@@ -0,0 +1,71 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: todo-jsonl
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Gabor Bata
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-09-15 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: |2
14
+ Usage: todo <command> <arguments>
15
+
16
+ Commands:
17
+ * add <text> add new task
18
+ * start <tasknumber> mark task as started
19
+ * done <tasknumber> mark task as completed
20
+ * block <tasknumber> mark task as blocked
21
+ * prio <tasknumber> toggle high priority flag
22
+
23
+ * append <tasknumber> <text> append text to task title
24
+ * rename <tasknumber> <text> rename task
25
+ * del <tasknumber> delete task
26
+ * note <tasknumber> <text> add note to task
27
+ * delnote <tasknumber> <text> delete all notes from task
28
+
29
+ * list <regex> [regex...] list tasks (only active tasks by default)
30
+ * show <tasknumber> show all task details
31
+ * repl enter read–eval–print loop mode
32
+ * help this help screen
33
+
34
+ With list command the following pre-defined regex patterns can be also used:
35
+ :active, :done, :blocked, :started, :new, :all
36
+
37
+ Legend:
38
+ new [ ], done [x], started [>], blocked [!], priority *
39
+ email:
40
+ executables:
41
+ - todo.rb
42
+ - todo
43
+ extensions: []
44
+ extra_rdoc_files: []
45
+ files:
46
+ - bin/todo
47
+ - bin/todo.rb
48
+ homepage: https://github.com/gaborbata/todo
49
+ licenses:
50
+ - MIT
51
+ metadata: {}
52
+ post_install_message:
53
+ rdoc_options: []
54
+ require_paths:
55
+ - lib
56
+ required_ruby_version: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ required_rubygems_version: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ requirements: []
67
+ rubygems_version: 3.0.3
68
+ signing_key:
69
+ specification_version: 4
70
+ summary: todo list manager inspired by todo.txt using the jsonl format
71
+ test_files: []