doing 1.0.42 → 1.0.47
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/README.md +5 -4
- data/bin/doing +87 -66
- data/lib/doing/version.rb +1 -1
- data/lib/doing/wwid.rb +697 -668
- metadata +2 -2
data/lib/doing/version.rb
CHANGED
data/lib/doing/wwid.rb
CHANGED
@@ -1,13 +1,14 @@
|
|
1
1
|
#!/usr/bin/ruby
|
2
2
|
|
3
3
|
require 'deep_merge'
|
4
|
+
require 'open3'
|
4
5
|
|
5
6
|
##
|
6
7
|
## @brief String helpers
|
7
8
|
##
|
8
9
|
class String
|
9
10
|
def cap_first
|
10
|
-
|
11
|
+
sub(/^\w/) do |m|
|
11
12
|
m.upcase
|
12
13
|
end
|
13
14
|
end
|
@@ -17,21 +18,21 @@ class String
|
|
17
18
|
##
|
18
19
|
## @param opt (Hash) Additional Options
|
19
20
|
##
|
20
|
-
def link_urls(opt={})
|
21
|
+
def link_urls(opt = {})
|
21
22
|
opt[:format] ||= :html
|
22
23
|
if opt[:format] == :html
|
23
|
-
|
24
|
+
gsub(%r{(?mi)((http|https)://)?([\w\-_]+(\.[\w\-_]+)+)([\w\-.,@?^=%&:/~+#]*[\w\-@^=%&/~+#])?}) do |_match|
|
24
25
|
m = Regexp.last_match
|
25
|
-
proto = m[1].nil? ?
|
26
|
-
%
|
27
|
-
|
26
|
+
proto = m[1].nil? ? 'http://' : ''
|
27
|
+
%(<a href="#{proto}#{m[0]}" title="Link to #{m[0]}">[#{m[3]}]</a>)
|
28
|
+
end.gsub(/<(\w+:.*?)>/) do |match|
|
28
29
|
m = Regexp.last_match
|
29
|
-
|
30
|
-
%Q{<a href="#{m[1]}" title="Link to #{m[1]}">[link]</a>}
|
31
|
-
else
|
30
|
+
if m[1] =~ /<a href/
|
32
31
|
match
|
32
|
+
else
|
33
|
+
%(<a href="#{m[1]}" title="Link to #{m[1]}">[link]</a>)
|
33
34
|
end
|
34
|
-
|
35
|
+
end
|
35
36
|
else
|
36
37
|
self
|
37
38
|
end
|
@@ -42,7 +43,8 @@ end
|
|
42
43
|
## @brief Main "What Was I Doing" methods
|
43
44
|
##
|
44
45
|
class WWID
|
45
|
-
attr_accessor :content, :sections, :current_section, :doing_file, :config, :user_home, :default_config_file,
|
46
|
+
attr_accessor :content, :sections, :current_section, :doing_file, :config, :user_home, :default_config_file,
|
47
|
+
:config_file, :results, :auto_tag
|
46
48
|
|
47
49
|
##
|
48
50
|
## @brief Initializes the object.
|
@@ -62,16 +64,13 @@ class WWID
|
|
62
64
|
## @return (String) A file path
|
63
65
|
##
|
64
66
|
def find_local_config
|
65
|
-
|
66
67
|
config = {}
|
67
68
|
dir = Dir.pwd
|
68
69
|
|
69
70
|
local_config_files = []
|
70
71
|
|
71
|
-
while
|
72
|
-
if File.
|
73
|
-
local_config_files.push(File.join(dir, @default_config_file))
|
74
|
-
end
|
72
|
+
while dir != '/' && (dir =~ %r{[A-Z]:/}).nil?
|
73
|
+
local_config_files.push(File.join(dir, @default_config_file)) if File.exist? File.join(dir, @default_config_file)
|
75
74
|
|
76
75
|
dir = File.dirname(dir)
|
77
76
|
end
|
@@ -82,32 +81,30 @@ class WWID
|
|
82
81
|
##
|
83
82
|
## @brief Reads a configuration.
|
84
83
|
##
|
85
|
-
def read_config(opt={})
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
end
|
92
|
-
end
|
84
|
+
def read_config(opt = {})
|
85
|
+
@config_file ||= if Dir.respond_to?('home')
|
86
|
+
File.join(Dir.home, @default_config_file)
|
87
|
+
else
|
88
|
+
File.join(File.expand_path('~'), @default_config_file)
|
89
|
+
end
|
93
90
|
|
94
|
-
if opt[:ignore_local]
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
91
|
+
additional_configs = if opt[:ignore_local]
|
92
|
+
[]
|
93
|
+
else
|
94
|
+
find_local_config
|
95
|
+
end
|
99
96
|
|
100
97
|
begin
|
101
98
|
@local_config = {}
|
102
99
|
|
103
|
-
@config = YAML.load_file(@config_file) || {} if File.
|
104
|
-
additional_configs.each
|
100
|
+
@config = YAML.load_file(@config_file) || {} if File.exist?(@config_file)
|
101
|
+
additional_configs.each do |cfg|
|
105
102
|
new_config = YAML.load_file(cfg) || {} if cfg
|
106
103
|
@local_config = @local_config.deep_merge(new_config)
|
107
|
-
|
104
|
+
end
|
108
105
|
|
109
106
|
# @config.deep_merge(@local_config)
|
110
|
-
rescue
|
107
|
+
rescue StandardError
|
111
108
|
@config = {}
|
112
109
|
@local_config = {}
|
113
110
|
# raise "error reading config"
|
@@ -119,24 +116,20 @@ class WWID
|
|
119
116
|
##
|
120
117
|
## @param opt (Hash) Additional Options
|
121
118
|
##
|
122
|
-
def configure(opt={})
|
119
|
+
def configure(opt = {})
|
123
120
|
@timers = {}
|
124
121
|
opt[:ignore_local] ||= false
|
125
122
|
|
126
|
-
|
127
|
-
@config_file = File.join(@user_home, @default_config_file)
|
128
|
-
end
|
129
|
-
|
130
|
-
read_config({:ignore_local => opt[:ignore_local]})
|
123
|
+
@config_file ||= File.join(@user_home, @default_config_file)
|
131
124
|
|
132
|
-
|
125
|
+
read_config({ ignore_local: opt[:ignore_local] })
|
133
126
|
|
134
127
|
@config = {} if @config.nil?
|
135
128
|
|
136
129
|
@config['autotag'] ||= {}
|
137
130
|
@config['autotag']['whitelist'] ||= []
|
138
131
|
@config['autotag']['synonyms'] ||= {}
|
139
|
-
@config['doing_file'] ||=
|
132
|
+
@config['doing_file'] ||= '~/what_was_i_doing.md'
|
140
133
|
@config['current_section'] ||= 'Currently'
|
141
134
|
@config['editor_app'] ||= nil
|
142
135
|
|
@@ -177,12 +170,12 @@ class WWID
|
|
177
170
|
'tags_bool' => 'OR'
|
178
171
|
},
|
179
172
|
'color' => {
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
173
|
+
'date_format' => '%F %_I:%M%P',
|
174
|
+
'template' => '%boldblack%date %boldgreen| %boldwhite%title%default%note',
|
175
|
+
'wrap_width' => 0,
|
176
|
+
'section' => 'Currently',
|
177
|
+
'count' => 10,
|
178
|
+
'order' => 'asc'
|
186
179
|
}
|
187
180
|
}
|
188
181
|
@config['marker_tag'] ||= 'flagged'
|
@@ -202,17 +195,13 @@ class WWID
|
|
202
195
|
# end
|
203
196
|
# end
|
204
197
|
|
205
|
-
unless File.
|
206
|
-
File.open(@config_file, 'w') { |yf| YAML::dump(@config, yf) }
|
207
|
-
end
|
198
|
+
File.open(@config_file, 'w') { |yf| YAML.dump(@config, yf) } unless File.exist?(@config_file)
|
208
199
|
|
209
200
|
@config = @local_config.deep_merge(@config)
|
210
201
|
|
211
202
|
@current_section = @config['current_section']
|
212
203
|
@default_template = @config['templates']['default']['template']
|
213
|
-
@default_date_format = @config['templates']['default']['date_format']
|
214
|
-
|
215
|
-
|
204
|
+
@default_date_format = @config['templates']['default']['date_format']
|
216
205
|
end
|
217
206
|
|
218
207
|
##
|
@@ -220,16 +209,16 @@ class WWID
|
|
220
209
|
##
|
221
210
|
## @param path (String) Override path to a doing file, optional
|
222
211
|
##
|
223
|
-
def init_doing_file(path=nil)
|
212
|
+
def init_doing_file(path = nil)
|
224
213
|
@doing_file = File.expand_path(@config['doing_file'])
|
225
214
|
|
226
215
|
input = path
|
227
216
|
|
228
217
|
if input.nil?
|
229
|
-
create(@doing_file) unless File.
|
218
|
+
create(@doing_file) unless File.exist?(@doing_file)
|
230
219
|
input = IO.read(@doing_file)
|
231
220
|
input = input.force_encoding('utf-8') if input.respond_to? :force_encoding
|
232
|
-
elsif File.
|
221
|
+
elsif File.exist?(File.expand_path(input)) && File.file?(File.expand_path(input)) && File.stat(File.expand_path(input)).size.positive?
|
233
222
|
@doing_file = File.expand_path(input)
|
234
223
|
input = IO.read(File.expand_path(input))
|
235
224
|
input = input.force_encoding('utf-8') if input.respond_to? :force_encoding
|
@@ -243,40 +232,36 @@ class WWID
|
|
243
232
|
@other_content_top = []
|
244
233
|
@other_content_bottom = []
|
245
234
|
|
246
|
-
section =
|
235
|
+
section = 'Uncategorized'
|
247
236
|
lines = input.split(/[\n\r]/)
|
248
237
|
current = 0
|
249
238
|
|
250
|
-
lines.each
|
239
|
+
lines.each do |line|
|
251
240
|
next if line =~ /^\s*$/
|
241
|
+
|
252
242
|
if line =~ /^(\S[\S ]+):\s*(@\S+\s*)*$/
|
253
|
-
section =
|
243
|
+
section = Regexp.last_match(1)
|
254
244
|
@content[section] = {}
|
255
245
|
@content[section]['original'] = line
|
256
246
|
@content[section]['items'] = []
|
257
247
|
current = 0
|
258
248
|
elsif line =~ /^\s*- (\d{4}-\d\d-\d\d \d\d:\d\d) \| (.*)/
|
259
|
-
date = Time.parse(
|
260
|
-
title =
|
261
|
-
@content[section]['items'].push({'title' => title, 'date' => date, 'section' => section})
|
249
|
+
date = Time.parse(Regexp.last_match(1))
|
250
|
+
title = Regexp.last_match(2)
|
251
|
+
@content[section]['items'].push({ 'title' => title, 'date' => date, 'section' => section })
|
262
252
|
current += 1
|
263
|
-
|
253
|
+
elsif current.zero?
|
264
254
|
# if content[section]['items'].length - 1 == current
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
@content[section]['items'][current - 1]['note'] = []
|
273
|
-
end
|
274
|
-
@content[section]['items'][current - 1]['note'].push(line.gsub(/ *$/,''))
|
275
|
-
end
|
276
|
-
end
|
255
|
+
@other_content_top.push(line)
|
256
|
+
elsif line =~ /^\S/
|
257
|
+
@other_content_bottom.push(line)
|
258
|
+
else
|
259
|
+
@content[section]['items'][current - 1]['note'] = [] unless @content[section]['items'][current - 1].key? 'note'
|
260
|
+
|
261
|
+
@content[section]['items'][current - 1]['note'].push(line.gsub(/ *$/, ''))
|
277
262
|
# end
|
278
263
|
end
|
279
|
-
|
264
|
+
end
|
280
265
|
end
|
281
266
|
|
282
267
|
##
|
@@ -300,14 +285,12 @@ class WWID
|
|
300
285
|
##
|
301
286
|
## @brief Create a new doing file
|
302
287
|
##
|
303
|
-
def create(filename=nil)
|
304
|
-
if filename.nil?
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
f.puts @current_section + ":"
|
310
|
-
end
|
288
|
+
def create(filename = nil)
|
289
|
+
filename = @doing_file if filename.nil?
|
290
|
+
return if File.exist?(filename) && File.stat(filename).size.positive?
|
291
|
+
|
292
|
+
File.open(filename, 'w+') do |f|
|
293
|
+
f.puts "#{@current_section}:"
|
311
294
|
end
|
312
295
|
end
|
313
296
|
|
@@ -316,22 +299,26 @@ class WWID
|
|
316
299
|
##
|
317
300
|
## @param input (String) Text input for editor
|
318
301
|
##
|
319
|
-
def fork_editor(input=
|
320
|
-
tmpfile = Tempfile.new(['doing','.md'])
|
302
|
+
def fork_editor(input = '')
|
303
|
+
tmpfile = Tempfile.new(['doing', '.md'])
|
321
304
|
|
322
|
-
File.open(tmpfile.path,'w+') do |f|
|
305
|
+
File.open(tmpfile.path, 'w+') do |f|
|
323
306
|
f.puts input
|
324
307
|
f.puts "\n# The first line is the entry title, any lines after that are added as a note"
|
325
308
|
end
|
326
309
|
|
327
310
|
pid = Process.fork { system("$EDITOR #{tmpfile.path}") }
|
328
311
|
|
329
|
-
trap(
|
330
|
-
|
312
|
+
trap('INT') do
|
313
|
+
begin
|
314
|
+
Process.kill(9, pid)
|
315
|
+
rescue StandardError
|
316
|
+
Errno::ESRCH
|
317
|
+
end
|
331
318
|
tmpfile.unlink
|
332
319
|
tmpfile.close!
|
333
320
|
exit 0
|
334
|
-
|
321
|
+
end
|
335
322
|
|
336
323
|
Process.wait(pid)
|
337
324
|
|
@@ -339,7 +326,7 @@ class WWID
|
|
339
326
|
if $?.exitstatus == 0
|
340
327
|
input = IO.read(tmpfile.path)
|
341
328
|
else
|
342
|
-
raise
|
329
|
+
raise 'Cancelled'
|
343
330
|
end
|
344
331
|
ensure
|
345
332
|
tmpfile.close
|
@@ -357,15 +344,13 @@ class WWID
|
|
357
344
|
# @param input (String) The string to parse
|
358
345
|
#
|
359
346
|
def format_input(input)
|
360
|
-
raise
|
347
|
+
raise 'No content in entry' if input.nil? || input.strip.empty?
|
348
|
+
|
361
349
|
input_lines = input.split(/[\n\r]+/)
|
362
350
|
title = input_lines[0].strip
|
363
351
|
note = input_lines.length > 1 ? input_lines[1..-1] : []
|
364
|
-
note.map!
|
365
|
-
|
366
|
-
}.delete_if { |line|
|
367
|
-
line =~ /^\s*$/ || line =~ /^#/
|
368
|
-
}
|
352
|
+
note.map!(&:strip)
|
353
|
+
note.delete_if { |line| line =~ /^\s*$/ || line =~ /^#/ }
|
369
354
|
|
370
355
|
[title, note]
|
371
356
|
end
|
@@ -383,21 +368,22 @@ class WWID
|
|
383
368
|
#
|
384
369
|
def chronify(input)
|
385
370
|
now = Time.now
|
386
|
-
raise "Invalid time expression #{input.inspect}" if input.to_s.strip ==
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
371
|
+
raise "Invalid time expression #{input.inspect}" if input.to_s.strip == ''
|
372
|
+
|
373
|
+
secs_ago = if input.match(/^(\d+)$/)
|
374
|
+
# plain number, assume minutes
|
375
|
+
Regexp.last_match(1).to_i * 60
|
376
|
+
elsif (m = input.match(/^(?:(?<day>\d+)d)?(?:(?<hour>\d+)h)?(?:(?<min>\d+)m)?$/i))
|
377
|
+
# day/hour/minute format e.g. 1d2h30m
|
378
|
+
[[m['day'], 24 * 3600],
|
379
|
+
[m['hour'], 3600],
|
380
|
+
[m['min'], 60]].map { |qty, secs| qty ? (qty.to_i * secs) : 0 }.reduce(0, :+)
|
381
|
+
end
|
396
382
|
|
397
383
|
if secs_ago
|
398
384
|
now - secs_ago
|
399
385
|
else
|
400
|
-
Chronic.parse(input, {:
|
386
|
+
Chronic.parse(input, { context: :past, ambiguous_time_range: 8 })
|
401
387
|
end
|
402
388
|
end
|
403
389
|
|
@@ -412,23 +398,24 @@ class WWID
|
|
412
398
|
#
|
413
399
|
def chronify_qty(qty)
|
414
400
|
minutes = 0
|
415
|
-
|
416
|
-
|
417
|
-
minutes +=
|
418
|
-
|
419
|
-
|
420
|
-
|
401
|
+
case qty.strip
|
402
|
+
when /^(\d+):(\d\d)$/
|
403
|
+
minutes += Regexp.last_match(1).to_i * 60
|
404
|
+
minutes += Regexp.last_match(2).to_i
|
405
|
+
when /^(\d+(?:\.\d+)?)([hmd])?$/
|
406
|
+
amt = Regexp.last_match(1)
|
407
|
+
type = Regexp.last_match(2).nil? ? 'm' : Regexp.last_match(2)
|
421
408
|
|
422
409
|
minutes = case type.downcase
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
410
|
+
when 'm'
|
411
|
+
amt.to_i
|
412
|
+
when 'h'
|
413
|
+
(amt.to_f * 60).round
|
414
|
+
when 'd'
|
415
|
+
(amt.to_f * 60 * 24).round
|
416
|
+
else
|
417
|
+
minutes
|
418
|
+
end
|
432
419
|
end
|
433
420
|
minutes * 60
|
434
421
|
end
|
@@ -448,8 +435,8 @@ class WWID
|
|
448
435
|
## @param title (String) The new section title
|
449
436
|
##
|
450
437
|
def add_section(title)
|
451
|
-
@content[title.cap_first] = {'original' => "#{title}:", 'items' => []}
|
452
|
-
@results.push(%
|
438
|
+
@content[title.cap_first] = { 'original' => "#{title}:", 'items' => [] }
|
439
|
+
@results.push(%(Added section "#{title.cap_first}"))
|
453
440
|
end
|
454
441
|
|
455
442
|
##
|
@@ -458,32 +445,32 @@ class WWID
|
|
458
445
|
## @param frag (String) The user-provided string
|
459
446
|
## @param guessed (Boolean) already guessed and failed
|
460
447
|
##
|
461
|
-
def guess_section(frag,guessed
|
462
|
-
return
|
463
|
-
|
448
|
+
def guess_section(frag, guessed: false)
|
449
|
+
return 'All' if frag =~ /all/i
|
450
|
+
|
451
|
+
sections.each { |section| return section.cap_first if frag.downcase == section.downcase }
|
464
452
|
section = false
|
465
|
-
re = frag.split('').join(
|
466
|
-
sections.each
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
453
|
+
re = frag.split('').join('.*?')
|
454
|
+
sections.each do |sect|
|
455
|
+
next unless sect =~ /#{re}/i
|
456
|
+
|
457
|
+
warn "Assuming you meant #{sect}"
|
458
|
+
section = sect
|
459
|
+
break
|
460
|
+
end
|
473
461
|
unless section || guessed
|
474
|
-
alt = guess_view(frag,true)
|
475
|
-
if alt
|
476
|
-
raise "Did you mean `doing view #{alt}`?"
|
477
|
-
else
|
478
|
-
res = yn("Section #{frag} not found, create it",false)
|
462
|
+
alt = guess_view(frag, true)
|
463
|
+
raise "Did you mean `doing view #{alt}`?" if alt
|
479
464
|
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
465
|
+
res = yn("Section #{frag} not found, create it", default_response: false)
|
466
|
+
|
467
|
+
if res
|
468
|
+
add_section(frag.cap_first)
|
469
|
+
write(@doing_file)
|
470
|
+
return frag.cap_first
|
486
471
|
end
|
472
|
+
|
473
|
+
raise "Unknown section: #{frag}"
|
487
474
|
end
|
488
475
|
section ? section.cap_first : guessed
|
489
476
|
end
|
@@ -496,37 +483,31 @@ class WWID
|
|
496
483
|
##
|
497
484
|
## @return (Bool) yes or no
|
498
485
|
##
|
499
|
-
def yn(question, default_response
|
500
|
-
|
501
|
-
|
502
|
-
else
|
503
|
-
default = 'n'
|
504
|
-
end
|
486
|
+
def yn(question, default_response: false)
|
487
|
+
default = default_response ? 'y' : 'n'
|
488
|
+
|
505
489
|
# if this isn't an interactive shell, answer default
|
506
|
-
unless $stdout.isatty
|
507
|
-
|
508
|
-
return true
|
509
|
-
else
|
510
|
-
return false
|
511
|
-
end
|
512
|
-
end
|
490
|
+
return default.downcase == 'y' unless $stdout.isatty
|
491
|
+
|
513
492
|
# clear the buffer
|
514
|
-
if ARGV
|
493
|
+
if ARGV&.length
|
515
494
|
ARGV.length.times do
|
516
495
|
ARGV.shift
|
517
496
|
end
|
518
497
|
end
|
519
498
|
system 'stty cbreak'
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
|
529
|
-
|
499
|
+
|
500
|
+
cw = colors['white']
|
501
|
+
cbw = colors['boldwhite']
|
502
|
+
cbg = colors['boldgreen']
|
503
|
+
cd = colors['default']
|
504
|
+
|
505
|
+
options = if default
|
506
|
+
default =~ /y/i ? "#{cw}[#{cbg}Y#{cw}/#{cbw}n#{cw}]#{cd}" : "#{cw}[#{cbw}y#{cw}/#{cbg}N#{cw}]#{cd}"
|
507
|
+
else
|
508
|
+
"#{cw}[#{cbw}y#{cw}/#{cbw}n#{cw}]#{cd}"
|
509
|
+
end
|
510
|
+
$stdout.syswrite "#{cbw}#{question.sub(/\?$/, '')} #{options}#{cbw}?#{cd} "
|
530
511
|
res = $stdin.sysread 1
|
531
512
|
puts
|
532
513
|
system 'stty cooked'
|
@@ -534,9 +515,9 @@ class WWID
|
|
534
515
|
res.chomp!
|
535
516
|
res.downcase!
|
536
517
|
|
537
|
-
res = default.downcase if res ==
|
518
|
+
res = default.downcase if res == ''
|
538
519
|
|
539
|
-
|
520
|
+
res =~ /y/i
|
540
521
|
end
|
541
522
|
|
542
523
|
##
|
@@ -545,19 +526,19 @@ class WWID
|
|
545
526
|
## @param frag (String) The user-provided string
|
546
527
|
## @param guessed (Boolean) already guessed
|
547
528
|
##
|
548
|
-
def guess_view(frag,guessed=false)
|
549
|
-
views.each {|view| return view if frag.downcase == view.downcase}
|
529
|
+
def guess_view(frag, guessed = false)
|
530
|
+
views.each { |view| return view if frag.downcase == view.downcase }
|
550
531
|
view = false
|
551
|
-
re = frag.split('').join(
|
552
|
-
views.each
|
553
|
-
|
554
|
-
|
555
|
-
|
556
|
-
|
557
|
-
|
558
|
-
|
532
|
+
re = frag.split('').join('.*?')
|
533
|
+
views.each do |v|
|
534
|
+
next unless v =~ /#{re}/i
|
535
|
+
|
536
|
+
warn "Assuming you meant #{v}"
|
537
|
+
view = v
|
538
|
+
break
|
539
|
+
end
|
559
540
|
unless view || guessed
|
560
|
-
alt = guess_section(frag,true)
|
541
|
+
alt = guess_section(frag, guessed: true)
|
561
542
|
if alt
|
562
543
|
raise "Did you mean `doing show #{alt}`?"
|
563
544
|
else
|
@@ -574,7 +555,7 @@ class WWID
|
|
574
555
|
## @param section (String) The section to add to
|
575
556
|
## @param opt (Hash) Additional Options {:date, :note, :back, :timed}
|
576
557
|
##
|
577
|
-
def add_item(title,section=nil,opt={})
|
558
|
+
def add_item(title, section = nil, opt = {})
|
578
559
|
section ||= @current_section
|
579
560
|
add_section(section) unless @content.has_key?(section)
|
580
561
|
opt[:date] ||= Time.now
|
@@ -582,7 +563,7 @@ class WWID
|
|
582
563
|
opt[:back] ||= Time.now
|
583
564
|
opt[:timed] ||= false
|
584
565
|
|
585
|
-
opt[:note] = [opt[:note]] if opt[:note].
|
566
|
+
opt[:note] = [opt[:note]] if opt[:note].instance_of?(String)
|
586
567
|
|
587
568
|
title = [title.strip.cap_first]
|
588
569
|
title = title.join(' ')
|
@@ -590,39 +571,37 @@ class WWID
|
|
590
571
|
if @auto_tag
|
591
572
|
title = autotag(title)
|
592
573
|
unless @config['default_tags'].empty?
|
593
|
-
|
594
|
-
|
595
|
-
|
596
|
-
|
597
|
-
|
598
|
-
|
599
|
-
|
600
|
-
|
574
|
+
default_tags = @config['default_tags'].map do |t|
|
575
|
+
next if t.nil?
|
576
|
+
|
577
|
+
dt = t.sub(/^ *@/, '').chomp
|
578
|
+
if title =~ /@#{dt}/
|
579
|
+
''
|
580
|
+
else
|
581
|
+
" @#{dt}"
|
601
582
|
end
|
602
|
-
|
583
|
+
end
|
584
|
+
default_tags.delete_if { |t| t == '' }
|
585
|
+
title += default_tags.join(' ')
|
603
586
|
end
|
604
587
|
end
|
605
|
-
title.gsub!(/ +/,' ')
|
606
|
-
entry = {'title' => title.strip, 'date' => opt[:back]}
|
607
|
-
unless opt[:note].join('').strip == ''
|
608
|
-
entry['note'] = opt[:note].map {|n| n.chomp}
|
609
|
-
end
|
588
|
+
title.gsub!(/ +/, ' ')
|
589
|
+
entry = { 'title' => title.strip, 'date' => opt[:back] }
|
590
|
+
entry['note'] = opt[:note].map(&:chomp) unless opt[:note].join('').strip == ''
|
610
591
|
items = @content[section]['items']
|
611
592
|
if opt[:timed]
|
612
593
|
items.reverse!
|
613
|
-
items.each_with_index
|
614
|
-
if i['title'] =~ / @done/
|
615
|
-
|
616
|
-
|
617
|
-
|
618
|
-
|
619
|
-
end
|
620
|
-
}
|
594
|
+
items.each_with_index do |i, x|
|
595
|
+
next if i['title'] =~ / @done/
|
596
|
+
|
597
|
+
items[x]['title'] = "#{i['title']} @done(#{opt[:back].strftime('%F %R')})"
|
598
|
+
break
|
599
|
+
end
|
621
600
|
items.reverse!
|
622
601
|
end
|
623
602
|
items.push(entry)
|
624
603
|
@content[section]['items'] = items
|
625
|
-
@results.push(%
|
604
|
+
@results.push(%(Added "#{entry['title']}" to #{section}))
|
626
605
|
end
|
627
606
|
|
628
607
|
##
|
@@ -631,27 +610,79 @@ class WWID
|
|
631
610
|
## @param section (String) The section to retrieve from, default
|
632
611
|
## All
|
633
612
|
##
|
634
|
-
def last_note(section=
|
613
|
+
def last_note(section = 'All')
|
635
614
|
section = guess_section(section)
|
636
615
|
if section =~ /^all$/i
|
637
|
-
combined = {'items' => []}
|
638
|
-
@content.each
|
616
|
+
combined = { 'items' => [] }
|
617
|
+
@content.each do |_k, v|
|
639
618
|
combined['items'] += v['items']
|
640
|
-
|
641
|
-
section = combined['items'].dup.sort_by{|item| item['date'] }.reverse[0]['section']
|
619
|
+
end
|
620
|
+
section = combined['items'].dup.sort_by { |item| item['date'] }.reverse[0]['section']
|
642
621
|
end
|
643
622
|
|
644
|
-
|
645
|
-
|
646
|
-
|
647
|
-
|
648
|
-
|
649
|
-
|
623
|
+
raise "Section #{section} not found" unless @content.key?(section)
|
624
|
+
|
625
|
+
last_item = @content[section]['items'].dup.sort_by { |item| item['date'] }.reverse[0]
|
626
|
+
warn "Editing note for #{last_item['title']}"
|
627
|
+
note = ''
|
628
|
+
note = last_item['note'].map(&:strip).join("\n") unless last_item['note'].nil?
|
629
|
+
"#{last_item['title']}\n# EDIT BELOW THIS LINE ------------\n#{note}"
|
630
|
+
end
|
631
|
+
|
632
|
+
##
|
633
|
+
## @brief Restart the last entry
|
634
|
+
##
|
635
|
+
## @param opt (Hash) Additional Options
|
636
|
+
##
|
637
|
+
def restart_last(opt = {})
|
638
|
+
opt[:section] ||= 'all'
|
639
|
+
opt[:note] ||= []
|
640
|
+
|
641
|
+
last = last_entry(opt)
|
642
|
+
if last.nil?
|
643
|
+
@results.push(%(No previous entry found))
|
644
|
+
return
|
645
|
+
end
|
646
|
+
# Remove @done tag
|
647
|
+
title = last['title'].sub(/\s*@done(\(.*?\))?/, '').chomp
|
648
|
+
@auto_tag = false
|
649
|
+
add_item(title, last['section'], { note: opt[:note], back: opt[:date], timed: true })
|
650
|
+
write(@doing_file)
|
651
|
+
end
|
652
|
+
|
653
|
+
##
|
654
|
+
## @brief Get the last entry
|
655
|
+
##
|
656
|
+
## @param opt (Hash) Additional Options
|
657
|
+
##
|
658
|
+
def last_entry(opt = {})
|
659
|
+
opt[:section] ||= @current_section
|
660
|
+
|
661
|
+
sec_arr = []
|
662
|
+
|
663
|
+
if opt[:section].nil?
|
664
|
+
sec_arr = [@current_section]
|
665
|
+
elsif opt[:section].instance_of?(String)
|
666
|
+
if opt[:section] =~ /^all$/i
|
667
|
+
combined = { 'items' => [] }
|
668
|
+
@content.each do |_k, v|
|
669
|
+
combined['items'] += v['items']
|
670
|
+
end
|
671
|
+
items = combined['items'].dup.sort_by { |item| item['date'] }.reverse
|
672
|
+
sec_arr.push(items[0]['section'])
|
673
|
+
|
674
|
+
sec_arr = sections
|
675
|
+
else
|
676
|
+
sec_arr = [guess_section(opt[:section])]
|
650
677
|
end
|
651
|
-
return "#{last_item['title']}\n# EDIT BELOW THIS LINE ------------\n#{note}"
|
652
|
-
else
|
653
|
-
raise "Section #{section} not found"
|
654
678
|
end
|
679
|
+
|
680
|
+
all_items = []
|
681
|
+
sec_arr.each do |section|
|
682
|
+
all_items.concat(@content[section]['items'].dup) if @content.key?(section)
|
683
|
+
end
|
684
|
+
|
685
|
+
all_items.max_by { |item| item['date'] }
|
655
686
|
end
|
656
687
|
|
657
688
|
##
|
@@ -659,33 +690,32 @@ class WWID
|
|
659
690
|
##
|
660
691
|
## @param opt (Hash) Additional Options
|
661
692
|
##
|
662
|
-
def tag_last(opt={})
|
693
|
+
def tag_last(opt = {})
|
663
694
|
opt[:section] ||= @current_section
|
664
695
|
opt[:count] ||= 1
|
665
696
|
opt[:archive] ||= false
|
666
|
-
opt[:tags] ||= [
|
697
|
+
opt[:tags] ||= ['done']
|
667
698
|
opt[:sequential] ||= false
|
668
699
|
opt[:date] ||= false
|
669
700
|
opt[:remove] ||= false
|
670
701
|
opt[:autotag] ||= false
|
671
702
|
opt[:back] ||= false
|
672
703
|
|
673
|
-
|
674
704
|
sec_arr = []
|
675
705
|
|
676
706
|
if opt[:section].nil?
|
677
707
|
sec_arr = [@current_section]
|
678
|
-
elsif opt[:section].
|
708
|
+
elsif opt[:section].instance_of?(String)
|
679
709
|
if opt[:section] =~ /^all$/i
|
680
710
|
if opt[:count] == 1
|
681
|
-
combined = {'items' => []}
|
682
|
-
@content.each
|
711
|
+
combined = { 'items' => [] }
|
712
|
+
@content.each do |_k, v|
|
683
713
|
combined['items'] += v['items']
|
684
|
-
|
685
|
-
items = combined['items'].dup.sort_by{|item| item['date'] }.reverse
|
714
|
+
end
|
715
|
+
items = combined['items'].dup.sort_by { |item| item['date'] }.reverse
|
686
716
|
sec_arr.push(items[0]['section'])
|
687
717
|
elsif opt[:count] > 1
|
688
|
-
raise
|
718
|
+
raise 'A count greater than one requires a section to be specified'
|
689
719
|
else
|
690
720
|
sec_arr = sections
|
691
721
|
end
|
@@ -694,21 +724,27 @@ class WWID
|
|
694
724
|
end
|
695
725
|
end
|
696
726
|
|
727
|
+
sec_arr.each do |section|
|
728
|
+
if @content.key?(section)
|
697
729
|
|
698
|
-
|
699
|
-
sec_arr.each {|section|
|
700
|
-
if @content.has_key?(section)
|
701
|
-
|
702
|
-
items = @content[section]['items'].dup.sort_by{|item| item['date'] }.reverse
|
730
|
+
items = @content[section]['items'].dup.sort_by { |item| item['date'] }.reverse
|
703
731
|
|
704
732
|
index = 0
|
705
733
|
done_date = Time.now
|
706
734
|
next_start = Time.now
|
707
|
-
count = opt[:count]
|
708
|
-
items.map!
|
735
|
+
count = (opt[:count]).zero? ? items.length : opt[:count]
|
736
|
+
items.map! do |item|
|
709
737
|
break if index == count
|
710
738
|
|
711
|
-
|
739
|
+
if opt[:autotag]
|
740
|
+
new_title = autotag(item['title']) if @auto_tag
|
741
|
+
if new_title == item['title']
|
742
|
+
@results.push(%(Autotag: No changes))
|
743
|
+
else
|
744
|
+
@results.push("Tags updated: #{new_title}")
|
745
|
+
item['title'] = new_title
|
746
|
+
end
|
747
|
+
else
|
712
748
|
if opt[:sequential]
|
713
749
|
done_date = next_start - 1
|
714
750
|
next_start = item['date']
|
@@ -719,62 +755,50 @@ class WWID
|
|
719
755
|
end
|
720
756
|
|
721
757
|
title = item['title']
|
722
|
-
opt[:tags].each
|
758
|
+
opt[:tags].each do |tag|
|
723
759
|
tag.strip!
|
724
|
-
if opt[:remove]
|
725
|
-
|
726
|
-
|
727
|
-
|
728
|
-
|
729
|
-
|
730
|
-
|
731
|
-
|
732
|
-
|
733
|
-
|
734
|
-
|
735
|
-
title += " @#{tag}"
|
736
|
-
end
|
737
|
-
@results.push(%Q{Added @#{tag}: "#{title}" in #{section}})
|
738
|
-
end
|
760
|
+
if opt[:remove] && title =~ /@#{tag}\b/
|
761
|
+
title.gsub!(/(^| )@#{tag}(\([^)]*\))?/, '')
|
762
|
+
@results.push(%(Removed @#{tag}: "#{title}" in #{section}))
|
763
|
+
elsif title !~ /@#{tag}/
|
764
|
+
title.chomp!
|
765
|
+
title += if opt[:date]
|
766
|
+
" @#{tag}(#{done_date.strftime('%F %R')})"
|
767
|
+
else
|
768
|
+
" @#{tag}"
|
769
|
+
end
|
770
|
+
@results.push(%(Added @#{tag}: "#{title}" in #{section}))
|
739
771
|
end
|
740
|
-
}
|
741
|
-
item['title'] = title
|
742
|
-
else
|
743
|
-
new_title = autotag(item['title']) if @auto_tag
|
744
|
-
unless new_title == item['title']
|
745
|
-
@results.push("Tags updated: #{new_title}")
|
746
|
-
item['title'] = new_title
|
747
|
-
else
|
748
|
-
@results.push(%Q{Autotag: No changes})
|
749
772
|
end
|
773
|
+
item['title'] = title
|
750
774
|
end
|
751
775
|
|
752
776
|
index += 1
|
753
777
|
|
754
778
|
item
|
755
|
-
|
779
|
+
end
|
756
780
|
|
757
781
|
@content[section]['items'] = items
|
758
782
|
|
759
|
-
if opt[:archive] && section !=
|
783
|
+
if opt[:archive] && section != 'Archive' && (opt[:count]).positive?
|
760
784
|
# concat [count] items from [section] and archive section
|
761
|
-
archived = @content[section]['items'][0..opt[:count]-1].map
|
762
|
-
i['title'].sub(/(?:@from\(.*?\))?(.*)$/,"\\1 @from(#{i['section']})")
|
763
|
-
|
785
|
+
archived = @content[section]['items'][0..opt[:count] - 1].map do |i|
|
786
|
+
i['title'].sub(/(?:@from\(.*?\))?(.*)$/, "\\1 @from(#{i['section']})")
|
787
|
+
end.concat(@content['Archive']['items'])
|
764
788
|
# chop [count] items off of [section] items
|
765
789
|
@content[opt[:section]]['items'] = @content[opt[:section]]['items'][opt[:count]..-1]
|
766
790
|
# overwrite archive section with concatenated array
|
767
791
|
@content['Archive']['items'] = archived
|
768
792
|
# log it
|
769
|
-
result = opt[:count] == 1 ?
|
793
|
+
result = opt[:count] == 1 ? '1 entry' : "#{opt[:count]} entries"
|
770
794
|
@results.push("Archived #{result} from #{section}")
|
771
|
-
elsif opt[:archive] && opt[:count]
|
772
|
-
@results.push(
|
795
|
+
elsif opt[:archive] && (opt[:count]).zero?
|
796
|
+
@results.push('Archiving is skipped when operating on all entries') if (opt[:count]).zero?
|
773
797
|
end
|
774
798
|
else
|
775
799
|
raise "Section not found: #{section}"
|
776
800
|
end
|
777
|
-
|
801
|
+
end
|
778
802
|
|
779
803
|
write(@doing_file)
|
780
804
|
end
|
@@ -786,49 +810,47 @@ class WWID
|
|
786
810
|
## @param note (String) The note to add
|
787
811
|
## @param replace (Bool) Should replace existing note
|
788
812
|
##
|
789
|
-
def note_last(section, note, replace
|
813
|
+
def note_last(section, note, replace: false)
|
790
814
|
section = guess_section(section)
|
791
815
|
|
792
816
|
if section =~ /^all$/i
|
793
|
-
combined = {'items' => []}
|
794
|
-
@content.each
|
817
|
+
combined = { 'items' => [] }
|
818
|
+
@content.each do |_k, v|
|
795
819
|
combined['items'] += v['items']
|
796
|
-
|
797
|
-
section = combined['items'].dup.sort_by{|item| item['date'] }.reverse[0]['section']
|
820
|
+
end
|
821
|
+
section = combined['items'].dup.sort_by { |item| item['date'] }.reverse[0]['section']
|
798
822
|
end
|
799
823
|
|
800
|
-
|
801
|
-
|
802
|
-
|
803
|
-
|
804
|
-
|
805
|
-
|
806
|
-
|
807
|
-
|
808
|
-
|
809
|
-
|
810
|
-
|
811
|
-
|
812
|
-
|
813
|
-
|
814
|
-
|
815
|
-
|
816
|
-
@results.push(%Q{Entry "#{title}" has no note})
|
817
|
-
end
|
818
|
-
elsif current_note.class == Array
|
819
|
-
items[0]['note'] = current_note.concat(note)
|
820
|
-
@results.push(%Q{Added note to "#{title}"}) if note.length > 0
|
824
|
+
raise "Section #{section} not found" unless @content.key?(section)
|
825
|
+
|
826
|
+
# sort_section(opt[:section])
|
827
|
+
items = @content[section]['items'].dup.sort_by { |item| item['date'] }.reverse
|
828
|
+
|
829
|
+
current_note = items[0]['note']
|
830
|
+
current_note = [] if current_note.nil?
|
831
|
+
title = items[0]['title']
|
832
|
+
if replace
|
833
|
+
items[0]['note'] = note
|
834
|
+
if note.empty? && !current_note.empty?
|
835
|
+
@results.push(%(Removed note from "#{title}"))
|
836
|
+
elsif !current_note.empty? && !note.empty?
|
837
|
+
@results.push(%(Replaced note from "#{title}"))
|
838
|
+
elsif !note.empty?
|
839
|
+
@results.push(%(Added note to "#{title}"))
|
821
840
|
else
|
822
|
-
|
823
|
-
@results.push(%Q{Added note to "#{title}"}) if note.length > 0
|
841
|
+
@results.push(%(Entry "#{title}" has no note))
|
824
842
|
end
|
825
|
-
|
826
|
-
|
843
|
+
elsif current_note.instance_of?(Array)
|
844
|
+
items[0]['note'] = current_note.concat(note)
|
845
|
+
@results.push(%(Added note to "#{title}")) unless note.empty?
|
827
846
|
else
|
828
|
-
|
847
|
+
items[0]['note'] = note
|
848
|
+
@results.push(%(Added note to "#{title}")) unless note.empty?
|
829
849
|
end
|
830
|
-
end
|
831
850
|
|
851
|
+
@content[section]['items'] = items
|
852
|
+
|
853
|
+
end
|
832
854
|
|
833
855
|
#
|
834
856
|
# @brief Accepts one tag and the raw text of a new item if the passed tag
|
@@ -840,7 +862,7 @@ class WWID
|
|
840
862
|
# @param tag (String) Tag to replace
|
841
863
|
# @param opt (Hash) Additional Options
|
842
864
|
#
|
843
|
-
def stop_start(tag,opt={})
|
865
|
+
def stop_start(tag, opt = {})
|
844
866
|
opt[:section] ||= @current_section
|
845
867
|
opt[:archive] ||= false
|
846
868
|
opt[:back] ||= Time.now
|
@@ -849,36 +871,36 @@ class WWID
|
|
849
871
|
|
850
872
|
opt[:section] = guess_section(opt[:section])
|
851
873
|
|
852
|
-
tag.sub!(/^@/,'')
|
874
|
+
tag.sub!(/^@/, '')
|
853
875
|
|
854
876
|
found_items = 0
|
855
|
-
@content[opt[:section]]['items'].each_with_index
|
856
|
-
|
857
|
-
|
858
|
-
|
859
|
-
|
860
|
-
|
861
|
-
|
862
|
-
|
863
|
-
|
864
|
-
|
865
|
-
|
866
|
-
|
867
|
-
|
868
|
-
|
869
|
-
|
870
|
-
|
871
|
-
|
877
|
+
@content[opt[:section]]['items'].each_with_index do |item, i|
|
878
|
+
next unless item['title'] =~ /@#{tag}/
|
879
|
+
|
880
|
+
title = item['title'].gsub(/(^| )@(#{tag}|done)(\([^)]*\))?/, '')
|
881
|
+
title += " @done(#{opt[:back].strftime('%F %R')})"
|
882
|
+
|
883
|
+
@content[opt[:section]]['items'][i]['title'] = title
|
884
|
+
found_items += 1
|
885
|
+
|
886
|
+
if opt[:archive] && opt[:section] != 'Archive'
|
887
|
+
@results.push(%(Completed and archived "#{@content[opt[:section]]['items'][i]['title']}"))
|
888
|
+
archive_item = @content[opt[:section]]['items'][i]
|
889
|
+
archive_item['title'] = i['title'].sub(/(?:@from\(.*?\))?(.*)$/, "\\1 @from(#{i['section']})")
|
890
|
+
@content['Archive']['items'].push(archive_item)
|
891
|
+
@content[opt[:section]]['items'].delete_at(i)
|
892
|
+
else
|
893
|
+
@results.push(%(Completed "#{@content[opt[:section]]['items'][i]['title']}"))
|
872
894
|
end
|
873
|
-
|
895
|
+
end
|
874
896
|
|
875
897
|
@results.push("No active @#{tag} tasks found.") if found_items == 0
|
876
898
|
|
877
899
|
if opt[:new_item]
|
878
900
|
title, note = format_input(opt[:new_item])
|
879
|
-
note.push(opt[:note].gsub(/ *$/,'')) if opt[:note]
|
901
|
+
note.push(opt[:note].gsub(/ *$/, '')) if opt[:note]
|
880
902
|
title += " @#{tag}"
|
881
|
-
add_item(title.cap_first, opt[:section], {:
|
903
|
+
add_item(title.cap_first, opt[:section], { note: note.join(' ').rstrip, back: opt[:back] })
|
882
904
|
end
|
883
905
|
|
884
906
|
write(@doing_file)
|
@@ -889,28 +911,34 @@ class WWID
|
|
889
911
|
##
|
890
912
|
## @param file (String) The filepath to write to
|
891
913
|
##
|
892
|
-
def write(file=nil)
|
893
|
-
|
894
|
-
|
895
|
-
|
896
|
-
output
|
914
|
+
def write(file = nil)
|
915
|
+
output = @other_content_top ? "#{@other_content_top.join("\n")}\n" : ''
|
916
|
+
|
917
|
+
@content.each do |title, section|
|
918
|
+
output += "#{section['original']}\n"
|
919
|
+
output += list_section({ section: title, template: "\t- %date | %title%note", highlight: false })
|
897
920
|
end
|
898
|
-
@content.each {|title, section|
|
899
|
-
output += section['original'] + "\n"
|
900
|
-
output += list_section({:section => title, :template => "\t- %date | %title%note", :highlight => false})
|
901
|
-
}
|
902
921
|
output += @other_content_bottom.join("\n") unless @other_content_bottom.nil?
|
903
922
|
if file.nil?
|
904
923
|
$stdout.puts output
|
905
924
|
else
|
906
|
-
|
925
|
+
file = File.expand_path(file)
|
926
|
+
if File.exist?(file)
|
907
927
|
# Create a backup copy for the undo command
|
908
|
-
FileUtils.cp(file,file
|
928
|
+
FileUtils.cp(file, "#{file}~")
|
909
929
|
|
910
|
-
File.open(
|
930
|
+
File.open(file, 'w+') do |f|
|
911
931
|
f.puts output
|
912
932
|
end
|
913
933
|
end
|
934
|
+
|
935
|
+
if @config.key?('run_after')
|
936
|
+
stdout, stderr, status = Open3.capture3(@config['run_after'])
|
937
|
+
if status.exitstatus.positive?
|
938
|
+
warn "Error running #{@config['run_after']}"
|
939
|
+
warn stderr
|
940
|
+
end
|
941
|
+
end
|
914
942
|
end
|
915
943
|
end
|
916
944
|
|
@@ -920,27 +948,27 @@ class WWID
|
|
920
948
|
## @param file (String) The filepath to restore
|
921
949
|
##
|
922
950
|
def restore_backup(file)
|
923
|
-
if File.
|
924
|
-
puts file+
|
925
|
-
FileUtils.cp(file+
|
951
|
+
if File.exist?(file + '~')
|
952
|
+
puts file + '~'
|
953
|
+
FileUtils.cp(file + '~', file)
|
926
954
|
@results.push("Restored #{file}")
|
927
955
|
end
|
928
956
|
end
|
929
957
|
|
930
|
-
|
931
958
|
##
|
932
959
|
## @brief Generate a menu of sections and allow user selection
|
933
960
|
##
|
934
961
|
## @return (String) The selected section name
|
935
962
|
##
|
936
963
|
def choose_section
|
937
|
-
sections.each_with_index
|
938
|
-
puts
|
939
|
-
|
964
|
+
sections.each_with_index do |section, i|
|
965
|
+
puts format('% 3d: %s', i + 1, section)
|
966
|
+
end
|
940
967
|
print "#{colors['green']}> #{colors['default']}"
|
941
968
|
num = STDIN.gets
|
942
969
|
return false if num =~ /^[a-z ]*$/i
|
943
|
-
|
970
|
+
|
971
|
+
sections[num.to_i - 1]
|
944
972
|
end
|
945
973
|
|
946
974
|
##
|
@@ -958,13 +986,14 @@ class WWID
|
|
958
986
|
## @return (String) The selected view name
|
959
987
|
##
|
960
988
|
def choose_view
|
961
|
-
views.each_with_index
|
962
|
-
puts
|
963
|
-
|
964
|
-
print
|
989
|
+
views.each_with_index do |view, i|
|
990
|
+
puts format('% 3d: %s', i + 1, view)
|
991
|
+
end
|
992
|
+
print '> '
|
965
993
|
num = STDIN.gets
|
966
994
|
return false if num =~ /^[a-z ]*$/i
|
967
|
-
|
995
|
+
|
996
|
+
views[num.to_i - 1]
|
968
997
|
end
|
969
998
|
|
970
999
|
##
|
@@ -973,9 +1002,8 @@ class WWID
|
|
973
1002
|
## @param title (String) The title of the view to retrieve
|
974
1003
|
##
|
975
1004
|
def get_view(title)
|
976
|
-
if @config['views'].has_key?(title)
|
977
|
-
|
978
|
-
end
|
1005
|
+
return @config['views'][title] if @config['views'].has_key?(title)
|
1006
|
+
|
979
1007
|
false
|
980
1008
|
end
|
981
1009
|
|
@@ -985,14 +1013,14 @@ class WWID
|
|
985
1013
|
##
|
986
1014
|
## @param opt (Hash) Additional Options
|
987
1015
|
##
|
988
|
-
def list_section(opt={})
|
1016
|
+
def list_section(opt = {})
|
989
1017
|
opt[:count] ||= 0
|
990
1018
|
count = opt[:count] - 1
|
991
1019
|
opt[:section] ||= nil
|
992
1020
|
opt[:format] ||= @default_date_format
|
993
1021
|
opt[:template] ||= @default_template
|
994
|
-
opt[:age] ||=
|
995
|
-
opt[:order] ||=
|
1022
|
+
opt[:age] ||= 'newest'
|
1023
|
+
opt[:order] ||= 'desc'
|
996
1024
|
opt[:today] ||= false
|
997
1025
|
opt[:tag_filter] ||= false
|
998
1026
|
opt[:tags_color] ||= false
|
@@ -1004,17 +1032,23 @@ class WWID
|
|
1004
1032
|
opt[:date_filter] ||= []
|
1005
1033
|
|
1006
1034
|
# opt[:highlight] ||= true
|
1007
|
-
section =
|
1035
|
+
section = ''
|
1008
1036
|
if opt[:section].nil?
|
1009
1037
|
section = choose_section
|
1010
1038
|
opt[:section] = @content[section]
|
1011
|
-
elsif opt[:section].
|
1039
|
+
elsif opt[:section].instance_of?(String)
|
1012
1040
|
if opt[:section] =~ /^all$/i
|
1013
|
-
combined = {'items' => []}
|
1014
|
-
@content.each
|
1041
|
+
combined = { 'items' => [] }
|
1042
|
+
@content.each do |_k, v|
|
1015
1043
|
combined['items'] += v['items']
|
1016
|
-
|
1017
|
-
section = opt[:tag_filter] && opt[:tag_filter]['bool'] != 'NONE'
|
1044
|
+
end
|
1045
|
+
section = if opt[:tag_filter] && opt[:tag_filter]['bool'] != 'NONE'
|
1046
|
+
opt[:tag_filter]['tags'].map do |tag|
|
1047
|
+
"@#{tag}"
|
1048
|
+
end.join(' + ')
|
1049
|
+
else
|
1050
|
+
'doing'
|
1051
|
+
end
|
1018
1052
|
opt[:section] = combined
|
1019
1053
|
else
|
1020
1054
|
section = guess_section(opt[:section])
|
@@ -1023,209 +1057,201 @@ class WWID
|
|
1023
1057
|
end
|
1024
1058
|
|
1025
1059
|
if opt[:section].class != Hash
|
1026
|
-
|
1060
|
+
warn 'Invalid section object'
|
1027
1061
|
return
|
1028
1062
|
end
|
1029
1063
|
|
1030
|
-
items = opt[:section]['items'].sort_by{|item| item['date'] }
|
1064
|
+
items = opt[:section]['items'].sort_by { |item| item['date'] }
|
1031
1065
|
|
1032
1066
|
if opt[:date_filter].length == 2
|
1033
1067
|
start_date = opt[:date_filter][0]
|
1034
1068
|
end_date = opt[:date_filter][1]
|
1035
|
-
items.keep_if
|
1069
|
+
items.keep_if do |item|
|
1036
1070
|
if end_date
|
1037
1071
|
item['date'] >= start_date && item['date'] <= end_date
|
1038
1072
|
else
|
1039
1073
|
item['date'].strftime('%F') == start_date.strftime('%F')
|
1040
1074
|
end
|
1041
|
-
|
1075
|
+
end
|
1042
1076
|
end
|
1043
1077
|
|
1044
1078
|
if opt[:tag_filter] && !opt[:tag_filter]['tags'].empty?
|
1045
|
-
items.delete_if
|
1079
|
+
items.delete_if do |item|
|
1046
1080
|
if opt[:tag_filter]['bool'] =~ /(AND|ALL)/
|
1047
1081
|
score = 0
|
1048
|
-
opt[:tag_filter]['tags'].each
|
1082
|
+
opt[:tag_filter]['tags'].each do |tag|
|
1049
1083
|
score += 1 if item['title'] =~ /@#{tag}/
|
1050
|
-
|
1084
|
+
end
|
1051
1085
|
score < opt[:tag_filter]['tags'].length
|
1052
1086
|
elsif opt[:tag_filter]['bool'] =~ /NONE/
|
1053
1087
|
del = false
|
1054
|
-
opt[:tag_filter]['tags'].each
|
1088
|
+
opt[:tag_filter]['tags'].each do |tag|
|
1055
1089
|
del = true if item['title'] =~ /@#{tag}/
|
1056
|
-
|
1090
|
+
end
|
1057
1091
|
del
|
1058
1092
|
elsif opt[:tag_filter]['bool'] =~ /(OR|ANY)/
|
1059
1093
|
del = true
|
1060
|
-
opt[:tag_filter]['tags'].each
|
1094
|
+
opt[:tag_filter]['tags'].each do |tag|
|
1061
1095
|
del = false if item['title'] =~ /@#{tag}/
|
1062
|
-
|
1096
|
+
end
|
1063
1097
|
del
|
1064
1098
|
end
|
1065
|
-
|
1099
|
+
end
|
1066
1100
|
end
|
1067
1101
|
|
1068
1102
|
if opt[:search]
|
1069
|
-
items.keep_if
|
1070
|
-
text = item['note'] ? item['title'] + item['note'].join(
|
1071
|
-
if opt[:search].strip =~
|
1072
|
-
|
1073
|
-
|
1074
|
-
|
1075
|
-
|
1103
|
+
items.keep_if do |item|
|
1104
|
+
text = item['note'] ? item['title'] + item['note'].join(' ') : item['title']
|
1105
|
+
pattern = if opt[:search].strip =~ %r{^/.*?/$}
|
1106
|
+
opt[:search].sub(%r{/(.*?)/}, '\1')
|
1107
|
+
else
|
1108
|
+
opt[:search].split('').join('.{0,3}')
|
1109
|
+
end
|
1076
1110
|
text =~ /#{pattern}/i
|
1077
|
-
|
1111
|
+
end
|
1078
1112
|
end
|
1079
1113
|
|
1080
1114
|
if opt[:only_timed]
|
1081
|
-
items.delete_if
|
1115
|
+
items.delete_if do |item|
|
1082
1116
|
get_interval(item) == false
|
1083
|
-
|
1117
|
+
end
|
1084
1118
|
end
|
1085
1119
|
|
1086
1120
|
if opt[:today]
|
1087
|
-
items.delete_if
|
1121
|
+
items.delete_if do |item|
|
1088
1122
|
item['date'] < Date.today.to_time
|
1089
|
-
|
1123
|
+
end.reverse!
|
1090
1124
|
section = Time.now.strftime('%A, %B %d')
|
1091
1125
|
elsif opt[:yesterday]
|
1092
|
-
items.delete_if
|
1093
|
-
|
1094
|
-
|
1126
|
+
items.delete_if do |item|
|
1127
|
+
item['date'] <= Date.today.prev_day.to_time or
|
1128
|
+
item['date'] >= Date.today.to_time
|
1129
|
+
end.reverse!
|
1130
|
+
elsif opt[:age] =~ /oldest/i
|
1131
|
+
items = items[0..count]
|
1095
1132
|
else
|
1096
|
-
|
1097
|
-
items = items[0..count]
|
1098
|
-
else
|
1099
|
-
items = items.reverse[0..count]
|
1100
|
-
end
|
1133
|
+
items = items.reverse[0..count]
|
1101
1134
|
end
|
1102
1135
|
|
1103
|
-
if opt[:order] =~ /^a/i
|
1104
|
-
items.reverse!
|
1105
|
-
end
|
1136
|
+
items.reverse! if opt[:order] =~ /^a/i
|
1106
1137
|
|
1107
|
-
out =
|
1138
|
+
out = ''
|
1108
1139
|
|
1109
|
-
if opt[:output]
|
1110
|
-
|
1111
|
-
|
1112
|
-
|
1113
|
-
|
1114
|
-
|
1115
|
-
note = ""
|
1140
|
+
raise 'Unknown output format' if opt[:output] && !(opt[:output] =~ /(template|html|csv|json|timeline)/)
|
1141
|
+
|
1142
|
+
if opt[:output] == 'csv'
|
1143
|
+
output = [CSV.generate_line(%w[date title note timer section])]
|
1144
|
+
items.each do |i|
|
1145
|
+
note = ''
|
1116
1146
|
if i['note']
|
1117
|
-
arr = i['note'].map{|line| line.strip}.delete_if{|e| e =~ /^\s*$/}
|
1147
|
+
arr = i['note'].map { |line| line.strip }.delete_if { |e| e =~ /^\s*$/ }
|
1118
1148
|
note = arr.join("\n") unless arr.nil?
|
1119
1149
|
end
|
1120
|
-
if i['title'] =~ /@done\((\d{4}-\d\d-\d\d \d\d:\d\d.*?)\)/ && opt[:times]
|
1121
|
-
interval = get_interval(i, false)
|
1122
|
-
end
|
1150
|
+
interval = get_interval(i, false) if i['title'] =~ /@done\((\d{4}-\d\d-\d\d \d\d:\d\d.*?)\)/ && opt[:times]
|
1123
1151
|
interval ||= 0
|
1124
|
-
output.push(CSV.generate_line([i['date'],i['title'],note,interval,i['section']]))
|
1125
|
-
|
1126
|
-
out = output.join(
|
1127
|
-
elsif opt[:output] ==
|
1152
|
+
output.push(CSV.generate_line([i['date'], i['title'], note, interval, i['section']]))
|
1153
|
+
end
|
1154
|
+
out = output.join('')
|
1155
|
+
elsif opt[:output] == 'json' || opt[:output] == 'timeline'
|
1128
1156
|
items_out = []
|
1129
1157
|
max = items[-1]['date'].strftime('%F')
|
1130
1158
|
min = items[0]['date'].strftime('%F')
|
1131
|
-
items.each_with_index
|
1159
|
+
items.each_with_index do |i, index|
|
1132
1160
|
if String.method_defined? :force_encoding
|
1133
1161
|
title = i['title'].force_encoding('utf-8')
|
1134
|
-
note = i['note'].map {|line| line.force_encoding('utf-8').strip } if i['note']
|
1162
|
+
note = i['note'].map { |line| line.force_encoding('utf-8').strip } if i['note']
|
1135
1163
|
else
|
1136
1164
|
title = i['title']
|
1137
1165
|
note = i['note'].map { |line| line.strip } if i['note']
|
1138
1166
|
end
|
1139
1167
|
if i['title'] =~ /@done\((\d{4}-\d\d-\d\d \d\d:\d\d.*?)\)/ && opt[:times]
|
1140
|
-
end_date = Time.parse(
|
1141
|
-
interval = get_interval(i,false)
|
1168
|
+
end_date = Time.parse(Regexp.last_match(1))
|
1169
|
+
interval = get_interval(i, false)
|
1142
1170
|
end
|
1143
|
-
end_date ||=
|
1171
|
+
end_date ||= ''
|
1144
1172
|
interval ||= 0
|
1145
|
-
note ||=
|
1173
|
+
note ||= ''
|
1146
1174
|
|
1147
1175
|
tags = []
|
1148
|
-
skip_tags = [
|
1149
|
-
i['title'].scan(/@([
|
1176
|
+
skip_tags = %w[meanwhile done cancelled flagged]
|
1177
|
+
i['title'].scan(/@([^(\s]+)(?:\((.*?)\))?/).each do |tag|
|
1150
1178
|
tags.push(tag[0]) unless skip_tags.include?(tag[0])
|
1151
|
-
|
1152
|
-
if opt[:output] ==
|
1179
|
+
end
|
1180
|
+
if opt[:output] == 'json'
|
1153
1181
|
|
1154
1182
|
items_out << {
|
1155
|
-
:
|
1156
|
-
:
|
1157
|
-
:
|
1158
|
-
:
|
1159
|
-
:
|
1160
|
-
:
|
1183
|
+
date: i['date'],
|
1184
|
+
end_date: end_date,
|
1185
|
+
title: title.strip, #+ " #{note}"
|
1186
|
+
note: note.instance_of?(Array) ? note.map(&:strip).join("\n") : note,
|
1187
|
+
time: '%02d:%02d:%02d' % fmt_time(interval),
|
1188
|
+
tags: tags
|
1161
1189
|
}
|
1162
1190
|
|
1163
|
-
elsif opt[:output] ==
|
1191
|
+
elsif opt[:output] == 'timeline'
|
1164
1192
|
new_item = {
|
1165
1193
|
'id' => index + 1,
|
1166
1194
|
'content' => title.strip, #+ " #{note}"
|
1167
|
-
'title' => title.strip + " (#{
|
1195
|
+
'title' => title.strip + " (#{'%02d:%02d:%02d' % fmt_time(interval)})",
|
1168
1196
|
'start' => i['date'].strftime('%F'),
|
1169
1197
|
'type' => 'point'
|
1170
1198
|
}
|
1171
1199
|
|
1172
1200
|
if interval && interval > 0
|
1173
1201
|
new_item['end'] = end_date.strftime('%F')
|
1174
|
-
if interval > 3600 * 3
|
1175
|
-
new_item['type'] = 'range'
|
1176
|
-
end
|
1202
|
+
new_item['type'] = 'range' if interval > 3600 * 3
|
1177
1203
|
end
|
1178
1204
|
items_out.push(new_item)
|
1179
1205
|
end
|
1180
|
-
|
1181
|
-
if opt[:output] ==
|
1206
|
+
end
|
1207
|
+
if opt[:output] == 'json'
|
1182
1208
|
out = {
|
1183
1209
|
'section' => section,
|
1184
1210
|
'items' => items_out,
|
1185
|
-
'timers' => tag_times(
|
1211
|
+
'timers' => tag_times('json', opt[:sort_tags])
|
1186
1212
|
}.to_json
|
1187
|
-
elsif opt[:output] ==
|
1188
|
-
|
1189
|
-
<!doctype html>
|
1190
|
-
<html>
|
1191
|
-
<head>
|
1192
|
-
|
1193
|
-
|
1194
|
-
</head>
|
1195
|
-
<body>
|
1196
|
-
|
1197
|
-
|
1198
|
-
|
1199
|
-
|
1200
|
-
|
1201
|
-
|
1202
|
-
|
1203
|
-
|
1204
|
-
|
1205
|
-
|
1206
|
-
|
1207
|
-
|
1208
|
-
|
1209
|
-
|
1210
|
-
|
1211
|
-
|
1212
|
-
|
1213
|
-
|
1214
|
-
|
1215
|
-
|
1216
|
-
|
1217
|
-
|
1218
|
-
|
1219
|
-
|
1220
|
-
</body>
|
1221
|
-
</html>
|
1222
|
-
EOTEMPLATE
|
1213
|
+
elsif opt[:output] == 'timeline'
|
1214
|
+
template = <<~EOTEMPLATE
|
1215
|
+
<!doctype html>
|
1216
|
+
<html>
|
1217
|
+
<head>
|
1218
|
+
<link href="http://visjs.org/dist/vis.css" rel="stylesheet" type="text/css" />
|
1219
|
+
<script src="http://visjs.org/dist/vis.js"></script>
|
1220
|
+
</head>
|
1221
|
+
<body>
|
1222
|
+
<div id="mytimeline"></div>
|
1223
|
+
#{' '}
|
1224
|
+
<script type="text/javascript">
|
1225
|
+
// DOM element where the Timeline will be attached
|
1226
|
+
var container = document.getElementById('mytimeline');
|
1227
|
+
#{' '}
|
1228
|
+
// Create a DataSet with data (enables two way data binding)
|
1229
|
+
var data = new vis.DataSet(#{items_out.to_json});
|
1230
|
+
#{' '}
|
1231
|
+
// Configuration for the Timeline
|
1232
|
+
var options = {
|
1233
|
+
width: '100%',
|
1234
|
+
height: '800px',
|
1235
|
+
margin: {
|
1236
|
+
item: 20
|
1237
|
+
},
|
1238
|
+
stack: true,
|
1239
|
+
min: '#{min}',
|
1240
|
+
max: '#{max}'
|
1241
|
+
};
|
1242
|
+
#{' '}
|
1243
|
+
// Create a Timeline
|
1244
|
+
var timeline = new vis.Timeline(container, data, options);
|
1245
|
+
</script>
|
1246
|
+
</body>
|
1247
|
+
</html>
|
1248
|
+
EOTEMPLATE
|
1223
1249
|
return template
|
1224
1250
|
end
|
1225
|
-
elsif opt[:output] ==
|
1251
|
+
elsif opt[:output] == 'html'
|
1226
1252
|
page_title = section
|
1227
1253
|
items_out = []
|
1228
|
-
items.each
|
1254
|
+
items.each do |i|
|
1229
1255
|
# if i.has_key?('note')
|
1230
1256
|
# note = '<span class="note">' + i['note'].map{|n| n.strip }.join('<br>') + '</span>'
|
1231
1257
|
# else
|
@@ -1233,83 +1259,81 @@ EOTEMPLATE
|
|
1233
1259
|
# end
|
1234
1260
|
if String.method_defined? :force_encoding
|
1235
1261
|
title = i['title'].force_encoding('utf-8').link_urls
|
1236
|
-
note = i['note'].map {|line| line.force_encoding('utf-8').strip.link_urls } if i['note']
|
1262
|
+
note = i['note'].map { |line| line.force_encoding('utf-8').strip.link_urls } if i['note']
|
1237
1263
|
else
|
1238
1264
|
title = i['title'].link_urls
|
1239
1265
|
note = i['note'].map { |line| line.strip.link_urls } if i['note']
|
1240
1266
|
end
|
1241
1267
|
|
1242
|
-
if i['title'] =~ /@done\((\d{4}-\d\d-\d\d \d\d:\d\d.*?)\)/ && opt[:times]
|
1243
|
-
interval = get_interval(i)
|
1244
|
-
end
|
1268
|
+
interval = get_interval(i) if i['title'] =~ /@done\((\d{4}-\d\d-\d\d \d\d:\d\d.*?)\)/ && opt[:times]
|
1245
1269
|
interval ||= false
|
1246
1270
|
|
1247
1271
|
items_out << {
|
1248
|
-
:
|
1249
|
-
:
|
1250
|
-
:
|
1251
|
-
:
|
1252
|
-
:
|
1272
|
+
date: i['date'].strftime('%a %-I:%M%p'),
|
1273
|
+
title: title.gsub(/(@[^ (]+(\(.*?\))?)/im, '<span class="tag">\1</span>').strip, #+ " #{note}"
|
1274
|
+
note: note,
|
1275
|
+
time: interval,
|
1276
|
+
section: i['section']
|
1253
1277
|
}
|
1254
|
-
}
|
1255
|
-
|
1256
|
-
if @config['html_template']['haml'] && File.exists?(File.expand_path(@config['html_template']['haml']))
|
1257
|
-
template = IO.read(File.expand_path(@config['html_template']['haml']))
|
1258
|
-
else
|
1259
|
-
template = haml_template
|
1260
1278
|
end
|
1261
1279
|
|
1262
|
-
if @config['html_template']['
|
1263
|
-
|
1264
|
-
|
1265
|
-
|
1266
|
-
|
1280
|
+
template = if @config['html_template']['haml'] && File.exist?(File.expand_path(@config['html_template']['haml']))
|
1281
|
+
IO.read(File.expand_path(@config['html_template']['haml']))
|
1282
|
+
else
|
1283
|
+
haml_template
|
1284
|
+
end
|
1285
|
+
|
1286
|
+
style = if @config['html_template']['css'] && File.exist?(File.expand_path(@config['html_template']['css']))
|
1287
|
+
IO.read(File.expand_path(@config['html_template']['css']))
|
1288
|
+
else
|
1289
|
+
css_template
|
1290
|
+
end
|
1267
1291
|
|
1268
|
-
totals = opt[:totals] ? tag_times(
|
1292
|
+
totals = opt[:totals] ? tag_times('html', opt[:sort_tags]) : ''
|
1269
1293
|
engine = Haml::Engine.new(template)
|
1270
|
-
puts engine.render(Object.new,
|
1294
|
+
puts engine.render(Object.new,
|
1295
|
+
{ :@items => items_out, :@page_title => page_title, :@style => style, :@totals => totals })
|
1271
1296
|
else
|
1272
|
-
items.each
|
1273
|
-
|
1297
|
+
items.each do |item|
|
1274
1298
|
if opt[:highlight] && item['title'] =~ /@#{@config['marker_tag']}\b/i
|
1275
1299
|
flag = colors[@config['marker_color']]
|
1276
1300
|
reset = colors['default']
|
1277
1301
|
else
|
1278
|
-
flag =
|
1279
|
-
reset =
|
1302
|
+
flag = ''
|
1303
|
+
reset = ''
|
1280
1304
|
end
|
1281
1305
|
|
1282
1306
|
if (item.has_key?('note') && !item['note'].empty?) && @config[:include_notes]
|
1283
|
-
note_lines = item['note'].delete_if
|
1307
|
+
note_lines = item['note'].delete_if do |line|
|
1308
|
+
line =~ /^\s*$/
|
1309
|
+
end.map { |line| "\t\t" + line.sub(/^\t*/, '').sub(/^-/, '—') + ' ' }
|
1284
1310
|
if opt[:wrap_width] && opt[:wrap_width] > 0
|
1285
1311
|
width = opt[:wrap_width]
|
1286
|
-
note_lines.map!
|
1312
|
+
note_lines.map! do |line|
|
1287
1313
|
line.strip.gsub(/(.{1,#{width}})(\s+|\Z)/, "\t\\1\n")
|
1288
|
-
|
1314
|
+
end
|
1289
1315
|
end
|
1290
1316
|
note = "\n#{note_lines.join("\n").chomp}"
|
1291
1317
|
else
|
1292
|
-
note =
|
1318
|
+
note = ''
|
1293
1319
|
end
|
1294
1320
|
output = opt[:template].dup
|
1295
1321
|
|
1296
1322
|
output.gsub!(/%[a-z]+/) do |m|
|
1297
|
-
if colors.has_key?(m.sub(/^%/,''))
|
1298
|
-
colors[m.sub(/^%/,'')]
|
1323
|
+
if colors.has_key?(m.sub(/^%/, ''))
|
1324
|
+
colors[m.sub(/^%/, '')]
|
1299
1325
|
else
|
1300
1326
|
m
|
1301
1327
|
end
|
1302
1328
|
end
|
1303
1329
|
|
1304
|
-
output.sub!(/%date/,item['date'].strftime(opt[:format]))
|
1330
|
+
output.sub!(/%date/, item['date'].strftime(opt[:format]))
|
1305
1331
|
|
1306
|
-
if item['title'] =~ /@done\((\d{4}-\d\d-\d\d \d\d:\d\d.*?)\)/ && opt[:times]
|
1307
|
-
|
1308
|
-
|
1309
|
-
interval ||= ""
|
1310
|
-
output.sub!(/%interval/,interval)
|
1332
|
+
interval = get_interval(item) if item['title'] =~ /@done\((\d{4}-\d\d-\d\d \d\d:\d\d.*?)\)/ && opt[:times]
|
1333
|
+
interval ||= ''
|
1334
|
+
output.sub!(/%interval/, interval)
|
1311
1335
|
|
1312
|
-
output.sub!(/%shortdate/)
|
1336
|
+
output.sub!(/%shortdate/) do
|
1313
1337
|
if item['date'] > Date.today.to_time
|
1314
1338
|
item['date'].strftime('%_I:%M%P')
|
1315
1339
|
elsif item['date'] > (Date.today - 7).to_time
|
@@ -1319,45 +1343,45 @@ EOTEMPLATE
|
|
1319
1343
|
else
|
1320
1344
|
item['date'].strftime('%b %d %Y, %-I:%M%P')
|
1321
1345
|
end
|
1322
|
-
|
1346
|
+
end
|
1323
1347
|
|
1324
|
-
output.sub!(/%title/)
|
1348
|
+
output.sub!(/%title/) do |_m|
|
1325
1349
|
if opt[:wrap_width] && opt[:wrap_width] > 0
|
1326
|
-
flag+item['title'].gsub(/(.{1,#{opt[:wrap_width]}})(\s+|\Z)/, "\\1\n\t ").chomp+reset
|
1350
|
+
flag + item['title'].gsub(/(.{1,#{opt[:wrap_width]}})(\s+|\Z)/, "\\1\n\t ").chomp + reset
|
1327
1351
|
else
|
1328
|
-
flag+item['title'].chomp+reset
|
1352
|
+
flag + item['title'].chomp + reset
|
1329
1353
|
end
|
1330
|
-
|
1354
|
+
end
|
1331
1355
|
|
1332
|
-
output.sub!(/%section/,item['section']) if item['section']
|
1356
|
+
output.sub!(/%section/, item['section']) if item['section']
|
1333
1357
|
|
1334
1358
|
if opt[:tags_color]
|
1335
1359
|
escapes = output.scan(/(\e\[[\d;]+m)[^\e]+@/)
|
1336
|
-
if escapes.length > 0
|
1337
|
-
|
1338
|
-
|
1339
|
-
|
1340
|
-
|
1341
|
-
output.gsub!(/\s(@[^
|
1360
|
+
last_color = if escapes.length > 0
|
1361
|
+
escapes[-1][0]
|
1362
|
+
else
|
1363
|
+
colors['default']
|
1364
|
+
end
|
1365
|
+
output.gsub!(/\s(@[^ (]+)/, " #{colors[opt[:tags_color]]}\\1#{last_color}")
|
1342
1366
|
end
|
1343
|
-
output.sub!(/%note/,note)
|
1344
|
-
output.sub!(/%odnote/,note.gsub(/^\t*/,
|
1345
|
-
output.sub!(/%chompnote/,note.gsub(/\n+/,' ').gsub(/(^\s*|\s*$)/,'').gsub(/\s+/,' '))
|
1346
|
-
output.gsub!(/%hr(_under)?/) do |
|
1347
|
-
o =
|
1367
|
+
output.sub!(/%note/, note)
|
1368
|
+
output.sub!(/%odnote/, note.gsub(/^\t*/, ''))
|
1369
|
+
output.sub!(/%chompnote/, note.gsub(/\n+/, ' ').gsub(/(^\s*|\s*$)/, '').gsub(/\s+/, ' '))
|
1370
|
+
output.gsub!(/%hr(_under)?/) do |_m|
|
1371
|
+
o = ''
|
1348
1372
|
`tput cols`.to_i.times do
|
1349
|
-
o +=
|
1373
|
+
o += Regexp.last_match(1).nil? ? '-' : '_'
|
1350
1374
|
end
|
1351
1375
|
o
|
1352
1376
|
end
|
1353
|
-
output.gsub!(/%n/,"\n")
|
1354
|
-
output.gsub!(/%t/,"\t")
|
1377
|
+
output.gsub!(/%n/, "\n")
|
1378
|
+
output.gsub!(/%t/, "\t")
|
1355
1379
|
|
1356
1380
|
out += output + "\n"
|
1357
|
-
|
1358
|
-
out += tag_times(
|
1381
|
+
end
|
1382
|
+
out += tag_times('text', opt[:sort_tags]) if opt[:totals]
|
1359
1383
|
end
|
1360
|
-
|
1384
|
+
out
|
1361
1385
|
end
|
1362
1386
|
|
1363
1387
|
##
|
@@ -1370,15 +1394,12 @@ EOTEMPLATE
|
|
1370
1394
|
## @param tags (Array) Tags to archive
|
1371
1395
|
## @param bool (String) Tag boolean combinator
|
1372
1396
|
##
|
1373
|
-
def archive(section=
|
1374
|
-
|
1397
|
+
def archive(section = 'Currently', count = 5, destination = nil, tags = nil, bool = nil, _export = nil)
|
1375
1398
|
section = choose_section if section.nil? || section =~ /choose/i
|
1376
1399
|
archive_all = section =~ /all/i # && !(tags.nil? || tags.empty?)
|
1377
1400
|
section = guess_section(section) unless archive_all
|
1378
1401
|
|
1379
|
-
if destination =~ /archive/i && !sections.include?(
|
1380
|
-
add_section("Archive")
|
1381
|
-
end
|
1402
|
+
add_section('Archive') if destination =~ /archive/i && !sections.include?('Archive')
|
1382
1403
|
|
1383
1404
|
destination = guess_section(destination)
|
1384
1405
|
|
@@ -1386,16 +1407,16 @@ EOTEMPLATE
|
|
1386
1407
|
if archive_all
|
1387
1408
|
to_archive = sections.dup
|
1388
1409
|
to_archive.delete(destination)
|
1389
|
-
to_archive.each
|
1390
|
-
do_archive(source, destination, { :
|
1391
|
-
|
1410
|
+
to_archive.each do |source, _v|
|
1411
|
+
do_archive(source, destination, { count: count, tags: tags, bool: bool, label: true })
|
1412
|
+
end
|
1392
1413
|
else
|
1393
|
-
do_archive(section, destination, { :
|
1414
|
+
do_archive(section, destination, { count: count, tags: tags, bool: bool, label: true })
|
1394
1415
|
end
|
1395
1416
|
|
1396
1417
|
write(doing_file)
|
1397
1418
|
else
|
1398
|
-
raise
|
1419
|
+
raise 'Either source or destination does not exist'
|
1399
1420
|
end
|
1400
1421
|
end
|
1401
1422
|
|
@@ -1406,63 +1427,66 @@ EOTEMPLATE
|
|
1406
1427
|
## @param destination (String) The destination section
|
1407
1428
|
## @param opt (Hash) Additional Options
|
1408
1429
|
##
|
1409
|
-
def do_archive(section, destination, opt={})
|
1430
|
+
def do_archive(section, destination, opt = {})
|
1410
1431
|
count = opt[:count] || 5
|
1411
1432
|
tags = opt[:tags] || []
|
1412
|
-
bool = opt[:bool] ||
|
1433
|
+
bool = opt[:bool] || 'AND'
|
1413
1434
|
label = opt[:label] || false
|
1414
1435
|
|
1415
1436
|
items = @content[section]['items']
|
1416
1437
|
moved_items = []
|
1417
1438
|
|
1418
1439
|
if tags && !tags.empty?
|
1419
|
-
items.delete_if
|
1440
|
+
items.delete_if do |item|
|
1420
1441
|
if bool =~ /(AND|ALL)/
|
1421
1442
|
score = 0
|
1422
|
-
tags.each
|
1443
|
+
tags.each do |tag|
|
1423
1444
|
score += 1 if item['title'] =~ /@#{tag}/i
|
1424
|
-
|
1445
|
+
end
|
1425
1446
|
res = score < tags.length
|
1426
1447
|
moved_items.push(item) if res
|
1427
1448
|
res
|
1428
1449
|
elsif bool =~ /NONE/
|
1429
1450
|
del = false
|
1430
|
-
tags.each
|
1451
|
+
tags.each do |tag|
|
1431
1452
|
del = true if item['title'] =~ /@#{tag}/i
|
1432
|
-
|
1453
|
+
end
|
1433
1454
|
moved_items.push(item) if del
|
1434
1455
|
del
|
1435
1456
|
elsif bool =~ /(OR|ANY)/
|
1436
1457
|
del = true
|
1437
|
-
tags.each
|
1458
|
+
tags.each do |tag|
|
1438
1459
|
del = false if item['title'] =~ /@#{tag}/i
|
1439
|
-
|
1460
|
+
end
|
1440
1461
|
moved_items.push(item) if del
|
1441
1462
|
del
|
1442
1463
|
end
|
1443
|
-
|
1444
|
-
moved_items.each
|
1445
|
-
if label
|
1446
|
-
item['title'] =
|
1464
|
+
end
|
1465
|
+
moved_items.each do |item|
|
1466
|
+
if label && !(section == 'Currently')
|
1467
|
+
item['title'] =
|
1468
|
+
item['title'].sub(/(?:@from\(.*?\))?(.*)$/, "\\1 @from(#{section})")
|
1447
1469
|
end
|
1448
|
-
|
1470
|
+
end
|
1449
1471
|
@content[section]['items'] = moved_items
|
1450
1472
|
@content[destination]['items'] += items
|
1451
1473
|
@results.push("Archived #{items.length} items from #{section} to #{destination}")
|
1452
1474
|
else
|
1453
1475
|
|
1454
1476
|
return if items.length < count
|
1455
|
-
if count == 0
|
1456
|
-
@content[section]['items'] = []
|
1457
|
-
else
|
1458
|
-
@content[section]['items'] = items[0..count-1]
|
1459
|
-
end
|
1460
1477
|
|
1461
|
-
items
|
1462
|
-
|
1463
|
-
|
1478
|
+
@content[section]['items'] = if count == 0
|
1479
|
+
[]
|
1480
|
+
else
|
1481
|
+
items[0..count - 1]
|
1482
|
+
end
|
1483
|
+
|
1484
|
+
items.each do |item|
|
1485
|
+
if label && !(section == 'Currently')
|
1486
|
+
item['title'] =
|
1487
|
+
item['title'].sub(/(?:@from\(.*?\))?(.*)$/, "\\1 @from(#{section})")
|
1464
1488
|
end
|
1465
|
-
|
1489
|
+
end
|
1466
1490
|
|
1467
1491
|
@content[destination]['items'] += items[count..-1]
|
1468
1492
|
@results.push("Archived #{items.length - count} items from #{section} to #{destination}")
|
@@ -1518,7 +1542,6 @@ EOTEMPLATE
|
|
1518
1542
|
color
|
1519
1543
|
end
|
1520
1544
|
|
1521
|
-
|
1522
1545
|
##
|
1523
1546
|
## @brief Show all entries from the current day
|
1524
1547
|
##
|
@@ -1526,12 +1549,13 @@ EOTEMPLATE
|
|
1526
1549
|
## @param output (String) output format
|
1527
1550
|
## @param opt (Hash) Options
|
1528
1551
|
##
|
1529
|
-
def today(times=true,output=nil,opt={})
|
1552
|
+
def today(times = true, output = nil, opt = {})
|
1530
1553
|
opt[:totals] ||= false
|
1531
1554
|
opt[:sort_tags] ||= false
|
1532
1555
|
|
1533
1556
|
cfg = @config['templates']['today']
|
1534
|
-
list_section({:
|
1557
|
+
list_section({ section: opt[:section], wrap_width: cfg['wrap_width'], count: 0,
|
1558
|
+
format: cfg['date_format'], template: cfg['template'], order: 'asc', today: true, times: times, output: output, totals: opt[:totals], sort_tags: opt[:sort_tags] })
|
1535
1559
|
end
|
1536
1560
|
|
1537
1561
|
##
|
@@ -1543,16 +1567,15 @@ EOTEMPLATE
|
|
1543
1567
|
## @param output (String) Output format
|
1544
1568
|
## @param opt (Hash) Additional Options
|
1545
1569
|
##
|
1546
|
-
def list_date(dates,section,times=nil,output=nil,opt={})
|
1570
|
+
def list_date(dates, section, times = nil, output = nil, opt = {})
|
1547
1571
|
opt[:totals] ||= false
|
1548
1572
|
opt[:sort_tags] ||= false
|
1549
1573
|
section = guess_section(section)
|
1550
1574
|
# :date_filter expects an array with start and end date
|
1551
|
-
if dates.
|
1552
|
-
dates = [dates, dates]
|
1553
|
-
end
|
1575
|
+
dates = [dates, dates] if dates.instance_of?(String)
|
1554
1576
|
|
1555
|
-
list_section({:
|
1577
|
+
list_section({ section: section, count: 0, order: 'asc', date_filter: dates, times: times,
|
1578
|
+
output: output, totals: opt[:totals], sort_tags: opt[:sort_tags] })
|
1556
1579
|
end
|
1557
1580
|
|
1558
1581
|
##
|
@@ -1563,11 +1586,12 @@ EOTEMPLATE
|
|
1563
1586
|
## @param output (String) Output format
|
1564
1587
|
## @param opt (Hash) Additional Options
|
1565
1588
|
##
|
1566
|
-
def yesterday(section,times=nil,output=nil,opt={})
|
1589
|
+
def yesterday(section, times = nil, output = nil, opt = {})
|
1567
1590
|
opt[:totals] ||= false
|
1568
1591
|
opt[:sort_tags] ||= false
|
1569
1592
|
section = guess_section(section)
|
1570
|
-
list_section({:
|
1593
|
+
list_section({ section: section, count: 0, order: 'asc', yesterday: true, times: times,
|
1594
|
+
output: output, totals: opt[:totals], sort_tags: opt[:sort_tags] })
|
1571
1595
|
end
|
1572
1596
|
|
1573
1597
|
##
|
@@ -1577,7 +1601,7 @@ EOTEMPLATE
|
|
1577
1601
|
## @param section (String) The section to show from, default Currently
|
1578
1602
|
## @param opt (Hash) Additional Options
|
1579
1603
|
##
|
1580
|
-
def recent(count=10,section=nil,opt={})
|
1604
|
+
def recent(count = 10, section = nil, opt = {})
|
1581
1605
|
times = opt[:t] || true
|
1582
1606
|
opt[:totals] ||= false
|
1583
1607
|
opt[:sort_tags] ||= false
|
@@ -1585,7 +1609,8 @@ EOTEMPLATE
|
|
1585
1609
|
cfg = @config['templates']['recent']
|
1586
1610
|
section ||= @current_section
|
1587
1611
|
section = guess_section(section)
|
1588
|
-
list_section({:
|
1612
|
+
list_section({ section: section, wrap_width: cfg['wrap_width'], count: count,
|
1613
|
+
format: cfg['date_format'], template: cfg['template'], order: 'asc', times: times, totals: opt[:totals], sort_tags: opt[:sort_tags] })
|
1589
1614
|
end
|
1590
1615
|
|
1591
1616
|
##
|
@@ -1594,11 +1619,12 @@ EOTEMPLATE
|
|
1594
1619
|
## @param times (Bool) Show times
|
1595
1620
|
## @param section (String) Section to pull from, default Currently
|
1596
1621
|
##
|
1597
|
-
def last(times=true,section=nil)
|
1622
|
+
def last(times = true, section = nil)
|
1598
1623
|
section ||= @current_section
|
1599
1624
|
section = guess_section(section)
|
1600
1625
|
cfg = @config['templates']['last']
|
1601
|
-
list_section({:
|
1626
|
+
list_section({ section: section, wrap_width: cfg['wrap_width'], count: 1, format: cfg['date_format'],
|
1627
|
+
template: cfg['template'], times: times })
|
1602
1628
|
end
|
1603
1629
|
|
1604
1630
|
##
|
@@ -1606,22 +1632,22 @@ EOTEMPLATE
|
|
1606
1632
|
##
|
1607
1633
|
## @param format (String) return format (html, json, or text)
|
1608
1634
|
##
|
1609
|
-
def tag_times(format=
|
1610
|
-
return
|
1635
|
+
def tag_times(format = 'text', sort_by_name = false)
|
1636
|
+
return '' if @timers.empty?
|
1611
1637
|
|
1612
|
-
max = @timers.keys.sort_by {|k| k.length }.reverse[0].length + 1
|
1638
|
+
max = @timers.keys.sort_by { |k| k.length }.reverse[0].length + 1
|
1613
1639
|
|
1614
|
-
total = @timers.delete(
|
1640
|
+
total = @timers.delete('All')
|
1615
1641
|
|
1616
|
-
tags_data = @timers.delete_if { |
|
1617
|
-
if sort_by_name
|
1618
|
-
|
1619
|
-
|
1620
|
-
|
1621
|
-
|
1642
|
+
tags_data = @timers.delete_if { |_k, v| v == 0 }
|
1643
|
+
sorted_tags_data = if sort_by_name
|
1644
|
+
tags_data.sort_by { |k, _v| k }.reverse
|
1645
|
+
else
|
1646
|
+
tags_data.sort_by { |_k, v| v }
|
1647
|
+
end
|
1622
1648
|
|
1623
|
-
if format ==
|
1624
|
-
output
|
1649
|
+
if format == 'html'
|
1650
|
+
output = <<EOS
|
1625
1651
|
<table>
|
1626
1652
|
<caption id="tagtotals">Tag Totals</caption>
|
1627
1653
|
<colgroup>
|
@@ -1636,10 +1662,12 @@ EOTEMPLATE
|
|
1636
1662
|
</thead>
|
1637
1663
|
<tbody>
|
1638
1664
|
EOS
|
1639
|
-
sorted_tags_data.reverse.each
|
1640
|
-
|
1641
|
-
|
1642
|
-
|
1665
|
+
sorted_tags_data.reverse.each do |k, v|
|
1666
|
+
if v > 0
|
1667
|
+
output += "<tr><td style='text-align:left;'>#{k}</td><td style='text-align:left;'>#{'%02d:%02d:%02d' % fmt_time(v)}</td></tr>\n"
|
1668
|
+
end
|
1669
|
+
end
|
1670
|
+
tail = <<EOS
|
1643
1671
|
<tr>
|
1644
1672
|
<td style="text-align:left;" colspan="2"></td>
|
1645
1673
|
</tr>
|
@@ -1647,34 +1675,34 @@ EOS
|
|
1647
1675
|
<tfoot>
|
1648
1676
|
<tr>
|
1649
1677
|
<td style="text-align:left;"><strong>Total</strong></td>
|
1650
|
-
<td style="text-align:left;">#{
|
1678
|
+
<td style="text-align:left;">#{'%02d:%02d:%02d' % fmt_time(total)}</td>
|
1651
1679
|
</tr>
|
1652
1680
|
</tfoot>
|
1653
1681
|
</table>
|
1654
1682
|
EOS
|
1655
1683
|
output + tail
|
1656
|
-
elsif format ==
|
1684
|
+
elsif format == 'json'
|
1657
1685
|
output = []
|
1658
|
-
sorted_tags_data.reverse.each
|
1686
|
+
sorted_tags_data.reverse.each do |k, v|
|
1659
1687
|
output << {
|
1660
1688
|
'tag' => k,
|
1661
1689
|
'seconds' => v,
|
1662
|
-
'formatted' =>
|
1690
|
+
'formatted' => '%02d:%02d:%02d' % fmt_time(v)
|
1663
1691
|
}
|
1664
|
-
|
1692
|
+
end
|
1665
1693
|
output
|
1666
1694
|
else
|
1667
1695
|
output = []
|
1668
|
-
sorted_tags_data.reverse.each
|
1669
|
-
spacer =
|
1696
|
+
sorted_tags_data.reverse.each do |k, v|
|
1697
|
+
spacer = ''
|
1670
1698
|
(max - k.length).times do
|
1671
|
-
spacer +=
|
1699
|
+
spacer += ' '
|
1672
1700
|
end
|
1673
|
-
output.push("#{k}:#{spacer}#{
|
1674
|
-
|
1701
|
+
output.push("#{k}:#{spacer}#{'%02d:%02d:%02d' % fmt_time(v)}")
|
1702
|
+
end
|
1675
1703
|
|
1676
|
-
output = output.empty? ?
|
1677
|
-
output += "\n\nTotal tracked: #{
|
1704
|
+
output = output.empty? ? '' : "\n--- Tag Totals ---\n" + output.join("\n")
|
1705
|
+
output += "\n\nTotal tracked: #{'%02d:%02d:%02d' % fmt_time(total)}\n"
|
1678
1706
|
output
|
1679
1707
|
end
|
1680
1708
|
end
|
@@ -1688,53 +1716,54 @@ EOS
|
|
1688
1716
|
def autotag(text)
|
1689
1717
|
return unless text
|
1690
1718
|
return text unless @auto_tag
|
1719
|
+
|
1691
1720
|
current_tags = text.scan(/@\w+/)
|
1692
1721
|
whitelisted = []
|
1693
|
-
@config['autotag']['whitelist'].each
|
1722
|
+
@config['autotag']['whitelist'].each do |tag|
|
1723
|
+
next if text =~ /@#{tag}\b/i
|
1724
|
+
|
1694
1725
|
text.sub!(/(?<!@)(#{tag.strip})\b/i) do |m|
|
1695
1726
|
m.downcase! if tag =~ /[a-z]/
|
1696
1727
|
whitelisted.push("@#{m}")
|
1697
1728
|
"@#{m}"
|
1698
|
-
end
|
1699
|
-
|
1729
|
+
end
|
1730
|
+
end
|
1700
1731
|
tail_tags = []
|
1701
|
-
@config['autotag']['synonyms'].each
|
1702
|
-
v.each
|
1703
|
-
|
1704
|
-
|
1705
|
-
|
1706
|
-
|
1707
|
-
|
1708
|
-
}
|
1709
|
-
}
|
1732
|
+
@config['autotag']['synonyms'].each do |tag, v|
|
1733
|
+
v.each do |word|
|
1734
|
+
next unless text =~ /\b#{word}\b/i
|
1735
|
+
|
1736
|
+
tail_tags.push(tag) unless current_tags.include?("@#{tag}") || whitelisted.include?("@#{tag}")
|
1737
|
+
end
|
1738
|
+
end
|
1710
1739
|
if @config['autotag'].key? 'transform'
|
1711
|
-
@config['autotag']['transform'].each
|
1712
|
-
|
1713
|
-
|
1714
|
-
|
1715
|
-
|
1716
|
-
|
1717
|
-
|
1718
|
-
|
1719
|
-
|
1720
|
-
|
1721
|
-
|
1722
|
-
|
1723
|
-
|
1724
|
-
|
1725
|
-
|
1726
|
-
|
1740
|
+
@config['autotag']['transform'].each do |tag|
|
1741
|
+
next unless tag =~ /\S+:\S+/
|
1742
|
+
|
1743
|
+
rx, r = tag.split(/:/)
|
1744
|
+
r.gsub!(/\$/, '\\')
|
1745
|
+
rx.sub!(/^@/, '')
|
1746
|
+
regex = Regexp.new('@' + rx + '\b')
|
1747
|
+
|
1748
|
+
matches = text.scan(regex)
|
1749
|
+
next unless matches
|
1750
|
+
|
1751
|
+
matches.each do |m|
|
1752
|
+
new_tag = r
|
1753
|
+
if m.is_a?(Array)
|
1754
|
+
index = 1
|
1755
|
+
m.each do |v|
|
1756
|
+
new_tag = new_tag.gsub('\\' + index.to_s, v)
|
1757
|
+
index += 1
|
1727
1758
|
end
|
1728
|
-
|
1729
|
-
|
1759
|
+
end
|
1760
|
+
tail_tags.push(new_tag)
|
1730
1761
|
end
|
1731
|
-
|
1732
|
-
end
|
1733
|
-
if whitelisted.length > 0
|
1734
|
-
@results.push("Whitelisted tags: #{whitelisted.join(', ')}")
|
1762
|
+
end
|
1735
1763
|
end
|
1764
|
+
@results.push("Whitelisted tags: #{whitelisted.join(', ')}") if whitelisted.length > 0
|
1736
1765
|
if tail_tags.length > 0
|
1737
|
-
tags = tail_tags.uniq.map {|t| '@'+t }.join(' ')
|
1766
|
+
tags = tail_tags.uniq.map { |t| '@' + t }.join(' ')
|
1738
1767
|
@results.push("Synonym tags: #{tags}")
|
1739
1768
|
text + ' ' + tags
|
1740
1769
|
else
|
@@ -1750,43 +1779,43 @@ EOS
|
|
1750
1779
|
## @param item (Hash) The entry
|
1751
1780
|
## @param formatted (Bool) Return human readable time (default seconds)
|
1752
1781
|
##
|
1753
|
-
def get_interval(item, formatted=true)
|
1782
|
+
def get_interval(item, formatted = true)
|
1754
1783
|
done = nil
|
1755
1784
|
start = nil
|
1756
1785
|
|
1757
1786
|
if @interval_cache.keys.include? item['title']
|
1758
1787
|
seconds = @interval_cache[item['title']]
|
1759
|
-
return seconds > 0 ?
|
1788
|
+
return seconds > 0 ? '%02d:%02d:%02d' % fmt_time(seconds) : false
|
1760
1789
|
end
|
1761
1790
|
|
1762
1791
|
if item['title'] =~ /@done\((\d{4}-\d\d-\d\d \d\d:\d\d.*?)\)/
|
1763
|
-
done = Time.parse(
|
1792
|
+
done = Time.parse(Regexp.last_match(1))
|
1764
1793
|
else
|
1765
1794
|
return nil
|
1766
1795
|
end
|
1767
1796
|
|
1768
|
-
if item['title'] =~ /@start\((\d{4}-\d\d-\d\d \d\d:\d\d.*?)\)/
|
1769
|
-
|
1770
|
-
|
1771
|
-
|
1772
|
-
|
1797
|
+
start = if item['title'] =~ /@start\((\d{4}-\d\d-\d\d \d\d:\d\d.*?)\)/
|
1798
|
+
Time.parse(Regexp.last_match(1))
|
1799
|
+
else
|
1800
|
+
item['date']
|
1801
|
+
end
|
1773
1802
|
|
1774
1803
|
seconds = (done - start).to_i
|
1775
1804
|
|
1776
|
-
item['title'].scan(/(?mi)@(\S+?)(\(.*\))?(?=\s|$)/).each
|
1777
|
-
k = m[0] ==
|
1805
|
+
item['title'].scan(/(?mi)@(\S+?)(\(.*\))?(?=\s|$)/).each do |m|
|
1806
|
+
k = m[0] == 'done' ? 'All' : m[0].downcase
|
1778
1807
|
if @timers.has_key?(k)
|
1779
1808
|
@timers[k] += seconds
|
1780
1809
|
else
|
1781
1810
|
@timers[k] = seconds
|
1782
1811
|
end
|
1783
|
-
|
1812
|
+
end
|
1784
1813
|
|
1785
1814
|
@interval_cache[item['title']] = seconds
|
1786
1815
|
|
1787
1816
|
return seconds unless formatted
|
1788
1817
|
|
1789
|
-
seconds > 0 ?
|
1818
|
+
seconds > 0 ? '%02d:%02d:%02d' % fmt_time(seconds) : false
|
1790
1819
|
end
|
1791
1820
|
|
1792
1821
|
##
|
@@ -1795,19 +1824,19 @@ EOS
|
|
1795
1824
|
## @param seconds The seconds
|
1796
1825
|
##
|
1797
1826
|
def fmt_time(seconds)
|
1798
|
-
if seconds.nil?
|
1799
|
-
|
1800
|
-
end
|
1827
|
+
return [0, 0, 0] if seconds.nil?
|
1828
|
+
|
1801
1829
|
if seconds =~ /(\d+):(\d+):(\d+)/
|
1802
|
-
h
|
1830
|
+
h = Regexp.last_match(1)
|
1831
|
+
m = Regexp.last_match(2)
|
1832
|
+
s = Regexp.last_match(3)
|
1803
1833
|
seconds = (h.to_i * 60 * 60) + (m.to_i * 60) + s.to_i
|
1804
1834
|
end
|
1805
|
-
minutes =
|
1835
|
+
minutes = (seconds / 60).to_i
|
1806
1836
|
hours = (minutes / 60).to_i
|
1807
1837
|
days = (hours / 24).to_i
|
1808
1838
|
hours = (hours % 24).to_i
|
1809
1839
|
minutes = (minutes % 60).to_i
|
1810
1840
|
[days, hours, minutes]
|
1811
1841
|
end
|
1812
|
-
|
1813
1842
|
end
|